|
|
|
@@ -50,6 +50,7 @@ def parseRenderers(entry: dict, context: dict = {}) -> dict:
|
|
|
|
match safeTraverse(list(entry.keys()), [0], default=""):
|
|
|
|
match safeTraverse(list(entry.keys()), [0], default=""):
|
|
|
|
|
|
|
|
|
|
|
|
case "videoRenderer": # represents a video
|
|
|
|
case "videoRenderer": # represents a video
|
|
|
|
|
|
|
|
# as of october 2025 slowly phased out in favor of lockupViewModel(?)
|
|
|
|
|
|
|
|
|
|
|
|
published_date = safeTraverse(entry, ["videoRenderer", "publishedTimeText", "simpleText"], default="now")
|
|
|
|
published_date = safeTraverse(entry, ["videoRenderer", "publishedTimeText", "simpleText"], default="now")
|
|
|
|
published_date = published_date.removeprefix("Streamed ")
|
|
|
|
published_date = published_date.removeprefix("Streamed ")
|
|
|
|
@@ -118,6 +119,7 @@ def parseRenderers(entry: dict, context: dict = {}) -> dict:
|
|
|
|
# retrieve the main channel's avatar
|
|
|
|
# retrieve the main channel's avatar
|
|
|
|
avatar_url = safeTraverse(livm, [0, "listItemViewModel", "leadingAccessory", "avatarViewModel", "image", "sources", 0, "url"], default=DEFAULT_AVATAR)
|
|
|
|
avatar_url = safeTraverse(livm, [0, "listItemViewModel", "leadingAccessory", "avatarViewModel", "image", "sources", 0, "url"], default=DEFAULT_AVATAR)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ythdd_globals.print_debug("videoRenderer fired")
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|
"type": "video",
|
|
|
|
"type": "video",
|
|
|
|
"title": safeTraverse(entry, ["videoRenderer", "title", "runs", 0, "text"]),
|
|
|
|
"title": safeTraverse(entry, ["videoRenderer", "title", "runs", 0, "text"]),
|
|
|
|
@@ -149,15 +151,67 @@ def parseRenderers(entry: dict, context: dict = {}) -> dict:
|
|
|
|
|
|
|
|
|
|
|
|
# modify the premiere timestamp afterwards here?
|
|
|
|
# modify the premiere timestamp afterwards here?
|
|
|
|
|
|
|
|
|
|
|
|
case "lockupViewModel": # represents playlists/mixes
|
|
|
|
case "lockupViewModel": # represents playlists/mixes (and videos since october 2025)
|
|
|
|
|
|
|
|
# related videos lvms are handled in ythdd_inv_tl.videos()
|
|
|
|
|
|
|
|
|
|
|
|
playlist_type = safeTraverse(entry, ["lockupViewModel", "contentImage", "collectionThumbnailViewModel", "primaryThumbnail", "thumbnailViewModel", "overlays", 0, "thumbnailOverlayBadgeViewModel", "thumbnailBadges", 0, "thumbnailBadgeViewModel", "icon", "sources", 0, "clientResource", "imageName"], default="PLAYLISTS")
|
|
|
|
lvm = entry["lockupViewModel"]
|
|
|
|
|
|
|
|
playlist_type = safeTraverse(lvm, ["contentImage", "collectionThumbnailViewModel", "primaryThumbnail", "thumbnailViewModel", "overlays", 0, "thumbnailOverlayBadgeViewModel", "thumbnailBadges", 0, "thumbnailBadgeViewModel", "icon", "sources", 0, "clientResource", "imageName"], default="")
|
|
|
|
|
|
|
|
|
|
|
|
if playlist_type == "MIX":
|
|
|
|
if playlist_type == "MIX":
|
|
|
|
# mixes aren't currently supported
|
|
|
|
# mixes aren't currently supported
|
|
|
|
return
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
lvm = entry["lockupViewModel"]
|
|
|
|
if not playlist_type:
|
|
|
|
|
|
|
|
# struct represents a video
|
|
|
|
|
|
|
|
ythdd_globals.print_debug("lockupViewModel fired (not a playlist). this is an a/b test; any following errors stem from it.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lmvm = safeTraverse(lvm, ['metadata', 'lockupMetadataViewModel'], default={})
|
|
|
|
|
|
|
|
video_id = safeTraverse(lvm, ['contentId'])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
author_name = safeTraverse(context, ["author_name"], default="Unknown author")
|
|
|
|
|
|
|
|
author_ucid = safeTraverse(context, ["author_ucid"], default="UNKNOWNCHANNELID")
|
|
|
|
|
|
|
|
verified = safeTraverse(context, ["verified"], default=False) # TODO: check if this can be retrieved here
|
|
|
|
|
|
|
|
avatar_url = safeTraverse(context, ["avatar"], default=DEFAULT_AVATAR)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
title = safeTraverse(lmvm, ["title", "content"], default="No title")
|
|
|
|
|
|
|
|
video_metadata = safeTraverse(lmvm, ["metadata", "contentMetadataViewModel", "metadataRows", 0, "metadataParts"], default=[])
|
|
|
|
|
|
|
|
view_count_text = safeTraverse(video_metadata, [0, "text", "content"], default="0 views")
|
|
|
|
|
|
|
|
published_date = safeTraverse(video_metadata, [1, "text", "content"], default="now")
|
|
|
|
|
|
|
|
length_text = safeTraverse(lvm, ["contentImage", "thumbnailViewModel", "overlays", ..., "thumbnailBottomOverlayViewModel", "badges", -1, "thumbnailBadgeViewModel", "text"], default="0:0")
|
|
|
|
|
|
|
|
view_count = parseViewsFromViewText(view_count_text)
|
|
|
|
|
|
|
|
length = parseLengthFromTimeBadge(length_text)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
resp = {
|
|
|
|
|
|
|
|
"type": "video",
|
|
|
|
|
|
|
|
"title": title,
|
|
|
|
|
|
|
|
"videoId": video_id,
|
|
|
|
|
|
|
|
"author": author_name,
|
|
|
|
|
|
|
|
"authorId": author_ucid,
|
|
|
|
|
|
|
|
"authorUrl": "/channel/" + author_ucid,
|
|
|
|
|
|
|
|
"authorVerified": verified, # TODO
|
|
|
|
|
|
|
|
"authorThumbnails": ythdd_extractor.generateChannelAvatarsFromUrl(avatar_url),
|
|
|
|
|
|
|
|
"videoThumbnails": ythdd_struct_builder.genThumbs(video_id),
|
|
|
|
|
|
|
|
"description": "", # can't be retrieved from lockupViewModel
|
|
|
|
|
|
|
|
"descriptionHtml": "",
|
|
|
|
|
|
|
|
"viewCount": view_count,
|
|
|
|
|
|
|
|
"viewCountText": view_count_text,
|
|
|
|
|
|
|
|
"published": int(dateparser.parse(published_date).timestamp()), # sadly best we can do, invidious does this too
|
|
|
|
|
|
|
|
"publishedText": published_date,
|
|
|
|
|
|
|
|
"lengthSeconds": length,
|
|
|
|
|
|
|
|
"liveNow": False, # can't be live if it's in creator's video feed
|
|
|
|
|
|
|
|
"premium": False, # todo: check this
|
|
|
|
|
|
|
|
"isUpcoming": False,
|
|
|
|
|
|
|
|
"isNew": False,
|
|
|
|
|
|
|
|
"is4k": False,
|
|
|
|
|
|
|
|
"is8k": False,
|
|
|
|
|
|
|
|
"isVr180": False,
|
|
|
|
|
|
|
|
"isVr360": False,
|
|
|
|
|
|
|
|
"is3d": False,
|
|
|
|
|
|
|
|
"hasCaptions": False
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return resp
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# struct represents a playlist
|
|
|
|
meta = safeTraverse(lvm, ["metadata"], default=[])
|
|
|
|
meta = safeTraverse(lvm, ["metadata"], default=[])
|
|
|
|
lmvm = safeTraverse(meta, ["lockupMetadataViewModel", "metadata", "contentMetadataViewModel", "metadataRows"], default=[])
|
|
|
|
lmvm = safeTraverse(meta, ["lockupMetadataViewModel", "metadata", "contentMetadataViewModel", "metadataRows"], default=[])
|
|
|
|
thumbnail = safeTraverse(lvm, ["contentImage", "collectionThumbnailViewModel", "primaryThumbnail", "thumbnailViewModel", "image", "sources", -1, "url"], default="no-url?")
|
|
|
|
thumbnail = safeTraverse(lvm, ["contentImage", "collectionThumbnailViewModel", "primaryThumbnail", "thumbnailViewModel", "image", "sources", -1, "url"], default="no-url?")
|
|
|
|
@@ -168,7 +222,7 @@ def parseRenderers(entry: dict, context: dict = {}) -> dict:
|
|
|
|
length = safeTraverse(lvm, ["contentImage", "collectionThumbnailViewModel", "primaryThumbnail", "thumbnailViewModel", "overlays", 0, "thumbnailOverlayBadgeViewModel", "thumbnailBadges", 0, "thumbnailBadgeViewModel", "text"], default="0 videos")
|
|
|
|
length = safeTraverse(lvm, ["contentImage", "collectionThumbnailViewModel", "primaryThumbnail", "thumbnailViewModel", "overlays", 0, "thumbnailOverlayBadgeViewModel", "thumbnailBadges", 0, "thumbnailBadgeViewModel", "text"], default="0 videos")
|
|
|
|
length = parseViewsFromViewText(length.split(" ")[0])
|
|
|
|
length = parseViewsFromViewText(length.split(" ")[0])
|
|
|
|
|
|
|
|
|
|
|
|
# Turns out for some responses we do some data, while not on others.
|
|
|
|
# Turns out for some responses we do have some data, while not on others.
|
|
|
|
# Data from context should be prioritized, thus even if something is found with safeTraverse,
|
|
|
|
# Data from context should be prioritized, thus even if something is found with safeTraverse,
|
|
|
|
# the parser will ignore it in favour of the context.
|
|
|
|
# the parser will ignore it in favour of the context.
|
|
|
|
ucid = safeTraverse(lmvm, [0, "metadataParts", 0, "text", "commandRuns", 0, "onTap", "innertubeCommand", "browseEndpoint", "browseId"], default="UNKNOWNCHANNELID")
|
|
|
|
ucid = safeTraverse(lmvm, [0, "metadataParts", 0, "text", "commandRuns", 0, "onTap", "innertubeCommand", "browseEndpoint", "browseId"], default="UNKNOWNCHANNELID")
|
|
|
|
@@ -176,6 +230,7 @@ def parseRenderers(entry: dict, context: dict = {}) -> dict:
|
|
|
|
ucid = safeTraverse(context, ["author_ucid"], default=ucid)
|
|
|
|
ucid = safeTraverse(context, ["author_ucid"], default=ucid)
|
|
|
|
author = safeTraverse(context, ["author_name"], default=author)
|
|
|
|
author = safeTraverse(context, ["author_name"], default=author)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ythdd_globals.print_debug("lockupViewModel fired (playlist)")
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|
"type": "playlist",
|
|
|
|
"type": "playlist",
|
|
|
|
"title": safeTraverse(meta, ["lockupMetadataViewModel", "title", "content"], default="ythdd: unknown title"),
|
|
|
|
"title": safeTraverse(meta, ["lockupMetadataViewModel", "title", "content"], default="ythdd: unknown title"),
|
|
|
|
@@ -227,6 +282,7 @@ def parseRenderers(entry: dict, context: dict = {}) -> dict:
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
avatar_url = "unknown"
|
|
|
|
avatar_url = "unknown"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ythdd_globals.print_debug("shortsLockupViewModel fired")
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|
"type": "video",
|
|
|
|
"type": "video",
|
|
|
|
"title": title,
|
|
|
|
"title": title,
|
|
|
|
@@ -269,6 +325,7 @@ def parseRenderers(entry: dict, context: dict = {}) -> dict:
|
|
|
|
published_date = safeTraverse(entry, ["gridVideoRenderer", "publishedTimeText", "simpleText"], default="now")
|
|
|
|
published_date = safeTraverse(entry, ["gridVideoRenderer", "publishedTimeText", "simpleText"], default="now")
|
|
|
|
published_date = published_date.removeprefix("Streamed ")
|
|
|
|
published_date = published_date.removeprefix("Streamed ")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ythdd_globals.print_debug("gridVideoRenderer fired")
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|
"type": "video",
|
|
|
|
"type": "video",
|
|
|
|
"title": safeTraverse(entry, ["gridVideoRenderer", "title", "simpleText"], default="unknown video title"),
|
|
|
|
"title": safeTraverse(entry, ["gridVideoRenderer", "title", "simpleText"], default="unknown video title"),
|
|
|
|
@@ -303,6 +360,7 @@ def parseRenderers(entry: dict, context: dict = {}) -> dict:
|
|
|
|
description, description_html = parseDescriptionSnippet(safeTraverse(entry, ["channelRenderer", "descriptionSnippet", "runs"], default=[]))
|
|
|
|
description, description_html = parseDescriptionSnippet(safeTraverse(entry, ["channelRenderer", "descriptionSnippet", "runs"], default=[]))
|
|
|
|
isVerified = ythdd_extractor.isVerified(safeTraverse(entry, ["channelRenderer", "ownerBadges", 0], default=[]))
|
|
|
|
isVerified = ythdd_extractor.isVerified(safeTraverse(entry, ["channelRenderer", "ownerBadges", 0], default=[]))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ythdd_globals.print_debug("channelRenderer fired")
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|
"type": "channel",
|
|
|
|
"type": "channel",
|
|
|
|
"author": safeTraverse(entry, ["channelRenderer", "title", "simpleText"], default="Unknown channel"),
|
|
|
|
"author": safeTraverse(entry, ["channelRenderer", "title", "simpleText"], default="Unknown channel"),
|
|
|
|
@@ -353,6 +411,7 @@ def parseRenderers(entry: dict, context: dict = {}) -> dict:
|
|
|
|
avatar_url = safeTraverse(entry, ["playlistVideoRenderer", "thumbnailOverlays", ..., "thumbnailOverlayAvatarStackViewModel", "avatarStack", "avatarStackViewModel", "avatars", 0, "avatarViewModel", "image", "sources", 0, "url"])
|
|
|
|
avatar_url = safeTraverse(entry, ["playlistVideoRenderer", "thumbnailOverlays", ..., "thumbnailOverlayAvatarStackViewModel", "avatarStack", "avatarStackViewModel", "avatars", 0, "avatarViewModel", "image", "sources", 0, "url"])
|
|
|
|
avatars = None if avatar_url is None else ythdd_extractor.generateChannelAvatarsFromUrl(avatar_url)
|
|
|
|
avatars = None if avatar_url is None else ythdd_extractor.generateChannelAvatarsFromUrl(avatar_url)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ythdd_globals.print_debug("playlistVideoRenderer fired")
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|
"type": "video",
|
|
|
|
"type": "video",
|
|
|
|
"title": title,
|
|
|
|
"title": title,
|
|
|
|
@@ -372,7 +431,7 @@ def parseRenderers(entry: dict, context: dict = {}) -> dict:
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
case _:
|
|
|
|
case _:
|
|
|
|
print("received an entry of unknown type:")
|
|
|
|
print("received an entry of unknown type (thus can't be parsed):")
|
|
|
|
print(entry)
|
|
|
|
print(entry)
|
|
|
|
print("")
|
|
|
|
print("")
|
|
|
|
# breakpoint()
|
|
|
|
# breakpoint()
|
|
|
|
|