From 256d21bbcd6c69a0711b8de817f0eed34e3cd733 Mon Sep 17 00:00:00 2001 From: sherl Date: Fri, 12 Sep 2025 00:15:06 +0200 Subject: [PATCH] fix: fixes to context creation, avatar url generation also implemented basic badge extraction and continuation for channels --- ythdd_extractor.py | 45 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/ythdd_extractor.py b/ythdd_extractor.py index db35dc4..cb0dac1 100644 --- a/ythdd_extractor.py +++ b/ythdd_extractor.py @@ -231,7 +231,7 @@ def makeWebContext(secondaryContextDict: dict): # Uses web_context_dict to create a context, returns a dict. # Essentially, expands the web_context_dict with a secondary one. - current_web_context_dict = web_context_dict + current_web_context_dict = web_context_dict.copy() for key in secondaryContextDict: current_web_context_dict[key] = secondaryContextDict[key] @@ -275,13 +275,17 @@ def getChannelAvatar(response_json: dict): def generateChannelAvatarsFromUrl(url: str, proxied: bool = True) -> list: # Generates channel avatars at default sizes. + # avatar urls for channels in search results start with //yt3.ggpht.com/ + if url.startswith("//yt3.ggpht.com/"): + url = url.replace("//yt3.ggpht.com/", "https://yt3.ggpht.com/") + avatars = [] - if not url.startswith("https://yt3.ggpht.com/"): + if not url.startswith("https://yt3.ggpht.com/") and not url.startswith("https://yt3.googleusercontent.com/"): return [] url = ythdd_globals.translateLinks(url) url_size_start = url.rfind("=s") + 2 - url_size_end = url. find("-", url_size_start) - 1 + url_size_end = url. find("-", url_size_start) default_sizes = [32, 48, 76, 100, 176, 512] @@ -296,20 +300,45 @@ def generateChannelAvatarsFromUrl(url: str, proxied: bool = True) -> list: return avatars -def isVerified(response_json: dict): +def isVerified(response_json: dict) -> bool: # Returns True if any user badge has been found (verified/artist). - badges = safeTraverse(response_json, [], default=False) - if badges: return True + if not isinstance(response_json, dict): + return False + + match safeTraverse(list(response_json.keys()), [0], default=""): + case "metadataBadgeRenderer": # channels in search results + verified = safeTraverse(response_json, ["metadataBadgeRenderer", "tooltip"], default="") in ("Verified") # room for support of artist channels + return verified + return False -def browseAbout(ucid: str): +def isPremium(response_json: dict) -> bool: + # Returns True if content is paid (member-only). + + if not isinstance(response_json, dict): + return False + + match safeTraverse(list(response_json.keys()), [0], default=""): + case "metadataBadgeRenderer": # channels in search results + paid = safeTraverse(response_json, ["metadataBadgeRenderer", "style"], default="") in ("BADGE_STYLE_TYPE_MEMBERS_ONLY") + return paid + + return False + +def browseChannel(ucid: str, params: str = None, ctoken: str = None): # Returns the response from innertubes browse endpoint for channels (as a dict). if len(ucid) != 24: raise ValueError(f"Something is wrong with the UCID {ucid}. Expected a 24-character long channel ID, not {len(ucid)}.") - context = makeWebContext({'browseId': ucid}) + additional_context = {'browseId': ucid} + if params is not None: + additional_context['params'] = params + if ctoken is not None: + additional_context['continuation'] = ctoken + + context = makeWebContext(additional_context) response = requests.post( 'https://www.youtube.com/youtubei/v1/browse?prettyPrint=false',