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
+30 -1
View File
@@ -13,6 +13,9 @@ Approach (verified 2026-07):
- Bilibili: public API ``api.live.bilibili.com/room/v1/Room/get_info`` with the
numeric room id taken from the ``live_url`` path; ``data.live_status == 1``
means live (0 offline, 2 replay — replay is treated as offline). No login.
- Douyu: public ``www.douyu.com/betard/{room_id}``; ``room.show_status == 1``
means live, ``== 2`` offline; ``room.videoLoop == 1`` (carousel) counts as
offline.
- Douyin: one shared cookie session is warmed up (www.douyin.com +
live.douyin.com), then per room we GET ``live.douyin.com/{web_rid}`` with a
browser UA (the numeric ``live_url`` path segment is the ``web_rid``). The
@@ -65,10 +68,12 @@ TIMEOUT = 20
# Douyin rate-limits aggressively; keep ~1s spacing between its requests.
DOUYIN_SPACING = 1.0
BILIBILI_SPACING = 0.5
DOUYU_SPACING = 0.3
DOUYIN_HOME = "https://www.douyin.com/"
DOUYIN_LIVE_HOME = "https://live.douyin.com/"
BILIBILI_INFO_URL = "https://api.live.bilibili.com/room/v1/Room/get_info?room_id={room_id}"
DOUYU_BETARD_URL = "https://www.douyu.com/betard/{room_id}"
# Escaped JSON inside the SSR pace chunks: \"roomStore\":{\"roomInfo\":{\"room\":{
DOUYIN_ROOMSTORE_RE = re.compile(
@@ -152,8 +157,27 @@ def probe_bilibili(room_id: str) -> bool:
return data.get("live_status") == 1
def probe_douyu(room_id: str) -> bool:
"""show_status: 1 live, 2 offline; videoLoop carousel counts as offline."""
payload = http_utils.http_json(
DOUYU_BETARD_URL.format(room_id=room_id), timeout=TIMEOUT
)
if not isinstance(payload, dict):
raise ValueError("douyu betard returned non-object")
room = payload.get("room")
if not isinstance(room, dict):
raise ValueError("douyu betard returned no room")
if int(room.get("videoLoop") or 0) == 1:
return False
status = room.get("show_status")
try:
return int(status) == 1
except (TypeError, ValueError) as e:
raise ValueError(f"douyu unexpected show_status {status!r}") from e
def room_ref_from_url(live_url: str) -> str | None:
"""First path segment of the live room URL (douyin web_rid / bilibili room id)."""
"""First path segment of the live room URL (douyin/bilibili/douyu room id)."""
path = urllib.parse.urlparse(live_url.strip()).path.strip("/")
if not path:
return None
@@ -167,6 +191,8 @@ def live_platform_from_url(live_url: str, fallback: str = "") -> str:
return "bilibili"
if "douyin.com" in host:
return "douyin"
if "douyu.com" in host:
return "douyu"
return (fallback or "").strip().lower()
@@ -210,6 +236,9 @@ def probe_streamers(
elif platform == "bilibili":
is_live = probe_bilibili(ref)
time.sleep(BILIBILI_SPACING)
elif platform == "douyu":
is_live = probe_douyu(ref)
time.sleep(DOUYU_SPACING)
else:
print(f"skip {sid}: platform={platform!r} unsupported", flush=True)
continue