Repository: yt-dlp/yt-dlp

Issue:
ERROR: 03354: An extractor error has occurred.
### Checklist

- [X] I'm reporting a bug unrelated to a specific site
- [X] I've verified that I'm running yt-dlp version **2022.07.18** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
- [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/ytdl-org/youtube-dl#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 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)

### Provide a description that is worded well enough to be understood

The command downloads a 26-episode series from Tubi. The entire download proceeds without issue, getting me all 26 episodes. But then it thinks there is a 27th episode, which is when I get the error:

[download] Downloading video 27 of 27
[debug] [TubiTv] Extracting URL: tubitv:03354
[TubiTv] 03354: Downloading JSON metadata
ERROR: 03354: An extractor error has occurred. (caused by KeyError('url')); please report this issue on  https://github.com/yt-dlp/yt-dlp/issues?q= , filling out the appropriate issue template. Confirm you are on the latest version using  yt-dlp -U
  File "/home/melissa/.local/lib/python3.8/site-packages/yt_dlp/extractor/common.py", line 644, in extract
    ie_result = self._real_extract(url)
  File "/home/melissa/.local/lib/python3.8/site-packages/yt_dlp/extractor/tubitv.py", line 76, in _real_extract
    url = video_data['url']
KeyError: 'url'

[download] Finished downloading playlist: stargate-infinity

I don't know if this is just some weird issue with the playlist data from Tubi, but I'm reporting it like the error text asked.

[yt-dlp-vU.txt](https://github.com/yt-dlp/yt-dlp/files/9163183/yt-dlp-vU.txt)


### 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] Copy the WHOLE output (starting with `[debug] Command-line config`) and insert it below

### Complete Verbose Output

```shell
The output is over 100k lines, so instead of pasting it I attached it as a file.
```



Discussion:
Geo-restricted to US, XFF IP masquerade ineffective.

However the problem seems to be that the playlist extractor has extracted the playlist itself (03354). Your playlist should only have 26 episodes, no?

Maybe there is a discriminator, like `isSeries`, in the JSON objects keyed by ID in `.video.fullContentById` that should be checked?
There are a few ways we could do it @dirkf 
There are type/detailed_type keys in each entry that are e/episode or s/series that we could use to skip them.
There is a statusById field that says to not fetch the playlist:
``` 
{'03354': {'isFetching': False,
 'ttl': 1658528226683,
 'error': None}},
```
We could also change the logic and  iterate over the episodeIds instead of fullContentById. If we get the key from the url It would require 0-padding though as the url is 3554 and the key to get the series byId is 03354.
Skipping if the fullContentById number as an int matches the url's series id as an int could also work.

I'm not familiar with tubi though so not sure which would be the most reliable.

Resolving patch:
diff --git a/yt_dlp/extractor/tubitv.py b/yt_dlp/extractor/tubitv.py
--- a/yt_dlp/extractor/tubitv.py
+++ b/yt_dlp/extractor/tubitv.py
@@ -7,6 +7,7 @@
     js_to_json,
     sanitized_Request,
     urlencode_postdata,
+    traverse_obj,
 )
 
 
@@ -135,6 +136,8 @@ def _entries(self, show_url, show_name):
             show_webpage, 'data'), show_name, transform_source=js_to_json)['video']
 
         for episode_id in show_json['fullContentById'].keys():
+            if traverse_obj(show_json, ('byId', episode_id, 'type')) == 's':
+                continue
             yield self.url_result(
                 'tubitv:%s' % episode_id,
                 ie=TubiTvIE.ie_key(), video_id=episode_id)


Test patch: