v0.5.114: add Douyu streamers with fan enrichment and live probe.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user