From ec7250f145c5ce7fdab666be7f3929ebdc0c4473 Mon Sep 17 00:00:00 2001 From: 7x11x13 Date: Wed, 1 Jan 2025 16:04:42 -0500 Subject: [PATCH] Implement nested --playlist-items --- yt_dlp/YoutubeDL.py | 6 +++- yt_dlp/__init__.py | 2 +- yt_dlp/utils/_utils.py | 76 +++++++++++++++++++++++++++++++++++++----- 3 files changed, 73 insertions(+), 11 deletions(-) diff --git a/yt_dlp/YoutubeDL.py b/yt_dlp/YoutubeDL.py index 764baf3a00..0e9a9cd6ce 100644 --- a/yt_dlp/YoutubeDL.py +++ b/yt_dlp/YoutubeDL.py @@ -635,6 +635,7 @@ class YoutubeDL: self._num_downloads = 0 self._num_videos = 0 self._playlist_level = 0 + self._nested_playlist_index = () self._playlist_urls = set() self.cache = Cache(self) self.__header_cookies = [] @@ -1987,7 +1988,7 @@ class YoutubeDL: self.to_screen(f'[download] Downloading {ie_result["_type"]}: {title}') all_entries = PlaylistEntries(self, ie_result) - entries = orderedSet(all_entries.get_requested_items(), lazy=True) + entries = orderedSet(all_entries.get_requested_items(self._nested_playlist_index), lazy=True) lazy = self.params.get('lazy_playlist') if lazy: @@ -2064,10 +2065,13 @@ class YoutubeDL: f'[download] Downloading item {self._format_screen(i + 1, self.Styles.ID)} ' f'of {self._format_screen(n_entries, self.Styles.EMPHASIS)}') + self._nested_playlist_index = (*self._nested_playlist_index, playlist_index) entry_result = self.__process_iterable_entry(entry, download, collections.ChainMap({ 'playlist_index': playlist_index, 'playlist_autonumber': i + 1, }, extra)) + self._nested_playlist_index = self._nested_playlist_index[:-1] + if not entry_result: failures += 1 if failures >= max_failures: diff --git a/yt_dlp/__init__.py b/yt_dlp/__init__.py index 20111175b1..bbceb661d5 100644 --- a/yt_dlp/__init__.py +++ b/yt_dlp/__init__.py @@ -431,7 +431,7 @@ def validate_options(opts): # Other options if opts.playlist_items is not None: try: - tuple(PlaylistEntries.parse_playlist_items(opts.playlist_items)) + tuple(PlaylistEntries.parse_playlist_items(opts.playlist_items, ())) except Exception as err: raise ValueError(f'Invalid playlist-items {opts.playlist_items!r}: {err}') diff --git a/yt_dlp/utils/_utils.py b/yt_dlp/utils/_utils.py index 699bf1e7f6..67e9f48ecf 100644 --- a/yt_dlp/utils/_utils.py +++ b/yt_dlp/utils/_utils.py @@ -2390,6 +2390,23 @@ class InAdvancePagedList(PagedList): yield from page_results +def index_in_slice_inclusive(idx: int, slice_: slice): + start, step, stop = slice_.start, slice_.step, slice_.stop + if start is None: + start = 0 + if step is None: + step = 1 + if stop is None or stop == math.inf or stop == -math.inf: + if (idx - start) % step != 0: + return False + if step > 0: + return idx >= start and stop != -math.inf + else: + return idx <= start and stop != math.inf + else: + return idx in range(start, int(stop) + 1, step) + + class PlaylistEntries: MissingEntry = object() is_exhausted = False @@ -2423,20 +2440,61 @@ class PlaylistEntries: (?::(?P[+-]?\d+))? )?''') + NESTED_PLAYLIST_RE = re.compile(r'''(?x) + (?:\[ + (?:[+-]?\d+)? + (?:[:-] + (?:[+-]?\d+|inf(?:inite)?)? + (?::(?:[+-]?\d+))? + )? + \])+''') + + NESTED_PLAYLIST_SEGMENT_RE = re.compile(r'''(?x) + \[ + (?P[+-]?\d+)? + (?P[:-] + (?P[+-]?\d+|inf(?:inite)?)? + (?::(?P[+-]?\d+))? + )? + \]''') + @classmethod - def parse_playlist_items(cls, string): + def parse_playlist_items(cls, string, playlist_index): for segment in string.split(','): if not segment: - raise ValueError('There is two or more consecutive commas') + raise ValueError('There are two or more consecutive commas') mobj = cls.PLAYLIST_ITEMS_RE.fullmatch(segment) - if not mobj: + if mobj: + start, end, step, has_range = mobj.group('start', 'end', 'step', 'range') + if int_or_none(step) == 0: + raise ValueError(f'Step in {segment!r} cannot be zero') + yield slice(int_or_none(start), float_or_none(end), int_or_none(step)) if has_range else int(start) + continue + + if not cls.NESTED_PLAYLIST_RE.fullmatch(segment): raise ValueError(f'{segment!r} is not a valid specification') - start, end, step, has_range = mobj.group('start', 'end', 'step', 'range') - if int_or_none(step) == 0: - raise ValueError(f'Step in {segment!r} cannot be zero') - yield slice(int_or_none(start), float_or_none(end), int_or_none(step)) if has_range else int(start) - def get_requested_items(self): + for depth, mobj in enumerate(cls.NESTED_PLAYLIST_SEGMENT_RE.finditer(segment)): + start, end, step, has_range = mobj.group('start', 'end', 'step', 'range') + if int_or_none(step) == 0: + raise ValueError(f'Step in {segment!r} cannot be zero') + + slice_ = ( + slice(int_or_none(start), float_or_none(end), int_or_none(step)) + if has_range + else slice(int(start), int(start)) + ) + + if depth == len(playlist_index): + yield slice_ + break + + if not index_in_slice_inclusive(playlist_index[depth], slice_): + break + else: + yield slice(None) + + def get_requested_items(self, playlist_index): playlist_items = self.ydl.params.get('playlist_items') playlist_start = self.ydl.params.get('playliststart', 1) playlist_end = self.ydl.params.get('playlistend') @@ -2448,7 +2506,7 @@ class PlaylistEntries: elif playlist_start != 1 or playlist_end: self.ydl.report_warning('Ignoring playliststart and playlistend because playlistitems was given', only_once=True) - for index in self.parse_playlist_items(playlist_items): + for index in self.parse_playlist_items(playlist_items, playlist_index): for i, entry in self[index]: yield i, entry if not entry: