Repository: yt-dlp/yt-dlp

Issue:
Bild.de Extractor not working
### 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'm running yt-dlp version **2023.07.06** ([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/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)
- [ ] 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

Germany

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

Today i tried downloading a video from bild.de i have never realy downloaded from there. But after the download starts the file is done. The content of the file is nothing just a black screen. 
The m3u8 file i get out looks like this. I already tried updating im on the nightly@2023.08.20.172604 now.

`#EXTM3U
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-VERSION:5
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-TARGETDURATION:6
#EXTINF:6.0,
segment_0_playlist_0.ts
#EXTINF:6.0,
segment_0_playlist_1.ts
#EXTINF:6.0,
segment_0_playlist_2.ts
#EXTINF:6.0,
segment_0_playlist_3.ts
#EXTINF:6.0,
segment_0_playlist_4.ts
#EXTINF:6.0,
segment_0_playlist_5.ts
#EXTINF:6.0,
segment_0_playlist_6.ts
#EXTINF:6.0,
segment_0_playlist_7.ts
#EXTINF:6.0,
segment_0_playlist_8.ts
#EXTINF:6.0,
segment_0_playlist_9.ts
#EXTINF:6.0,
segment_0_playlist_10.ts
#EXTINF:3.96,
segment_0_playlist_11.ts
#EXT-X-ENDLIST
#EXTM3U
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-VERSION:5
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-TARGETDURATION:6
#EXTINF:6.0,
segment_1_playlist_0.ts
#EXTINF:6.0,
segment_1_playlist_1.ts
#EXTINF:6.0,
segment_1_playlist_2.ts
#EXTINF:6.0,
segment_1_playlist_3.ts
#EXTINF:6.0,
segment_1_playlist_4.ts
#EXTINF:6.0,
segment_1_playlist_5.ts
#EXTINF:6.0,
segment_1_playlist_6.ts
#EXTINF:6.0,
segment_1_playlist_7.ts
#EXTINF:6.0,
segment_1_playlist_8.ts
#EXTINF:6.0,
segment_1_playlist_9.ts
#EXTINF:6.0,
segment_1_playlist_10.ts
#EXTINF:3.96,
segment_1_playlist_11.ts
#EXT-X-ENDLIST
#EXTM3U
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-VERSION:5
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-TARGETDURATION:6
#EXTINF:6.0,
segment_2_playlist_0.ts
#EXTINF:6.0,
segment_2_playlist_1.ts
#EXTINF:6.0,
segment_2_playlist_2.ts
#EXTINF:6.0,
segment_2_playlist_3.ts
#EXTINF:6.0,
segment_2_playlist_4.ts
#EXTINF:6.0,
segment_2_playlist_5.ts
#EXTINF:6.0,
segment_2_playlist_6.ts
#EXTINF:6.0,
segment_2_playlist_7.ts
#EXTINF:6.0,
segment_2_playlist_8.ts
#EXTINF:6.0,
segment_2_playlist_9.ts
#EXTINF:6.0,
segment_2_playlist_10.ts
#EXTINF:3.96,
segment_2_playlist_11.ts
#EXT-X-ENDLIST
`

### Provide verbose output that clearly demonstrates the problem

- [X] Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
- [ ] 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

```shell
yt-dlp.exe -vU https://www.bild.de/video/clip/news-ausland/deftiger-abgang-vom-10m-turm-bademeister-sorgt-fuer-skandal-85158620.bild.html
[debug] Command-line config: ['-vU', 'https://www.bild.de/video/clip/news-ausland/deftiger-abgang-vom-10m-turm-bademeister-sorgt-fuer-skandal-85158620.bild.html']
[debug] Encodings: locale cp1252, fs utf-8, pref cp1252, out utf-8, error utf-8, screen utf-8
[debug] yt-dlp version nightly@2023.08.20.172604 [7cccab79e] (win_exe)
[debug] Python 3.8.10 (CPython AMD64 64bit) - Windows-10-10.0.19045-SP0 (OpenSSL 1.1.1k  25 Mar 2021)
[debug] exe versions: ffmpeg 2023-06-11-git-09621fd7d9-essentials_build-www.gyan.dev (setts), ffprobe 2023-06-11-git-09621fd7d9-essentials_build-www.gyan.dev
[debug] Optional libraries: Cryptodome-3.18.0, brotli-1.0.9, certifi-2023.07.22, mutagen-1.46.0, sqlite3-2.6.0, websockets-11.0.3
[debug] Proxy map: {}
[debug] Loaded 1864 extractors
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp-nightly-builds/releases/latest
Available version: nightly@2023.08.20.172604, Current version: nightly@2023.08.20.172604
Current Build Hash: 71c3f327a60b9cbbaa27ff92ed08abe76e600010298e392b98a41e2cdac634f8
yt-dlp is up to date (nightly@2023.08.20.172604)
[Bild] Extracting URL: https://www.bild.de/video/clip/news-ausland/deftiger-abgang-vom-10m-turm-bademeister-sorgt-fuer-skandal-85158620.bild.html
[Bild] 85158620: Downloading JSON metadata
[debug] Formats sorted by: hasvid, ie_pref, lang, quality, res, fps, hdr:12(7), vcodec:vp9.2(10), channels, acodec, size, br, asr, proto, vext, aext, hasaud, source, id
[debug] Default format spec: bestvideo*+bestaudio/best
[info] 85158620: Downloading 1 format(s): 0
[debug] Invoking hlsnative downloader on "https://hds.ak.token.bild.de/85158620,delivery=hls.m3u8"
[hlsnative] Downloading m3u8 manifest
[hlsnative] Total fragments: 3
[download] Destination: Bademeister sorgt für Skandal [85158620].m3u8
[debug] File locking is not supported. Proceeding without locking
[download] 100% of    1.64KiB in 00:00:00 at 6.58KiB/s
```



Discussion:
The bild.de extractor isn't aware of pages which deliver a choice of an MP4 file and an M3U8, and doesn't parse the M3U8 (instead downloads it as a file).

A quick hack to ignore the M3U8:
```
diff --git a/yt_dlp/extractor/bild.py b/yt_dlp/extractor/bild.py
index f3dea33c4..426a78b60 100644
--- a/yt_dlp/extractor/bild.py
+++ b/yt_dlp/extractor/bild.py
@@ -27,11 +27,18 @@ def _real_extract(self, url):
         video_data = self._download_json(
             url.split('.bild.html')[0] + ',view=json.bild.html', video_id)

+        url = None
+        for src in video_data['clipList'][0]['srces']:
+            if src['type'] == 'video/mp4':
+                url = src['src']
+                break
+
         return {
             'id': video_id,
             'title': unescapeHTML(video_data['title']).strip(),
             'description': unescapeHTML(video_data.get('description')),
-            'url': video_data['clipList'][0]['srces'][0]['src'],
+            'url': url,
             'thumbnail': video_data.get('poster'),
             'duration': int_or_none(video_data.get('durationSec')),
         }
```
(Yes - there's no error checking, no fallbacks, and a lot of assumptions in this simple hack.)
In a proper solution, the type of each provided URL would need to be checked, and the M3U8 parsed in order to produce further formats and proper URLs.

A workaround for you right now is to throw the downloaded M3U8 at yt-dlp again:
`yt-dlp "Bademeister sorgt für Skandal [85158620].m3u8"`
As background information, here's part of the JSON this extractor expects and assumes:
```
    "clipList": [
        {
            "type": "main",
            "srces": [
                {
                    "src": "https://videos-world.ak.token.bild.de/BILD/38/18/41/46/38184146,property=Video.mp4",
                    "type": "video/mp4"
                }
            ]
        }
    ],
```
and here's what it's getting for the clip mentioned in this ticket:
```
    "clipList": [
        {
            "type": "main",
            "srces": [
                {
                    "src": "https://hds.ak.token.bild.de/85158620,delivery=hls.m3u8",
                    "type": "application/x-mpegURL"
                },
                {
                    "src": "https://hds.ak.token.bild.de/85158620,delivery=pmd.mp4",
                    "type": "video/mp4"
                }
            ]
        }
    ],
```


Resolving patch:
diff --git a/yt_dlp/extractor/bild.py b/yt_dlp/extractor/bild.py
--- a/yt_dlp/extractor/bild.py
+++ b/yt_dlp/extractor/bild.py
@@ -1,6 +1,7 @@
 from .common import InfoExtractor
 from ..utils import (
     int_or_none,
+    traverse_obj,
     unescapeHTML,
 )
 
@@ -8,7 +9,8 @@
 class BildIE(InfoExtractor):
     _VALID_URL = r'https?://(?:www\.)?bild\.de/(?:[^/]+/)+(?P<display_id>[^/]+)-(?P<id>\d+)(?:,auto=true)?\.bild\.html'
     IE_DESC = 'Bild.de'
-    _TEST = {
+    _TESTS = [{
+        'note': 'static MP4 only',
         'url': 'http://www.bild.de/video/clip/apple-ipad-air/das-koennen-die-neuen-ipads-38184146.bild.html',
         'md5': 'dd495cbd99f2413502a1713a1156ac8a',
         'info_dict': {
@@ -19,7 +21,19 @@ class BildIE(InfoExtractor):
             'thumbnail': r're:^https?://.*\.jpg$',
             'duration': 196,
         }
-    }
+    }, {
+        'note': 'static MP4 and HLS',
+        'url': 'https://www.bild.de/video/clip/news-ausland/deftiger-abgang-vom-10m-turm-bademeister-sorgt-fuer-skandal-85158620.bild.html',
+        'md5': 'fb0ed4f09c495d4ba7ce2eee0bb90de1',
+        'info_dict': {
+            'id': '85158620',
+            'ext': 'mp4',
+            'title': 'Der Sprungturm-Skandal',
+            'description': 'md5:709b543c24dc31bbbffee73bccda34ad',
+            'thumbnail': r're:^https?://.*\.jpg$',
+            'duration': 69,
+        }
+    }]
 
     def _real_extract(self, url):
         video_id = self._match_id(url)
@@ -27,11 +41,23 @@ def _real_extract(self, url):
         video_data = self._download_json(
             url.split('.bild.html')[0] + ',view=json.bild.html', video_id)
 
+        formats = []
+        for src in traverse_obj(video_data, ('clipList', 0, 'srces', lambda _, v: v['src'])):
+            src_type = src.get('type')
+            if src_type == 'application/x-mpegURL':
+                formats.extend(
+                    self._extract_m3u8_formats(
+                        src['src'], video_id, 'mp4', m3u8_id='hls', fatal=False))
+            elif src_type == 'video/mp4':
+                formats.append({'url': src['src'], 'format_id': 'http-mp4'})
+            else:
+                self.report_warning(f'Skipping unsupported format type: "{src_type}"')
+
         return {
             'id': video_id,
             'title': unescapeHTML(video_data['title']).strip(),
             'description': unescapeHTML(video_data.get('description')),
-            'url': video_data['clipList'][0]['srces'][0]['src'],
+            'formats': formats,
             'thumbnail': video_data.get('poster'),
             'duration': int_or_none(video_data.get('durationSec')),
         }


Test patch: