Repository: yt-dlp/yt-dlp
Issue category: Bug Report

[youtube:tab] Tab/playlist extraction intermittently yielding 0 items
### DO NOT REMOVE OR SKIP THE ISSUE TEMPLATE

- [X] I understand that I will be **blocked** if I *intentionally* remove or skip any mandatory\* field

### Checklist

- [X] I'm reporting that yt-dlp is broken on a **supported** site
- [X] I've verified that I have **updated yt-dlp to nightly or master** ([update instructions](https://github.com/yt-dlp/yt-dlp#update-channels))
- [X] I've checked that all provided URLs are playable in a browser with the same IP and same login details
- [X] I've checked that all URLs and arguments with special characters are [properly quoted or escaped](https://github.com/yt-dlp/yt-dlp/wiki/FAQ#video-url-contains-an-ampersand--and-im-getting-some-strange-output-1-2839-or-v-is-not-recognized-as-an-internal-or-external-command)
- [X] I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766) and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues **including closed ones**. DO NOT post duplicates
- [X] I've read the [guidelines for opening an issue](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue)
- [X] I've read about [sharing account credentials](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#are-you-willing-to-share-account-details-if-needed) and I'm willing to share it if required

### Region

Switzerland

### Provide a description that is worded well enough to be understood

I'm using yt-dlp on a script of mine and I realised that one of my tests was flakey. In particular, listing playlists for a given YouTube channel would fail about 33% of the time, but nothing in my code seemed amiss.

Searching through the issues to see if I was doing something wrong or there was someone having similar trouble, I found https://github.com/yt-dlp/yt-dlp/issues/11511 (not related to my issue per-se), so I tried the exact command that was suggested to see if I could replicate my issues there, and indeed, I'm having the same trouble using the CLI command.

More than half the times, running the command `yt-dlp -vU --print "%(id)s|%(title)s|%(playlist_index)s" --flat-playlist "https://www.youtube.com/@LibraryoftheUntold/playlists"` works fine. But occasionally, it silently fails with no output or warning. It just doesn't print anything. When using the Python library directly, the issue manifests as an empty list in the `entries` key, whilst all other fields seem to be populated as usual.

### Provide verbose output that clearly demonstrates the problem

- [X] Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
- [X] If using API, add `'verbose': True` to `YoutubeDL` params instead
- [X] Copy the WHOLE output (starting with `[debug] Command-line config`) and insert it below

### Complete Verbose Output

#### Expected Behaviour

> Most of the time, this is the output.

```shell
❯ yt-dlp -vU --print "%(id)s|%(title)s|%(playlist_index)s" --flat-playlist "https://www.youtube.com/@LibraryoftheUntold/playlists"

[debug] Command-line config: ['-vU', '--print', '%(id)s|%(title)s|%(playlist_index)s', '--flat-playlist', 'https://www.youtube.com/@LibraryoftheUntold/playlists']
[debug] Encodings: locale UTF-8, fs utf-8, pref UTF-8, out utf-8, error utf-8, screen utf-8
[debug] yt-dlp version stable@2024.11.04 from yt-dlp/yt-dlp [197d0b03b] (pip)
[debug] Python 3.13.0 (CPython arm64 64bit) - macOS-15.1-arm64-arm-64bit-Mach-O (OpenSSL 3.4.0 22 Oct 2024)
[debug] exe versions: ffmpeg 7.1 (setts), ffprobe 7.1
[debug] Optional libraries: Cryptodome-3.21.0, brotli-1.1.0, certifi-2024.08.30, mutagen-1.47.0, requests-2.32.3, sqlite3-3.47.0, urllib3-2.2.3, websockets-13.1
[debug] Proxy map: {}
[debug] Request Handlers: urllib, requests, websockets
[debug] Loaded 1838 extractors
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
Latest version: stable@2024.11.04 from yt-dlp/yt-dlp
yt-dlp is up to date (stable@2024.11.04 from yt-dlp/yt-dlp)
[youtube:tab] Extracting URL: https://www.youtube.com/@LibraryoftheUntold/playlists
[youtube:tab] @LibraryoftheUntold/playlists: Downloading webpage
[debug] [youtube:tab] Selected tab: 'playlists' (playlists), Requested tab: 'playlists'
[download] Downloading playlist: Library of the Untold - Playlists
[youtube:tab] Playlist Library of the Untold - Playlists: Downloading 9 items of 9
[debug] The information of all playlist entries will be held in memory
[download] Downloading item 1 of 9
PL_39VJI5VnWsEY21lkFbMtTte1FecamGO|Paranormal / Strange Happenings|1
[download] Downloading item 2 of 9
PL_39VJI5VnWtOERqIiMX_Yt3_wEwJ8J-O|Podcasts with Incredible Minds|2
[download] Downloading item 3 of 9
PL_39VJI5VnWslKfsegFKYnslK3aUMwfRQ|Zen|3
[download] Downloading item 4 of 9
PL_39VJI5VnWsdRQcmUh3tNuyrdo0RbkbN|Alchemical Transmutation|4
[download] Downloading item 5 of 9
PL_39VJI5VnWvcHp6c1aO1fq3ir-1-lwMK|Gnostic Thought|5
[download] Downloading item 6 of 9
PL_39VJI5VnWvbkUkqZa38hPoJtQEclDAW|Unexplained / Great Mysteries|6
[download] Downloading item 7 of 9
PL_39VJI5VnWsT05y2pHAmjJsTh-E1qS30|Quick Summaries|7
[download] Downloading item 8 of 9
PL_39VJI5VnWvNy-mRKjkhq_hxTzaWa0RT|Book of Wonderful Suffering -by J. W. Phipps|8
[download] Downloading item 9 of 9
PL_39VJI5VnWuC9OnKk86lBa_IjRHQWqHw|ALL Documentaries|9
[download] Finished downloading playlist: Library of the Untold - Playlists
```

#### Occasional wrong behaviour

> Sometimes, this happens. I haven't found a clear pattern when it does.

```shell
❯ yt-dlp -vU --print "%(id)s|%(title)s|%(playlist_index)s" --flat-playlist "https://www.youtube.com/@LibraryoftheUntold/playlists"

[debug] Command-line config: ['-vU', '--print', '%(id)s|%(title)s|%(playlist_index)s', '--flat-playlist', 'https://www.youtube.com/@LibraryoftheUntold/playlists']
[debug] Encodings: locale UTF-8, fs utf-8, pref UTF-8, out utf-8, error utf-8, screen utf-8
[debug] yt-dlp version stable@2024.11.04 from yt-dlp/yt-dlp [197d0b03b] (pip)
[debug] Python 3.13.0 (CPython arm64 64bit) - macOS-15.1-arm64-arm-64bit-Mach-O (OpenSSL 3.4.0 22 Oct 2024)
[debug] exe versions: ffmpeg 7.1 (setts), ffprobe 7.1
[debug] Optional libraries: Cryptodome-3.21.0, brotli-1.1.0, certifi-2024.08.30, mutagen-1.47.0, requests-2.32.3, sqlite3-3.47.0, urllib3-2.2.3, websockets-13.1
[debug] Proxy map: {}
[debug] Request Handlers: urllib, requests, websockets
[debug] Loaded 1838 extractors
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
Latest version: stable@2024.11.04 from yt-dlp/yt-dlp
yt-dlp is up to date (stable@2024.11.04 from yt-dlp/yt-dlp)
[youtube:tab] Extracting URL: https://www.youtube.com/@LibraryoftheUntold/playlists
[youtube:tab] @LibraryoftheUntold/playlists: Downloading webpage
[debug] [youtube:tab] Selected tab: 'playlists' (playlists), Requested tab: 'playlists'
[download] Downloading playlist: Library of the Untold - Playlists
[youtube:tab] Playlist Library of the Untold - Playlists: Downloading 0 items
[debug] The information of all playlist entries will be held in memory
[download] Finished downloading playlist: Library of the Untold - Playlists
```



Functions to edit:
yt_dlp/extractor/youtube.py:YoutubeTabBaseInfoExtractor._grid_entries
yt_dlp/extractor/youtube.py:YoutubeTabBaseInfoExtractor._rich_entries
yt_dlp/extractor/youtube.py:YoutubeTabIE
Functions added: yt_dlp/extractor/youtube.py:YoutubeTabBaseInfoExtractor._extract_lockup_view_model
yt_dlp/extractor/youtube.py:YoutubeTabIE

Patch:
diff --git a/yt_dlp/extractor/youtube.py b/yt_dlp/extractor/youtube.py
index a02a2428ab05..7a9133466d9b 100644
--- a/yt_dlp/extractor/youtube.py
+++ b/yt_dlp/extractor/youtube.py
@@ -4986,6 +4986,10 @@ def _grid_entries(self, grid_renderer):
         for item in grid_renderer['items']:
             if not isinstance(item, dict):
                 continue
+            if lockup_view_model := traverse_obj(item, ('lockupViewModel', {dict})):
+                if entry := self._extract_lockup_view_model(lockup_view_model):
+                    yield entry
+                continue
             renderer = self._extract_basic_item_renderer(item)
             if not isinstance(renderer, dict):
                 continue
@@ -5084,10 +5088,30 @@ def _playlist_entries(self, video_list_renderer):
                 continue
             yield self._extract_video(renderer)
 
+    def _extract_lockup_view_model(self, view_model):
+        content_id = view_model.get('contentId')
+        if not content_id:
+            return
+        content_type = view_model.get('contentType')
+        if content_type not in ('LOCKUP_CONTENT_TYPE_PLAYLIST', 'LOCKUP_CONTENT_TYPE_PODCAST'):
+            self.report_warning(
+                f'Unsupported lockup view model content type "{content_type}"{bug_reports_message()}', only_once=True)
+            return
+        return self.url_result(
+            f'https://www.youtube.com/playlist?list={content_id}', ie=YoutubeTabIE, video_id=content_id,
+            title=traverse_obj(view_model, (
+                'metadata', 'lockupMetadataViewModel', 'title', 'content', {str})),
+            thumbnails=self._extract_thumbnails(view_model, (
+                'contentImage', 'collectionThumbnailViewModel', 'primaryThumbnail', 'thumbnailViewModel', 'image'), final_key='sources'))
+
     def _rich_entries(self, rich_grid_renderer):
+        if lockup_view_model := traverse_obj(rich_grid_renderer, ('content', 'lockupViewModel', {dict})):
+            if entry := self._extract_lockup_view_model(lockup_view_model):
+                yield entry
+            return
         renderer = traverse_obj(
             rich_grid_renderer,
-            ('content', ('videoRenderer', 'reelItemRenderer', 'playlistRenderer', 'shortsLockupViewModel', 'lockupViewModel'), any)) or {}
+            ('content', ('videoRenderer', 'reelItemRenderer', 'playlistRenderer', 'shortsLockupViewModel'), any)) or {}
         video_id = renderer.get('videoId')
         if video_id:
             yield self._extract_video(renderer)
@@ -5114,18 +5138,6 @@ def _rich_entries(self, rich_grid_renderer):
                 })),
                 thumbnails=self._extract_thumbnails(renderer, 'thumbnail', final_key='sources'))
             return
-        # lockupViewModel extraction
-        content_id = renderer.get('contentId')
-        if content_id and renderer.get('contentType') == 'LOCKUP_CONTENT_TYPE_PODCAST':
-            yield self.url_result(
-                f'https://www.youtube.com/playlist?list={content_id}',
-                ie=YoutubeTabIE, video_id=content_id,
-                **traverse_obj(renderer, {
-                    'title': ('metadata', 'lockupMetadataViewModel', 'title', 'content', {str}),
-                }),
-                thumbnails=self._extract_thumbnails(renderer, (
-                    'contentImage', 'collectionThumbnailViewModel', 'primaryThumbnail', 'thumbnailViewModel', 'image'), final_key='sources'))
-            return
 
     def _video_entry(self, video_renderer):
         video_id = video_renderer.get('videoId')
@@ -5794,7 +5806,7 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
         'info_dict': {
             'id': 'UCYO_jab_esuFRV4b17AJtAw',
             'title': '3Blue1Brown - Playlists',
-            'description': 'md5:4d1da95432004b7ba840ebc895b6b4c9',
+            'description': 'md5:602e3789e6a0cb7d9d352186b720e395',
             'channel_url': 'https://www.youtube.com/channel/UCYO_jab_esuFRV4b17AJtAw',
             'channel': '3Blue1Brown',
             'channel_id': 'UCYO_jab_esuFRV4b17AJtAw',