Fix comments (Python API), add dislike bar
Comments: switch from CLI --write-comments to yt-dlp Python API with getcomments=True — more reliable, proper extractor_args dict format Dislikes: add dislike_count column, fetch from returnyoutubedislike.com after each video metadata upsert (5s timeout, non-fatal) UI: replace emoji like count with a like/dislike ratio bar — blue fill showing like proportion, labels on each end; views stay in meta row Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -76,6 +76,7 @@ def _normalize_video(info: dict) -> dict:
|
||||
"chapters": json.dumps(chapters) if chapters else None,
|
||||
"view_count": info.get("view_count"),
|
||||
"like_count": info.get("like_count"),
|
||||
"dislike_count": info.get("dislike_count"),
|
||||
"channel": {
|
||||
"youtube_channel_id": info.get("channel_id"),
|
||||
"name": info.get("channel") or info.get("uploader", ""),
|
||||
@@ -385,24 +386,47 @@ def fetch_channel_links(channel_id: str) -> list[str]:
|
||||
|
||||
|
||||
def fetch_video_comments(youtube_video_id: str, max_comments: int = 20) -> list[dict]:
|
||||
"""Fetch top comments for a single video. Returns empty list on failure."""
|
||||
url = f"https://www.youtube.com/watch?v={youtube_video_id}"
|
||||
args = [
|
||||
"yt-dlp", url,
|
||||
"--dump-json",
|
||||
"--write-comments",
|
||||
"--extractor-args", f"youtube:max_comments={max_comments};comment_sort=top",
|
||||
"--no-download",
|
||||
"--no-playlist",
|
||||
"--quiet",
|
||||
*_cookie_args(),
|
||||
]
|
||||
stdout, _, code = _run(args, timeout=60)
|
||||
if not stdout.strip():
|
||||
return []
|
||||
"""Fetch top comments via yt-dlp Python API. Returns empty list on failure."""
|
||||
import yt_dlp as _yt_dlp
|
||||
|
||||
with _cookies_lock:
|
||||
cf = _cookies_file
|
||||
b = _cookies_browser
|
||||
oauth2 = _use_oauth2
|
||||
|
||||
ydl_opts = {
|
||||
"quiet": True,
|
||||
"no_warnings": True,
|
||||
"skip_download": True,
|
||||
"getcomments": True,
|
||||
"extractor_args": {
|
||||
"youtube": {
|
||||
"max_comments": [str(max_comments), str(max_comments)],
|
||||
"comment_sort": ["top"],
|
||||
}
|
||||
},
|
||||
}
|
||||
if oauth2:
|
||||
ydl_opts["username"] = "oauth2"
|
||||
ydl_opts["password"] = ""
|
||||
elif cf and Path(cf).exists():
|
||||
ydl_opts["cookiefile"] = cf
|
||||
else:
|
||||
for candidate in _AUTO_COOKIES_PATHS:
|
||||
if Path(candidate).exists():
|
||||
ydl_opts["cookiefile"] = candidate
|
||||
break
|
||||
else:
|
||||
if b and not cf:
|
||||
ydl_opts["cookiesfrombrowser"] = (b,)
|
||||
|
||||
try:
|
||||
info = json.loads(stdout.strip())
|
||||
except json.JSONDecodeError:
|
||||
with _yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
info = ydl.extract_info(f"https://www.youtube.com/watch?v={youtube_video_id}", download=False)
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
if not info:
|
||||
return []
|
||||
|
||||
result = []
|
||||
@@ -410,20 +434,29 @@ def fetch_video_comments(youtube_video_id: str, max_comments: int = 20) -> list[
|
||||
if c.get("parent") not in (None, "root"):
|
||||
continue # skip replies
|
||||
ts = c.get("timestamp")
|
||||
published_at = datetime.utcfromtimestamp(ts) if ts else None
|
||||
result.append({
|
||||
"youtube_comment_id": c.get("id"),
|
||||
"author": c.get("author"),
|
||||
"text": c.get("text"),
|
||||
"likes": c.get("like_count") or 0,
|
||||
"is_pinned": bool(c.get("is_pinned")),
|
||||
"published_at": published_at,
|
||||
"published_at": datetime.utcfromtimestamp(ts) if ts else None,
|
||||
})
|
||||
# Sort pinned first, then by likes
|
||||
result.sort(key=lambda c: (not c["is_pinned"], -(c["likes"] or 0)))
|
||||
return result[:max_comments]
|
||||
|
||||
|
||||
def fetch_dislike_count(youtube_video_id: str) -> int | None:
|
||||
"""Fetch dislike count from returnyoutubedislike.com (crowdsourced)."""
|
||||
try:
|
||||
url = f"https://returnyoutubedislikeapi.com/votes?videoId={youtube_video_id}"
|
||||
with urllib.request.urlopen(url, timeout=5) as resp:
|
||||
data = json.loads(resp.read())
|
||||
return data.get("dislikes")
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
QUALITY_FORMATS = {
|
||||
"best": "bestvideo[ext=mp4][vcodec^=avc1]+bestaudio[ext=m4a]/bestvideo[ext=mp4]+bestaudio[ext=m4a]/22/18/bestvideo+bestaudio/best",
|
||||
"2160p": "bestvideo[ext=mp4][height<=2160]+bestaudio[ext=m4a]/bestvideo[height<=2160]+bestaudio/best[height<=2160]",
|
||||
|
||||
Reference in New Issue
Block a user