реальные таблицы: football-data.org + фолбэк на демо
This commit is contained in:
parent
181440b81d
commit
38ea559b5f
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,3 +1,5 @@
|
|||||||
.vibe42-run.log
|
.vibe42-run.log
|
||||||
.vibe42-run.pid
|
.vibe42-run.pid
|
||||||
node_modules/
|
node_modules/
|
||||||
|
.env
|
||||||
|
.league-cache.json
|
||||||
|
|||||||
@ -10,3 +10,99 @@ up to date in 355ms
|
|||||||
|
|
||||||
up to date in 326ms
|
up to date in 326ms
|
||||||
Сервер на порту 3000
|
Сервер на порту 3000
|
||||||
|
--- run Wed Sep 23 10:20:53 AM UTC 2026 :: npm start --silent
|
||||||
|
|
||||||
|
up to date in 343ms
|
||||||
|
Сервер на порту 3000
|
||||||
|
--- run Wed Sep 23 10:21:30 AM UTC 2026 :: npm start --silent
|
||||||
|
|
||||||
|
up to date in 334ms
|
||||||
|
Сервер на порту 3000
|
||||||
|
--- run Wed Sep 23 10:22:20 AM UTC 2026 :: npm start --silent
|
||||||
|
|
||||||
|
up to date in 346ms
|
||||||
|
Сервер на порту 3000
|
||||||
|
--- run Wed Sep 23 10:23:01 AM UTC 2026 :: npm start --silent
|
||||||
|
|
||||||
|
up to date in 388ms
|
||||||
|
Сервер на порту 3000
|
||||||
|
--- run Wed Sep 23 10:23:39 AM UTC 2026 :: npm start --silent
|
||||||
|
|
||||||
|
up to date in 373ms
|
||||||
|
Сервер на порту 3000
|
||||||
|
--- run Wed Sep 23 10:24:40 AM UTC 2026 :: npm start --silent
|
||||||
|
|
||||||
|
up to date in 360ms
|
||||||
|
Сервер на порту 3000
|
||||||
|
--- run Wed Sep 23 10:25:25 AM UTC 2026 :: npm start --silent
|
||||||
|
|
||||||
|
up to date in 322ms
|
||||||
|
Сервер на порту 3000
|
||||||
|
--- run Wed Sep 23 10:28:35 AM UTC 2026 :: npm start --silent
|
||||||
|
|
||||||
|
up to date in 356ms
|
||||||
|
Сервер на порту 3000
|
||||||
|
--- run Wed Sep 23 10:29:19 AM UTC 2026 :: npm start --silent
|
||||||
|
|
||||||
|
up to date in 328ms
|
||||||
|
Сервер на порту 3000
|
||||||
|
--- run Wed Sep 23 10:30:51 AM UTC 2026 :: npm start --silent
|
||||||
|
|
||||||
|
up to date in 369ms
|
||||||
|
Сервер на порту 3000
|
||||||
|
--- run Wed Sep 23 10:31:35 AM UTC 2026 :: npm start --silent
|
||||||
|
|
||||||
|
up to date in 328ms
|
||||||
|
Сервер на порту 3000
|
||||||
|
--- run Wed Sep 23 10:32:17 AM UTC 2026 :: npm start --silent
|
||||||
|
|
||||||
|
up to date in 366ms
|
||||||
|
Сервер на порту 3000
|
||||||
|
--- run Wed Sep 23 10:33:20 AM UTC 2026 :: npm start --silent
|
||||||
|
|
||||||
|
up to date in 343ms
|
||||||
|
Сервер на порту 3000
|
||||||
|
[standings] premier: ошибка Unexpected end of JSON input
|
||||||
|
[standings] laliga: ошибка Unexpected end of JSON input
|
||||||
|
[standings] bundesliga: ошибка Unexpected end of JSON input
|
||||||
|
[standings] seriea: ошибка Unexpected end of JSON input
|
||||||
|
--- run Wed Sep 23 10:34:33 AM UTC 2026 :: npm start --silent
|
||||||
|
|
||||||
|
up to date in 339ms
|
||||||
|
Сервер на порту 3000
|
||||||
|
[standings] premier: ошибка TheSportsDB: не-JSON ответ, status=200 len=0
|
||||||
|
[standings] laliga: ошибка TheSportsDB: не-JSON ответ, status=200 len=0
|
||||||
|
[standings] bundesliga: ошибка TheSportsDB: не-JSON ответ, status=200 len=0
|
||||||
|
[standings] seriea: ошибка TheSportsDB: не-JSON ответ, status=200 len=0
|
||||||
|
--- run Wed Sep 23 10:35:56 AM UTC 2026 :: npm start --silent
|
||||||
|
|
||||||
|
up to date in 321ms
|
||||||
|
Сервер на порту 3000
|
||||||
|
[standings] premier: ошибка не найдена актуальная таблица для premier
|
||||||
|
[standings] laliga: ошибка не найдена актуальная таблица для laliga
|
||||||
|
[standings] bundesliga: ошибка не найдена актуальная таблица для bundesliga
|
||||||
|
[standings] seriea: ошибка не найдена актуальная таблица для seriea
|
||||||
|
--- run Wed Sep 23 10:37:05 AM UTC 2026 :: npm start --silent
|
||||||
|
|
||||||
|
up to date in 314ms
|
||||||
|
Сервер на порту 3000
|
||||||
|
[standings] premier: ошибка не найдена актуальная таблица для premier
|
||||||
|
[standings] laliga: ошибка не найдена актуальная таблица для laliga
|
||||||
|
[standings] bundesliga: ошибка не найдена актуальная таблица для bundesliga
|
||||||
|
[standings] seriea: ошибка не найдена актуальная таблица для seriea
|
||||||
|
--- run Wed Sep 23 10:39:07 AM UTC 2026 :: npm start --silent
|
||||||
|
|
||||||
|
up to date in 312ms
|
||||||
|
Сервер на порту 3000
|
||||||
|
[standings] premier: ошибка исчерпаны источники
|
||||||
|
[standings] laliga: ошибка исчерпаны источники
|
||||||
|
[standings] bundesliga: ошибка исчерпаны источники
|
||||||
|
[standings] seriea: ошибка исчерпаны источники
|
||||||
|
--- run Wed Sep 23 10:40:16 AM UTC 2026 :: npm start --silent
|
||||||
|
|
||||||
|
up to date in 321ms
|
||||||
|
Сервер на порту 3000
|
||||||
|
[standings] premier: ошибка исчерпаны источники
|
||||||
|
[standings] laliga: ошибка исчерпаны источники
|
||||||
|
[standings] bundesliga: ошибка исчерпаны источники
|
||||||
|
[standings] seriea: ошибка исчерпаны источники
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
1345
|
8240
|
||||||
|
|||||||
17
index.html
17
index.html
@ -24,8 +24,19 @@
|
|||||||
.kt-ai-table td { padding: 10px; border-bottom: 1px solid var(--kt-ai-border); }
|
.kt-ai-table td { padding: 10px; border-bottom: 1px solid var(--kt-ai-border); }
|
||||||
.kt-ai-table tr:last-child td { border-bottom: none; }
|
.kt-ai-table tr:last-child td { border-bottom: none; }
|
||||||
.kt-ai-table tr.top td { font-weight: 500; }
|
.kt-ai-table tr.top td { font-weight: 500; }
|
||||||
|
.kt-ai-table tr.zone-1 td:first-child { box-shadow: inset 3px 0 0 var(--kt-ai-fg); }
|
||||||
|
.kt-ai-table tr.zone-1 td, .kt-ai-table tr.zone-2 td { background: var(--kt-ai-bg-soft); }
|
||||||
|
.kt-ai-table tr.zone-2 td:first-child { box-shadow: inset 3px 0 0 var(--kt-ai-fg-muted); }
|
||||||
|
.kt-ai-table tr.zone-4 td:first-child { box-shadow: inset 3px 0 0 var(--kt-ai-fg-faint); }
|
||||||
.pos { display: inline-grid; place-items: center; width: 24px; height: 24px; border-radius: 6px; background: var(--kt-ai-bg); border: 1px solid var(--kt-ai-border); font-size: 12px; color: var(--kt-ai-fg-muted); }
|
.pos { display: inline-grid; place-items: center; width: 24px; height: 24px; border-radius: 6px; background: var(--kt-ai-bg); border: 1px solid var(--kt-ai-border); font-size: 12px; color: var(--kt-ai-fg-muted); }
|
||||||
tr.top .pos { background: var(--kt-ai-primary-subtle); border-color: transparent; color: var(--kt-ai-fg); }
|
tr .pos.lead { background: var(--kt-ai-primary); border-color: transparent; color: var(--kt-ai-bg); }
|
||||||
|
.zone-legend { display: flex; gap: 16px; flex-wrap: wrap; margin-top: 12px; font-size: 12px; color: var(--kt-ai-fg-muted); }
|
||||||
|
.zone-legend span::before { content: ""; display: inline-block; width: 8px; height: 8px; margin-right: 6px; border-radius: 2px; vertical-align: 1px; }
|
||||||
|
.leg-white::before { background: var(--kt-ai-fg); }
|
||||||
|
.leg-mut::before { background: var(--kt-ai-fg-muted); }
|
||||||
|
.leg-dim::before { background: var(--kt-ai-fg-faint); }
|
||||||
|
.table-status { font-size: 12px; color: var(--kt-ai-fg-muted); margin-top: 10px; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
||||||
|
.table-status .kt-ai-chip { font-size: 11px; }
|
||||||
.nums td { text-align: center; font-variant-numeric: tabular-nums; }
|
.nums td { text-align: center; font-variant-numeric: tabular-nums; }
|
||||||
.nums th { text-align: center; }
|
.nums th { text-align: center; }
|
||||||
.table-scroll { overflow-x: auto; }
|
.table-scroll { overflow-x: auto; }
|
||||||
@ -87,12 +98,14 @@
|
|||||||
<div class="kt-ai-wrap">
|
<div class="kt-ai-wrap">
|
||||||
<div class="kt-ai-section-head">
|
<div class="kt-ai-section-head">
|
||||||
<h2>Таблицы чемпионатов</h2>
|
<h2>Таблицы чемпионатов</h2>
|
||||||
<p>Текущее положение команд в главных лигах. Зелёные строки — лидеры.</p>
|
<p>Реальные данные из футбольных лиг — обновляются каждые 15 минут. Подсвечены лидеры.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="league-tabs" id="league-tabs"></div>
|
<div class="league-tabs" id="league-tabs"></div>
|
||||||
|
<div id="table-status" class="table-status"></div>
|
||||||
<div class="kt-ai-card table-scroll" style="margin-top:16px; padding: 8px 12px;">
|
<div class="kt-ai-card table-scroll" style="margin-top:16px; padding: 8px 12px;">
|
||||||
<table class="kt-ai-table" id="league-table"></table>
|
<table class="kt-ai-table" id="league-table"></table>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="zone-legend" id="zone-legend"></div>
|
||||||
<div id="matches" style="margin-top: var(--kt-ai-space-8, 28px)">
|
<div id="matches" style="margin-top: var(--kt-ai-space-8, 28px)">
|
||||||
<h3 style="margin-bottom: 12px; font-weight: 500;">Ближайшие матчи</h3>
|
<h3 style="margin-bottom: 12px; font-weight: 500;">Ближайшие матчи</h3>
|
||||||
<div class="match-strip" id="match-strip"></div>
|
<div class="match-strip" id="match-strip"></div>
|
||||||
|
|||||||
@ -2,5 +2,5 @@
|
|||||||
"name": "sports-portal",
|
"name": "sports-portal",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"scripts": { "start": "node server.js" }
|
"scripts": { "start": "node --env-file=.env server.js" }
|
||||||
}
|
}
|
||||||
|
|||||||
105
script.js
105
script.js
@ -27,9 +27,10 @@ const SPORTS = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Демо-данные: работают только если API не доступно (например, в Предпросмотре без сервера)
|
||||||
const LEAGUES = {
|
const LEAGUES = {
|
||||||
premier: {
|
premier: {
|
||||||
label: "Премьер-лига",
|
label: "АПЛ, Англия",
|
||||||
cols: ["И", "В", "Н", "П", "ГЗ", "ГП"],
|
cols: ["И", "В", "Н", "П", "ГЗ", "ГП"],
|
||||||
rows: [
|
rows: [
|
||||||
["Арсенал", 38, 24, 10, 4, 78, 32],
|
["Арсенал", 38, 24, 10, 4, 78, 32],
|
||||||
@ -41,7 +42,7 @@ const LEAGUES = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
laliga: {
|
laliga: {
|
||||||
label: "Ла Лига",
|
label: "Ла Лига, Испания",
|
||||||
cols: ["И", "В", "Н", "П", "ГЗ", "ГП"],
|
cols: ["И", "В", "Н", "П", "ГЗ", "ГП"],
|
||||||
rows: [
|
rows: [
|
||||||
["Реал Мадрид", 38, 27, 4, 7, 78, 33],
|
["Реал Мадрид", 38, 27, 4, 7, 78, 33],
|
||||||
@ -149,10 +150,72 @@ function renderSports() {
|
|||||||
`).join("");
|
`).join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TABLE_COLS = ["И", "В", "Н", "П", "ГЗ", "ГП"];
|
||||||
|
const CURRENT = {
|
||||||
|
premier: { label: "АПЛ, Англия", cols: TABLE_COLS, rows: [], live: true },
|
||||||
|
laliga: { label: "Ла Лига, Испания", cols: TABLE_COLS, rows: [], live: true },
|
||||||
|
bundesliga: { label: "Бундеслига, Германия", cols: TABLE_COLS, rows: [], live: true },
|
||||||
|
seriea: { label: "Серия А, Италия", cols: TABLE_COLS, rows: [], live: true },
|
||||||
|
};
|
||||||
|
const ZONES = {
|
||||||
|
premier: { ucl: 4, uel: 1, releg: 3 },
|
||||||
|
laliga: { ucl: 4, uel: 1, releg: 3 },
|
||||||
|
bundesliga: { ucl: 4, uel: 1, releg: 3 },
|
||||||
|
seriea: { ucl: 4, uel: 0, releg: 3 },
|
||||||
|
};
|
||||||
|
|
||||||
|
function applyLive(key, payload) {
|
||||||
|
if (!payload || !Array.isArray(payload.rows)) return;
|
||||||
|
CURRENT[key] = {
|
||||||
|
label: payload.name || CURRENT[key].label,
|
||||||
|
cols: CURRENT[key].cols,
|
||||||
|
rows: payload.rows.map((r) => [r.name, r.played, r.win, r.draw, r.lost, r.gf, r.ga]),
|
||||||
|
live: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function liveError(key) {
|
||||||
|
CURRENT[key].rows = [];
|
||||||
|
CURRENT[key].live = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadStandings() {
|
||||||
|
try {
|
||||||
|
const res = await fetch("api/standings");
|
||||||
|
const j = await res.json();
|
||||||
|
if (!j.ok || !j.data) throw new Error("bad payload");
|
||||||
|
for (const key of Object.keys(CURRENT)) {
|
||||||
|
const d = j.data[key];
|
||||||
|
if (!d || d.error) { liveError(key); continue; }
|
||||||
|
applyLive(key, d);
|
||||||
|
}
|
||||||
|
const anyLive = Object.values(CURRENT).some((l) => l.live);
|
||||||
|
const status = document.getElementById("table-status");
|
||||||
|
if (status) {
|
||||||
|
const when = new Date(j.updatedAt).toLocaleString("ru-RU", { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" });
|
||||||
|
status.innerHTML = anyLive
|
||||||
|
? `<span class="kt-ai-chip">обновлено в ${when}</span><span>источник: TheSportsDB · повтор через 15 минут</span>`
|
||||||
|
: `<span>нет связи с источником — показаны демо-данные</span>`;
|
||||||
|
}
|
||||||
|
for (const key of Object.keys(CURRENT)) {
|
||||||
|
if (!CURRENT[key].live) CURRENT[key].rows = (LEAGUES[key] && LEAGUES[key].rows) || [];
|
||||||
|
}
|
||||||
|
return anyLive;
|
||||||
|
} catch (_) {
|
||||||
|
for (const key of Object.keys(CURRENT)) {
|
||||||
|
CURRENT[key].rows = (LEAGUES[key] && LEAGUES[key].rows) || [];
|
||||||
|
CURRENT[key].live = false;
|
||||||
|
}
|
||||||
|
const status = document.getElementById("table-status");
|
||||||
|
if (status) status.innerHTML = `<span>нет связи с источником — показаны демо-данные</span>`;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function renderLeagueTabs() {
|
function renderLeagueTabs() {
|
||||||
const tabs = document.getElementById("league-tabs");
|
const tabs = document.getElementById("league-tabs");
|
||||||
if (!tabs) return;
|
if (!tabs) return;
|
||||||
tabs.innerHTML = Object.entries(LEAGUES).map(([key, l], i) => `
|
tabs.innerHTML = Object.entries(CURRENT).map(([key, l], i) => `
|
||||||
<button class="kt-ai-btn" data-league="${key}" data-variant="${i === 0 ? "primary" : "ghost"}">${l.label}</button>
|
<button class="kt-ai-btn" data-league="${key}" data-variant="${i === 0 ? "primary" : "ghost"}">${l.label}</button>
|
||||||
`).join("");
|
`).join("");
|
||||||
tabs.addEventListener("click", (e) => {
|
tabs.addEventListener("click", (e) => {
|
||||||
@ -164,22 +227,38 @@ function renderLeagueTabs() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function zoneClass(key, i, total) {
|
||||||
|
const z = ZONES[key] || {};
|
||||||
|
if (i < (z.ucl || 0)) return "zone-1";
|
||||||
|
if (i < (z.ucl || 0) + (z.uel || 0)) return "zone-2";
|
||||||
|
if (i >= total - (z.releg || 0)) return "zone-4";
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
function renderTable(key) {
|
function renderTable(key) {
|
||||||
const league = LEAGUES[key] || LEAGUES.premier;
|
const league = CURRENT[key] || CURRENT.premier;
|
||||||
const table = document.getElementById("league-table");
|
const table = document.getElementById("league-table");
|
||||||
if (!table) return;
|
if (!table) return;
|
||||||
|
if (!league.rows.length) {
|
||||||
|
table.innerHTML = `<tr><td style="padding: 16px; color: var(--kt-ai-fg-muted)">Данные по этой лиге временно недоступны.</td></tr>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const total = league.rows.length;
|
||||||
const head = `<tr><th>#</th><th>Команда</th>${league.cols.map((c) => `<th>${c}</th>`).join("")}</tr>`;
|
const head = `<tr><th>#</th><th>Команда</th>${league.cols.map((c) => `<th>${c}</th>`).join("")}</tr>`;
|
||||||
const body = league.rows.map((r, i) => {
|
const body = league.rows.map((r, i) => {
|
||||||
const pts = r[1] * 3 + r[2];
|
const pts = r[2] * 3 + r[3];
|
||||||
|
const zone = zoneClass(key, i, total);
|
||||||
return `
|
return `
|
||||||
<tr class="${i < 3 ? "top" : ""}">
|
<tr class="${zone}">
|
||||||
<td><span class="pos">${i + 1}</span></td>
|
<td><span class="pos${i === 0 ? " lead" : ""}">${i + 1}</span></td>
|
||||||
<td>${r[0]}</td>
|
<td>${r[0]}</td>
|
||||||
<td style="text-align:center; font-variant-numeric: tabular-nums;">${pts}</td>
|
<td style="text-align:center; font-variant-numeric: tabular-nums; font-weight:500">${pts}</td>
|
||||||
${r.slice(1).map((v) => `<td style="text-align:center; font-variant-numeric: tabular-nums;">${v}</td>`).join("")}
|
${r.slice(1).map((v) => `<td style="text-align:center; font-variant-numeric: tabular-nums;">${v}</td>`).join("")}
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}).join("");
|
}).join("");
|
||||||
table.innerHTML = head + `<tbody class="nums">${body}</tbody>`;
|
table.innerHTML = head + `<tbody class="nums">${body}</tbody>`;
|
||||||
|
const legend = document.querySelector(".zone-legend");
|
||||||
|
if (legend) legend.innerHTML = `<span class="leg-white">Лига чемпионов</span><span class="leg-mut">Еврокубки</span><span class="leg-dim">Вылет</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderMatches() {
|
function renderMatches() {
|
||||||
@ -210,10 +289,16 @@ function renderArticles() {
|
|||||||
`).join("");
|
`).join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
renderSports();
|
renderSports();
|
||||||
renderLeagueTabs();
|
renderLeagueTabs();
|
||||||
renderTable("premier");
|
|
||||||
renderMatches();
|
renderMatches();
|
||||||
renderArticles();
|
renderArticles();
|
||||||
|
await loadStandings();
|
||||||
|
renderTable("premier");
|
||||||
|
setInterval(async () => {
|
||||||
|
await loadStandings();
|
||||||
|
const activeBtn = document.querySelector("#league-tabs [data-variant='primary']");
|
||||||
|
renderTable(activeBtn ? activeBtn.dataset.league : "premier");
|
||||||
|
}, 15 * 60 * 1000);
|
||||||
});
|
});
|
||||||
|
|||||||
106
server.js
106
server.js
@ -3,11 +3,103 @@ const fs = require("fs");
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
|
const CACHE_FILE = path.join(__dirname, ".league-cache.json");
|
||||||
|
const CACHE_TTL = 15 * 60 * 1000;
|
||||||
|
|
||||||
http.createServer((req, res) => {
|
// football-data.org: лиги и бесплатный ключ из .env (https://www.football-data.org/client/register)
|
||||||
let file = req.url === "/" ? "/index.html" : req.url.split("?")[0];
|
const LEAGUES = {
|
||||||
const decoded = decodeURIComponent(file);
|
premier: "PL",
|
||||||
const full = path.normalize(path.join(__dirname, decoded));
|
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 (!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"); }
|
if (!fs.existsSync(full) || !fs.statSync(full).isFile()) { res.writeHead(404); return res.end("Not found"); }
|
||||||
const ext = path.extname(full).toLowerCase();
|
const ext = path.extname(full).toLowerCase();
|
||||||
@ -21,4 +113,8 @@ http.createServer((req, res) => {
|
|||||||
}[ext] || "application/octet-stream";
|
}[ext] || "application/octet-stream";
|
||||||
res.writeHead(200, { "Content-Type": type + "; charset=utf-8" });
|
res.writeHead(200, { "Content-Type": type + "; charset=utf-8" });
|
||||||
res.end(fs.readFileSync(full));
|
res.end(fs.readFileSync(full));
|
||||||
}).listen(PORT, () => console.log("Сервер на порту " + PORT));
|
}).listen(PORT, () => {
|
||||||
|
console.log("Сервер на порту " + PORT);
|
||||||
|
setTimeout(() => { refresh().catch(() => {}); }, 1500);
|
||||||
|
setInterval(() => { refresh().catch(() => {}); }, CACHE_TTL);
|
||||||
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user