From 981e6c10035f5bd270b39a70d51dbb6802c0f548 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 19:59:32 +0000 Subject: [PATCH] Propagate missing playlist context fields (playlist_count, playlist_autonumber, n_entries, __last_playlist_index) The playlist/channel processing loop now sets playlist_count, playlist_autonumber, n_entries, and __last_playlist_index on each video entry so that templates like %(playlist_autonumber)s, %(playlist_count)s, and %(playlist_index&{} - |)s resolve correctly instead of showing NA. Also updates _compact_persisted_entry to preserve n_entries and __last_playlist_index across restarts. Fixes #692 Agent-Logs-Url: https://github.com/alexta69/metube/sessions/b5aeb55a-3197-4a14-b8b4-96c9a67796e8 Co-authored-by: alexta69 <7450369+alexta69@users.noreply.github.com> --- app/tests/test_ytdl_utils.py | 37 ++++++++++++++++++++++++++++++++++++ app/ytdl.py | 14 +++++++++++--- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/app/tests/test_ytdl_utils.py b/app/tests/test_ytdl_utils.py index a577339..0f2e66d 100644 --- a/app/tests/test_ytdl_utils.py +++ b/app/tests/test_ytdl_utils.py @@ -93,6 +93,35 @@ class ResolveOuttmplFieldsTests(unittest.TestCase): result = _resolve_outtmpl_fields("%(playlist_index+100)d", info, ("playlist",)) self.assertEqual(result, "103") + def test_playlist_count_and_autonumber(self): + info = { + "playlist_title": "My PL", + "playlist_index": "03", + "playlist_count": 10, + "playlist_autonumber": 3, + "n_entries": 10, + "__last_playlist_index": 10, + } + result = _resolve_outtmpl_fields( + "%(playlist_title)s/%(playlist_autonumber)s of %(playlist_count)s - %(title)s.%(ext)s", + info, + ("playlist",), + ) + # playlist_autonumber is auto-padded by yt-dlp using __last_playlist_index + self.assertEqual(result, "My PL/03 of 10 - %(title)s.%(ext)s") + + def test_conditional_playlist_index(self): + info = { + "playlist_index": "5", + "playlist_count": 10, + } + result = _resolve_outtmpl_fields( + "%(playlist_index&{} - |)s%(title)s.%(ext)s", + info, + ("playlist",), + ) + self.assertEqual(result, "5 - %(title)s.%(ext)s") + class SanitizeEntryForPickleTests(unittest.TestCase): def test_nested(self): @@ -250,8 +279,12 @@ class CompactPersistedEntryTests(unittest.TestCase): entry = { "playlist_index": "01", "playlist_title": "Playlist", + "playlist_count": 10, + "playlist_autonumber": 1, "channel_index": "02", "channel_title": "Channel", + "n_entries": 10, + "__last_playlist_index": 10, "formats": [{"id": "huge"}], "description": "big blob", } @@ -263,8 +296,12 @@ class CompactPersistedEntryTests(unittest.TestCase): { "playlist_index": "01", "playlist_title": "Playlist", + "playlist_count": 10, + "playlist_autonumber": 1, "channel_index": "02", "channel_title": "Channel", + "n_entries": 10, + "__last_playlist_index": 10, }, ) diff --git a/app/ytdl.py b/app/ytdl.py index 15f42f1..65b5ed4 100644 --- a/app/ytdl.py +++ b/app/ytdl.py @@ -295,13 +295,16 @@ _PERSISTED_DOWNLOAD_FIELDS = ( ) +_COMPACT_ENTRY_EXTRA_KEYS = frozenset(("n_entries", "__last_playlist_index")) + + def _compact_persisted_entry(entry: Any) -> Optional[dict[str, Any]]: if not isinstance(entry, dict): return None compact = { key: value for key, value in entry.items() - if key.startswith("playlist") or key.startswith("channel") + if key.startswith("playlist") or key.startswith("channel") or key in _COMPACT_ENTRY_EXTRA_KEYS } return compact or None @@ -893,8 +896,9 @@ class DownloadQueue: # Convert generator to list if needed (for len() and slicing operations) if isinstance(entries, types.GeneratorType): entries = list(entries) - log.info(f'{etype} detected with {len(entries)} entries') - index_digits = len(str(len(entries))) + total_entries = len(entries) + log.info(f'{etype} detected with {total_entries} entries') + index_digits = len(str(total_entries)) results = [] if playlist_item_limit > 0: log.info(f'Item limit is set. Processing only first {playlist_item_limit} entries') @@ -906,6 +910,10 @@ class DownloadQueue: etr["_type"] = "video" etr[etype] = entry.get("id") or entry.get("channel_id") or entry.get("channel") etr[f"{etype}_index"] = '{{0:0{0:d}d}}'.format(index_digits).format(index) + etr[f"{etype}_count"] = total_entries + etr[f"{etype}_autonumber"] = index + etr["n_entries"] = total_entries + etr["__last_playlist_index"] = total_entries for property in ("id", "title", "uploader", "uploader_id"): if property in entry: etr[f"{etype}_{property}"] = entry[property]