Update to TD Records and new cars

This commit is contained in:
Jan-Peter von Hunnius 2025-02-13 08:18:05 +01:00
parent 5783bb4826
commit 2fa2966aaa
7 changed files with 526 additions and 1 deletions

401
src/records.rs Normal file
View file

@ -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<Value> =
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<String> {
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::<usize, String>(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,
}
}