diff --git a/README.md b/README.md index 1214c31cb4..498a93d0de 100644 --- a/README.md +++ b/README.md @@ -857,6 +857,7 @@ # OUTPUT TEMPLATE - `is_live` (boolean): Whether this video is a live stream or a fixed-length video - `was_live` (boolean): Whether this video was originally a live stream - `playable_in_embed` (string): Whether this video is allowed to play in embedded players on other sites + - `availability` (string): Whether the video is 'private', 'premium_only', 'subscriber_only', 'needs_auth', 'unlisted' or 'public' - `start_time` (numeric): Time in seconds where the reproduction should start, as specified in the URL - `end_time` (numeric): Time in seconds where the reproduction should end, as specified in the URL - `format` (string): A human-readable description of the format diff --git a/yt_dlp/extractor/common.py b/yt_dlp/extractor/common.py index 839bdeaf30..4205a3f8c0 100644 --- a/yt_dlp/extractor/common.py +++ b/yt_dlp/extractor/common.py @@ -301,7 +301,11 @@ class InfoExtractor(object): playable_in_embed: Whether this video is allowed to play in embedded players on other sites. Can be True (=always allowed), False (=never allowed), None (=unknown), or a string - specifying the criteria for embedability (Eg: 'whitelist'). + specifying the criteria for embedability (Eg: 'whitelist') + availability: Under what condition the video is available. One of + 'private', 'premium_only', 'subscriber_only', 'needs_auth', + 'unlisted' or 'public'. Use 'InfoExtractor._availability' + to set it __post_extractor: A function to be called just before the metadata is written to either disk, logger or console. The function must return a dict which will be added to the info_dict. @@ -3332,6 +3336,20 @@ def _generic_id(self, url): def _generic_title(self, url): return compat_urllib_parse_unquote(os.path.splitext(url_basename(url))[0]) + @staticmethod + def _availability(is_private, needs_premium, needs_subscription, needs_auth, is_unlisted): + all_known = all(map( + lambda x: x is not None, + (is_private, needs_premium, needs_subscription, needs_auth, is_unlisted))) + return ( + 'private' if is_private + else 'premium_only' if needs_premium + else 'subscriber_only' if needs_subscription + else 'needs_auth' if needs_auth + else 'unlisted' if is_unlisted + else 'public' if all_known + else None) + class SearchInfoExtractor(InfoExtractor): """ diff --git a/yt_dlp/extractor/youtube.py b/yt_dlp/extractor/youtube.py index b0f52ff96b..6c93517d44 100644 --- a/yt_dlp/extractor/youtube.py +++ b/yt_dlp/extractor/youtube.py @@ -25,6 +25,7 @@ ) from ..jsinterp import JSInterpreter from ..utils import ( + bool_or_none, clean_html, dict_get, ExtractorError, @@ -2066,7 +2067,7 @@ def feed_entry(name): 'tags': keywords, 'is_live': is_live, 'playable_in_embed': playability_status.get('playableInEmbed'), - 'was_live': video_details.get('isLiveContent') + 'was_live': video_details.get('isLiveContent'), } pctr = try_get( @@ -2283,6 +2284,30 @@ def chapter_time(mmlir): if v: info[d_k] = v + is_private = bool_or_none(video_details.get('isPrivate')) + is_unlisted = bool_or_none(microformat.get('isUnlisted')) + is_membersonly = None + if initial_data and is_private is not None: + is_membersonly = False + contents = try_get(initial_data, lambda x: x['contents']['twoColumnWatchNextResults']['results']['results']['contents'], list) + for content in contents or []: + badges = try_get(content, lambda x: x['videoPrimaryInfoRenderer']['badges'], list) + for badge in badges or []: + label = try_get(badge, lambda x: x['metadataBadgeRenderer']['label']) or '' + if label.lower() == 'members only': + is_membersonly = True + break + if is_membersonly: + break + + # TODO: Add this for playlists + info['availability'] = self._availability( + is_private=is_private, + needs_premium=False, # Youtube no longer have premium-only videos? + needs_subscription=is_membersonly, + needs_auth=info['age_limit'] >= 18, + is_unlisted=None if is_private is None else is_unlisted) + # get xsrf for annotations or comments get_annotations = self._downloader.params.get('writeannotations', False) get_comments = self._downloader.params.get('getcomments', False)