Я пытаюсь создать HTML-страницу (для iframe позже), которая предоставляет мне данные в реальном времени для одной конкретной остановки метро здесь, в Брюсселе. Проблема в том, что что бы я ни пробовал, мой ключ API не используется, а просто проходит через общедоступный API, у которого очень маленькая квота. Я перепробовал все, что мог придумать (и что мог придумать ИИ
Вот мой последний файл code.gs (без ключа API), если вы хотите попробовать, вы можете легко создать его на https://api-management-opendata-product ... i.net/apis, мои параметры — этоwhere=pointid: 8271 и 8272.
Код: Выделить всё
const POINT_IDS = ["8271", "8272"];
const BASE_URL = "https://api-management-discovery-production.azure-api.net/api/datasets/stibmivb/rt/WaitingTimes";
const API_KEY = "--------";
const CACHE_KEY = "stib_arrivals";
function doGet(e) {
if (e && e.parameter && e.parameter.data === "1") {
const cache = CacheService.getScriptCache();
const cached = cache.get(CACHE_KEY);
const data = cached || "[]";
return ContentService
.createTextOutput(data)
.setMimeType(ContentService.MimeType.JSON);
} else {
return HtmlService.createTemplateFromFile("Page").evaluate()
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
}
function refreshCache() {
const allArrivals = [];
const cache = CacheService.getScriptCache();
for (const pointId of POINT_IDS) {
const url = `${BASE_URL}?where=pointid%3D%22${pointId}%22&limit=10`;
let response;
try {
response = UrlFetchApp.fetch(url, {
headers: {
"bmc-partner-key": API_KEY // Custom header name
},
muteHttpExceptions: true
});
} catch (err) {
Logger.log(`Fetch error ${pointId}: ${err.message}`);
continue;
}
if (response.getResponseCode() !== 200) {
Logger.log(`HTTP ${response.getResponseCode()} ${pointId}: ${response.getContentText()}`);
continue;
}
let json;
try {
json = JSON.parse(response.getContentText());
} catch (err) {
Logger.log(`JSON parse error ${pointId}: ${err.message}`);
continue;
}
const records = json.results || [];
for (const record of records) {
let times;
try {
// From curl: \\\"destination\\\" → replace \\\" → "
const cleanTimes = record.passingtimes.replace(/\\\"/g, '"');
times = JSON.parse(cleanTimes);
} catch (err) {
Logger.log(`passingtimes error ${pointId}: ${err.message}`);
continue;
}
for (const pt of times) {
if (!pt?.destination) continue;
allArrivals.push({
line: pt.lineId || "?",
dest: pt.destination.fr || pt.destination.nl || "—",
arrival: pt.expectedArrivalTime,
pointid: pointId
});
}
}
}
Logger.log(`Cached ${allArrivals.length} arrivals`);
cache.put(CACHE_KEY, JSON.stringify(allArrivals), 120);
}
Код: Выделить всё
* { box-sizing: border-box; }
body {
margin: 0;
padding: 10px;
font-family: sans-serif;
background: #1c1c1c;
color: #e1e1e1;
}
h4 {
margin: 10px 0 6px 0;
font-size: 0.78em;
text-transform: uppercase;
letter-spacing: 0.1em;
opacity: 0.5;
font-weight: normal;
}
h4:first-of-type { margin-top: 0; }
table {
width: 100%;
border-collapse: collapse;
font-size: 0.92em;
margin-bottom: 14px;
}
th {
text-align: left;
padding: 4px 8px;
font-weight: normal;
opacity: 0.45;
font-size: 0.82em;
}
td { padding: 7px 8px; }
tr:not(:last-child) td {
}
.badge {
display: inline-block;
border-radius: 5px;
padding: 2px 8px;
font-weight: bold;
font-size: 0.88em;
min-width: 22px;
text-align: center;
color: #fff;
}
.line-1 { background: #B9378D; }
.line-5 { background: #FFB537; color: #1c1c1c; }
.normal { font-weight: bold; }
.soon { font-weight: bold; color: #f28b31; }
.asap { font-weight: bold; color: #e05c5c; }
.divider {
border: none;
border-top: 1px solid rgba(255,255,255,0.1);
margin: 4px 0 12px 0;
}
#footer {
font-size: 0.68em;
opacity: 0.3;
text-align: right;
margin-top: 4px;
}
LigneDirectionDans
LigneDirectionDans
—
const PROXY_URL = "?data=1";
const DEST_A = ["ERASME", "GARE DE L'OUEST"];
const DEST_B = ["STOCKEL", "HERRMANN-DEBROUX"];
function showUnavailable() {
["tbody-a", "tbody-b"].forEach(id => {
document.getElementById(id).innerHTML =
`🚧 Service indisponible`;
});
}
function renderTable(tbodyId, arrivals) {
const tbody = document.getElementById(tbodyId);
if (arrivals.length === 0) {
tbody.innerHTML = `Aucun passage prévu.`;
return;
}
tbody.innerHTML = arrivals.map(a => {
const mins = Math.floor(a.diffMs / 60000);
const secs = Math.floor((a.diffMs % 60000) / 1000);
let label, cls;
if (mins < 1) { label = `${secs} sec`; cls = "asap"; }
else if (mins < 3) { label = `${mins} min`; cls = "soon"; }
else { label = `${mins} min`; cls = "normal"; }
const badgeCls = `badge line-${a.line}`;
return `
${a.line}
${a.dest}
${label}
`;
}).join("");
}
async function refresh() {
const footer = document.getElementById("footer");
try {
const res = await fetch(PROXY_URL);
const arrivals = await res.json();
const now = new Date();
// API is up but returning no data — service outage
if (!Array.isArray(arrivals) || arrivals.length === 0) {
showUnavailable();
footer.textContent = "API hors service – " + now.toLocaleTimeString("fr-BE");
return;
}
const upcoming = arrivals
.map(a => ({ ...a, diffMs: new Date(a.arrival) - now }))
.filter(a => a.diffMs > 0)
.sort((a, b) => a.diffMs - b.diffMs);
const groupA = upcoming.filter(a => DEST_A.includes(a.dest.toUpperCase()));
const groupB = upcoming.filter(a => DEST_B.includes(a.dest.toUpperCase()));
renderTable("tbody-a", groupA);
renderTable("tbody-b", groupB);
footer.textContent = "Mis à jour : " + now.toLocaleTimeString("fr-BE");
} catch (err) {
["tbody-a","tbody-b"].forEach(id => {
document.getElementById(id).innerHTML =
`⚠️ Erreur : ${err.message}`;
});
footer.textContent = "Échec – " + new Date().toLocaleTimeString("fr-BE");
}
}
refresh();
setInterval(refresh, 20000);
Спасибо!!!
Подробнее здесь: https://stackoverflow.com/questions/798 ... ng-api-key
Мобильная версия