proshu-sozdat-sportivnyy-say/server.js

121 lines
4.2 KiB
JavaScript

const http = require("http");
const fs = require("fs");
const path = require("path");
const PORT = process.env.PORT || 3000;
const CACHE_FILE = path.join(__dirname, ".league-cache.json");
const CACHE_TTL = 15 * 60 * 1000;
// football-data.org: лиги и бесплатный ключ из .env (https://www.football-data.org/client/register)
const LEAGUES = {
premier: "PL",
laliga: "PD",
bundesliga: "BL1",
seriea: "SA",
};
const FDB_API_KEY = process.env.FOOTBALL_DATA_KEY || "";
function readCache() {
try { return JSON.parse(fs.readFileSync(CACHE_FILE, "utf8")); } catch (_) { return null; }
}
function writeCache(d) {
try { fs.writeFileSync(CACHE_FILE, JSON.stringify(d)); } catch (_) {}
}
async function getLeagueTable(key) {
if (!FDB_API_KEY) return null;
const url = "https://api.football-data.org/v4/competitions/" + LEAGUES[key] + "/standings";
const res = await fetch(url, { headers: { "X-Auth-Token": FDB_API_KEY } });
if (!res.ok) throw new Error("football-data status " + res.status);
const j = await res.json();
const standing = (j.standings || []).find((s) => s.type === "TOTAL") || (j.standings || [])[0];
if (!standing) throw new Error("нет таблицы в ответе");
const rows = standing.table.map((t) => ({
name: t.team.name,
played: t.playedGames,
win: t.win,
draw: t.draw,
lost: t.lost,
gf: t.goalsFor,
ga: t.goalsAgainst,
}));
const name = j.competition && j.competition.name ? j.competition.name : "лига";
return { name, rows };
}
let refreshing = null;
async function refresh() {
if (refreshing) return refreshing;
refreshing = (async () => {
const prev = readCache();
const out = { ts: Date.now(), source: "TheSportsDB", data: {} };
for (const key of Object.keys(LEAGUE_IDS)) {
try {
const t = await getLeagueTable(LEAGUE_IDS[key]);
if (t) {
out.data[key] = { key, name: t.name, rows: t.rows };
console.log("[standings] " + key + ": " + t.rows.length + " команд");
} else {
throw new Error("исчерпаны источники");
}
} catch (e) {
out.data[key] = (prev && prev.data && prev.data[key]) || { error: "данные временно недоступны" };
console.log("[standings] " + key + ": ошибка " + String(e && e.message || e).slice(0, 80));
}
}
writeCache(out);
return out;
})();
try {
return await refreshing;
} finally {
refreshing = null;
}
}
async function getStandings() {
const cache = readCache();
if (cache && Date.now() - cache.ts < CACHE_TTL) return cache;
return refresh();
}
http.createServer(async (req, res) => {
const url = req.url.split("?")[0];
if (url === "/api/standings") {
try {
const j = await getStandings();
if (!j || !j.data) { res.writeHead(503, { "Content-Type": "application/json; charset=utf-8" }); return res.end(JSON.stringify({ ok: false })); }
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
return res.end(JSON.stringify({ ok: true, updatedAt: j.ts, source: j.source, data: j.data }));
} catch (e) {
const cache = readCache();
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
return res.end(JSON.stringify(cache && cache.data ? cache : { ok: false }));
}
}
let file = url === "/" ? "/index.html" : decodeURIComponent(url);
const full = path.normalize(path.join(__dirname, file));
if (!full.startsWith(__dirname)) { res.writeHead(403); return res.end(); }
if (!fs.existsSync(full) || !fs.statSync(full).isFile()) { res.writeHead(404); return res.end("Not found"); }
const ext = path.extname(full).toLowerCase();
const type = {
".html": "text/html",
".css": "text/css",
".js": "application/javascript",
".svg": "image/svg+xml",
".png": "image/png",
".ico": "image/x-icon",
}[ext] || "application/octet-stream";
res.writeHead(200, { "Content-Type": type + "; charset=utf-8" });
res.end(fs.readFileSync(full));
}).listen(PORT, () => {
console.log("Сервер на порту " + PORT);
setTimeout(() => { refresh().catch(() => {}); }, 1500);
setInterval(() => { refresh().catch(() => {}); }, CACHE_TTL);
});