v0.5.114: add Douyu streamers with fan enrichment and live probe.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
voson
2026-07-30 02:42:47 +08:00
co-authored by Cursor
parent fdd926e9bb
commit 38f46ad2ea
17 changed files with 477 additions and 38 deletions
+15 -1
View File
@@ -4364,7 +4364,14 @@ function buildStreamerMetaLine(row) {
const parts = [];
if (uniqueId) {
parts.push({
label: platform === "douyin" ? "抖音号" : platform === "bilibili" ? "UID" : "账号",
label:
platform === "douyin"
? "抖音号"
: platform === "bilibili"
? "UID"
: platform === "douyu"
? "房间号"
: "账号",
value: String(uniqueId),
account: true,
});
@@ -4373,6 +4380,13 @@ function buildStreamerMetaLine(row) {
if (Number.isFinite(Number(row.following_count))) {
parts.push({ label: "关注", value: formatStreamerCount(row.following_count) });
}
} else if (platform === "douyu") {
if (Number.isFinite(Number(row.following_count))) {
parts.push({ label: "关注", value: formatStreamerCount(row.following_count) });
}
if (Number.isFinite(Number(row.total_favorited))) {
parts.push({ label: "播放", value: formatStreamerCount(row.total_favorited) });
}
} else if (Number.isFinite(Number(row.total_favorited))) {
parts.push({ label: "获赞", value: formatStreamerCount(row.total_favorited) });
}
+1 -1
View File
@@ -1,5 +1,5 @@
/* Local defaults; production export overwrites via export_relations_site.py. */
var SITE_VERSION = "0.5.112";
var SITE_VERSION = "0.5.114";
var ABILITY_VIDEO_BASE = "";
var STATIC_ASSET_BASE = "";
+24 -1
View File
@@ -10,6 +10,8 @@
* Probe logic is a JS port of fetch_streamer_live.py:
* - Bilibili: api.live.bilibili.com Room/get_info; data.live_status === 1 is
* live (0 offline, 2 replay counts as offline).
* - Douyu: www.douyu.com/betard/<room_id>; room.show_status === 1 is live
* (2 offline); room.videoLoop === 1 (carousel) counts as offline.
* - Douyin: warm up cookies (www.douyin.com + live.douyin.com), then GET
* live.douyin.com/<web_rid> with a browser UA and parse the escaped JSON in
* the SSR pace chunks: roomStore.roomInfo.room.status (2 live / 4 offline);
@@ -44,6 +46,7 @@ const DOUYIN_HOME = "https://www.douyin.com/";
const DOUYIN_LIVE_HOME = "https://live.douyin.com/";
const BILIBILI_INFO_URL =
"https://api.live.bilibili.com/room/v1/Room/get_info?room_id=";
const DOUYU_BETARD_URL = "https://www.douyu.com/betard/";
// Escaped JSON inside the SSR pace chunks: \"roomStore\":{\"roomInfo\":{\"room\":{
const DOUYIN_ROOMSTORE_RE = /\\"roomStore\\":\s*\{\\"roomInfo\\":\s*\{\\"room\\":\s*\{/;
@@ -162,7 +165,25 @@ export async function probeBilibiliRoom(roomId) {
return data.live_status === 1;
}
/** First path segment of the live room URL (douyin web_rid / bilibili room id). */
/** show_status: 1 live, 2 offline; videoLoop carousel counts as offline. */
export async function probeDouyuRoom(roomId) {
const res = await fetchWithTimeout(DOUYU_BETARD_URL + roomId, {
headers: {
"User-Agent": BROWSER_UA,
Accept: "application/json",
Referer: "https://www.douyu.com/",
},
});
const payload = await res.json();
const room = payload && payload.room;
if (!room || typeof room !== "object") {
throw new Error("douyu betard returned no room");
}
if (Number(room.videoLoop || 0) === 1) return false;
return Number(room.show_status) === 1;
}
/** First path segment of the live room URL (douyin/bilibili/douyu room id). */
export function roomRefFromUrl(liveUrl) {
let path = "";
try {
@@ -180,6 +201,7 @@ export function livePlatformFromUrl(liveUrl, fallback = "") {
const host = new URL(String(liveUrl).trim()).hostname.toLowerCase();
if (host.includes("bilibili.com")) return "bilibili";
if (host.includes("douyin.com")) return "douyin";
if (host.includes("douyu.com")) return "douyu";
} catch {
/* keep fallback */
}
@@ -245,6 +267,7 @@ async function probeAll(targets) {
let isLive;
if (t.platform === "douyin") isLive = await probeDouyinRoom(t.ref, jar);
else if (t.platform === "bilibili") isLive = await probeBilibiliRoom(t.ref);
else if (t.platform === "douyu") isLive = await probeDouyuRoom(t.ref);
else throw new Error(`unsupported platform ${t.platform}`);
results.set(t.id, { is_live: isLive });
} catch (err) {
+5 -5
View File
@@ -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.112" />
<script src="/mobile-gate.js?v=0.5.112"></script>
<link rel="stylesheet" href="/style.css?v=0.5.114" />
<script src="/mobile-gate.js?v=0.5.114"></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.112"></script>
<script src="/router.js?v=0.5.112"></script>
<script src="/app.js?v=0.5.112"></script>
<script src="/config.js?v=0.5.114"></script>
<script src="/router.js?v=0.5.114"></script>
<script src="/app.js?v=0.5.114"></script>
</body>
</html>