Repository: yt-dlp/yt-dlp

Issue:
ON ORF
### 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 a new site support request
- [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 none of provided URLs [violate any copyrights](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#is-the-website-primarily-used-for-piracy) or contain any [DRM](https://en.wikipedia.org/wiki/Digital_rights_management) to the best of my knowledge
- [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 am willing to share it if required

### Region

Austria

### Example URLs

https://on.orf.at/video/14207227/biester-folge-1

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

The ORF is currently doing a rollout of its new platform on.orf.at, which is soon going to substitute tvthek.orf.at: [https://www.diepresse.com/17916269/orf-die-tvthek-wird-2024-komplett-ersetzt](url)

### 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
[debug] Command-line config: ['-vU', '-i', 'https://on.orf.at/video/14207227/biester-folge-1', '-o', '...\\Biester S01E01.mp4']
[debug] Encodings: locale cp1252, fs utf-8, pref cp1252, out utf-8, error utf-8, screen utf-8
[debug] yt-dlp version stable@2023.12.30 from yt-dlp/yt-dlp [f10589e34] (pip)
[debug] Python 3.12.1 (CPython AMD64 64bit) - Windows-10-10.0.19045-SP0 (OpenSSL 3.0.11 19 Sep 2023)
[debug] exe versions: ffmpeg 6.1-essentials_build-www.gyan.dev (setts), ffprobe 6.1-essentials_build-www.gyan.dev
[debug] Optional libraries: Cryptodome-3.19.0, brotli-1.1.0, certifi-2023.11.17, mutagen-1.47.0, requests-2.31.0, sqlite3-3.43.1, urllib3-2.1.0, websockets-12.0
[debug] Proxy map: {}
[debug] Request Handlers: urllib, requests, websockets
[debug] Loaded 1798 extractors
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
Latest version: stable@2023.12.30 from yt-dlp/yt-dlp
yt-dlp is up to date (stable@2023.12.30 from yt-dlp/yt-dlp)
[generic] Extracting URL: https://on.orf.at/video/14207227/biester-folge-1
[generic] biester-folge-1: Downloading webpage
WARNING: [generic] Falling back on generic information extractor
[generic] biester-folge-1: Extracting information
[debug] Looking for embeds
ERROR: Unsupported URL: https://on.orf.at/video/14207227/biester-folge-1
Traceback (most recent call last):
  File "C:\Users\...\AppData\Local\Programs\Python\Python312\Lib\site-packages\yt_dlp\YoutubeDL.py", line 1587, in wrapper
    return func(self, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\...\AppData\Local\Programs\Python\Python312\Lib\site-packages\yt_dlp\YoutubeDL.py", line 1722, in __extract_info
    ie_result = ie.extract(url)
                ^^^^^^^^^^^^^^^
  File "C:\Users\...\AppData\Local\Programs\Python\Python312\Lib\site-packages\yt_dlp\extractor\common.py", line 718, in extract
    ie_result = self._real_extract(url)
                ^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\...\AppData\Local\Programs\Python\Python312\Lib\site-packages\yt_dlp\extractor\generic.py", line 2531, in _real_extract
    raise UnsupportedError(url)
yt_dlp.utils.UnsupportedError: Unsupported URL: https://on.orf.at/video/14207227/biester-folge-1
```



Discussion:
Until support is added the way to download from there is as follows:

1. Open developer tools (F12) and head to the Network tab
2. Reload the page
3. Copy the URL of "manifest.mpd", e.g. `https://apasfiis.sf.apa.at/dash/cms-austria/2023-12-24_0340_in_01_Biester--Folge-_____14207227__o__3105401976__s15540100_QXB.mp4/manifest.mpd`
4. Pass that URL to yt-dlp to download the file.
Is this gonna work for geoblocked content as well?
I was able to download geoblocked content from ORF until maybe 2 months ago. But it does not work any more and the guide from @Esokrates  does not work for geoblocked content.

yt-dlp asked me to report a bug, but I think this would have been a duplicate of this one.
so if anybody wants URLs for testing...

bump for on.orf.at support.
The OP link geo-blocked from me because of that, use other link (`https://on.orf.at/video/14210000/school-of-champions-48`) that not restricted to particular region. The websites seems using Nuxt to load the pages which give us metadata dan video manifest in webpage.

Inside of `__NUXT_DATA__`, we could get the video url:
<details><summary>Example of the video manifest in __NUXT_DATA__</summary>

```js
...
{
          "is_uhd": 60,
          "quality_key": 76,
          "quality_description": 77,
          "src": 78,
          "is_adaptive_stream": 42,
          "is_drm_protected": 60
        }, "QXA", "Adaptiv", "https://apasfiis.sf.apa.at/ipad/cms-worldwide/2024-01-29_2105_in_01_School-of-Champ_____14210000__o__1696083865__s15558584_QXA.mp4/playlist.m3u8", {
          "is_uhd": 60,
          "quality_key": 80,
          "quality_description": 77,
          "src": 81,
          "is_adaptive_stream": 42,
          "is_drm_protected": 60
        }, "QXB", "https://apasfiis.sf.apa.at/ipad/cms-worldwide/2024-01-29_2105_in_01_School-of-Champ_____14210000__o__1696083865__s15558584_QXB.mp4/playlist.m3u8", [83, 85], {
          "is_uhd": 60,
          "quality_key": 76,
          "quality_description": 77,
          "src": 84,
          "is_adaptive_stream": 42,
          "is_drm_protected": 60
        }, "https://apasfiis.sf.apa.at/f4m/cms-worldwide/2024-01-29_2105_in_01_School-of-Champ_____14210000__o__1696083865__s15558584_QXA.mp4/manifest.f4m", {
          "is_uhd": 60,
          "quality_key": 80,
          "quality_description": 77,
          "src": 86,
          "is_adaptive_stream": 42,
          "is_drm_protected": 60
        }, "https://apasfiis.sf.apa.at/f4m/cms-worldwide/2024-01-29_2105_in_01_School-of-Champ_____14210000__o__1696083865__s15558584_QXB.mp4/manifest.f4m", [88, 90], {
          "is_uhd": 60,
          "quality_key": 76,
          "quality_description": 77,
          "src": 89,
          "is_adaptive_stream": 42,
          "is_drm_protected": 60
        }, "https://apasfiis.sf.apa.at/smooth/cms-worldwide/2024-01-29_2105_in_01_School-of-Champ_____14210000__o__1696083865__s15558584_QXA.mp4/manifest", {
          "is_uhd": 60,
          "quality_key": 80,
          "quality_description": 77,
          "src": 91,
          "is_adaptive_stream": 42,
          "is_drm_protected": 60
        }, "https://apasfiis.sf.apa.at/smooth/cms-worldwide/2024-01-29_2105_in_01_School-of-Champ_____14210000__o__1696083865__s15558584_QXB.mp4/manifest", [93, 95], {
          "is_uhd": 60,
          "quality_key": 76,
          "quality_description": 77,
          "src": 94,
          "is_adaptive_stream": 42,
          "is_drm_protected": 60
        }, "https://apasfiis.sf.apa.at/dash/cms-worldwide/2024-01-29_2105_in_01_School-of-Champ_____14210000__o__1696083865__s15558584_QXA.mp4/manifest.mpd", {
          "is_uhd": 60,
          "quality_key": 80,
          "quality_description": 77,
          "src": 96,
          "is_adaptive_stream": 42,
          "is_drm_protected": 60
        }, "https://apasfiis.sf.apa.at/dash/cms-worldwide/2024-01-29_2105_in_01_School-of-Champ_____14210000__o__1696083865__s15558584_QXB.mp4/manifest.mpd", "Film & Serie | Neue Serie", "Luca hat sein ganzes Leben in den Bergen Südtirols verbracht und ist bei seiner Mutter aufgewachsen, die als Skihüttenpächterin ums finanzielle Überleben kämpft. Als absolutes Ausnahme-Talent schafft er es an die Schule, obwohl er offensichtlich soziale Defizite hat. Er will professionell Skifahren, doch bei zunehmenden sozialen Verpflichtungen wird er zur tickenden Zeitbombe.", {
          "hls": 100,
          "hds": 107,
          "smooth": 112,
          "dash": 117
        },
        [101, 104], {
          "quality_key": 102,
          "src": 103
        }, "Q6A", "https://apasfiis.sf.apa.at/ipad/cms-preview-clips/2024-01-29_2105_in_01_School-of-Champ_____14210000__o__1696083865__s15558584_clip_Q6A.mp4/playlist.m3u8", {
          "quality_key": 105,
          "src": 106
        }, "Q8C", "https://apasfiis.sf.apa.at/ipad/cms-preview-clips/2024-01-29_2105_in_01_School-of-Champ_____14210000__o__1696083865__s15558584_clip_Q8C.mp4/playlist.m3u8", [108, 110], {
          "quality_key": 102,
          "src": 109
        }, "https://apasfiis.sf.apa.at/f4m/cms-preview-clips/2024-01-29_2105_in_01_School-of-Champ_____14210000__o__1696083865__s15558584_clip_Q6A.mp4/manifest.f4m", {
          "quality_key": 105,
          "src": 111
        }, "https://apasfiis.sf.apa.at/f4m/cms-preview-clips/2024-01-29_2105_in_01_School-of-Champ_____14210000__o__1696083865__s15558584_clip_Q8C.mp4/manifest.f4m", [113, 115], {
          "quality_key": 102,
          "src": 114
        }, "https://apasfiis.sf.apa.at/smooth/cms-preview-clips/2024-01-29_2105_in_01_School-of-Champ_____14210000__o__1696083865__s15558584_clip_Q6A.mp4/Manifest", {
          "quality_key": 105,
          "src": 116
        }, "https://apasfiis.sf.apa.at/smooth/cms-preview-clips/2024-01-29_2105_in_01_School-of-Champ_____14210000__o__1696083865__s15558584_clip_Q8C.mp4/Manifest", [118, 120], {
          "quality_key": 102,
          "src": 119
        }, "https://apasfiis.sf.apa.at/dash/cms-preview-clips/2024-01-29_2105_in_01_School-of-Champ_____14210000__o__1696083865__s15558584_clip_Q6A.mp4/manifest.mpd", {
          "quality_key": 105,
          "src": 121
        }, "https://apasfiis.sf.apa.at/dash/cms-preview-clips/2024-01-29_2105_in_01_School-of-Champ_____14210000__o__1696083865__s15558584_clip_Q8C.mp4/manifest.mpd", [], "episode", "2024-01-29T11:52:01+01:00", "limited-20-06", {
          "self": 127,
          "channel": 129,
          "profile": 131,
          "segments": 133,
          "tags": 135,
          "advertising_tags": 137,
          "links": 139,
          "audio_description_episode": 141,
          "image": 143,
          "image16x9_with_logo": 145,
          "image2x3_with_logo": 147,
          "image2x3": 149,
          "subtitle": 151,
          "related_livestream": 153,
          "related_episodes": 155,
          "dds_item": 157
        }, 
...
```
</details>

As Esokrates said in https://github.com/yt-dlp/yt-dlp/issues/8903#issuecomment-1873850382, the manifest link can be processed by yt-dlp

<details><summary>Test to check format from m3u8 file

```shell
>python -m yt_dlp -v -F "https://apasfiis.sf.apa.at/ipad/cms-worldwide/2024-01-29_2105_in_01_School-of-Champ_____14210000__o__1696083865__s15558584_QXA.mp4/playlist.m3u8"
[debug] Command-line config: ['-v', '-F', 'https://apasfiis.sf.apa.at/ipad/cms-worldwide/2024-01-29_2105_in_01_School-of-Champ_____14210000__o__1696083865__s15558584_QXA.mp4/playlist.m3u8']
[debug] Encodings: locale cp1252, fs utf-8, pref cp1252, out utf-8, error utf-8, screen utf-8 
[debug] yt-dlp version stable@2023.12.30 from yt-dlp/yt-dlp [f10589e34] (source)
[debug] Lazy loading extractors is disabled
[debug] Git HEAD: 41b6cdb41 
[debug] Python 3.11.1 (CPython AMD64 64bit) - Windows-10-10.0.22621-SP0 (OpenSSL 1.1.1q  5 Jul 2022) 
[debug] exe versions: ffmpeg 5.1.2-full_build-www.gyan.dev (setts), ffprobe 5.1.2-full_build-www.gyan.dev 
[debug] Optional libraries: sqlite3-3.39.4
[debug] Proxy map: {}
[debug] Request Handlers: urllib
[debug] Loaded 1824 extractors 
[generic] Extracting URL: https://apasfiis.sf.apa.at/ipad/cms-worldwide/2024-01-29_2105_in_01_School-of-Champ_____14210000__o__1696083865__s15558584_QXA.mp4/playlist.m3u8
[generic] playlist: Downloading webpage
[redirect] Following redirect to https://vod-ww.mdn.ors.at/cms-worldwide_nas/_definst_/nas/cms-worldwide/online/2024-01-29_2105_in_01_School-of-Champ_____14210000__o__1696083865__s15558584.smil/playlist.m3u8
[generic] Extracting URL: https://vod-ww.mdn.ors.at/cms-worldwide_nas/_definst_/nas/cms-worldwide/online/2024-01-29_2105_in_01_School-of-Champ_____14210000__o__1696083865__s15558584.smil/playlist.m3u8
[generic] playlist: Downloading webpage
[debug] Identified a direct video link 
[generic] playlist: Downloading m3u8 information
[generic] playlist: Checking m3u8 live status 
[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
[info] Available formats for playlist:
ID   EXT RESOLUTION │   FILESIZE   TBR PROTO │ VCODEC  ACODEC
──────────────────────────────────────────────────────────────
992  mp4 640x360    │ ~321.02MiB  992k m3u8  │ unknown unknown
1992 mp4 960x540    │ ~644.63MiB 1992k m3u8  │ unknown unknown
3192 mp4 1280x720   │ ~  1.01GiB 3192k m3u8  │ unknown unknown

```

</details>

---
Metadata can be found either in `meta` tag (including the `og` and `twitter`) or in `ld+json` and possibly in `__NUXT_DATA__` (as i didn't test to parse the `__NUXT_DATA__`)

 

Resolving patch:
diff --git a/yt_dlp/extractor/_extractors.py b/yt_dlp/extractor/_extractors.py
--- a/yt_dlp/extractor/_extractors.py
+++ b/yt_dlp/extractor/_extractors.py
@@ -1386,6 +1386,7 @@
 from .orf import (
     ORFTVthekIE,
     ORFFM4StoryIE,
+    ORFONIE,
     ORFRadioIE,
     ORFPodcastIE,
     ORFIPTVIE,
diff --git a/yt_dlp/extractor/orf.py b/yt_dlp/extractor/orf.py
--- a/yt_dlp/extractor/orf.py
+++ b/yt_dlp/extractor/orf.py
@@ -1,3 +1,4 @@
+import base64
 import functools
 import re
 
@@ -565,3 +566,66 @@ def _real_extract(self, url):
             })
 
         return self.playlist_result(entries)
+
+
+class ORFONIE(InfoExtractor):
+    IE_NAME = 'orf:on'
+    _VALID_URL = r'https?://on\.orf\.at/video/(?P<id>\d{8})/(?P<slug>[\w-]+)'
+    _TESTS = [{
+        'url': 'https://on.orf.at/video/14210000/school-of-champions-48',
+        'info_dict': {
+            'id': '14210000',
+            'ext': 'mp4',
+            'duration': 2651.08,
+            'thumbnail': 'https://api-tvthek.orf.at/assets/segments/0167/98/thumb_16697671_segments_highlight_teaser.jpeg',
+            'title': 'School of Champions (4/8)',
+            'description': 'md5:d09ad279fc2e8502611e7648484b6afd',
+            'media_type': 'episode',
+            'timestamp': 1706472362,
+            'upload_date': '20240128',
+        }
+    }]
+
+    def _extract_video(self, video_id, display_id):
+        encrypted_id = base64.b64encode(f'3dSlfek03nsLKdj4Jsd{video_id}'.encode()).decode()
+        api_json = self._download_json(
+            f'https://api-tvthek.orf.at/api/v4.3/public/episode/encrypted/{encrypted_id}', display_id)
+
+        formats, subtitles = [], {}
+        for manifest_type in traverse_obj(api_json, ('sources', {dict.keys}, ...)):
+            for manifest_url in traverse_obj(api_json, ('sources', manifest_type, ..., 'src', {url_or_none})):
+                if manifest_type == 'hls':
+                    fmts, subs = self._extract_m3u8_formats_and_subtitles(
+                        manifest_url, display_id, fatal=False, m3u8_id='hls')
+                elif manifest_type == 'dash':
+                    fmts, subs = self._extract_mpd_formats_and_subtitles(
+                        manifest_url, display_id, fatal=False, mpd_id='dash')
+                else:
+                    continue
+                formats.extend(fmts)
+                self._merge_subtitles(subs, target=subtitles)
+
+        return {
+            'id': video_id,
+            'formats': formats,
+            'subtitles': subtitles,
+            **traverse_obj(api_json, {
+                'duration': ('duration_second', {float_or_none}),
+                'title': (('title', 'headline'), {str}),
+                'description': (('description', 'teaser_text'), {str}),
+                'media_type': ('video_type', {str}),
+            }, get_all=False),
+        }
+
+    def _real_extract(self, url):
+        video_id, display_id = self._match_valid_url(url).group('id', 'slug')
+        webpage = self._download_webpage(url, display_id)
+
+        return {
+            'id': video_id,
+            'title': self._html_search_meta(['og:title', 'twitter:title'], webpage, default=None),
+            'description': self._html_search_meta(
+                ['description', 'og:description', 'twitter:description'], webpage, default=None),
+            **self._search_json_ld(webpage, display_id, fatal=False),
+            **self._extract_video(video_id, display_id),
+        }


Test patch: