Get rid of DB, parse into memory

This commit is contained in:
Jan-Peter von Hunnius 2025-03-31 12:15:01 +02:00
parent 12a3868a46
commit 1a6ea07ed9
5 changed files with 118 additions and 455 deletions

View file

@ -15,7 +15,6 @@ path = "src/records.rs"
scraper = "0.23.1" scraper = "0.23.1"
reqwest = "0.12.15" reqwest = "0.12.15"
tokio = { version = "1.44.1", features = ["full"] } tokio = { version = "1.44.1", features = ["full"] }
rusqlite = { version = "0.32.1", features = ["bundled", "backup"] }
serde_json = "1.0.140" serde_json = "1.0.140"
futures = "0.3.31" futures = "0.3.31"
once_cell = "1.20.2" once_cell = "1.20.2"

View file

@ -1,8 +0,0 @@
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;

View file

@ -1,48 +0,0 @@
WITH RECURSIVE rq_range AS (
SELECT DISTINCT rq from cars
),
track_combinations AS (
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;

View file

@ -1,51 +0,0 @@
WITH RECURSIVE rq_range AS (
SELECT DISTINCT rq from cars
),
track_combinations AS (
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;

View file

@ -1,14 +1,13 @@
use std::collections::HashMap; use futures::future::join_all;
use once_cell::sync::Lazy;
use reqwest::get; use reqwest::get;
use rusqlite::backup::Backup;
use rusqlite::{params, CachedStatement, Connection};
use serde_json::Value; use serde_json::Value;
use std::collections::HashMap;
use std::env; use std::env;
use std::fs; use std::fs;
use once_cell::sync::Lazy;
use std::sync::Mutex; use std::sync::Mutex;
use futures::future::{join_all};
use tokio::sync::Semaphore; use tokio::sync::Semaphore;
use tokio::task;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct Car { struct Car {
@ -17,20 +16,20 @@ struct Car {
only_name: String, only_name: String,
year: u16, year: u16,
rq: u8, rq: u8,
drive: String,
tyres: String,
top_speed: u32,
acel: f64,
hand: u32,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct BestCarRecord { struct BestCarRecord {
car_rid: String, car_rid: String,
configuration: String, brand: String,
only_name: String,
year: u16,
rq: u8,
config: String,
time: f64, time: f64,
} }
type TripletKey = (String, u8, String); // (Track ID, RQ, Surface) type TripletKey = (String, u8, String); // (Track ID, RQ, Surface)
static BEST_CARS: Lazy<Mutex<HashMap<TripletKey, BestCarRecord>>> = Lazy::new(|| Mutex::new(HashMap::new())); static BEST_CARS: Lazy<Mutex<HashMap<TripletKey, BestCarRecord>>> =
Lazy::new(|| Mutex::new(HashMap::new()));
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
@ -43,62 +42,24 @@ async fn main() {
Some(argument) => { Some(argument) => {
let command = argument.as_str(); let command = argument.as_str();
match command { match command {
"init" => {
let mut connection =
Connection::open_in_memory().expect("Issue opening in-memory DB!");
init_db(&mut connection);
parse_cars(&mut connection);
let _ = fs::remove_file("top_drives.db");
let mut file =
Connection::open("top_drives.db").expect("Issue opening DB file!");
let backup =
Backup::new(&connection, &mut file).expect("Could not create backup!");
backup.step(-1).expect("Could not create backup!"); // -1 copies all pages at once
drop(backup);
connection.close().unwrap();
file.close().unwrap();
}
"fetch" => { "fetch" => {
get_cars_file().await; get_cars_file().await;
get_tracks_file().await; get_tracks_file().await;
fetch_times().await; fetch_times().await;
} }
"parse" => {
let mut file =
Connection::open("top_drives.db").expect("Issue opening DB file!");
let mut connection =
Connection::open_in_memory().expect("Issue opening in-memory DB!");
let backup =
Backup::new(&file, &mut connection).expect("Could not create backup!");
backup.step(-1).expect("Could not create backup!"); // -1 copies all pages at once
drop(backup);
file.close().unwrap();
parse_all_times(&mut connection).await;
let _ = fs::remove_file("top_drives.db");
file = Connection::open("top_drives.db").expect("Issue opening DB file!");
let backup =
Backup::new(&connection, &mut file).expect("Could not create backup!");
backup.step(-1).expect("Could not create backup!"); // -1 copies all pages at once
drop(backup);
connection.close().unwrap();
file.close().unwrap();
}
"best" => { "best" => {
list_best_cars().await; parse_all_times().await;
print_best_cars();
} }
"details" => { "details" => {
list_details().await; parse_all_times().await;
print_best_times();
} }
"track" => { "track" => {
if args.get(4).is_none() { if args.get(4).is_none() {
println!("Track name, condition and RQ limit required."); println!("Track name, condition and RQ limit required.");
} else { } else {
parse_all_times().await;
let track = args.get(2).unwrap(); let track = args.get(2).unwrap();
let condition = args.get(3).unwrap(); let condition = args.get(3).unwrap();
let rq = args.get(4).unwrap(); let rq = args.get(4).unwrap();
@ -112,182 +73,72 @@ async fn main() {
} }
} }
} }
fn print_best_times() {
let best_cars = BEST_CARS.lock().unwrap();
fn print_best_car(track: &String, condition: &String, rq: &String) { // Collect keys and sort them by RQ and Track ID
let connection = Connection::open("top_drives.db").expect("Issue opening DB file!"); let mut sorted_keys: Vec<_> = best_cars.keys().collect();
sorted_keys.sort_by(|a, b| a.1.cmp(&b.1).then(a.0.cmp(&b.0))); // Sort by RQ, then Track ID
let mut stmt = connection for key in sorted_keys {
.prepare( let best_car = best_cars.get(key).unwrap();
r#"
SELECT rq, brand, onlyName, year, configuration, time
FROM car_track_times t
JOIN cars c ON t.car_rid = c.rid
WHERE track_id = ?
AND track_type = ?
AND rq <= ?
ORDER BY CAST(t.time AS DECIMAL) ASC
LIMIT 1;"#
).expect("Could not prepare query!");
let mut rows = stmt.query(params!(track, condition, rq)).expect("Could not execute query!");
while let Ok(Some(row)) = rows.next() {
let rq :u8 = row.get(0).expect("Cannot extract RQ");
let brand: String = row.get(1).expect("Cannot extract RQ");
let only_name: String = row.get(2).expect("Cannot extract RQ");
let year: u16 = row.get(3).expect("Cannot extract RQ");
let configuration: String = row.get(4).expect("Cannot extract RQ");
let time: f32 = row.get(5).expect("Cannot extract RQ");
println!("{} {} {} {} ({}): {}", rq, brand, only_name, year, configuration, time );
}
}
async fn list_details() {
let file = Connection::open("top_drives.db").expect("Issue opening DB file!");
let mut connection = Connection::open_in_memory().expect("Issue opening in-memory DB!");
let backup = Backup::new(&file, &mut connection).expect("Could not create backup!");
backup.step(-1).expect("Could not create backup!"); // -1 copies all pages at once
drop(backup);
drop(file);
let stmt = connection.prepare(
r#"
WITH RECURSIVE rq_range AS (
SELECT DISTINCT rq from cars
),
track_combinations AS (
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 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
ORDER BY max_rq, track_id, track_type
"#,
);
let mut result = stmt.expect("Query failed!");
let rows = result
.query_map([], |row| {
Ok((
row.get::<_, i32>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
row.get::<_, i32>(3)?,
row.get::<_, String>(4)?,
row.get::<_, String>(5)?,
row.get::<_, i32>(6)?,
row.get::<_, String>(7)?,
row.get::<_, f64>(8)?,
))
})
.expect("Result retrieval failed!");
for row in rows {
let (max_rq, track_id, track_type, rq, brand, only_name, year, configuration, best_time) =
row.expect("Row retrieval failed!");
println!( println!(
"{},{},{},{},{},{},{},{},{}", "{},{},{},{},{},{},{},{},{}",
max_rq, track_id, track_type, rq, brand, only_name, year, configuration, best_time key.1,
key.0,
key.2,
best_car.rq,
best_car.brand,
best_car.only_name,
best_car.year,
best_car.config,
best_car.time
); );
} }
drop(result);
connection.close().unwrap();
} }
fn print_best_cars() {
let best_cars = BEST_CARS.lock().unwrap();
let mut car_count: HashMap<String, (usize, u8, BestCarRecord)> = HashMap::new();
async fn list_best_cars() { // Count occurrences and store RQ
let file = Connection::open("top_drives.db").expect("Issue opening DB file!"); for ((_, rq, _), best_car) in best_cars.iter() {
let mut connection = Connection::open_in_memory().expect("Issue opening in-memory DB!"); let entry = car_count
.entry(best_car.car_rid.clone())
let backup = Backup::new(&file, &mut connection).expect("Could not create backup!"); .or_insert((0, *rq, best_car.clone()));
backup.step(-1).expect("Could not create backup!"); // -1 copies all pages at once entry.0 += 1;
drop(backup);
drop(file);
let stmt = connection.prepare(
r#"
WITH RECURSIVE rq_range AS (
SELECT DISTINCT rq from cars
),
track_combinations AS (
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 AND t.time IS NOT NULL AND c.rq <= rq_range.rq
)
SELECT rq, brand, onlyName, year, COUNT(time) AS occurrences
FROM ranked_cars
WHERE rank = 1
GROUP BY brand, onlyName, year
ORDER BY rq, brand, onlyName, year
"#,
);
let mut result = stmt.expect("Query failed!");
let rows = result
.query_map([], |row| {
Ok((
row.get::<_, i32>(0).expect("Could not assign value"),
row.get::<_, String>(1).expect("Could not assign value"),
row.get::<_, String>(2).expect("Could not assign value"),
row.get::<_, i32>(3).expect("Could not assign value"),
row.get::<_, i32>(4).expect("Could not assign value"),
))
})
.expect("Result retrieval failed!");
for row in rows {
let (rq, brand, only_name, year, occurrences) = row.expect("Row retrieval failed!");
println!("{},{},{},{},{}", rq, brand, only_name, year, occurrences);
} }
drop(result); // Convert to vector and sort by RQ first, then frequency
connection.close().unwrap(); let mut car_count_vec: Vec<_> = car_count.iter().collect();
car_count_vec.sort_by(|a, b| a.1 .1.cmp(&b.1 .1).then(a.0.cmp(b.0)));
for (_car_rid, (count, rq, car_record)) in car_count_vec {
println!(
"{},{},{},{},{}",
rq, car_record.brand, car_record.only_name, car_record.year, count
);
}
}
fn print_best_car(track: &String, condition: &String, rq: &String) {
let best_cars = BEST_CARS.lock().unwrap();
let key = (track.clone(), rq.parse::<u8>().unwrap_or(115), condition.clone());
if let Some(best_car) = best_cars.get(&key) {
println!(
"Best car for Track: {}, Condition: {}, RQ: {} -> {} {} ({}) with time {:.2}",
track, condition, rq,
best_car.brand, best_car.only_name, best_car.year, best_car.time
);
} else {
println!(
"No best car found for Track: {}, Condition: {}, RQ: {}",
track, condition, rq
);
}
} }
async fn parse_car_times(rid: String, statement: &mut CachedStatement<'_>) { async fn parse_car_times(car: Car) {
let file_name = rid.to_owned() + ".json"; let file_name = car.rid.to_owned() + ".json";
let local_file_path = "times/".to_owned() + file_name.as_str(); let local_file_path = "times/".to_owned() + file_name.as_str();
let file_content = fs::read_to_string(local_file_path); let file_content = fs::read_to_string(local_file_path);
@ -317,86 +168,53 @@ async fn parse_car_times(rid: String, statement: &mut CachedStatement<'_>) {
// Insert into the database // Insert into the database
if time > 1.2 && no_down_votes { if time > 1.2 && no_down_votes {
let _ = statement.execute([ update_best_car(
rid.as_str(),
translate_track_name(cleaned_track_id), translate_track_name(cleaned_track_id),
car.rq,
translate_track_type(track_type), translate_track_type(track_type),
time.to_string().as_str(), BestCarRecord {
config, car_rid: car.rid.clone(),
]); brand: car.brand.clone(),
//TODO: update_best_car(cleaned_track_id, translate_track_type(track_type), None) only_name: car.only_name.clone(),
year: car.year,
rq: car.rq,
config: config.clone(),
time,
},
);
} }
} }
} }
} }
} }
} }
println!("Track times imported for {}", rid);
} }
fn update_best_car(track_id: &str, rq: u8, surface: &str, new_record: BestCarRecord) { fn update_best_car(track_id: &str, car_rq: u8, surface: &str, new_record: BestCarRecord) {
let key = (track_id.to_string(), rq, surface.to_string());
let mut best_cars = BEST_CARS.lock().unwrap(); let mut best_cars = BEST_CARS.lock().unwrap();
if let Some(existing) = best_cars.get(&key) { for rq in car_rq..116 {
if new_record.time < existing.time { let key = (track_id.to_string(), rq, surface.to_string());
best_cars.insert(key, new_record);
if let Some(existing) = best_cars.get(&key) {
if new_record.time < existing.time {
best_cars.insert(key, new_record.clone());
}
} else {
best_cars.insert(key, new_record.clone());
} }
} else {
best_cars.insert(key, new_record);
} }
} }
async fn parse_all_times(connection: &mut Connection) { async fn parse_all_times() {
let mut statement = connection.prepare_cached("INSERT INTO car_track_times (car_rid, track_id, track_type, time, configuration) VALUES (?1, ?2, ?3, ?4, ?5)").expect("Could not prepare statement!"); let mut handles = vec![];
for car in get_cars_from_db() { for car in get_car_list() {
parse_car_times(car, &mut statement).await; let handle = task::spawn(async move {
parse_car_times(car).await;
});
handles.push(handle);
} }
connection.flush_prepared_statement_cache(); join_all(handles).await;
let _ = connection.execute("DELETE FROM car_track_times WHERE time < 1.2;", ());
}
fn parse_cars(connection: &mut Connection) {
println!("Parsing cars into DB...");
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 {
if let Err(e) = 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().unwrap(),
car["rq"].as_u64().unwrap(),
car["onlyName"].as_str().unwrap(),
car["brand"].as_str().unwrap(),
car["year"].as_u64().unwrap(),
car["abs"].as_bool().unwrap(),
car["tcs"].as_bool().unwrap(),
car["clearance"].as_str().unwrap(),
car["country"].as_str().unwrap(),
car["topSpeed"].as_u64().unwrap(),
car["acel"].as_f64().unwrap_or_default(),
car["hand"].as_u64().unwrap(),
car["drive"].as_str().unwrap(),
car["tyres"].as_str().unwrap(),
car["mra"].as_f64().unwrap_or_default(),
car["weight"].as_u64().unwrap(),
car["name"].as_str().unwrap(),
car["rid"].as_str().unwrap(),
car["prize"].as_bool().unwrap(),
Some(serde_json::to_string(&car["bodyTypes"]).unwrap().as_str()).unwrap_or_default(),
car["fuel"].as_str().unwrap(),
car["seats"].as_u64().unwrap_or_default(),
car["engine"].as_str().unwrap(),
Some(serde_json::to_string(&car["tags"]).unwrap().as_str()).unwrap_or_default(),
car["guid"].as_str().unwrap(),
car["brake"].as_str().unwrap(),
]) {
eprintln!("Insertion error: {}", e);
};
}
} }
fn split_prefix_postfix<'a>(input: &'a str, delimiter: &'a str) -> Option<(&'a str, &'a str)> { fn split_prefix_postfix<'a>(input: &'a str, delimiter: &'a str) -> Option<(&'a str, &'a str)> {
input.find(delimiter).map(|index| { input.find(delimiter).map(|index| {
@ -406,13 +224,13 @@ fn split_prefix_postfix<'a>(input: &'a str, delimiter: &'a str) -> Option<(&'a s
}) })
} }
async fn fetch_times() { async fn fetch_times() {
let semaphore = std::sync::Arc::new(Semaphore::new(20)); // Adjust the number of concurrent requests let semaphore = std::sync::Arc::new(Semaphore::new(20));
let mut handles = vec![]; let mut handles = vec![];
for car in get_cars_from_db() { for car in get_car_list() {
let permit = semaphore.clone().acquire_owned().await.unwrap(); let permit = semaphore.clone().acquire_owned().await.unwrap();
handles.push(tokio::spawn(async move { handles.push(tokio::spawn(async move {
fetch_car(car).await; fetch_car(car.rid).await;
drop(permit); drop(permit);
})); }));
} }
@ -426,80 +244,34 @@ async fn fetch_car(rid: String) {
println!("Fetching {}.", url); println!("Fetching {}.", url);
get_file(url.as_str(), local_file_path.as_str()).await; get_file(url.as_str(), local_file_path.as_str()).await;
} }
fn get_cars_from_db() -> Vec<String> { fn get_car_list() -> Vec<Car> {
let mut car_list = Vec::new(); let mut car_list: Vec<Car> = Vec::new();
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();
let mut stmt = connection for car in cars_data {
.prepare("SELECT rid FROM cars") let new_car = Car {
.expect("Could not prepare query!"); rid: String::from(car["rid"].as_str().unwrap_or("No RID")),
let mut rows = stmt.query([]).expect("Could not execute query!"); brand: String::from(car["brand"].as_str().unwrap_or("No Brand")),
only_name: String::from(car["onlyName"].as_str().unwrap_or("No Name")),
year: car["year"].as_u64().unwrap_or(1900) as u16,
rq: car["rq"].as_u64().unwrap_or(200) as u8,
};
while let Ok(Some(row)) = rows.next() { car_list.push(new_car);
car_list.push(row.get::<usize, String>(0).expect("Could not get column!"));
} }
car_list car_list
} }
fn init_db(connection: &mut Connection) {
println!("Initializing DB...");
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() { async fn get_tracks_file() {
println!("Fetching tracks file..."); println!("Fetching tracks file...");
get_file("https://github.com/TiagoXavi/topdrives-records/raw/refs/heads/main/src/database/tracks_repo.json","tracks_repo.json").await; get_file("https://github.com/TiagoXavi/topdrives-records/raw/refs/heads/main/src/database/tracks_repo.json","tracks_repo.json").await;
} }
async fn get_cars_file() { async fn get_cars_file() {
println!("Fetching cars file..."); println!("Fetching cars file...");
get_file("https://github.com/TiagoXavi/topdrives-records/raw/refs/heads/main/src/database/cars_final_PL25-fixed.json", "cars_repo.json").await; get_file("https://github.com/TiagoXavi/topdrives-records/raw/refs/heads/main/src/database/cars_final.json", "cars_repo.json").await;
} }
async fn get_file(file_url: &str, local_file_path: &str) { async fn get_file(file_url: &str, local_file_path: &str) {
let _ = fs::remove_file(local_file_path); let _ = fs::remove_file(local_file_path);
@ -627,7 +399,6 @@ fn translate_track_name(track_code: &str) -> &str {
"miBeachDrag" => "Miami Beach Drag", "miBeachDrag" => "Miami Beach Drag",
"mnGforce" => "Monaco G-Force Test", "mnGforce" => "Monaco G-Force Test",
"mnHairpin" => "Monaco Hairpin", "mnHairpin" => "Monaco Hairpin",
// "mnNarrowStreets" => "Monaco Narrow Streets",
"mnCityNarrow" => "Monaco Narrow Streets", "mnCityNarrow" => "Monaco Narrow Streets",
"mnCity" => "Monaco Streets", "mnCity" => "Monaco Streets",
"mnCityLong" => "Monaco Streets Long", "mnCityLong" => "Monaco Streets Long",