feat: playlist browsing

pagination still needs refinement for some of the clients.
on another note, this is an anniversary commit, as ythdd turns 1 year
today.
This commit is contained in:
2025-09-25 23:30:59 +02:00
parent 6d0c70696b
commit 1c9174c888
4 changed files with 228 additions and 3 deletions

View File

@@ -312,6 +312,34 @@ def parseRenderers(entry: dict, context: dict = {}) -> dict:
"descriptionHtml": description_html
}
case "playlistVideoRenderer":
video_id = safeTraverse(entry, ["playlistVideoRenderer", "videoId"], default="UnknownVideoId")
title = safeTraverse(entry, ["playlistVideoRenderer", "title", "runs", 0, "text"], default="Unknown video title")
author_ucid = safeTraverse(entry, ["playlistVideoRenderer", "shortBylineText", "runs", 0, "navigationEndpoint", "browseEndpoint", "browseId"], default="UNKNOWNCHANNELID")
author_name = safeTraverse(entry, ["playlistVideoRenderer", "shortBylineText", "runs", 0, "text"], default="Unknown author")
video_index = int(safeTraverse(entry, ["playlistVideoRenderer", "index", "simpleText"], default="1")) - 1
length = parseLengthFromTimeBadge(safeTraverse(entry, ["playlistVideoRenderer", "lengthText", "simpleText"], default="0:0"))
published_date = safeTraverse(entry, ["playlistVideoRenderer", "videoInfo", "runs", -1, "text"], default="2000-01-01")
published_date = published_date.removeprefix("Streamed ")
return {
"type": "video",
"title": title,
"videoId": video_id,
"author": author_name,
"authorId": author_ucid,
"authorUrl": "/channel/" + author_ucid,
"videoThumbnails": genThumbs(video_id),
"index": video_index,
"lengthSeconds": length,
"liveNow": False, # todo: check this?
# these do not need to be returned, but some clients try to read it
# so we return an approximation here:
"published": int(dateparser.parse(published_date).timestamp()),
"publishedText": published_date
}
case _:
print("received an entry of unknown type:")
print(entry)
@@ -372,3 +400,27 @@ def parseDescriptionSnippet(snippet: list):
text_html = escape(text_html).replace("\r\n", "<br>").replace("\n", "<br>")
return text, text_html
def runsToText(runs: list, default: str = "") -> str:
# "default" will be returned when text extraction fails.
extracted_text = ""
for field in runs:
extracted_text += safeTraverse(field, ["text"], default="")
if extracted_text:
return extracted_text
return default
def extractTextFromSimpleOrRuns(obj: dict, default: str = "") -> str:
# Extracts the text both from "runs" and "simpleText"
# with failsafe to default.
text = default
if "runs" in obj:
text = runsToText(obj["runs"])
elif "simpleText" in obj:
text = obj["simpleText"]
else:
print(f"error(extractTextFromSimpleOrRuns): text extraction failed for {obj}")
return text