[cleanup] Minor cleanup

Closes #1696, Closes #1673
This commit is contained in:
pukkandan 2021-11-19 05:36:28 +05:30
parent 467b6b8387
commit 9222c38182
No known key found for this signature in database
GPG Key ID: 0F00D95A001F4698
12 changed files with 74 additions and 60 deletions

View File

@ -209,7 +209,7 @@ ## Adding support for a new site
``` ```
1. Add an import in [`yt_dlp/extractor/extractors.py`](yt_dlp/extractor/extractors.py). 1. Add an import in [`yt_dlp/extractor/extractors.py`](yt_dlp/extractor/extractors.py).
1. Run `python test/test_download.py TestDownload.test_YourExtractor`. This *should fail* at first, but you can continually re-run it until you're done. If you decide to add more than one test, the tests will then be named `TestDownload.test_YourExtractor`, `TestDownload.test_YourExtractor_1`, `TestDownload.test_YourExtractor_2`, etc. Note that tests with `only_matching` key in test's dict are not counted in. You can also run all the tests in one go with `TestDownload.test_YourExtractor_all` 1. Run `python test/test_download.py TestDownload.test_YourExtractor`. This *should fail* at first, but you can continually re-run it until you're done. If you decide to add more than one test, the tests will then be named `TestDownload.test_YourExtractor`, `TestDownload.test_YourExtractor_1`, `TestDownload.test_YourExtractor_2`, etc. Note that tests with `only_matching` key in test's dict are not counted in. You can also run all the tests in one go with `TestDownload.test_YourExtractor_all`
1. Make sure you have atleast one test for your extractor. Even if all videos covered by the extractor are expected to be inaccessible for automated testing, tests should still be added with a `skip` parameter indicating why the purticular test is disabled from running. 1. Make sure you have atleast one test for your extractor. Even if all videos covered by the extractor are expected to be inaccessible for automated testing, tests should still be added with a `skip` parameter indicating why the particular test is disabled from running.
1. Have a look at [`yt_dlp/extractor/common.py`](yt_dlp/extractor/common.py) for possible helper methods and a [detailed description of what your extractor should and may return](yt_dlp/extractor/common.py#L91-L426). Add tests and code for as many as you want. 1. Have a look at [`yt_dlp/extractor/common.py`](yt_dlp/extractor/common.py) for possible helper methods and a [detailed description of what your extractor should and may return](yt_dlp/extractor/common.py#L91-L426). Add tests and code for as many as you want.
1. Make sure your code follows [yt-dlp coding conventions](#yt-dlp-coding-conventions) and check the code with [flake8](https://flake8.pycqa.org/en/latest/index.html#quickstart): 1. Make sure your code follows [yt-dlp coding conventions](#yt-dlp-coding-conventions) and check the code with [flake8](https://flake8.pycqa.org/en/latest/index.html#quickstart):

View File

@ -40,7 +40,7 @@ ### 2021.11.10
* [fragment] Fix progress display in fragmented downloads * [fragment] Fix progress display in fragmented downloads
* [downloader/ffmpeg] Fix vtt download with ffmpeg * [downloader/ffmpeg] Fix vtt download with ffmpeg
* [ffmpeg] Detect presence of setts and libavformat version * [ffmpeg] Detect presence of setts and libavformat version
* [ExtractAudio] Rescale --audio-quality correctly by [CrypticSignal](https://github.com/CrypticSignal), [pukkandan](https://github.com/pukkandan) * [ExtractAudio] Rescale `--audio-quality` correctly by [CrypticSignal](https://github.com/CrypticSignal), [pukkandan](https://github.com/pukkandan)
* [ExtractAudio] Use `libfdk_aac` if available by [CrypticSignal](https://github.com/CrypticSignal) * [ExtractAudio] Use `libfdk_aac` if available by [CrypticSignal](https://github.com/CrypticSignal)
* [FormatSort] `eac3` is better than `ac3` * [FormatSort] `eac3` is better than `ac3`
* [FormatSort] Fix some fields' defaults * [FormatSort] Fix some fields' defaults

View File

@ -96,7 +96,7 @@ # NEW FEATURES
* **New playlist extractors**: bilibili categories, eroprofile albums, hotstar series, hungama albums, newgrounds user, niconico search/users, paramountplus series, patreon user, peertube playlist/channels, roosterteeth series, sonyliv series, tiktok user, trovo channels, voot series * **New playlist extractors**: bilibili categories, eroprofile albums, hotstar series, hungama albums, newgrounds user, niconico search/users, paramountplus series, patreon user, peertube playlist/channels, roosterteeth series, sonyliv series, tiktok user, trovo channels, voot series
* **Fixed/improved extractors**: 7plus, 9now, afreecatv, akamai, aljazeera, amcnetworks, animalplanet, archive.org, arte, atv, bbc, bilibili, bitchute, bravotv, camtube, cbc, cda, ceskatelevize, chingari, comedycentral, coub, crackle, crunchyroll, curiositystream, diynetwork, dw, eroprofile, facebook, francetv, funimation, globo, hearthisatie, hidive, hotstar, hungama, imdb, ina, instagram, iprima, itv, iwara, kakao, la7, linkedinlearning, linuxacadamy, mediaset, mediasite, motherless, mxplayer, nbcolympics, ndr, newgrounds, niconico, nitter, nova, nrk, nuvid, oreilly, paramountplus, parliamentlive, patreon, pbs, peertube, plutotv, polskieradio, pornhub, reddit, reddit, redtube, rmcdecouverte, roosterteeth, rtp, rumble, saml verizon login, skyit, sonyliv, soundcloud, southparkde, spankbang, spreaker, streamable, tagesschau, tbs, tennistv, tenplay, tiktok, tubi, tv2, tv2hu, tv5mondeplus, tvp, twitcasting, vh1, viafree, videa, vidio, vidme, viewlift, viki, vimeo, viu, vk, vlive, vrt, wakanim, xhamster, yahoo * **Fixed/improved extractors**: 7plus, 9now, afreecatv, akamai, aljazeera, amcnetworks, animalplanet, archive.org, arte, atv, bbc, bilibili, bitchute, bravotv, camtube, cbc, cda, ceskatelevize, chingari, comedycentral, coub, crackle, crunchyroll, curiositystream, diynetwork, dw, eroprofile, facebook, francetv, funimation, globo, hearthisatie, hidive, hotstar, hungama, imdb, ina, instagram, iprima, itv, iwara, kakao, la7, linkedinlearning, linuxacadamy, mediaset, mediasite, motherless, mxplayer, nbcolympics, ndr, newgrounds, niconico, nitter, nova, nrk, nuvid, oreilly, paramountplus, parliamentlive, patreon, pbs, peertube, plutotv, polskieradio, pornhub, reddit, redtube, rmcdecouverte, roosterteeth, rtp, rumble, saml verizon login, skyit, sonyliv, soundcloud, southparkde, spankbang, spreaker, streamable, tagesschau, tbs, tennistv, tenplay, tiktok, tubi, tv2, tv2hu, tv5mondeplus, tvp, twitcasting, vh1, viafree, videa, vidio, vidme, viewlift, viki, vimeo, viu, vk, vlive, vrt, wakanim, xhamster, yahoo
* **New MSOs**: Philo, Spectrum, SlingTV, Cablevision, RCN * **New MSOs**: Philo, Spectrum, SlingTV, Cablevision, RCN
@ -136,7 +136,7 @@ ### Differences in default behavior
* Unlike youtube-dlc, yt-dlp does not allow merging multiple audio/video streams into one file by default (since this conflicts with the use of `-f bv*+ba`). If needed, this feature must be enabled using `--audio-multistreams` and `--video-multistreams`. You can also use `--compat-options multistreams` to enable both * Unlike youtube-dlc, yt-dlp does not allow merging multiple audio/video streams into one file by default (since this conflicts with the use of `-f bv*+ba`). If needed, this feature must be enabled using `--audio-multistreams` and `--video-multistreams`. You can also use `--compat-options multistreams` to enable both
* `--ignore-errors` is enabled by default. Use `--abort-on-error` or `--compat-options abort-on-error` to abort on errors instead * `--ignore-errors` is enabled by default. Use `--abort-on-error` or `--compat-options abort-on-error` to abort on errors instead
* When writing metadata files such as thumbnails, description or infojson, the same information (if available) is also written for playlists. Use `--no-write-playlist-metafiles` or `--compat-options no-playlist-metafiles` to not write these files * When writing metadata files such as thumbnails, description or infojson, the same information (if available) is also written for playlists. Use `--no-write-playlist-metafiles` or `--compat-options no-playlist-metafiles` to not write these files
* `--add-metadata` attaches the `infojson` to `mkv` files in addition to writing the metadata when used with `--write-infojson`. Use `--compat-options no-attach-info-json` to revert this * `--add-metadata` attaches the `infojson` to `mkv` files in addition to writing the metadata when used with `--write-info-json`. Use `--no-embed-info-json` or `--compat-options no-attach-info-json` to revert this
* Some metadata are embedded into different fields when using `--add-metadata` as compared to youtube-dl. Most notably, `comment` field contains the `webpage_url` and `synopsis` contains the `description`. You can [use `--parse-metadata`](https://github.com/yt-dlp/yt-dlp#modifying-metadata) to modify this to your liking or use `--compat-options embed-metadata` to revert this * Some metadata are embedded into different fields when using `--add-metadata` as compared to youtube-dl. Most notably, `comment` field contains the `webpage_url` and `synopsis` contains the `description`. You can [use `--parse-metadata`](https://github.com/yt-dlp/yt-dlp#modifying-metadata) to modify this to your liking or use `--compat-options embed-metadata` to revert this
* `playlist_index` behaves differently when used with options like `--playlist-reverse` and `--playlist-items`. See [#302](https://github.com/yt-dlp/yt-dlp/issues/302) for details. You can use `--compat-options playlist-index` if you want to keep the earlier behavior * `playlist_index` behaves differently when used with options like `--playlist-reverse` and `--playlist-items`. See [#302](https://github.com/yt-dlp/yt-dlp/issues/302) for details. You can use `--compat-options playlist-index` if you want to keep the earlier behavior
* The output of `-F` is listed in a new format. Use `--compat-options list-formats` to revert this * The output of `-F` is listed in a new format. Use `--compat-options list-formats` to revert this
@ -196,7 +196,7 @@ ### With [PIP](https://pypi.org/project/pip)
If you want to be on the cutting edge, you can also install the master branch with: If you want to be on the cutting edge, you can also install the master branch with:
``` ```
python3 -m pip3 install --force-reinstall https://github.com/yt-dlp/yt-dlp/archive/master.zip python3 -m pip install --force-reinstall https://github.com/yt-dlp/yt-dlp/archive/master.zip
``` ```
Note that on some systems, you may need to use `py` or `python` instead of `python3` Note that on some systems, you may need to use `py` or `python` instead of `python3`
@ -793,7 +793,7 @@ ## Post-Processing Options:
--audio-format FORMAT Specify audio format to convert the audio --audio-format FORMAT Specify audio format to convert the audio
to when -x is used. Currently supported to when -x is used. Currently supported
formats are: best (default) or one of formats are: best (default) or one of
best|aac|flac|mp3|m4a|opus|vorbis|wav best|aac|flac|mp3|m4a|opus|vorbis|wav|alac
--audio-quality QUALITY Specify ffmpeg audio quality, insert a --audio-quality QUALITY Specify ffmpeg audio quality, insert a
value between 0 (best) and 10 (worst) for value between 0 (best) and 10 (worst) for
VBR or a specific bitrate like 128K VBR or a specific bitrate like 128K
@ -844,15 +844,20 @@ ## Post-Processing Options:
--no-embed-subs Do not embed subtitles (default) --no-embed-subs Do not embed subtitles (default)
--embed-thumbnail Embed thumbnail in the video as cover art --embed-thumbnail Embed thumbnail in the video as cover art
--no-embed-thumbnail Do not embed thumbnail (default) --no-embed-thumbnail Do not embed thumbnail (default)
--embed-metadata Embed metadata to the video file. Also adds --embed-metadata Embed metadata to the video file. Also
chapters to file unless --no-add-chapters embeds chapters/infojson if present unless
is used (Alias: --add-metadata) --no-embed-chapters/--no-embed-info-json
are used (Alias: --add-metadata)
--no-embed-metadata Do not add metadata to file (default) --no-embed-metadata Do not add metadata to file (default)
(Alias: --no-add-metadata) (Alias: --no-add-metadata)
--embed-chapters Add chapter markers to the video file --embed-chapters Add chapter markers to the video file
(Alias: --add-chapters) (Alias: --add-chapters)
--no-embed-chapters Do not add chapter markers (default) --no-embed-chapters Do not add chapter markers (default)
(Alias: --no-add-chapters) (Alias: --no-add-chapters)
--embed-info-json Embed the infojson as an attachment to
mkv/mka video files
--no-embed-info-json Do not embed the infojson as an attachment
to the video file
--parse-metadata FROM:TO Parse additional metadata like title/artist --parse-metadata FROM:TO Parse additional metadata like title/artist
from other fields; see "MODIFYING METADATA" from other fields; see "MODIFYING METADATA"
for details for details
@ -1210,11 +1215,14 @@ #### Output template examples
Note that on Windows you need to use double quotes instead of single. Note that on Windows you need to use double quotes instead of single.
```bash ```bash
$ yt-dlp --get-filename -o 'test video.%(ext)s' BaW_jenozKc
test video.webm # Literal name with correct extension
$ yt-dlp --get-filename -o '%(title)s.%(ext)s' BaW_jenozKc $ yt-dlp --get-filename -o '%(title)s.%(ext)s' BaW_jenozKc
youtube-dl test video ''_ä↭𝕐.mp4 # All kinds of weird characters youtube-dl test video ''_ä↭𝕐.webm # All kinds of weird characters
$ yt-dlp --get-filename -o '%(title)s.%(ext)s' BaW_jenozKc --restrict-filenames $ yt-dlp --get-filename -o '%(title)s.%(ext)s' BaW_jenozKc --restrict-filenames
youtube-dl_test_video_.mp4 # A simple file name youtube-dl_test_video_.webm # Restricted file name
# Download YouTube playlist videos in separate directory indexed by video order in a playlist # Download YouTube playlist videos in separate directory indexed by video order in a playlist
$ yt-dlp -o '%(playlist)s/%(playlist_index)s - %(title)s.%(ext)s' https://www.youtube.com/playlist?list=PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re $ yt-dlp -o '%(playlist)s/%(playlist_index)s - %(title)s.%(ext)s' https://www.youtube.com/playlist?list=PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re

View File

@ -74,6 +74,10 @@
'https://www.youtube.com/s/player/f8cb7a3b/player_ias.vflset/en_US/base.js', 'https://www.youtube.com/s/player/f8cb7a3b/player_ias.vflset/en_US/base.js',
'oBo2h5euWy6osrUt', 'ivXHpm7qJjJN', 'oBo2h5euWy6osrUt', 'ivXHpm7qJjJN',
), ),
(
'https://www.youtube.com/s/player/2dfe380c/player_ias.vflset/en_US/base.js',
'oBo2h5euWy6osrUt', '3DIBbn3qdQ',
),
] ]

View File

@ -528,7 +528,6 @@ def __init__(self, params=None, auto_init=True):
self.cache = Cache(self) self.cache = Cache(self)
windows_enable_vt_mode() windows_enable_vt_mode()
# FIXME: This will break if we ever print color to stdout
self._allow_colors = { self._allow_colors = {
'screen': not self.params.get('no_color') and supports_terminal_sequences(self._screen_file), 'screen': not self.params.get('no_color') and supports_terminal_sequences(self._screen_file),
'err': not self.params.get('no_color') and supports_terminal_sequences(self._err_file), 'err': not self.params.get('no_color') and supports_terminal_sequences(self._err_file),
@ -2012,10 +2011,10 @@ def selector_function(ctx):
# TODO: Add allvideo, allaudio etc by generalizing the code with best/worst selector # TODO: Add allvideo, allaudio etc by generalizing the code with best/worst selector
if format_spec == 'all': if format_spec == 'all':
def selector_function(ctx): def selector_function(ctx):
yield from _check_formats(ctx['formats']) yield from _check_formats(ctx['formats'][::-1])
elif format_spec == 'mergeall': elif format_spec == 'mergeall':
def selector_function(ctx): def selector_function(ctx):
formats = list(_check_formats(ctx['formats'])) formats = list(_check_formats(ctx['formats'][::-1]))
if not formats: if not formats:
return return
merged_format = formats[-1] merged_format = formats[-1]
@ -3163,7 +3162,7 @@ def format_resolution(format, default='unknown'):
return 'images' return 'images'
else: else:
return default return default
return f'{res} images' if is_images else res return f'img {res}' if is_images else res
def _format_note(self, fdict): def _format_note(self, fdict):
res = '' res = ''

View File

@ -378,8 +378,6 @@ def metadataparser_actions(f):
opts.sponsorblock_remove = set() opts.sponsorblock_remove = set()
sponsorblock_query = opts.sponsorblock_mark | opts.sponsorblock_remove sponsorblock_query = opts.sponsorblock_mark | opts.sponsorblock_remove
if (opts.addmetadata or opts.sponsorblock_mark) and opts.addchapters is None:
opts.addchapters = True
opts.remove_chapters = opts.remove_chapters or [] opts.remove_chapters = opts.remove_chapters or []
if (opts.remove_chapters or sponsorblock_query) and opts.sponskrub is not False: if (opts.remove_chapters or sponsorblock_query) and opts.sponskrub is not False:
@ -400,40 +398,32 @@ def metadataparser_actions(f):
opts.remuxvideo = False opts.remuxvideo = False
if opts.allow_unplayable_formats: if opts.allow_unplayable_formats:
if opts.extractaudio: def report_unplayable_conflict(opt_name, arg, default=False, allowed=None):
report_conflict('--allow-unplayable-formats', '--extract-audio') val = getattr(opts, opt_name)
opts.extractaudio = False if (not allowed and val) or not allowed(val):
if opts.remuxvideo: report_conflict('--allow-unplayable-formats', arg)
report_conflict('--allow-unplayable-formats', '--remux-video') setattr(opts, opt_name, default)
opts.remuxvideo = False
if opts.recodevideo: report_unplayable_conflict('extractaudio', '--extract-audio')
report_conflict('--allow-unplayable-formats', '--recode-video') report_unplayable_conflict('remuxvideo', '--remux-video')
opts.recodevideo = False report_unplayable_conflict('recodevideo', '--recode-video')
if opts.addmetadata: report_unplayable_conflict('addmetadata', '--embed-metadata')
report_conflict('--allow-unplayable-formats', '--add-metadata') report_unplayable_conflict('addchapters', '--embed-chapters')
opts.addmetadata = False report_unplayable_conflict('embed_infojson', '--embed-info-json')
if opts.embedsubtitles: opts.embed_infojson = False
report_conflict('--allow-unplayable-formats', '--embed-subs') report_unplayable_conflict('embedsubtitles', '--embed-subs')
opts.embedsubtitles = False report_unplayable_conflict('embedthumbnail', '--embed-thumbnail')
if opts.embedthumbnail: report_unplayable_conflict('xattrs', '--xattrs')
report_conflict('--allow-unplayable-formats', '--embed-thumbnail') report_unplayable_conflict('fixup', '--fixup', default='never', allowed=lambda x: x in (None, 'never', 'ignore'))
opts.embedthumbnail = False
if opts.xattrs:
report_conflict('--allow-unplayable-formats', '--xattrs')
opts.xattrs = False
if opts.fixup and opts.fixup.lower() not in ('never', 'ignore'):
report_conflict('--allow-unplayable-formats', '--fixup')
opts.fixup = 'never' opts.fixup = 'never'
if opts.remove_chapters: report_unplayable_conflict('remove_chapters', '--remove-chapters', default=[])
report_conflict('--allow-unplayable-formats', '--remove-chapters') report_unplayable_conflict('sponsorblock_remove', '--sponsorblock-remove', default=set())
opts.remove_chapters = [] report_unplayable_conflict('sponskrub', '--sponskrub', default=set())
if opts.sponsorblock_remove:
report_conflict('--allow-unplayable-formats', '--sponsorblock-remove')
opts.sponsorblock_remove = set()
if opts.sponskrub:
report_conflict('--allow-unplayable-formats', '--sponskrub')
opts.sponskrub = False opts.sponskrub = False
if (opts.addmetadata or opts.sponsorblock_mark) and opts.addchapters is None:
opts.addchapters = True
# PostProcessors # PostProcessors
postprocessors = list(opts.add_postprocessors) postprocessors = list(opts.add_postprocessors)
if sponsorblock_query: if sponsorblock_query:

View File

@ -185,7 +185,7 @@ def _extract_video(self, video_id, catalogue=None):
'vcodec': 'none', 'vcodec': 'none',
'ext': 'mhtml', 'ext': 'mhtml',
'protocol': 'mhtml', 'protocol': 'mhtml',
'url': 'about:dummy', 'url': 'about:invalid',
'fragments': [{ 'fragments': [{
'path': sheet, 'path': sheet,
# XXX: not entirely accurate; each spritesheet seems to be # XXX: not entirely accurate; each spritesheet seems to be

View File

@ -276,7 +276,7 @@ def _real_extract(self, url):
def _get_subtitles(self, subtitles, experience_id, episode, display_id, format_name): def _get_subtitles(self, subtitles, experience_id, episode, display_id, format_name):
if isinstance(episode, str): if isinstance(episode, str):
webpage = self._download_webpage( webpage = self._download_webpage(
f'https://www.funimation.com/player/{experience_id}', display_id, f'https://www.funimation.com/player/{experience_id}/', display_id,
fatal=False, note=f'Downloading player webpage for {format_name}') fatal=False, note=f'Downloading player webpage for {format_name}')
episode, _, _ = self._get_episode(webpage, episode_id=episode, fatal=False) episode, _, _ = self._get_episode(webpage, episode_id=episode, fatal=False)

View File

@ -193,7 +193,7 @@ class PBSIE(InfoExtractor):
# Article with embedded player (or direct video) # Article with embedded player (or direct video)
(?:www\.)?pbs\.org/(?:[^/]+/){1,5}(?P<presumptive_id>[^/]+?)(?:\.html)?/?(?:$|[?\#]) | (?:www\.)?pbs\.org/(?:[^/]+/){1,5}(?P<presumptive_id>[^/]+?)(?:\.html)?/?(?:$|[?\#]) |
# Player # Player
(?:video|player)\.pbs\.org/(?:widget/)?partnerplayer/(?P<player_id>[^/]+)/ (?:video|player)\.pbs\.org/(?:widget/)?partnerplayer/(?P<player_id>[^/]+)
) )
''' % '|'.join(list(zip(*_STATIONS))[0]) ''' % '|'.join(list(zip(*_STATIONS))[0])

View File

@ -58,7 +58,7 @@ def _get_bearer_token(self, video_id):
'email': username, 'email': username,
'password': password, 'password': password,
})) }))
return "Bearer " + data['jwt']['accessToken'] return 'Bearer ' + data['jwt']['accessToken']
def _real_extract(self, url): def _real_extract(self, url):
content_id = self._match_id(url) content_id = self._match_id(url)

View File

@ -508,9 +508,9 @@ def _extract_visitor_data(*args):
Extracts visitorData from an API response or ytcfg Extracts visitorData from an API response or ytcfg
Appears to be used to track session state Appears to be used to track session state
""" """
return traverse_obj( return get_first(
args, (..., ('VISITOR_DATA', ('INNERTUBE_CONTEXT', 'client', 'visitorData'), ('responseContext', 'visitorData'))), args, (('VISITOR_DATA', ('INNERTUBE_CONTEXT', 'client', 'visitorData'), ('responseContext', 'visitorData'))),
expected_type=compat_str, get_all=False) expected_type=str)
@property @property
def is_authenticated(self): def is_authenticated(self):
@ -1674,7 +1674,20 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
# shorts # shorts
'url': 'https://www.youtube.com/shorts/BGQWPY4IigY', 'url': 'https://www.youtube.com/shorts/BGQWPY4IigY',
'only_matching': True, 'only_matching': True,
}, }, {
'note': 'Storyboards',
'url': 'https://www.youtube.com/watch?v=5KLPxDtMqe8',
'info_dict': {
'id': '5KLPxDtMqe8',
'ext': 'mhtml',
'format_id': 'sb0',
'title': 'Your Brain is Plastic',
'uploader_id': 'scishow',
'description': 'md5:89cd86034bdb5466cd87c6ba206cd2bc',
'upload_date': '20140324',
'uploader': 'SciShow',
}, 'params': {'format': 'mhtml', 'skip_download': True}
}
] ]
@classmethod @classmethod
@ -1920,9 +1933,9 @@ def _extract_signature_timestamp(self, video_id, player_url, ytcfg=None, fatal=F
return sts return sts
def _mark_watched(self, video_id, player_responses): def _mark_watched(self, video_id, player_responses):
playback_url = traverse_obj( playback_url = get_first(
player_responses, (..., 'playbackTracking', 'videostatsPlaybackUrl', 'baseUrl'), player_responses, ('playbackTracking', 'videostatsPlaybackUrl', 'baseUrl'),
expected_type=url_or_none, get_all=False) expected_type=url_or_none)
if not playback_url: if not playback_url:
self.report_warning('Unable to mark watched') self.report_warning('Unable to mark watched')
return return