feat: add video duration to YouTube announcements

Fetches duration via InnerTube player API for new videos at
announcement time. Displayed as compact h:mm:ss before views/likes.
Gracefully omitted for Shorts and unavailable content.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
user
2026-02-19 18:28:40 +01:00
parent 2d00360bc3
commit 8ce6922cc3
3 changed files with 111 additions and 6 deletions

View File

@@ -137,6 +137,47 @@ def _resolve_via_innertube(video_id: str) -> str | None:
return None
def _fetch_duration(video_id: str) -> int:
"""Fetch video duration in seconds via InnerTube player API. Blocking.
Returns 0 on failure or for live content.
"""
payload = json.dumps({
"context": {
"client": {
"clientName": "WEB",
"clientVersion": _YT_CLIENT_VERSION,
},
},
"videoId": video_id,
}).encode()
req = urllib.request.Request(_YT_PLAYER_URL, data=payload, method="POST")
req.add_header("Content-Type", "application/json")
try:
resp = _urlopen(req, timeout=_FETCH_TIMEOUT)
raw = resp.read()
resp.close()
data = json.loads(raw)
details = data.get("videoDetails") or {}
if details.get("isLiveContent") and details.get("isLive"):
return 0
secs = int(details.get("lengthSeconds", 0))
return secs
except Exception:
return 0
def _format_duration(seconds: int) -> str:
"""Format seconds as compact duration: 62 -> '1:02', 3661 -> '1:01:01'."""
if seconds <= 0:
return ""
h, rem = divmod(seconds, 3600)
m, s = divmod(rem, 60)
if h:
return f"{h}:{m:02d}:{s:02d}"
return f"{m}:{s:02d}"
def _resolve_channel(url: str) -> str | None:
"""Fetch YouTube page HTML and extract channel ID. Blocking.
@@ -337,11 +378,32 @@ async def _poll_once(bot, key: str, announce: bool = True) -> None:
channel = data["channel"]
name = data["name"]
shown = new_items[:_MAX_ANNOUNCE]
# Fetch durations for announced videos concurrently
durations: dict[str, int] = {}
video_ids = []
for item in shown:
vid = item["id"].removeprefix("yt:video:")
if vid != item["id"]:
video_ids.append((item["id"], vid))
if video_ids:
futs = {
item_id: loop.run_in_executor(None, _fetch_duration, vid)
for item_id, vid in video_ids
}
for item_id, fut in futs.items():
try:
durations[item_id] = await fut
except Exception:
pass
for item in shown:
title = _truncate(item["title"]) if item["title"] else "(no title)"
link = item["link"]
# Build metadata suffix
parts = []
dur = durations.get(item["id"], 0)
dur_str = _format_duration(dur)
if dur_str:
parts.append(dur_str)
views = item.get("views", 0)
likes = item.get("likes", 0)
if views: