From 2fa2966aaa644a9f92fd88e5e9183421fb79cb6e Mon Sep 17 00:00:00 2001 From: Jan-Peter von Hunnius Date: Thu, 13 Feb 2025 08:18:05 +0100 Subject: [PATCH] Update to TD Records and new cars --- Cargo.toml | 10 + bestCarForTrack.sql | 8 + bestCars.sql | 49 +++++ bestCarsPerTrack.sql | 52 +++++ correctTestBowls.sql | 3 + src/{main.rs => club.rs} | 4 +- src/records.rs | 401 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 526 insertions(+), 1 deletion(-) create mode 100644 bestCarForTrack.sql create mode 100644 bestCars.sql create mode 100644 bestCarsPerTrack.sql create mode 100644 correctTestBowls.sql rename src/{main.rs => club.rs} (99%) create mode 100644 src/records.rs diff --git a/Cargo.toml b/Cargo.toml index a126a11..07b8056 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,17 @@ name = "top_drives" version = "0.2.0" edition = "2021" +[[bin]] +name = "club" +path = "src/club.rs" + +[[bin]] +name = "records" +path = "src/records.rs" + [dependencies] scraper = "0.20.0" reqwest = "0.12.7" tokio = { version = "1.40.0", features = ["full"] } +rusqlite = { version = "0.32.0", features = ["bundled"] } +serde_json = "1.0.133" diff --git a/bestCarForTrack.sql b/bestCarForTrack.sql new file mode 100644 index 0000000..1bbbcc0 --- /dev/null +++ b/bestCarForTrack.sql @@ -0,0 +1,8 @@ +SELECT rq, brand, onlyName, year, configuration, time +FROM car_track_times t +JOIN cars c ON t.car_rid = c.rid +WHERE track_id = 'Mountain Twisty Road' + AND track_type = 'Wet' + AND rq <= 90 +ORDER BY CAST(t.time AS DECIMAL) ASC +LIMIT 1; \ No newline at end of file diff --git a/bestCars.sql b/bestCars.sql new file mode 100644 index 0000000..13f356d --- /dev/null +++ b/bestCars.sql @@ -0,0 +1,49 @@ +WITH RECURSIVE rq_range AS ( + SELECT DISTINCT rq from cars +), +track_combinations AS ( + -- Extract unique human-readable track_id and track_type combinations + SELECT DISTINCT track_id, track_type + FROM car_track_times +), +ranked_cars AS ( + SELECT + rq_range.rq AS max_rq, + t.track_id, + t.track_type, + c.rq, + c.brand, + c.onlyName, + c.year, + t.configuration, + t.time, + ROW_NUMBER() OVER ( + PARTITION BY rq_range.rq, t.track_id, t.track_type + ORDER BY t.time ASC + ) AS rank + FROM + rq_range + CROSS JOIN + track_combinations tc + JOIN + car_track_times t ON tc.track_id = t.track_id AND tc.track_type = t.track_type + JOIN + cars c ON t.car_rid = c.rid + WHERE + t.time > 0 -- Exclude invalid times + AND t.time IS NOT NULL + AND c.rq <= rq_range.rq +) +SELECT + rq, + brand, + onlyName, + year, + Count(time) +FROM + ranked_cars +WHERE + rank = 1 -- Only the best car per combination +GROUP BY brand, onlyName, year +ORDER BY + rq, brand, onlyName, year; diff --git a/bestCarsPerTrack.sql b/bestCarsPerTrack.sql new file mode 100644 index 0000000..e717b5f --- /dev/null +++ b/bestCarsPerTrack.sql @@ -0,0 +1,52 @@ +WITH RECURSIVE rq_range AS ( + SELECT DISTINCT rq from cars +), +track_combinations AS ( + -- Extract unique human-readable track_id and track_type combinations + SELECT DISTINCT track_id, track_type + FROM car_track_times +), +ranked_cars AS ( + SELECT + rq_range.rq AS max_rq, + t.track_id, + t.track_type, + c.rq, + c.brand, + c.onlyName, + c.year, + t.configuration, + t.time, + ROW_NUMBER() OVER ( + PARTITION BY rq_range.rq, t.track_id, t.track_type + ORDER BY t.time ASC + ) AS rank + FROM + rq_range + CROSS JOIN + track_combinations tc + JOIN + car_track_times t ON tc.track_id = t.track_id AND tc.track_type = t.track_type + JOIN + cars c ON t.car_rid = c.rid + WHERE + t.time > 0 -- Exclude invalid times + AND t.time IS NOT NULL + AND c.rq <= rq_range.rq +) +SELECT + max_rq, + track_id, + track_type, + rq, + brand, + onlyName, + year, + configuration, + time AS best_time +FROM + ranked_cars +WHERE + rank = 1 -- Only the best car per combination +ORDER BY + max_rq, track_id, track_type; diff --git a/correctTestBowls.sql b/correctTestBowls.sql new file mode 100644 index 0000000..7456f8e --- /dev/null +++ b/correctTestBowls.sql @@ -0,0 +1,3 @@ +UPDATE car_track_times +SET time = 2000 / time +WHERE track_id LIKE 'Test Bowl%'; \ No newline at end of file diff --git a/src/main.rs b/src/club.rs similarity index 99% rename from src/main.rs rename to src/club.rs index 617d2aa..7339943 100644 --- a/src/main.rs +++ b/src/club.rs @@ -1,10 +1,12 @@ -use scraper::{Html, Selector}; use std::collections::{HashMap}; use std::env; use std::fs::read_to_string; use std::str::FromStr; + use tokio; +use scraper::{Html, Selector}; + struct Car { class: String, rq: u8, diff --git a/src/records.rs b/src/records.rs new file mode 100644 index 0000000..e3563dc --- /dev/null +++ b/src/records.rs @@ -0,0 +1,401 @@ +use reqwest::get; +use rusqlite::{params, Connection}; +use serde_json::Value; +use std::env; +use std::fs; +#[tokio::main] +async fn main() { + let args: Vec<_> = env::args().collect(); + + match args.get(1) { + None => { + println!("Please provide a command.") + } + Some(argument) => { + let command = argument.as_str(); + match command { + "init" => { + get_cars_file().await; + get_tracks_file().await; + init_db(); + parse_cars(); + } + "fetch" => { + fetch_times().await; + } + "parse" => { + parse_all_times().await; + } + "analyze" => { + list_best_cars().await; + } + &_ => { + println!("Unknown command '{}'.", argument) + } + } + } + } +} +async fn list_best_cars() { + todo!() +} +async fn parse_car_times(rid: String, connection: &Connection) { + let file_name = rid.to_owned() + ".json"; + let local_file_path = "times/".to_owned() + file_name.as_str(); + let file_content = fs::read_to_string(local_file_path); + + let json_data: Value = + serde_json::from_str(&file_content.unwrap_or(String::default())).unwrap_or(Value::Null); + + if let Some(configurations) = json_data["data"].as_object() { + for (config, entry) in configurations { + if let Some(times) = entry["times"].as_object() { + for (track_id, details) in times { + // Remove postfix from track ID + if let Some((cleaned_track_id, track_type)) = + split_prefix_postfix(track_id, "_a") + { + // Extract time and configuration + let mut time = details["t"].as_f64().unwrap_or(0.0); + if track_id.starts_with("testBowl") { + time = 2000.0 / time; + } + + // Insert into the database + if time > 0.0 { + let _ = connection.execute( + "INSERT INTO car_track_times (car_rid, track_id, track_type, time, configuration) + VALUES (?1, ?2, ?3, ?4, ?5)", + params![rid, translate_track_name(cleaned_track_id), translate_track_type(track_type), time, config], + ); + } + } + } + } + } + } + println!("Track times imported for {}", rid); +} +async fn parse_all_times() { + let connection = Connection::open("top_drives.db").expect("Issue opening DB file!"); + for car in get_cars_from_db() { + parse_car_times(car, &connection).await; + } +} +fn parse_cars() { + let connection = Connection::open("top_drives.db").expect("Issue opening DB file!"); + + let cars_file = "cars_repo.json"; + let cars_data: Vec = + serde_json::from_str(&fs::read_to_string(cars_file).unwrap()).unwrap(); + + for car in cars_data { + let _ = connection.execute( + "INSERT OR IGNORE INTO cars + (class, rq, onlyName, brand, year, abs, tcs, clearance, country, topSpeed, acel, hand, + drive, tyres, mra, weight, name, rid, prize, bodyTypes, fuel, seats, engine, tags, guid, brake) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26)", + params![ + car["class"].as_str(), + car["rq"].as_i64(), + car["onlyName"].as_str(), + car["brand"].as_str(), + car["year"].as_i64(), + car["abs"].as_bool(), + car["tcs"].as_bool(), + car["clearance"].as_str(), + car["country"].as_str(), + car["topSpeed"].as_i64(), + car["acel"].as_f64(), + car["hand"].as_i64(), + car["drive"].as_str(), + car["tyres"].as_str(), + car["mra"].as_f64(), + car["weight"].as_i64(), + car["name"].as_str(), + car["rid"].as_str(), + car["prize"].as_bool(), + serde_json::to_string(&car["bodyTypes"]).unwrap(), + car["fuel"].as_str(), + car["seats"].as_str(), + car["engine"].as_str(), + serde_json::to_string(&car["tags"]).unwrap(), + car["guid"].as_str(), + car["brake"].as_str(), + ], + ); + } +} +fn split_prefix_postfix<'a>(input: &'a str, delimiter: &'a str) -> Option<(&'a str, &'a str)> { + input.find(delimiter).map(|index| { + let prefix = &input[..index]; + let postfix = &input[index + delimiter.len()..]; + (prefix, postfix) + }) +} +async fn fetch_times() { + for car in get_cars_from_db() { + fetch_car(car).await; + } +} +async fn fetch_car(rid: String) { + let file_name = rid.to_owned() + ".json"; + let url = "https://api.topdrivesrecords.com/car/".to_owned() + rid.as_str(); + println!("Fetching {}.", url); + let local_file_path = "times/".to_owned() + file_name.as_str(); + get_file(url.as_str(), local_file_path.as_str()).await; +} +fn get_cars_from_db() -> Vec { + let mut car_list = Vec::new(); + + let connection = Connection::open("top_drives.db").expect("Issue opening DB file!"); + + let mut stmt = connection + .prepare("SELECT rid FROM cars") + .expect("Could not prepare query!"); + let mut rows = stmt.query([]).expect("Could not execute query!"); + + while let Ok(Some(row)) = rows.next() { + car_list.push(row.get::(0).expect("Could not get column!")); + } + + car_list +} +fn init_db() { + let _ = fs::remove_file("top_drives.db"); + let connection = Connection::open("top_drives.db").expect("Issue opening DB file!"); + + let _ = connection.execute( + "CREATE TABLE IF NOT EXISTS cars ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + class TEXT, + rq INTEGER, + onlyName TEXT, + brand TEXT, + year INTEGER, + abs BOOLEAN, + tcs BOOLEAN, + clearance TEXT, + country TEXT, + topSpeed INTEGER, + acel REAL, + hand INTEGER, + drive TEXT, + tyres TEXT, + mra REAL, + weight INTEGER, + name TEXT, + rid TEXT UNIQUE, + prize BOOLEAN, + bodyTypes TEXT, + fuel TEXT, + seats TEXT, + engine TEXT, + tags TEXT, + guid TEXT UNIQUE, + brake TEXT, + owned BOOLEAN + );", + [], + ); + + let _ = connection.execute( + "CREATE TABLE IF NOT EXISTS car_track_times ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + car_rid TEXT, + configuration TEXT, + track_id TEXT, + track_type INTEGER, + time DECIMAL, + FOREIGN KEY (car_rid) REFERENCES cars(rid), + UNIQUE(car_rid, track_id, track_type, configuration) + );", + [], + ); +} +async fn get_tracks_file() { + get_file("https://raw.githubusercontent.com/TiagoXavi/topdrives-records/refs/heads/main/src/database/tracks_repo.json","tracks_repo.json").await; +} +async fn get_cars_file() { + get_file("https://github.com/TiagoXavi/topdrives-records/raw/refs/heads/main/src/database/cars_final_PL24_2-fixed.json", "cars_repo.json").await; +} +async fn get_file(file_url: &str, local_file_path: &str) { + let _ = fs::remove_file(local_file_path); + match get(file_url).await { + Ok(response) => { + fs::write(local_file_path, response.text().await.unwrap()) + .expect("Unable to write file!"); + } + Err(error) => { + println!("Failed to fetch the file. Status: {}", error.to_string()); + } + } +} +fn translate_track_type(track_code: &str) -> &str { + match track_code { + "00" => "Dry", + "01" => "Wet", + "10" => "Dirt", + "11" => "Dirt Wet", + "20" => "Gravel", + "30" => "Ice", + "40" => "Asphalt Dirt", + "41" => "Asphalt Dirt Wet", + "50" => "Sand", + "60" => "Snow", + "70" => "Grass", + "71" => "Grass Wet", + "b0" => "Asphalt Gravel", + "c0" => "Asphalt Sand", + "c1" => "Asphalt Sand Wet", + "d0" => "Asphalt Snow", + "e0" => "Sand Dirt", + "f0" => "Asphalt Grass Dirt", + "g0" => "Ice Snow", + "h1" => "Snow Dirt Wet", + _ => track_code, + } +} +fn translate_track_name(track_code: &str) -> &str { + match track_code { + "drag100b" => "0-100-0mph", + "drag100" => "0-100mph", + "drag120" => "0-120mph", + "drag124" => "0-124mph", + "drag150b" => "0-150-0mph", + "drag150" => "0-150mph", + "drag170" => "0-170mph", + "drag200" => "0-200mph", + "drag60b" => "0-60-0mph", + "drag60" => "0-60mph", + "drag62" => "0-62mph", + "mile1" => "1 Mile Drag", + "mile1r" => "1 Mile Drag R", + "mile2" => "1/2 Mile Drag", + "mile4" => "1/4 Mile Drag", + "mile4r" => "1/4 Mile Drag R", + "drag100150" => "100-150mph", + "drag30130" => "30-130mph", + "drag50150" => "50-150mph", + "airplaneHangars" => "Airplane Hangars", + "airplaneSlalom" => "Airplane Slalom", + "smallRoad" => "Back Road", + "butte" => "Butte", + "canyonButte" => "Canyon Butte", + "lookout" => "Lookout", + "canyonLookout" => "Canyon Lookout", + "carPark" => "Car Park", + "townSlalom" => "City Slalom", + "townSprint" => "City Sprint", + "csMed" => "City Streets Medium", + "csSmall" => "City Streets Small", + "townTour" => "City Tour", + "cross" => "Cross-Country", + "dealsGap" => "Deals Gap", + "dealsGap1" => "Deals Gap Relay 1", + "dealsGap2" => "Deals Gap Relay 2", + "dealsGap3" => "Deals Gap Relay 3", + "dealsGap4" => "Deals Gap Relay 4", + "dealsGap5" => "Deals Gap Relay 5", + "dealsGapBack" => "Deals Gap Backroads", + "desertBig" => "Desert Rally Big", + "desertHill" => "Desert Rally Hill", + "desertRallyDirt" => "Desert Rally Dirt", + "desertSmall" => "Desert Rally Small", + "dockAltCirc" => "Dockyard Alternative Circuit", + "dockCirc" => "Dockyard Circuit", + "dockCity" => "Dockyard City Streets", + "dockGforceDrag" => "Dockyard G-Force Drag", + "dockKart" => "Dockyard Karting", + "dockUdrag" => "Dockyard U-Drag", + "dtRoad" => "Dirt Road", + "canyonDtRoad" => "Canyon Dirt Road", + "fast" => "Fast Circuit", + "fastr" => "Fast Circuit R", + "fieldSlalom" => "Field Slalom", + "figureEight" => "Figure Of Eight", + "forestRiver" => "Forest River Crossing", + "forest" => "Forest Road", + "forestSlalom" => "Forest Slalom", + "frozenLake" => "Frozen Lake", + "frozenRivSla" => "Frozen River Slalom", + "gForce" => "G-Force Test", + "gForcer" => "G-Force Test R", + "hairpin" => "Hairpin Road", + "hClimb" => "Hill Climb", + "hClimbr" => "Hill Climb R", + "indoorKart" => "Indoor Karting", + "kart" => "Karting Circuit", + "draglshape" => "L-Shape Drag", + "laguna" => "Laguna Seca", + "lumberForest" => "Lumber Mill Forest Rally", + "lumberRiver" => "Lumber Mill River Rally", + "lumberTwisty" => "Lumber Mill Twisty Circuit", + "miCircuit" => "Miami Circuit", + "miStreets1" => "Miami Streets 1", + "miStreets2" => "Miami Streets 2", + "miUdrag" => "Miami U-Drag", + "miCause" => "Miami Causeway", + "miCauseDrag" => "Miami Causeway Drag", + "miBridge" => "Miami Bridge", + "miFigure" => "Miami Figure Of Eight", + "miSlalom" => "Miami Slalom", + "miGforce" => "Miami G-Force", + "miBeachDrag" => "Miami Beach Drag", + "mnGforce" => "Monaco G-Force Test", + "mnHairpin" => "Monaco Hairpin", + "mnCityNarrow" => "Monaco Narrow Streets", + "mnCity" => "Monaco Streets", + "mnCityLong" => "Monaco Streets Long", + "moto" => "Motocross Track", + "mtHairpin" => "Mountain Hairpin", + "mtHill" => "Mountain Hill Climb", + "mtIncline" => "Mountain Incline Road", + "mtSlalom" => "Mountain Slalom", + "mtTour" => "Mountain Tour", + "mtTwisty" => "Mountain Twisty Road", + "nwCircuit" => "New York Circuit", + "nwDrag" => "New York Drag", + "nwGforce" => "New York G-Force", + "nwLoop" => "New York Times Square Loop", + "nwCathedral" => "New York Cathedral", + "nwTour" => "New York Central Park Tour", + "nwSlalom" => "New York Train Slalom", + "northloop" => "North Loop", + "northloop1" => "North Loop Relay 1", + "northloop2" => "North Loop Relay 2", + "northloop3" => "North Loop Relay 3", + "northloop4" => "North Loop Relay 4", + "northloop5" => "North Loop Relay 5", + "oceanSlalom" => "Ocean Beach Slalom", + "oceanCity" => "Ocean City Streets", + "oceanHighway" => "Ocean Highway", + "oceanLongDrag" => "Ocean Long Drag", + "oceanParking" => "Ocean Parking Lot", + "oceanShortDrag" => "Ocean Short Drag", + "rallyMed" => "Rallycross Medium Circuit", + "rallySmall" => "Rallycross Small Circuit", + "riverSprint" => "River Sprint", + "runwayDrag" => "Runway Drag", + "runwayDragSlalom" => "Runway Drag & Slalom", + "runwayLoop" => "Runway Loop", + "serviceRoads" => "Service Roads", + "slalom" => "Slalom Test", + "slalomr" => "Slalom Test R", + "speedbump12km" => "Speedbump 1 km", + "speedbump1km" => "Speedbump 1/2 km", + "testBowl" => "Test Bowl", + "testBowlr" => "Test Bowl R", + "tokyoBridge" => "Tokyo Bridge", + "tokyoDrag" => "Tokyo Drag", + "tokyoGforce" => "Tokyo G-Force Test", + "tokyoLoop" => "Tokyo Loop", + "tokyoOffRamp" => "Tokyo Off-Ramp", + "tokyoOverpass" => "Tokyo Overpass", + "tCircuit" => "Twisty Circuit", + "tCircuitr" => "Twisty Circuit R", + "tRoad" => "Twisty Road", + "waterDrag" => "Waterfront Drag", + _ => track_code, + } +}