v0.5.110: stop carrying stale live badges; probe live status in local serve.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -60,6 +60,7 @@
|
||||
{
|
||||
"id": "yaseguilai",
|
||||
"platform": "douyin",
|
||||
"live_url": "https://live.douyin.com/146909015971",
|
||||
"profile_url": "https://v.douyin.com/gQDc0B9yPv4/",
|
||||
"heroes": [
|
||||
"slark"
|
||||
@@ -80,6 +81,7 @@
|
||||
{
|
||||
"id": "gudu",
|
||||
"platform": "douyin",
|
||||
"live_url": "https://live.douyin.com/750711463576",
|
||||
"profile_url": "https://v.douyin.com/411MHPZgDeY/",
|
||||
"heroes": [
|
||||
"kez"
|
||||
@@ -119,6 +121,7 @@
|
||||
{
|
||||
"id": "dadigua",
|
||||
"platform": "douyin",
|
||||
"live_url": "https://live.douyin.com/499355244796",
|
||||
"profile_url": "https://v.douyin.com/t1E-4M1xrJ4/",
|
||||
"heroes": [
|
||||
"axe"
|
||||
|
||||
@@ -52,7 +52,7 @@ from shared.paths import (
|
||||
|
||||
from serve_relations import WEB_DIR, build_payload
|
||||
|
||||
SITE_VERSION = "0.5.109"
|
||||
SITE_VERSION = "0.5.110"
|
||||
DEFAULT_OSS_BASE = "https://climperor.oss-cn-shanghai.aliyuncs.com"
|
||||
|
||||
|
||||
|
||||
+8
-6
@@ -4749,9 +4749,9 @@ function syncStreamerCardLive(card, row) {
|
||||
|
||||
/**
|
||||
* Visit-triggered live refresh: the Pages Function coalesces concurrent
|
||||
* visitors through a 5-minute edge cache. Local `serve_relations.py` returns
|
||||
* an empty stub (no probing), so that path keeps the data.json is_live
|
||||
* fallback. Fires at most once per page load.
|
||||
* visitors through a 5-minute edge cache; the local development server uses
|
||||
* the same platform probes with a short in-memory cache. Fires at most once
|
||||
* per page load.
|
||||
*/
|
||||
let liveStatusFetched = false;
|
||||
|
||||
@@ -4776,9 +4776,11 @@ function refreshStreamerLiveStatus() {
|
||||
for (const row of rows) {
|
||||
const cell = row && row.id ? probed[row.id] : null;
|
||||
if (!cell || typeof cell.is_live !== "boolean") continue;
|
||||
if (cell.stale) console.info(`live-status: ${row.id} using stale carry-over`);
|
||||
if (row.is_live !== cell.is_live) {
|
||||
row.is_live = cell.is_live;
|
||||
// A failed/stale probe is unknown, never evidence that a room is live.
|
||||
const isLive = cell.stale ? false : cell.is_live;
|
||||
if (cell.stale) console.info(`live-status: ${row.id} probe stale; hiding badge`);
|
||||
if (row.is_live !== isLive) {
|
||||
row.is_live = isLive;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* Local defaults; production export overwrites via export_relations_site.py. */
|
||||
var SITE_VERSION = "0.5.109";
|
||||
var SITE_VERSION = "0.5.110";
|
||||
var ABILITY_VIDEO_BASE = "";
|
||||
var STATIC_ASSET_BASE = "";
|
||||
|
||||
|
||||
@@ -15,12 +15,10 @@
|
||||
* the SSR pace chunks: roomStore.roomInfo.room.status (2 live / 4 offline);
|
||||
* the embedded web_rid must match the requested one.
|
||||
*
|
||||
* Soft-fail everywhere: douyin blocks from datacenter IPs are expected. When a
|
||||
* single probe fails, the streamer carries over the last known is_live from
|
||||
* data.json / the previous (stale) cache entry with `stale: true`; when every
|
||||
* probe fails the stale cache entry is served wholesale (X-Live-Cache:
|
||||
* stale-override), or an empty payload when nothing was ever cached
|
||||
* (X-Live-Cache: error). The handler never throws a 500 for probe failures.
|
||||
* Soft-fail everywhere: douyin blocks from datacenter IPs are expected. A
|
||||
* failed probe is reported as offline with `stale: true`; a stale positive is
|
||||
* never carried over because the live badge must only reflect a successful
|
||||
* current probe. The handler never throws a 500 for probe failures.
|
||||
*
|
||||
* Named exports double as the local test surface; the Pages runtime only
|
||||
* routes onRequest* handlers.
|
||||
@@ -292,6 +290,23 @@ function emptyPayload() {
|
||||
return { probed_at: new Date().toISOString(), ttl: FRESH_TTL_S, streamers: {} };
|
||||
}
|
||||
|
||||
/** Failed probes must never preserve a stale live badge. */
|
||||
function staleOfflinePayload(source) {
|
||||
const streamers = {};
|
||||
const previous =
|
||||
source && source.streamers && typeof source.streamers === "object"
|
||||
? source.streamers
|
||||
: {};
|
||||
for (const id of Object.keys(previous)) {
|
||||
streamers[id] = { is_live: false, stale: true };
|
||||
}
|
||||
return {
|
||||
probed_at: (source && source.probed_at) || new Date().toISOString(),
|
||||
ttl: FRESH_TTL_S,
|
||||
streamers,
|
||||
};
|
||||
}
|
||||
|
||||
async function putCache(body) {
|
||||
const cached = new Response(JSON.stringify(body), {
|
||||
headers: {
|
||||
@@ -324,17 +339,12 @@ async function probeFresh(requestUrl, cachedData) {
|
||||
}
|
||||
|
||||
if (!targets.length) {
|
||||
if (cachedData) return { body: cachedData, cacheState: "stale-override" };
|
||||
if (cachedData) {
|
||||
return { body: staleOfflinePayload(cachedData), cacheState: "stale-override" };
|
||||
}
|
||||
return { body: emptyPayload(), cacheState: "error" };
|
||||
}
|
||||
|
||||
const seeded = seedLiveFromPayload(dataPayload);
|
||||
const cachedStreamers =
|
||||
cachedData && cachedData.streamers && typeof cachedData.streamers === "object"
|
||||
? cachedData.streamers
|
||||
: {};
|
||||
// Prefer previous edge cache over the daily snapshot when both exist.
|
||||
const staleStreamers = { ...seeded, ...cachedStreamers };
|
||||
const probed = await probeAll(targets);
|
||||
|
||||
const streamers = {};
|
||||
@@ -346,31 +356,19 @@ async function probeFresh(requestUrl, cachedData) {
|
||||
freshCount += 1;
|
||||
continue;
|
||||
}
|
||||
// Per-streamer soft-fail: carry over the last known state, marked stale.
|
||||
const prev = staleStreamers[t.id];
|
||||
if (prev && typeof prev.is_live === "boolean") {
|
||||
streamers[t.id] = { is_live: prev.is_live, stale: true };
|
||||
}
|
||||
// Unknown is not live: never carry a stale positive badge forward.
|
||||
streamers[t.id] = { is_live: false, stale: true };
|
||||
}
|
||||
|
||||
if (freshCount === 0) {
|
||||
// Total probe failure (e.g. douyin blocking this colo): serve the stale
|
||||
// snapshot if one exists, otherwise an explicitly empty payload.
|
||||
if (cachedData) return { body: cachedData, cacheState: "stale-override" };
|
||||
if (Object.keys(seeded).length) {
|
||||
const body = {
|
||||
probed_at: new Date().toISOString(),
|
||||
ttl: FRESH_TTL_S,
|
||||
streamers: Object.fromEntries(
|
||||
Object.entries(seeded).map(([id, cell]) => [
|
||||
id,
|
||||
{ is_live: cell.is_live, stale: true },
|
||||
])
|
||||
),
|
||||
};
|
||||
return { body, cacheState: "stale-override" };
|
||||
}
|
||||
return { body: emptyPayload(), cacheState: "error" };
|
||||
// Total probe failure (e.g. douyin blocking this colo): keep the target
|
||||
// shape but suppress every unconfirmed live badge.
|
||||
const body = {
|
||||
probed_at: new Date().toISOString(),
|
||||
ttl: FRESH_TTL_S,
|
||||
streamers,
|
||||
};
|
||||
return { body, cacheState: "stale-override" };
|
||||
}
|
||||
|
||||
const body = { probed_at: new Date().toISOString(), ttl: FRESH_TTL_S, streamers };
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>DOTA2 上分帝</title>
|
||||
<link rel="icon" href="/ui-icon/dota2_logo.png" type="image/png" />
|
||||
<link rel="stylesheet" href="/style.css?v=0.5.109" />
|
||||
<script src="/mobile-gate.js?v=0.5.109"></script>
|
||||
<link rel="stylesheet" href="/style.css?v=0.5.110" />
|
||||
<script src="/mobile-gate.js?v=0.5.110"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1 class="sr-only">DOTA2 上分帝</h1>
|
||||
@@ -178,8 +178,8 @@
|
||||
|
||||
<section class="detail" id="detail" aria-live="polite"></section>
|
||||
|
||||
<script src="/config.js?v=0.5.109"></script>
|
||||
<script src="/router.js?v=0.5.109"></script>
|
||||
<script src="/app.js?v=0.5.109"></script>
|
||||
<script src="/config.js?v=0.5.110"></script>
|
||||
<script src="/router.js?v=0.5.110"></script>
|
||||
<script src="/app.js?v=0.5.110"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+63
-3
@@ -19,7 +19,9 @@ import argparse
|
||||
import json
|
||||
import mimetypes
|
||||
import threading
|
||||
import time
|
||||
import webbrowser
|
||||
from datetime import datetime, timezone
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from urllib.parse import urlparse
|
||||
|
||||
@@ -47,11 +49,15 @@ from shared.paths import (
|
||||
)
|
||||
from shared.relations import DEFAULT_RELATIONS, load_relations
|
||||
|
||||
from fetch_streamer_live import probe_streamers
|
||||
from mechanic_tags import QUERY_MECHANIC_ORDER, mechanic_query_payload
|
||||
|
||||
WEB_DIR = WEB_FRONTEND
|
||||
MOBILE_DEMAND_PATH = ROOT / "web" / ".refresh" / "mobile_demand.json"
|
||||
_MOBILE_DEMAND_LOCK = threading.Lock()
|
||||
_LIVE_STATUS_LOCK = threading.Lock()
|
||||
_LIVE_STATUS_TTL = 60
|
||||
_LIVE_STATUS_CACHE: tuple[float, dict] | None = None
|
||||
|
||||
|
||||
def _read_mobile_demand_count() -> int:
|
||||
@@ -76,6 +82,62 @@ def _inc_mobile_demand_count() -> int:
|
||||
return n
|
||||
|
||||
|
||||
def _probe_live_status() -> dict:
|
||||
"""Probe local streamer rooms without mutating streamers.json."""
|
||||
global _LIVE_STATUS_CACHE
|
||||
now = time.monotonic()
|
||||
cached = _LIVE_STATUS_CACHE
|
||||
if cached and now - cached[0] < _LIVE_STATUS_TTL:
|
||||
return cached[1]
|
||||
|
||||
with _LIVE_STATUS_LOCK:
|
||||
now = time.monotonic()
|
||||
cached = _LIVE_STATUS_CACHE
|
||||
if cached and now - cached[0] < _LIVE_STATUS_TTL:
|
||||
return cached[1]
|
||||
|
||||
try:
|
||||
payload = json.loads(STREAMERS_PATH.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
result = {
|
||||
"probed_at": datetime.now(timezone.utc).isoformat(),
|
||||
"ttl": _LIVE_STATUS_TTL,
|
||||
"streamers": {},
|
||||
}
|
||||
_LIVE_STATUS_CACHE = (now, result)
|
||||
return result
|
||||
|
||||
rows = payload.get("streamers")
|
||||
if not isinstance(rows, list):
|
||||
rows = []
|
||||
for row in rows:
|
||||
if isinstance(row, dict) and row.get("live_url"):
|
||||
# A failed probe is unknown and must not preserve a stale live badge.
|
||||
row["is_live"] = False
|
||||
row.pop("live_probed_at", None)
|
||||
|
||||
probe_streamers(payload)
|
||||
streamers = {}
|
||||
for row in rows:
|
||||
if not isinstance(row, dict) or not row.get("live_url"):
|
||||
continue
|
||||
sid = str(row.get("id") or "").strip()
|
||||
if not sid:
|
||||
continue
|
||||
cell = {"is_live": row.get("is_live") is True}
|
||||
if not row.get("live_probed_at"):
|
||||
cell["stale"] = True
|
||||
streamers[sid] = cell
|
||||
|
||||
result = {
|
||||
"probed_at": datetime.now(timezone.utc).isoformat(),
|
||||
"ttl": _LIVE_STATUS_TTL,
|
||||
"streamers": streamers,
|
||||
}
|
||||
_LIVE_STATUS_CACHE = (time.monotonic(), result)
|
||||
return result
|
||||
|
||||
|
||||
GRID_ORDER_PATH = DATA / "hero_grid_order.json"
|
||||
HERO_ITEMS_PATH = DATA / "hero_items.json"
|
||||
HERO_STATS_PATH = DATA / "hero_stats.json"
|
||||
@@ -842,9 +904,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self._json(200, build_payload())
|
||||
return
|
||||
if path == "/api/live-status":
|
||||
# Local-dev stub for the Pages Function (edge probing runs in
|
||||
# production only); the empty map keeps the UI on data.json is_live.
|
||||
self._json(200, {"probed_at": None, "ttl": 300, "streamers": {}})
|
||||
self._json(200, _probe_live_status())
|
||||
return
|
||||
if path == "/api/mobile-demand":
|
||||
self._json(200, {"count": _read_mobile_demand_count()})
|
||||
|
||||
Reference in New Issue
Block a user