"""Douyin source display from URL list + relay log (ported from sh2/src/douyin_source_display.py).""" from __future__ import annotations import re from typing import Any _DOUYIN_LIVE_URL_RE = re.compile(r"https?://(?:www\.)?live\.douyin\.com/([a-zA-Z0-9_-]+)", re.IGNORECASE) _DOUYIN_LIVE_ROOM_RE = re.compile(r"(?:https?://(?:www\.)?)?live\.douyin\.com/([a-zA-Z0-9_-]+)", re.IGNORECASE) _STATUS_TITLE_RE = re.compile( r"(?:序号\s*\d+\s*)?(.{1,80}?)\s*(?:等待直播|正在直播|直播中|已开播|开始推流|主播已下播|下播|录制中|推流中)", re.IGNORECASE, ) _RECORDING_TITLE_RE = re.compile(r"(?:正在录制|录制中)\s*\d*\s*个?\s*[::]\s*([^\[\r\n]{1,80})", re.IGNORECASE) _LIVE_TITLE_RE = re.compile( r"(?:序号\s*\d+\s*)?(.{1,80}?)\s*(?:正在直播中|正在直播|直播中|已开播|开始推流|录制中|推流中)", re.IGNORECASE, ) _WAIT_TITLE_RE = re.compile(r"(?:序号\s*\d+\s*)?(.{1,80}?)\s*(?:等待直播|主播已下播|下播)", re.IGNORECASE) _LABEL_TITLE_RE = re.compile(r"(?:主播|房间名|直播间|抖音源|源站|标题)\s*[::]\s*(.{1,80})", re.IGNORECASE) _BAD_TITLE_RE = re.compile(r"^\s*(?:-|—|_|无|未知|unknown|null|undefined|none|nan)?\s*$", re.IGNORECASE) _ERROR_CONTEXT_RE = re.compile(r"(?:获取失败|错误|异常|error|failed|exception)", re.IGNORECASE) def _clean_source_title(value: str) -> str: text = (value or "").strip().strip("\"'`[]()()【】") text = re.sub(r"^\[[A-Z]+\]\s*", "", text, flags=re.IGNORECASE) text = re.sub(r"^序号\s*\d+\s*", "", text) text = re.sub(r"^(?:主播|房间名|直播间|抖音源|源站|标题)\s*[::]\s*", "", text, flags=re.IGNORECASE) text = _DOUYIN_LIVE_ROOM_RE.sub("", text) text = re.sub(r"(?:等待直播|正在直播|直播中|已开播|开始推流|主播已下播|下播|录制中|推流中).*$", "", text).strip() text = text.strip(" ,,;;||-—::") if _BAD_TITLE_RE.match(text): return "" if "live.douyin.com" in text.lower(): return "" if re.fullmatch(r"\d{5,}", text): return "" if text.startswith(("监测", "格式", "错误", "无录制任务")): return "" return text[:48] def _url_entries_from_lines(url_lines: str) -> list[dict[str, str]]: entries: list[dict[str, str]] = [] for line in (url_lines or "").splitlines(): raw = line.strip() if not raw or raw.startswith("#"): continue match = _DOUYIN_LIVE_ROOM_RE.search(raw) if not match: continue room_id = match.group(1) tail = raw[match.end() :].strip(" ,,;;||-—::") title = _clean_source_title(tail) if not title: parts = re.split(r"[,,||]", raw, maxsplit=1) if len(parts) > 1: title = _clean_source_title(parts[1]) entries.append( { "room_id": room_id, "source_url": f"https://live.douyin.com/{room_id}", "source_title": title, } ) return entries def _log_indicates_active_relay(recent_log: str) -> bool: text = (recent_log or "").lower() if "frame=" in text and "speed=" in text: return True return bool(_RECORDING_TITLE_RE.search(recent_log or "")) or bool(_LIVE_TITLE_RE.search(recent_log or "")) def _title_from_waiting_log_only(recent_log: str) -> str: lines = [line.strip() for line in (recent_log or "").splitlines() if line.strip()] for line in reversed(lines[-240:]): if _ERROR_CONTEXT_RE.search(line): continue match = _WAIT_TITLE_RE.search(line) or _STATUS_TITLE_RE.search(line) if not match: continue title = _clean_source_title(match.group(1)) if title: return title return "" def _source_title_from_log(recent_log: str) -> str: lines = [line.strip() for line in (recent_log or "").splitlines() if line.strip()] tail = lines[-240:] for pattern in (_RECORDING_TITLE_RE, _LIVE_TITLE_RE): for line in reversed(tail): match = pattern.search(line) if not match: continue title = _clean_source_title(match.group(1)) if title: return title for line in reversed(tail): if _ERROR_CONTEXT_RE.search(line): continue match = _LABEL_TITLE_RE.search(line) if not match: continue title = _clean_source_title(match.group(1)) if title: return title for line in reversed(tail): match = _WAIT_TITLE_RE.search(line) or _STATUS_TITLE_RE.search(line) if not match: continue title = _clean_source_title(match.group(1)) if title: return title return "" def _current_room_id_from_log(recent_log: str) -> str: room_id = "" for match in _DOUYIN_LIVE_ROOM_RE.finditer(recent_log or ""): room_id = match.group(1) return room_id def _normalize_title_for_match(value: str) -> str: return re.sub(r"\s+", "", _clean_source_title(value)).casefold() def _entry_for_room(entries: list[dict[str, str]], room_id: str) -> dict[str, str] | None: if not room_id: return None for entry in entries: if entry.get("room_id") == room_id: return entry return None def _entry_for_title(entries: list[dict[str, str]], title: str) -> dict[str, str] | None: needle = _normalize_title_for_match(title) if not needle: return None for entry in entries: candidate = _normalize_title_for_match(entry.get("source_title", "")) if not candidate: continue if candidate == needle: return entry if len(candidate) >= 3 and len(needle) >= 3 and (candidate in needle or needle in candidate): return entry return None def extract_douyin_source_meta(url_lines: str = "", recent_log: str = "") -> dict[str, Any]: entries = _url_entries_from_lines(url_lines) log_room_id = _current_room_id_from_log(recent_log) log_title = _source_title_from_log(recent_log) matched_entry = _entry_for_title(entries, log_title) or _entry_for_room(entries, log_room_id) source_ambiguous = False source_current = False if matched_entry: room_id = matched_entry["room_id"] source_url = matched_entry["source_url"] title = log_title or matched_entry.get("source_title", "") source_current = bool(log_room_id or log_title) elif log_room_id: room_id = log_room_id source_url = f"https://live.douyin.com/{room_id}" title = log_title source_current = True elif len(entries) == 1: room_id = entries[0]["room_id"] source_url = entries[0]["source_url"] title = entries[0].get("source_title", "") or log_title elif log_title: room_id = "" source_url = "" title = log_title source_ambiguous = bool(entries) source_current = True elif entries: room_id = entries[0]["room_id"] source_url = entries[0]["source_url"] title = entries[0].get("source_title", "") source_ambiguous = len(entries) > 1 else: room_id = "" source_url = "" title = "" if ( len(entries) > 1 and title and not _log_indicates_active_relay(recent_log) and title == _title_from_waiting_log_only(recent_log) ): pinned = entries[0] room_id = pinned["room_id"] source_url = pinned["source_url"] title = pinned.get("source_title", "") or title source_ambiguous = True source_current = False display = title or (f"抖音直播间 {room_id}" if room_id else "") return { "source_title": title, "source_room_id": room_id, "source_url": source_url, "source_display": display, "source_ambiguous": source_ambiguous, "source_current": source_current, }