const axios = require("axios") const _ = require("lodash") const fs = require("fs") const schedule = require("node-schedule") const express = require("express") const favicon = require("serve-favicon") const morgan = require("morgan") const dayjs = require("dayjs") const process = require("process") const driversPath = "./assets/drivers.json" const statsPath = "./assets/stats.json" const flag = { "British": "gb", "Spanish": "es", "Polish": "pl", "Japanese": "jp", "Mexican": "mx", "Australian": "au", "Russian": "ru", "Dutch": "nl", "Belgian": "be", "Canadian": "ca", "New Zealander": "nz", "Thai": "th", "Finnish": "fi", "Brazilian": "br", "German": "de", "French": "fr", "Venezuelan": "ve", "Danish": "dk", "Swedish": "se", "American": "us", "Indonesian": "id", "Italian": "it", "Monegasque": "mc", "Chinese": "cn", "Argentine": "ar", "Andorran": "ad", "Emirati": "ae", "Afghan": "af", "Antiguan": "ag", "Anguillan": "ai", "Albanian": "al", "Armenian": "am", "Angolan": "ao", "Azerbaijani": "az", "Austrian": "at", "Bahamian": "bs", "Bangladeshi": "bd", "Barbadian": "bb", "Beninese": "bj", "Bhutanese": "bt", "Botswanan": "bw", "Bulgarian": "bg", "Burkinabé": "bf", "Burundian": "bi", "Cambodian": "kh", "Cameroonian": "cm", "Cape Verdean": "cv", "Chadian": "td", "Chilean": "cl", "Colombian": "co", "Costa Rican": "cr", "Croatian": "hr", "Cuban": "cu", "Cypriot": "cy", "Czech": "cz", "Dominican": "do", "Ecuadorian": "ec", "Egyptian": "eg", "Salvadoran": "sv", "Equatorial Guinean": "gq", "Eritrean": "er", "Estonian": "ee", "Ethiopian": "et", "Fijian": "fj", "Gabonese": "ga", "Gambian": "gm", "Georgian": "ge", "Ghanaian": "gh", "Gibraltarian": "gi", "Greek": "gr", "Grenadian": "gd", "Guatemalan": "gt", "Guinean": "gn", "Guyanese": "gy", "Haitian": "ht", "Honduran": "hn", "Hong Konger": "hk", "Hungarian": "hu", "Icelandic": "is", "Indian": "in", "Iranian": "ir", "Iraqi": "iq", "Israeli": "il", "Jamaican": "jm", "Jordanian": "jo", "Kazakh": "kz", "Kenyan": "ke", "North Korean": "kp", "South Korean": "kr", "Kuwaiti": "kw", "Kyrgyz": "kg", "Laotian": "la", "Latvian": "lv", "Lebanese": "lb", "Liberian": "lr", "Libyan": "ly", "Liechtensteiner": "li", "Lithuanian": "lt", "Luxembourgish": "lu", "Macedonian": "mk", "Malagasy": "mg", "Malawian": "mw", "Malaysian": "my", "Malian": "ml", "Maltese": "mt", "Marshallese": "mh", "Mauritanian": "mr", "Mauritian": "mu", "Micronesian": "fm", "Moldovan": "md", "Mongolian": "mn", "Montenegrin": "me", "Moroccan": "ma", "Mozambican": "mz", "Namibian": "na", "Nepalese": "np", "Nicaraguan": "ni", "Nigerien": "ne", "Nigerian": "ng", "Norwegian": "no", "Omani": "om", "Pakistani": "pk", "Palauan": "pw", "Panamanian": "pa", "Papua New Guinean": "pg", "Paraguayan": "py", "Peruvian": "pe", "Filipino": "ph", "Portuguese": "pt", "Qatari": "qa", "Romanian": "ro", "Rwandan": "rw", "Saint Lucian": "lc", "Saint Vincentian": "vc", "Samoan": "ws", "San Marinese": "sm", "Saudi": "sa", "Senegalese": "sn", "Serbian": "rs", "Seychellois": "sc", "Sierra Leonean": "sl", "Singaporean": "sg", "Slovak": "sk", "Slovenian": "si", "Solomon Islander": "sb", "Somali": "so", "South African": "za", "South Sudanese": "ss", "Sri Lankan": "lk", "Sudanese": "sd", "Surinamese": "sr", "Syrian": "sy", "Taiwanese": "tw", "Tajikistani": "tj", "Tanzanian": "tz", "Togolese": "tg", "Tongan": "to", "Trinidadian": "tt", "Tunisian": "tn", "Turkish": "tr", "Turkmen": "tm", "Tuvaluan": "tv", "Ugandan": "ug", "Ukrainian": "ua", "Uruguayan": "uy", "Uzbekistani": "uz", "Vanuatuan": "vu", "Vietnamese": "vn", "Yemeni": "ye", "Zambian": "zm", "Zimbabwean": "zw" } function team(teamName, year) { switch(teamName) { case "McLaren": return "mclaren" break case "Alpine F1 Team": return "alpine" break case "Mercedes": return "mercedes" break case "Sauber": if (year < 2024) { return "sauber" } else { return "kick" } break case "Haas F1 Team": return "haas" break case "Lotus F1": return "lotus" break case "Marussia": return "marussia" break case "Manor Marussia": return "marussia" break case "Renault": return "renault" break case "Alfa Romeo": return "alfa" break case "Williams": return "williams" break case "Aston Martin": return "aston" break case "Caterham": return "caterham" break case "Red Bull": return "red" break case "Toro Rosso": return "toro" break case "AlphaTauri": return "alpha" break case "Ferrari": return "ferrari" break case "RB F1 Team": return "rb" break } } let stats = { "visits": 0, "guesses": 0 } let drivers = {} let driver let year = new Date().getFullYear() function sleep(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); }); } axios.get("https://api.jolpi.ca/ergast/f1/1950/driverStandings.json?limit=1000").then(async () => { await updateDrivers() }).catch(() => { console.log("API is unreachable! Not updating drivers...") if (fs.existsSync(driversPath)) { let data = fs.readFileSync(driversPath) drivers = JSON.parse(data) } else { throw "Jolpica API is unreachable and the drivers.json cache has not been built. Please try again when the Jolpica API is online." } }).catch(err => { console.log(err) return process.exit(1) }).then(() => { dotd() server() }) schedule.scheduleJob("59 23 * * *", async () => { axios.get("https://api.jolpi.ca/ergast/f1/1950/driverStandings.json?limit=1000").then(async () => { await updateDrivers() }).catch(() => { console.log("API is unreachable! Not updating drivers...") drivers = JSON.parse(fs.readFileSync(driversPath)) }) }) schedule.scheduleJob("0 0 * * *", () => { dotd() }) schedule.scheduleJob("* * * * *", () => { processStats() }) async function updateDrivers() { let newDrivers = {} for (let i = 2000; i <= year; i++) { console.log(`Scraping F1 ${i} Season...`) try { await axios.get(`https://api.jolpi.ca/ergast/f1/${i}/driverStandings.json?limit=1000`).then(res => { res.data.MRData.StandingsTable.StandingsLists[0].DriverStandings.forEach(driver => { if (driver.Driver.driverId in newDrivers) { newDrivers[driver.Driver.driverId].wins += parseInt(driver.wins) if (newDrivers[driver.Driver.driverId].constructors[newDrivers[driver.Driver.driverId].constructors.length - 1] !== team(driver.Constructors[0].name, i) || newDrivers[driver.Driver.driverId].constructors.length === 0) newDrivers[driver.Driver.driverId].constructors.push(team(driver.Constructors[0].name, i)) } else if (driver.Driver.hasOwnProperty("permanentNumber")) { newDrivers[driver.Driver.driverId] = { "firstName": driver.Driver.givenName, "lastName": driver.Driver.familyName, "code": driver.Driver.code, "nationality": flag[driver.Driver.nationality.trim()], "constructors": [team(driver.Constructors[0].name, i)], "permanentNumber": driver.Driver.permanentNumber, "age": getAge(driver.Driver.dateOfBirth), "firstYear": i, "wins": parseInt(driver.wins), } } }) }) } catch (e) { if (i !== year) throw "" } await sleep(300) } drivers = newDrivers if (fs.existsSync("assets/drivers.json")) { console.log("Deleting drivers.json...") fs.unlinkSync("assets/drivers.json") } console.log(`Writing ${_.keys(drivers).length} Drivers to drivers.json...`) fs.writeFileSync("assets/drivers.json", JSON.stringify(drivers), (error) => { if (error) throw error }) } function processStats(dotd = false) { const date = dayjs().format("YYYY-MM-DD"); let statsFile = {}; if (fs.existsSync(statsPath)) { statsFile = JSON.parse(fs.readFileSync(statsPath)); } if (statsFile.hasOwnProperty(date)) { if (statsFile[date].visits > stats.visits) { statsFile[date].visits += stats.visits stats.visits = statsFile[date].visits } else statsFile[date].visits = stats.visits if (statsFile[date].guesses > stats.guesses) { statsFile[date].guesses += stats.guesses stats.guesses = statsFile[date].guesses } else statsFile[date].guesses = stats.guesses } else if (dotd) { statsFile[date] = { "driver": stats.driver } } else return fs.writeFileSync(statsPath, JSON.stringify(statsFile)); } function dotd() { console.log("Selecting Driver of the Day...") let date = dayjs().format("YYYY-MM-DD") let pastDrivers = [] let pastDates = [] if (fs.existsSync(statsPath)) { let statsFile = JSON.parse(fs.readFileSync(statsPath)) pastDates = Object.keys(statsFile) pastDrivers = Object.values(statsFile).map(x => x.driver).filter((x) => { return typeof x === "string"}) } if (pastDrivers.length > 0 && pastDates.length > 0 && pastDates[pastDates.length - 1] === date) { driver = pastDrivers[pastDrivers.length - 1] } else { let newDriver = getRandomProperty(drivers) while (pastDrivers.slice(-14).includes(newDriver)) { newDriver = getRandomProperty(drivers) } driver = newDriver } stats = { "visits": 0, "guesses": 0, "driver": driver } processStats(true) console.log(`Driver of the Day is ${driver}!`) console.log(drivers[driver]) } function getRandomProperty(obj) { let keys = Object.keys(obj) return keys[Math.floor(Math.random() * keys.length)] } function getAge(dateString) { var today = new Date(); var birthDate = new Date(dateString); var age = today.getFullYear() - birthDate.getFullYear(); var m = today.getMonth() - birthDate.getMonth(); if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) { age--; } return age; } function server() { var app = express() app.enable("trust proxy") app.use(express.urlencoded({ extended: true })) app.use(express.static("assets", { setHeaders: function(res, path, stat) { // Set cache control headers res.set('Cache-Control', 'no-cache, no-store, must-revalidate'); res.set('Pragma', 'no-cache'); res.set('Expires', '0'); } })) app.use(favicon('assets/favicon.ico')) app.use(morgan("combined")) app.set("views", "views") app.set("view engine", "ejs") app.get("/", (req, res) => { res.render("index") stats.visits++ }) app.get("/winner", (req, res) => { if (req.headers.authorization !== "Bearer kRyX3RYMRY$&yEc8") return res.end() res.json({ "winner": drivers[driver].firstName + " " + drivers[driver].lastName, }) }) app.get("/driver", (req, res) => { if (!req.query.driver) return res.statusSend(400) let search = false let response = [] for (let query in drivers) { if (req.query.driver === drivers[query].firstName + " " + drivers[query].lastName) { search = true let guess = drivers[query] let actual = drivers[driver] // nationality if (guess.nationality === actual.nationality) response.push(1) // correct nationality else response.push(3) // incorrect nationality // constructors if (guess.constructors[guess.constructors.length - 1] === actual.constructors[actual.constructors.length - 1]) response.push(1) // correct constructor else if (actual.constructors.includes(guess.constructors[guess.constructors.length - 1])) response.push(4) // previous constructor else response.push(3) // incorrect constructor // permanent number if (parseInt(guess.permanentNumber) > parseInt(actual.permanentNumber)) response.push(0) // go down else if (parseInt(guess.permanentNumber) === parseInt(actual.permanentNumber)) response.push(1) // stay the same else if (parseInt(guess.permanentNumber) < parseInt(actual.permanentNumber)) response.push(2) // go up // age if (parseInt(guess.age) > parseInt(actual.age)) response.push(0) // go down else if (parseInt(guess.age) === parseInt(actual.age)) response.push(1) // stay the same else if (parseInt(guess.age) < parseInt(actual.age)) response.push(2) // go up // first year if (parseInt(guess.firstYear) > parseInt(actual.firstYear)) response.push(0) // go down else if (parseInt(guess.firstYear) === parseInt(actual.firstYear)) response.push(1) // stay the same else if (parseInt(guess.firstYear) < parseInt(actual.firstYear)) response.push(2) // go up // wins if (parseInt(guess.wins) > parseInt(actual.wins)) response.push(0) // go down else if (parseInt(guess.wins) === parseInt(actual.wins)) response.push(1) // stay the same else if (parseInt(guess.wins) < parseInt(actual.wins)) response.push(2) // go up } } if (!search) return res.sendStatus(400) res.json({ "nationality": response[0], "constructor": response[1], "permanentNumber": response[2], "age": response[3], "firstYear": response[4], "wins": response[5] }) stats.guesses++ }) let port = 3000 app.listen(port, () => { console.log(`Listening on port ${port}!`) }) }