Repository: yt-dlp/yt-dlp
Pull request #10388: [ie/rtve] Rework extractors
Author: login: meGAmeS1
Labels: name: site-enhancement
description: Feature request for some website
name: site-bug
description: Issue with a specific website
Created: 2024-07-08T18:10:46Z | Merged: 2025-04-18T23:47:14Z

### Description of your *pull request* and other information

<!--

Explanation of your *pull request* in arbitrary form goes here. Please **make sure the description explains the purpose and effect** of your *pull request* and is worded well enough to be understood. Provide as much **context and examples** as possible

-->

Added RTVE Play compatibility. I reverse engineered the Javascript from the website to adapt the download from RTVE player.

Fixed every tests for RTVE extractors (in order to use long available files).

Closes #1346 


<details open><summary>Template</summary> <!-- OPEN is intentional -->

<!--

# PLEASE FOLLOW THE GUIDE BELOW

- You will be asked some questions, please read them **carefully** and answer honestly
- Put an `x` into all the boxes `[ ]` relevant to your *pull request* (like [x])
- Use *Preview* tab to see how your *pull request* will actually look like

-->

### Before submitting a *pull request* make sure you have:
- [x] At least skimmed through [contributing guidelines](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#developer-instructions) including [yt-dlp coding conventions](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#yt-dlp-coding-conventions)
- [x] [Searched](https://github.com/yt-dlp/yt-dlp/search?q=is%3Apr&type=Issues) the bugtracker for similar pull requests

### In order to be accepted and merged into yt-dlp each piece of code must be in public domain or released under [Unlicense](http://unlicense.org/). Check all of the following options that apply:
- [x] I am the original author of this code and I am willing to release it under [Unlicense](http://unlicense.org/)

### What is the purpose of your *pull request*?
- [x] Fix or improvement to an extractor (Make sure to add/update tests)
- [x] New extractor ([Piracy websites will not be accepted](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#is-the-website-primarily-used-for-piracy))

</details>


Linked issue #1346: Rtve.es Play
The list of the supported sites say that Rtve Alacarta has supported. This site has replaced with Rtve Play and extractor doesn't work with new site. Please could you add support for Rtve Play? This is example to video https://www.rtve.es/play/videos/comando-actualidad/
Linked issue comments: user: login: nggfng
created_at: 2021-10-24T23:05:49Z
body: Someone suggested replacing /play/ for /alacarta/ in the URL.
After that, I'm getting a "Payment Required" error with this video:
https://www.rtve.es/alacarta/videos/la-caza/caza-monteperdido-capitulo-1-deshielo/5092259/
author_association: NONE
user: login: Jiriss
created_at: 2021-10-25T18:44:28Z
body: Wow, It works if video is free and it hasn't any restrictions as paywall or geo-blocking. For instance, this video works https://www.rtve.es/play/videos/lazos-de-sangre/t3-concha-piquer/5655688/ when replacing play for alacarta Yt-dlp will output a destination url with http:// prefix but it also works with https:// one.
author_association: NONE
user: login: hansworzt
created_at: 2021-10-30T17:39:38Z
body: Any video on RTVE Play - including the geo-restricted and paywalled ones - can be transformed into a freely accessible URL and downloaded with "yt-dlp", after some additional steps:

> 1) Example program URL: https://www.rtve.es/play/videos/somos-cine/somos-cine-altamira/5621023/
Copy this to https://www.piraminetlab.com/ to generate the video URL.
Resulting video URL: https://rtve-mediavod.secure.footprint.net/resources/TE_GLESP/mp4/7/4/1594294885247.mp4?nvb=20211030172651&nva=20211030232651&token=047dbe539d7132ff9bc98
The video URL can sometimes also be found in the HAR file saved from the network tab in Firefox.

> 2) This geo-restricted video only plays from Spanish IP addresses.
Transform into the geo-unrestricted URL:
https://rtve-hlsvod.secure.footprint.net/resources/TE_NGVA/mp4/7/4/1594294885247.mp4/playlist.m3u8?idasset=5621023

> 3) yt-dlp -F ... shows the available format(s), etc.

No doubt some Python wizzard could implement this into an extractor.
author_association: NONE
user: login: hansworzt
created_at: 2021-10-30T17:50:53Z
body: > 
> 
> Someone suggested replacing /play/ for /alacarta/ in the URL. After that, I'm getting a "Payment Required" error with this video: https://www.rtve.es/alacarta/videos/la-caza/caza-monteperdido-capitulo-1-deshielo/5092259/

This transformed URL works just fine:
https://rtve-hlsvod.secure.footprint.net/resources/TE_NGVA/mp4/8/9/1553557872798.mp4/playlist.m3u8?idasset=5092259
author_association: NONE
user: login: Jiriss
created_at: 2022-01-15T18:28:14Z
body: When will the yt-dlp project be added this change to the production?
author_association: NONE
user: login: Jiriss
created_at: 2022-03-10T21:37:48Z
body: It isn't work anymore with the latest version 2022.03.08.1 although using workground replacing /play/ for /alacarta/ in the URL. Output listed below

$ yt-dlp --version
2022.03.08.1

$ yt-dlp -F https://www.rtve.es/alacarta/videos/telediario-2/telediario-21-horas-10-03-22/6431098/
Traceback (most recent call last):
  File "/usr/local/bin/yt-dlp/yt_dlp/extractor/common.py", line 615, in extract
  File "/usr/local/bin/yt-dlp/yt_dlp/extractor/common.py", line 523, in initialize
  File "/usr/local/bin/yt-dlp/yt_dlp/extractor/rtve.py", line 73, in _real_initialize
KeyError: 'User-Agent'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/local/bin/yt-dlp/yt_dlp/YoutubeDL.py", line 1389, in wrapper
  File "/usr/local/bin/yt-dlp/yt_dlp/YoutubeDL.py", line 1459, in __extract_info
  File "/usr/local/bin/yt-dlp/yt_dlp/extractor/common.py", line 647, in extract
yt_dlp.utils.ExtractorError: 6431098: An extractor error has occurred. (caused by KeyError('User-Agent')); please report this issue on  https://github.com/yt-dlp/yt-dlp , filling out the "Broken site" issue template properly. Confirm you are on the latest version using  yt-dlp -U

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/lib/python3.8/runpy.py", line 194, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "/usr/lib/python3.8/runpy.py", line 87, in _run_code
    exec(code, run_globals)
  File "/usr/local/bin/yt-dlp/__main__.py", line 19, in <module>
  File "/usr/local/bin/yt-dlp/yt_dlp/__init__.py", line 864, in main
  File "/usr/local/bin/yt-dlp/yt_dlp/__init__.py", line 854, in _real_main
  File "/usr/local/bin/yt-dlp/yt_dlp/YoutubeDL.py", line 3254, in download
  File "/usr/local/bin/yt-dlp/yt_dlp/YoutubeDL.py", line 3227, in wrapper
  File "/usr/local/bin/yt-dlp/yt_dlp/YoutubeDL.py", line 1380, in extract_info
  File "/usr/local/bin/yt-dlp/yt_dlp/YoutubeDL.py", line 1407, in wrapper
  File "/usr/local/bin/yt-dlp/yt_dlp/utils.py", line 1088, in format_traceback
TypeError: format_exception() missing 2 required positional arguments: 'value' and 'tb'
author_association: NONE
user: login: hansworzt
created_at: 2022-03-11T11:35:49Z
body: They changed it so new streams can no longer be downloaded. The https://www.piraminetlab.com/ site that used to offer the URLs of RTVE programmes is thus basically dead. To alleviate this never ending cat-and-mouse game, use software that can record played streams. I use TubeDigger (payware) for this. No matter what they change from now on, if it plays in a web browser it will be recorded losslessly with this software.
author_association: NONE
user: login: pukkandan
created_at: 2022-04-07T02:23:30Z
body: https://github.com/ytdl-org/youtube-dl/pull/29816
author_association: MEMBER
user: login: dirkf
created_at: 2022-04-07T03:06:31Z
body: >... This is example to video https://www.rtve.es/play/videos/comando-actualidad/

But the actual episode that you can play from that page: https://www.rtve.es/play/videos/comando-actualidad/abandonados-bancos/6459510/

The corresponding series page: https://www.rtve.es/play/videos/modulos/capitulos/11332/?currentpage=pf_serie, from which we need to harvest these elements:
`<a class="goto_media" href="https://www.rtve.es/play/videos/comando-actualidad/abandonados-bancos/6459510/" title="Empezar a ver">`.
author_association: CONTRIBUTOR
user: login: alexanderadam
created_at: 2022-06-21T21:08:37Z
body: Apart from the `play` vs `alacarta` issue, it's also not possible to [fetch/embed the subtitles](https://forum.videohelp.com/threads/380455-Downloading-subtitles-from-rtve-es) (it seems that there was once [another Python script](https://github.com/tanty/rtve-subtitles) for this).
Is this related or should I create a new issue for this?
author_association: NONE
user: login: mildsunrise
created_at: 2022-09-23T07:13:47Z
body: I've tried all links to videos mentioned in this issue (except https://www.rtve.es/play/videos/comando-actualidad/, which as pointed by @dirkf is NOT the link to a video, but to a show's landing page), and I can confirm they all work with https://github.com/ytdl-org/youtube-dl/pull/31241 applied (from a spanish IP). They work regardless of `play` or `alacarta` in the URL.
author_association: NONE
user: login: Jiriss
created_at: 2022-10-05T20:12:09Z
body: Support for rtve.es is still unavailable

$ yt-dlp --version
2022.10.04

Tested with European IP address but outside of Spain.

$ yt-dlp -v -F https://www.rtve.es/play/videos/telediario-2/21-horas-04-10-22/6706793/
[debug] Command-line config: ['-v', '-F', 'https://www.rtve.es/play/videos/telediario-2/21-horas-04-10-22/6706793/']
[debug] Encodings: locale UTF-8, fs utf-8, pref UTF-8, out utf-8, error utf-8, screen utf-8
[debug] yt-dlp version 2022.10.04 [4e0511f] (zip)
[debug] Python 3.8.10 (CPython 64bit) - Linux-5.4.0-126-generic-x86_64-with-glibc2.29 (glibc 2.31)
[debug] Checking exe version: ffmpeg -bsfs
[debug] Checking exe version: ffprobe -bsfs
[debug] exe versions: ffmpeg 4.2.7, ffprobe 4.2.7, rtmpdump 2.4
[debug] Optional libraries: certifi-2019.11.28, pycrypto-2.6.1, pyxattr-0.6.1, secretstorage-2.3.1, sqlite3-2.6.0
[debug] Proxy map: {}
[debug] Loaded 1690 extractors
[debug] [generic] Extracting URL: https://www.rtve.es/play/videos/telediario-2/21-horas-04-10-22/6706793/
[generic] 6706793: Downloading webpage
WARNING: [generic] Falling back on generic information extractor
[generic] 6706793: Extracting information
[debug] Looking for Brightcove embeds
[debug] Looking for embeds
ERROR: Unsupported URL: https://www.rtve.es/play/videos/telediario-2/21-horas-04-10-22/6706793/
Traceback (most recent call last):
  File "/usr/local/bin/yt-dlp/yt_dlp/YoutubeDL.py", line 1477, in wrapper
    return func(self, *args, **kwargs)
  File "/usr/local/bin/yt-dlp/yt_dlp/YoutubeDL.py", line 1553, in __extract_info
    ie_result = ie.extract(url)
  File "/usr/local/bin/yt-dlp/yt_dlp/extractor/common.py", line 672, in extract
    ie_result = self._real_extract(url)
  File "/usr/local/bin/yt-dlp/yt_dlp/extractor/generic.py", line 3062, in _real_extract
    raise UnsupportedError(url)
yt_dlp.utils.UnsupportedError: Unsupported URL: https://www.rtve.es/play/videos/telediario-2/21-horas-04-10-22/6706793/

Situation is same although replaced /play/ with /alacarta/ in the URL.

$ yt-dlp -v -F https://www.rtve.es/alacarta/videos/telediario-2/21-horas-04-10-22/6706793/
[debug] Command-line config: ['-v', '-F', 'https://www.rtve.es/alacarta/videos/telediario-2/21-horas-04-10-22/6706793/']
[debug] Encodings: locale UTF-8, fs utf-8, pref UTF-8, out utf-8, error utf-8, screen utf-8
[debug] yt-dlp version 2022.10.04 [4e0511f] (zip)
[debug] Python 3.8.10 (CPython 64bit) - Linux-5.4.0-126-generic-x86_64-with-glibc2.29 (glibc 2.31)
[debug] Checking exe version: ffmpeg -bsfs
[debug] Checking exe version: ffprobe -bsfs
[debug] exe versions: ffmpeg 4.2.7, ffprobe 4.2.7, rtmpdump 2.4
[debug] Optional libraries: certifi-2019.11.28, pycrypto-2.6.1, pyxattr-0.6.1, secretstorage-2.3.1, sqlite3-2.6.0
[debug] Proxy map: {}
[debug] Loaded 1690 extractors
[rtve.es:alacarta] Fetching manager info
[debug] [rtve.es:alacarta] Extracting URL: https://www.rtve.es/alacarta/videos/telediario-2/21-horas-04-10-22/6706793/
[rtve.es:alacarta] 6706793: Downloading JSON metadata
[rtve.es:alacarta] 6706793: Downloading url information
ERROR: not enough values to unpack (expected 2, got 1)
Traceback (most recent call last):
  File "/usr/local/bin/yt-dlp/yt_dlp/YoutubeDL.py", line 1477, in wrapper
    return func(self, *args, **kwargs)
  File "/usr/local/bin/yt-dlp/yt_dlp/YoutubeDL.py", line 1553, in __extract_info
    ie_result = ie.extract(url)
  File "/usr/local/bin/yt-dlp/yt_dlp/extractor/common.py", line 672, in extract
    ie_result = self._real_extract(url)
  File "/usr/local/bin/yt-dlp/yt_dlp/extractor/rtve.py", line 144, in _real_extract
    formats = self._extract_png_formats(video_id)
  File "/usr/local/bin/yt-dlp/yt_dlp/extractor/rtve.py", line 118, in _extract_png_formats
    for quality, video_url in self._decrypt_url(png):
  File "/usr/local/bin/yt-dlp/yt_dlp/extractor/rtve.py", line 81, in _decrypt_url
    quality, url_data = text.split(b'%%')
ValueError: not enough values to unpack (expected 2, got 1)

author_association: NONE
user: login: dincel
created_at: 2023-01-22T06:59:57Z
body: # Manual way of retrieving direct content links from RTVE.es/play portal
sample URL: https://www.rtve.es/play/videos/carlos-rey-emperador/carlos-rey-emperador-capitulo-1/3312421/

1. While playing the media, goto devtools (F12) > Network tab and search for ".m3u8" (refresh with F5 if necessary)
there will be two results:
https://rtvehlsvodlote7.rtve.es/mediavodv2/resources/TE_SCARLOS/mp4/5/9/1641318615695.mp4/video.m3u8?hls_no_audio_only=true&idasset=3312421
https://rtvehlsvodlote7.rtve.es/mediavodv2/resources/TE_SCARLOS/mp4/5/9/1641318615695.mp4/1641318615695-audio=192975-video=2759000.m3u8?idasset=3312421
2. take either one and remove the part after .mp4
3. change the URL's "https://rtvehlsvodlote7.rtve.es/mediavodv2/" part to "https://mediavod-lvlt.rtve.es/" (domain at the download links for radio RNE content) resulting the final working URL: https://mediavod-lvlt.rtve.es/resources/TE_SCARLOS/mp4/5/9/1641318615695.mp4
4. Now the subtitles. As in step (1) this time search for ".vtt" and directly copy the link "https://www.rtve.es/resources/vtt/0/3/1648225256930.vtt" (you don't have to download it)
add both links to your in your text editor and save as .m3u playlist (e.g. play.m3u) . Like this;
...
https://mediavod-lvlt.rtve.es/resources/TE_SCARLOS/mp4/5/9/1641318615695.mp4
https://www.rtve.es/resources/vtt/0/3/1648225256930.vtt
...
5. When you open Celluloid player it only plays .mp4 file. And you can copy the .vtt link address from the playlist items (F9) and open it wiht Ctrl + L shortcut. This will activate while playing, subtitles too.

This seems a bit player specific and but at least I found a way to retrieve both video and subs.
But manually it takes too long. It is up to you programmers to apply this workaround to yt-dlp API and allow us to watch shows directly from the visible URLs like https://www.rtve.es/play/videos/carlos-rey-emperador/carlos-rey-emperador-capitulo-1/3312421/
author_association: NONE
user: login: hansworzt
created_at: 2023-01-23T16:54:48Z
body: There's a simpler manual method that maybe automated:

1) Copy the page URL: https://www.rtve.es/play/videos/carlos-rey-emperador/carlos-rey-emperador-capitulo-1/3312421/

2) Put this into https://www.descargavideos.tv/ and hit the search button.

3) Hit "Descargar (opción 1, supuestamente sin geobloqueo".
This displays the video and shows the direct URL :
https://mediavod-lvlt.rtve.es/resources/TE_SCARLOS/mp4/5/9/1641318615695.mp4
A shorter URL that doesn't bother with certificates is:
http://www.rtve.es/resources/TE_SCARLOS/mp4/5/9/1641318615695.mp4
This can be downloaded with:
yt-dlp -f mp4 http://www.rtve.es/resources/TE_SCARLOS/mp4/5/9/1641318615695.mp4
Or with:
wget http://www.rtve.es/resources/TE_SCARLOS/mp4/5/9/1641318615695.mp4
There's another button on the "Descargar" page that gives you the direct subtitles link.
author_association: NONE
user: login: pabgan
created_at: 2025-03-12T09:53:04Z
body: @hansworzt I thought I might give that a try and came up with https://gitlab.com/pabgan/rtve-dl

You can dowload specific videos 

```
./rtve-dl  https://www.rtve.es/play/videos/la-corona-magica/corona-magica-cristal-hansto/2663281/
```

or complete series:

```
./rtve-dl  --coleccion https://www.rtve.es/play/videos/la-corona-magica/
```

When I have time I might try to include this functionality into yt-dlp itself, but right now I really wanted this and it was quicker to create this script from scratch.


author_association: NONE
user: login: Lol274
created_at: 2025-03-26T11:11:28Z
body: Maybe this is a bit of off-topic, but I got this link from rtve with a bunch more that played the videos but now it does not due to a IP issue

https://rtve-hlsvod.secure.footprint.net/resources/TE_GLESP/mp4/3/0/1479563027703.mp4/playlist.m3u8

Maybe this now probably doesn't have any solution, but if there is, how I can fix it? Thank you
author_association: NONE
user: login: Jiriss
created_at: 2025-03-26T18:27:12Z
body: @pabgan These links doesn't work outside Spain
author_association: NONE
user: login: pabgan
created_at: 2025-03-27T08:15:11Z
body: @Jiriss hmm you mean the ones I pasted in my previous comment or those that the script I created generates?

Because if you mean those in my comment there is not much I can do about it, if you can't access those probably you will not want to use that script. The other problem might be workaroundable.
author_association: NONE
user: login: Jiriss
created_at: 2025-03-27T10:25:28Z
body: @pabgan Sorry for the misunderstanding. Earlier I tested the links via www.descargavideos.tv and links behind the "Opción 1 (Sin geobloqueo. No funciona)" didn't work. I continued testing and noticed that links behind the  "Opción 3 (Sin geobloqueo. No funciona)" ja "Opción 4 (Sin geobloqueo. No funciona)" also work outside Spain.

The newest version of the yt-dlp can't show direct video links from RTVE Play at least outside Spain.
author_association: NONE
user: login: pabgan
created_at: 2025-03-27T15:12:28Z
body: OK, I can make the script try those links in order until one works... But I can not tell you when :)
author_association: NONE
user: login: VinceLeone
created_at: 2025-03-29T17:35:46Z
body: @pabgan : Could you add to your script the possibility to download the subtitles?
With this line I could extract the vtt url subtitle:

`curl --silent --data "web=${link}" 'https://www.descargavideos.tv' | grep vtt | head --lines=1 | cut --delimiter='"' --fields=6`

Thanks for your work.
author_association: NONE
user: login: pabgan
created_at: 2025-03-31T07:35:02Z
body: @Jiriss , @VinceLeone : You can follow your requests in https://gitlab.com/pabgan/rtve-dl/-/issues/1 and https://gitlab.com/pabgan/rtve-dl/-/issues/2.
author_association: NONE
user: login: meGAmeS1
created_at: 2025-03-31T09:58:35Z
body: By the way, I worked on an extractor a while ago that solves this issue and adds RTVE Play in yt-dlp (supports multiple audio tracks and subtitles, and it doesn't need an external service like descargavideos.tv, though you need to have access to the video in case it is geo-restricted so you may have to use a VPN if you're outside Spain). Sadly it hasn't been merged already (you may support it by putting 👍 or ❤️ on it) : https://github.com/yt-dlp/yt-dlp/pull/10388.

The only thing is it doesn't support collection (yet) but I may add it as I already reversed engineer their API
author_association: CONTRIBUTOR
user: login: VinceLeone
created_at: 2025-03-31T14:52:50Z
body: @meGAmeS1 : I tested your version with the changes you made and it worked for me (I'm in Spain so I suppose RTVE Play+ works without username/password). By default it took the hls format with the best quality.
I have this output:
> ```
> user@linux:~/yt-dlp-add-rtve-play$ ./yt-dlp.sh -F https://www.rtve.es/play/videos/el-comisario-montalbano/perro-terracota/6641981/
> [rtve.es:alacarta] Fetching manager info
> [rtve.es:alacarta] Extracting URL: https://www.rtve.es/play/videos/el-comisario-montalbano/perro-terracota/6641981/
> [rtve.es:alacarta] 6641981: Downloading JSON metadata
> [rtve.es:alacarta] 6641981: Downloading url information
> [rtve.es:alacarta] 6641981: Downloading m3u8 information
> [rtve.es:alacarta] 6641981: Downloading MPD manifest
> [rtve.es:alacarta] 6641981: Downloading url information
> [rtve.es:alacarta] 6641981: Downloading m3u8 information
> [rtve.es:alacarta] 6641981: Downloading MPD manifest
> [info] Available formats for 6641981:
> ID                  EXT RESOLUTION FPS │   FILESIZE   TBR PROTO │ VCODEC        VBR ACODEC      ABR ASR MORE INFO
> ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
> dash-audio=194477-0 m4a audio only     │ ~142.46MiB  194k dash  │ audio only        mp4a.40.2  194k 48k DASH audio, m4a_dash
> dash-audio=194477-1 m4a audio only     │ ~142.46MiB  194k dash  │ audio only        mp4a.40.2  194k 48k DASH audio, m4a_dash
> 2                   mp4 unknown        │                  http  │ unknown           unknown
> 3                   mp4 unknown        │                  http  │ unknown           unknown
> dash-video=2766000  mp4 1280x720    25 │ ~  1.98GiB 2766k dash  │ avc1.640029 2766k video only          DASH video, mp4_dash
> hls-3139            mp4 1280x720    25 │ ~  2.25GiB 3139k m3u8  │ avc1.640029 2766k mp4a.40.2  194k
> dash-video=4520000  mp4 1920x1080   25 │ ~  3.23GiB 4520k dash  │ avc1.640029 4520k video only          DASH video, mp4_dash
> hls-4998            mp4 1920x1080   25 │ ~  3.58GiB 4998k m3u8  │ avc1.640029 4520k mp4a.40.2  194k

Hope it's integrated soon in the master branch.
author_association: NONE
user: login: VinceLeone
created_at: 2025-05-06T14:10:02Z
body: @meGAmeS1 : I just tried to get a video with multiaudio (it's available only with movies) but I only get the same id types that in my post earlier but it's in fact the same audio language. Have you found a solution with that?

P.S. By the way, thanks for your work. It's now integrated in the stable version.
author_association: NONE
user: login: meGAmeS1
created_at: 2025-05-06T14:44:55Z
body: > @meGAmeS1 : I just tried to get a video with multiaudio (it's available only with movies) but I only get the same id types that in my post earlier but it's in fact the same audio language. Have you found a solution with that?
> 
> P.S. By the way, thanks for your work. It's now integrated in the stable version.

Hi. Glad my work helps people! The video you're trying to download is [this one](https://www.rtve.es/play/videos/el-comisario-montalbano/perro-terracota/6641981/)? I'm currently on my phone but I can't take look tonight or tomorrow
author_association: CONTRIBUTOR
user: login: VinceLeone
created_at: 2025-05-06T22:24:34Z
body: No I tried a movie that has 2 audios (original english and spanish dub) like this : https://www.rtve.es/play/videos/cine-internacional/aisha/15972529/
The series, live shows, tv programs are only in spanish audio.

> ```
>[rtve.es:alacarta] Extracting URL: https://www.rtve.es/play/videos/cine-internacional/aisha/15972529/
>[rtve.es:alacarta] 15972529: Downloading JSON metadata
>[rtve.es:alacarta] 15972529: Downloading url information
>[rtve.es:alacarta] 15972529: Downloading m3u8 information
>[rtve.es:alacarta] 15972529: Downloading MPD manifest
>[rtve.es:alacarta] 15972529: Downloading url information
>[rtve.es:alacarta] 15972529: Downloading m3u8 information
>[rtve.es:alacarta] 15972529: Downloading MPD manifest
>[info] Available formats for 15972529:
>ID                  EXT RESOLUTION FPS │   FILESIZE   TBR PROTO │ VCODEC        VBR ACODEC      ABR ASR MORE INFO
>────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
>dash-audio=193341-0 m4a audio only     │ ~125.02MiB  193k dash  │ audio only        mp4a.40.2  193k 48k DASH audio, m4a_dash
>dash-audio=193341-1 m4a audio only     │ ~125.02MiB  193k dash  │ audio only        mp4a.40.2  193k 48k DASH audio, m4a_dash
>2                   mp4 unknown        │                  http  │ unknown           unknown
>3                   mp4 unknown        │                  http  │ unknown           unknown
>dash-video=2750000  mp4 1280x720    25 │ ~  1.74GiB 2750k dash  │ avc1.640029 2750k video only          DASH video, mp4_dash
>hls-3120            mp4 1280x720    25 │ ~  1.97GiB 3120k m3u8  │ avc1.640029 2750k mp4a.40.2  193k
>dash-video=4500000  mp4 1920x1080   25 │ ~  2.84GiB 4500k dash  │ avc1.640029 4500k video only          DASH video, mp4_dash
>hls-4975            mp4 1920x1080   25 │ ~  3.14GiB 4975k m3u8  │ avc1.640029 4500k mp4a.40.2  193k

the dash audio 193341-0 and 193341-1 are exactly the same spanish dub audio. But in the web RTVE Play when watching the video you can select the original english audio.

P.S. I didn't check that your link is an italian series that do have original italian and spanish dub in the web page.
author_association: NONE
user: login: meGAmeS1
created_at: 2025-05-08T09:08:36Z
body: @VinceLeone So, I've managed to have time to look into your issue and as I supposed, this is the same stuff that happened to an other member that wanted to download both audio tracks with different languages: the content you see in the browser is DRM protected, the content retrieved by yt-dlp isn't.

Sadly, the code that was recently merged won't allow you to download both languages, and only Spanish will be available (and we're kinda lucky that is available). The reason is that what yt-dlp allows you to download isn't exactly the same content you see in the RTVE webpage. What you see in your browser is DRM protected, and as yt-dlp cannot be used to infringe copyright ([as written in its guidelines](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#is-the-website-primarily-used-for-piracy)) we only retrieve non encrypted content through RTVE APIs. The fact that RTVE decides to distribute DRM protected content also via its non protected APIs is kinda lucky I guess and might change in the future. Also it's up to RTVE to decide which content should be protected or not (mainly their content from their own programs isn't protected, and that content they buy from elsewhere is DRM protected).

author_association: CONTRIBUTOR
user: login: VinceLeone
created_at: 2025-05-08T15:12:17Z
body: Ok thanks for the response. I didn't think about that some parts of the content was DRM and others don't.
Thanks to clarify it.
author_association: NONE

Diff:
diff --git a/yt_dlp/extractor/_extractors.py b/yt_dlp/extractor/_extractors.py
index f7e3f25c3ba8..bae8c8335c13 100644
--- a/yt_dlp/extractor/_extractors.py
+++ b/yt_dlp/extractor/_extractors.py
@@ -1783,7 +1783,6 @@
 from .rtve import (
     RTVEALaCartaIE,
     RTVEAudioIE,
-    RTVEInfantilIE,
     RTVELiveIE,
     RTVETelevisionIE,
 )
diff --git a/yt_dlp/extractor/rtve.py b/yt_dlp/extractor/rtve.py
index 7e0b666ab371..2812d93059bf 100644
--- a/yt_dlp/extractor/rtve.py
+++ b/yt_dlp/extractor/rtve.py
@@ -1,35 +1,142 @@
 import base64
 import io
 import struct
+import urllib.parse
 
 from .common import InfoExtractor
 from ..utils import (
     ExtractorError,
+    clean_html,
     determine_ext,
     float_or_none,
+    make_archive_id,
+    parse_iso8601,
     qualities,
-    remove_end,
-    remove_start,
-    try_get,
+    url_or_none,
 )
+from ..utils.traversal import subs_list_to_dict, traverse_obj
 
 
-class RTVEALaCartaIE(InfoExtractor):
+class RTVEBaseIE(InfoExtractor):
+    # Reimplementation of https://js2.rtve.es/pages/app-player/3.5.1/js/pf_video.js
+    @staticmethod
+    def _decrypt_url(png):
+        encrypted_data = io.BytesIO(base64.b64decode(png)[8:])
+        while True:
+            length_data = encrypted_data.read(4)
+            length = struct.unpack('!I', length_data)[0]
+            chunk_type = encrypted_data.read(4)
+            if chunk_type == b'IEND':
+                break
+            data = encrypted_data.read(length)
+            if chunk_type == b'tEXt':
+                data = bytes(filter(None, data))
+                alphabet_data, _, url_data = data.partition(b'#')
+                quality_str, _, url_data = url_data.rpartition(b'%%')
+                quality_str = quality_str.decode() or ''
+                alphabet = RTVEBaseIE._get_alphabet(alphabet_data)
+                url = RTVEBaseIE._get_url(alphabet, url_data)
+                yield quality_str, url
+            encrypted_data.read(4)  # CRC
+
+    @staticmethod
+    def _get_url(alphabet, url_data):
+        url = ''
+        f = 0
+        e = 3
+        b = 1
+        for char in url_data.decode('iso-8859-1'):
+            if f == 0:
+                l = int(char) * 10
+                f = 1
+            else:
+                if e == 0:
+                    l += int(char)
+                    url += alphabet[l]
+                    e = (b + 3) % 4
+                    f = 0
+                    b += 1
+                else:
+                    e -= 1
+        return url
+
+    @staticmethod
+    def _get_alphabet(alphabet_data):
+        alphabet = []
+        e = 0
+        d = 0
+        for char in alphabet_data.decode('iso-8859-1'):
+            if d == 0:
+                alphabet.append(char)
+                d = e = (e + 1) % 4
+            else:
+                d -= 1
+        return alphabet
+
+    def _extract_png_formats_and_subtitles(self, video_id, media_type='videos'):
+        formats, subtitles = [], {}
+        q = qualities(['Media', 'Alta', 'HQ', 'HD_READY', 'HD_FULL'])
+        for manager in ('rtveplayw', 'default'):
+            png = self._download_webpage(
+                f'http://www.rtve.es/ztnr/movil/thumbnail/{manager}/{media_type}/{video_id}.png',
+                video_id, 'Downloading url information', query={'q': 'v2'}, fatal=False)
+            if not png:
+                continue
+
+            for quality, video_url in self._decrypt_url(png):
+                ext = determine_ext(video_url)
+                if ext == 'm3u8':
+                    fmts, subs = self._extract_m3u8_formats_and_subtitles(
+                        video_url, video_id, 'mp4', m3u8_id='hls', fatal=False)
+                    formats.extend(fmts)
+                    self._merge_subtitles(subs, target=subtitles)
+                elif ext == 'mpd':
+                    fmts, subs = self._extract_mpd_formats_and_subtitles(
+                        video_url, video_id, 'dash', fatal=False)
+                    formats.extend(fmts)
+                    self._merge_subtitles(subs, target=subtitles)
+                else:
+                    formats.append({
+                        'format_id': quality,
+                        'quality': q(quality),
+                        'url': video_url,
+                    })
+        return formats, subtitles
+
+    def _parse_metadata(self, metadata):
+        return traverse_obj(metadata, {
+            'title': ('title', {str.strip}),
+            'alt_title': ('alt', {str.strip}),
+            'description': ('description', {clean_html}),
+            'timestamp': ('dateOfEmission', {parse_iso8601(delimiter=' ')}),
+            'release_timestamp': ('publicationDate', {parse_iso8601(delimiter=' ')}),
+            'modified_timestamp': ('modificationDate', {parse_iso8601(delimiter=' ')}),
+            'thumbnail': (('thumbnail', 'image', 'imageSEO'), {url_or_none}, any),
+            'duration': ('duration', {float_or_none(scale=1000)}),
+            'is_live': ('live', {bool}),
+            'series': (('programTitle', ('programInfo', 'title')), {clean_html}, any),
+        })
+
+
+class RTVEALaCartaIE(RTVEBaseIE):
     IE_NAME = 'rtve.es:alacarta'
-    IE_DESC = 'RTVE a la carta'
-    _VALID_URL = r'https?://(?:www\.)?rtve\.es/(m/)?(alacarta/videos|filmoteca)/[^/]+/[^/]+/(?P<id>\d+)'
+    IE_DESC = 'RTVE a la carta and Play'
+    _VALID_URL = [
+        r'https?://(?:www\.)?rtve\.es/(?:m/)?(?:(?:alacarta|play)/videos|filmoteca)/(?!directo)(?:[^/?#]+/){2}(?P<id>\d+)',
+        r'https?://(?:www\.)?rtve\.es/infantil/serie/[^/?#]+/video/[^/?#]+/(?P<id>\d+)',
+    ]
 
     _TESTS = [{
-        'url': 'http://www.rtve.es/alacarta/videos/balonmano/o-swiss-cup-masculina-final-espana-suecia/2491869/',
-        'md5': '1d49b7e1ca7a7502c56a4bf1b60f1b43',
+        'url': 'http://www.rtve.es/alacarta/videos/la-aventura-del-saber/aventuraentornosilla/3088905/',
+        'md5': 'a964547824359a5753aef09d79fe984b',
         'info_dict': {
-            'id': '2491869',
+            'id': '3088905',
             'ext': 'mp4',
-            'title': 'Balonmano - Swiss Cup masculina. Final: España-Suecia',
-            'duration': 5024.566,
-            'series': 'Balonmano',
+            'title': 'En torno a la silla',
+            'duration': 1216.981,
+            'series': 'La aventura del Saber',
+            'thumbnail': 'https://img2.rtve.es/v/aventuraentornosilla_3088905.png',
         },
-        'expected_warnings': ['Failed to download MPD manifest', 'Failed to download m3u8 information'],
     }, {
         'note': 'Live stream',
         'url': 'http://www.rtve.es/alacarta/videos/television/24h-live/1694255/',
@@ -38,140 +145,88 @@ class RTVEALaCartaIE(InfoExtractor):
             'ext': 'mp4',
             'title': 're:^24H LIVE [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
             'is_live': True,
+            'live_status': 'is_live',
+            'thumbnail': r're:https://img2\.rtve\.es/v/.*\.png',
         },
         'params': {
             'skip_download': 'live stream',
         },
     }, {
         'url': 'http://www.rtve.es/alacarta/videos/servir-y-proteger/servir-proteger-capitulo-104/4236788/',
-        'md5': 'd850f3c8731ea53952ebab489cf81cbf',
+        'md5': 'f3cf0d1902d008c48c793e736706c174',
         'info_dict': {
             'id': '4236788',
             'ext': 'mp4',
-            'title': 'Servir y proteger - Capítulo 104',
-            'duration': 3222.0,
+            'title': 'Episodio 104',
+            'duration': 3222.8,
+            'thumbnail': r're:https://img2\.rtve\.es/v/.*\.png',
+            'series': 'Servir y proteger',
         },
-        'expected_warnings': ['Failed to download MPD manifest', 'Failed to download m3u8 information'],
     }, {
         'url': 'http://www.rtve.es/m/alacarta/videos/cuentame-como-paso/cuentame-como-paso-t16-ultimo-minuto-nuestra-vida-capitulo-276/2969138/?media=tve',
         'only_matching': True,
     }, {
         'url': 'http://www.rtve.es/filmoteca/no-do/not-1-introduccion-primer-noticiario-espanol/1465256/',
         'only_matching': True,
+    }, {
+        'url': 'https://www.rtve.es/play/videos/saber-vivir/07-07-24/16177116/',
+        'md5': 'a5b24fcdfa3ff5cb7908aba53d22d4b6',
+        'info_dict': {
+            'id': '16177116',
+            'ext': 'mp4',
+            'title': 'Saber vivir - 07/07/24',
+            'thumbnail': r're:https://img2\.rtve\.es/v/.*\.png',
+            'duration': 2162.68,
+            'series': 'Saber vivir',
+        },
+    }, {
+        'url': 'https://www.rtve.es/infantil/serie/agus-lui-churros-crafts/video/gusano/7048976/',
+        'info_dict': {
+            'id': '7048976',
+            'ext': 'mp4',
+            'title': 'Gusano',
+            'thumbnail': r're:https://img2\.rtve\.es/v/.*\.png',
+            'duration': 292.86,
+            'series': 'Agus & Lui: Churros y Crafts',
+            '_old_archive_ids': ['rtveinfantil 7048976'],
+        },
     }]
 
-    def _real_initialize(self):
-        user_agent_b64 = base64.b64encode(self.get_param('http_headers')['User-Agent'].encode()).decode('utf-8')
-        self._manager = self._download_json(
-            'http://www.rtve.es/odin/loki/' + user_agent_b64,
-            None, 'Fetching manager info')['manager']
-
-    @staticmethod
-    def _decrypt_url(png):
-        encrypted_data = io.BytesIO(base64.b64decode(png)[8:])
-        while True:
-            length = struct.unpack('!I', encrypted_data.read(4))[0]
-            chunk_type = encrypted_data.read(4)
-            if chunk_type == b'IEND':
-                break
-            data = encrypted_data.read(length)
-            if chunk_type == b'tEXt':
-                alphabet_data, text = data.split(b'\0')
-                quality, url_data = text.split(b'%%')
-                alphabet = []
-                e = 0
-                d = 0
-                for l in alphabet_data.decode('iso-8859-1'):
-                    if d == 0:
-                        alphabet.append(l)
-                        d = e = (e + 1) % 4
-                    else:
-                        d -= 1
-                url = ''
-                f = 0
-                e = 3
-                b = 1
-                for letter in url_data.decode('iso-8859-1'):
-                    if f == 0:
-                        l = int(letter) * 10
-                        f = 1
-                    else:
-                        if e == 0:
-                            l += int(letter)
-                            url += alphabet[l]
-                            e = (b + 3) % 4
-                            f = 0
-                            b += 1
-                        else:
-                            e -= 1
-
-                yield quality.decode(), url
-            encrypted_data.read(4)  # CRC
-
-    def _extract_png_formats(self, video_id):
-        png = self._download_webpage(
-            f'http://www.rtve.es/ztnr/movil/thumbnail/{self._manager}/videos/{video_id}.png',
-            video_id, 'Downloading url information', query={'q': 'v2'})
-        q = qualities(['Media', 'Alta', 'HQ', 'HD_READY', 'HD_FULL'])
-        formats = []
-        for quality, video_url in self._decrypt_url(png):
-            ext = determine_ext(video_url)
-            if ext == 'm3u8':
-                formats.extend(self._extract_m3u8_formats(
-                    video_url, video_id, 'mp4', 'm3u8_native',
-                    m3u8_id='hls', fatal=False))
-            elif ext == 'mpd':
-                formats.extend(self._extract_mpd_formats(
-                    video_url, video_id, 'dash', fatal=False))
-            else:
-                formats.append({
-                    'format_id': quality,
-                    'quality': q(quality),
-                    'url': video_url,
-                })
-        return formats
+    def _get_subtitles(self, video_id):
+        subtitle_data = self._download_json(
+            f'https://api2.rtve.es/api/videos/{video_id}/subtitulos.json', video_id,
+            'Downloading subtitles info')
+        return traverse_obj(subtitle_data, ('page', 'items', ..., {
+            'id': ('lang', {str}),
+            'url': ('src', {url_or_none}),
+        }, all, {subs_list_to_dict(lang='es')}))
 
     def _real_extract(self, url):
         video_id = self._match_id(url)
-        info = self._download_json(
+        metadata = self._download_json(
             f'http://www.rtve.es/api/videos/{video_id}/config/alacarta_videos.json',
             video_id)['page']['items'][0]
-        if info['state'] == 'DESPU':
+        if metadata['state'] == 'DESPU':
             raise ExtractorError('The video is no longer available', expected=True)
-        title = info['title'].strip()
-        formats = self._extract_png_formats(video_id)
+        formats, subtitles = self._extract_png_formats_and_subtitles(video_id)
 
-        subtitles = None
-        sbt_file = info.get('sbtFile')
-        if sbt_file:
-            subtitles = self.extract_subtitles(video_id, sbt_file)
+        self._merge_subtitles(self.extract_subtitles(video_id), target=subtitles)
 
-        is_live = info.get('live') is True
+        is_infantil = urllib.parse.urlparse(url).path.startswith('/infantil/')
 
         return {
             'id': video_id,
-            'title': title,
             'formats': formats,
-            'thumbnail': info.get('image'),
             'subtitles': subtitles,
-            'duration': float_or_none(info.get('duration'), 1000),
-            'is_live': is_live,
-            'series': info.get('programTitle'),
+            **self._parse_metadata(metadata),
+            '_old_archive_ids': [make_archive_id('rtveinfantil', video_id)] if is_infantil else None,
         }
 
-    def _get_subtitles(self, video_id, sub_file):
-        subs = self._download_json(
-            sub_file + '.json', video_id,
-            'Downloading subtitles info')['page']['items']
-        return dict(
-            (s['lang'], [{'ext': 'vtt', 'url': s['src']}])
-            for s in subs)
-
 
-class RTVEAudioIE(RTVEALaCartaIE):  # XXX: Do not subclass from concrete IE
+class RTVEAudioIE(RTVEBaseIE):
     IE_NAME = 'rtve.es:audio'
     IE_DESC = 'RTVE audio'
-    _VALID_URL = r'https?://(?:www\.)?rtve\.es/(alacarta|play)/audios/[^/]+/[^/]+/(?P<id>[0-9]+)'
+    _VALID_URL = r'https?://(?:www\.)?rtve\.es/(alacarta|play)/audios/(?:[^/?#]+/){2}(?P<id>\d+)'
 
     _TESTS = [{
         'url': 'https://www.rtve.es/alacarta/audios/a-hombros-de-gigantes/palabra-ingeniero-codigos-informaticos-27-04-21/5889192/',
@@ -180,9 +235,11 @@ class RTVEAudioIE(RTVEALaCartaIE):  # XXX: Do not subclass from concrete IE
             'id': '5889192',
             'ext': 'mp3',
             'title': 'Códigos informáticos',
-            'thumbnail': r're:https?://.+/1598856591583.jpg',
+            'alt_title': 'Códigos informáticos - Escuchar ahora',
             'duration': 349.440,
             'series': 'A hombros de gigantes',
+            'description': 'md5:72b0d7c1ca20fd327bdfff7ac0171afb',
+            'thumbnail': 'https://img2.rtve.es/a/palabra-ingeniero-codigos-informaticos-270421_5889192.png',
         },
     }, {
         'url': 'https://www.rtve.es/play/audios/en-radio-3/ignatius-farray/5791165/',
@@ -191,9 +248,11 @@ class RTVEAudioIE(RTVEALaCartaIE):  # XXX: Do not subclass from concrete IE
             'id': '5791165',
             'ext': 'mp3',
             'title': 'Ignatius Farray',
+            'alt_title': 'En Radio 3 - Ignatius Farray - 13/02/21 - escuchar ahora',
             'thumbnail': r're:https?://.+/1613243011863.jpg',
             'duration': 3559.559,
             'series': 'En Radio 3',
+            'description': 'md5:124aa60b461e0b1724a380bad3bc4040',
         },
     }, {
         'url': 'https://www.rtve.es/play/audios/frankenstein-o-el-moderno-prometeo/capitulo-26-ultimo-muerte-victor-juan-jose-plans-mary-shelley/6082623/',
@@ -202,126 +261,101 @@ class RTVEAudioIE(RTVEALaCartaIE):  # XXX: Do not subclass from concrete IE
             'id': '6082623',
             'ext': 'mp3',
             'title': 'Capítulo 26 y último: La muerte de Victor',
+            'alt_title': 'Frankenstein o el moderno Prometeo - Capítulo 26 y último: La muerte de Victor',
             'thumbnail': r're:https?://.+/1632147445707.jpg',
             'duration': 3174.086,
             'series': 'Frankenstein o el moderno Prometeo',
+            'description': 'md5:4ee6fcb82ebe2e46d267e1d1c1a8f7b5',
         },
     }]
 
-    def _extract_png_formats(self, audio_id):
-        """
-        This function retrieves media related png thumbnail which obfuscate
-        valuable information about the media. This information is decrypted
-        via base class _decrypt_url function providing media quality and
-        media url
-        """
-        png = self._download_webpage(
-            f'http://www.rtve.es/ztnr/movil/thumbnail/{self._manager}/audios/{audio_id}.png',
-            audio_id, 'Downloading url information', query={'q': 'v2'})
-        q = qualities(['Media', 'Alta', 'HQ', 'HD_READY', 'HD_FULL'])
-        formats = []
-        for quality, audio_url in self._decrypt_url(png):
-            ext = determine_ext(audio_url)
-            if ext == 'm3u8':
-                formats.extend(self._extract_m3u8_formats(
-                    audio_url, audio_id, 'mp4', 'm3u8_native',
-                    m3u8_id='hls', fatal=False))
-            elif ext == 'mpd':
-                formats.extend(self._extract_mpd_formats(
-                    audio_url, audio_id, 'dash', fatal=False))
-            else:
-                formats.append({
-                    'format_id': quality,
-                    'quality': q(quality),
-                    'url': audio_url,
-                })
-        return formats
-
     def _real_extract(self, url):
         audio_id = self._match_id(url)
-        info = self._download_json(
-            f'https://www.rtve.es/api/audios/{audio_id}.json',
-            audio_id)['page']['items'][0]
+        metadata = self._download_json(
+            f'https://www.rtve.es/api/audios/{audio_id}.json', audio_id)['page']['items'][0]
+
+        formats, subtitles = self._extract_png_formats_and_subtitles(audio_id, media_type='audios')
 
         return {
             'id': audio_id,
-            'title': info['title'].strip(),
-            'thumbnail': info.get('thumbnail'),
-            'duration': float_or_none(info.get('duration'), 1000),
-            'series': try_get(info, lambda x: x['programInfo']['title']),
-            'formats': self._extract_png_formats(audio_id),
+            'formats': formats,
+            'subtitles': subtitles,
+            **self._parse_metadata(metadata),
         }
 
 
-class RTVEInfantilIE(RTVEALaCartaIE):  # XXX: Do not subclass from concrete IE
-    IE_NAME = 'rtve.es:infantil'
-    IE_DESC = 'RTVE infantil'
-    _VALID_URL = r'https?://(?:www\.)?rtve\.es/infantil/serie/[^/]+/video/[^/]+/(?P<id>[0-9]+)/'
-
-    _TESTS = [{
-        'url': 'http://www.rtve.es/infantil/serie/cleo/video/maneras-vivir/3040283/',
-        'md5': '5747454717aedf9f9fdf212d1bcfc48d',
-        'info_dict': {
-            'id': '3040283',
-            'ext': 'mp4',
-            'title': 'Maneras de vivir',
-            'thumbnail': r're:https?://.+/1426182947956\.JPG',
-            'duration': 357.958,
-        },
-        'expected_warnings': ['Failed to download MPD manifest', 'Failed to download m3u8 information'],
-    }]
-
-
-class RTVELiveIE(RTVEALaCartaIE):  # XXX: Do not subclass from concrete IE
+class RTVELiveIE(RTVEBaseIE):
     IE_NAME = 'rtve.es:live'
     IE_DESC = 'RTVE.es live streams'
-    _VALID_URL = r'https?://(?:www\.)?rtve\.es/directo/(?P<id>[a-zA-Z0-9-]+)'
+    _VALID_URL = [
+        r'https?://(?:www\.)?rtve\.es/directo/(?P<id>[a-zA-Z0-9-]+)',
+        r'https?://(?:www\.)?rtve\.es/play/videos/directo/[^/?#]+/(?P<id>[a-zA-Z0-9-]+)',
+    ]
 
     _TESTS = [{
         'url': 'http://www.rtve.es/directo/la-1/',
         'info_dict': {
             'id': 'la-1',
             'ext': 'mp4',
-            'title': 're:^La 1 [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
+            'live_status': 'is_live',
+            'title': str,
+            'description': str,
+            'thumbnail': r're:https://img\d\.rtve\.es/resources/thumbslive/\d+\.jpg',
+            'timestamp': int,
+            'upload_date': str,
         },
-        'params': {
-            'skip_download': 'live stream',
+        'params': {'skip_download': 'live stream'},
+    }, {
+        'url': 'https://www.rtve.es/play/videos/directo/deportes/tdp/',
+        'info_dict': {
+            'id': 'tdp',
+            'ext': 'mp4',
+            'live_status': 'is_live',
+            'title': str,
+            'description': str,
+            'thumbnail': r're:https://img2\d\.rtve\.es/resources/thumbslive/\d+\.jpg',
+            'timestamp': int,
+            'upload_date': str,
         },
+        'params': {'skip_download': 'live stream'},
+    }, {
+        'url': 'http://www.rtve.es/play/videos/directo/canales-lineales/la-1/',
+        'only_matching': True,
     }]
 
     def _real_extract(self, url):
-        mobj = self._match_valid_url(url)
-        video_id = mobj.group('id')
-
+        video_id = self._match_id(url)
         webpage = self._download_webpage(url, video_id)
-        title = remove_end(self._og_search_title(webpage), ' en directo en RTVE.es')
-        title = remove_start(title, 'Estoy viendo ')
 
-        vidplayer_id = self._search_regex(
-            (r'playerId=player([0-9]+)',
-             r'class=["\'].*?\blive_mod\b.*?["\'][^>]+data-assetid=["\'](\d+)',
-             r'data-id=["\'](\d+)'),
-            webpage, 'internal video ID')
+        data_setup = self._search_json(
+            r'<div[^>]+class="[^"]*videoPlayer[^"]*"[^>]*data-setup=\'',
+            webpage, 'data_setup', video_id)
+
+        formats, subtitles = self._extract_png_formats_and_subtitles(data_setup['idAsset'])
 
         return {
             'id': video_id,
-            'title': title,
-            'formats': self._extract_png_formats(vidplayer_id),
+            **self._search_json_ld(webpage, video_id, fatal=False),
+            'title': self._html_extract_title(webpage),
+            'formats': formats,
+            'subtitles': subtitles,
             'is_live': True,
         }
 
 
 class RTVETelevisionIE(InfoExtractor):
     IE_NAME = 'rtve.es:television'
-    _VALID_URL = r'https?://(?:www\.)?rtve\.es/television/[^/]+/[^/]+/(?P<id>\d+).shtml'
+    _VALID_URL = r'https?://(?:www\.)?rtve\.es/television/[^/?#]+/[^/?#]+/(?P<id>\d+).shtml'
 
     _TEST = {
-        'url': 'http://www.rtve.es/television/20160628/revolucion-del-movil/1364141.shtml',
+        'url': 'https://www.rtve.es/television/20091103/video-inedito-del-8o-programa/299020.shtml',
         'info_dict': {
-            'id': '3069778',
+            'id': '572515',
             'ext': 'mp4',
-            'title': 'Documentos TV - La revolución del móvil',
-            'duration': 3496.948,
+            'title': 'Clase inédita',
+            'duration': 335.817,
+            'thumbnail': r're:https://img2\.rtve\.es/v/.*\.png',
+            'series': 'El coro de la cárcel',
         },
         'params': {
             'skip_download': True,
@@ -332,11 +366,8 @@ def _real_extract(self, url):
         page_id = self._match_id(url)
         webpage = self._download_webpage(url, page_id)
 
-        alacarta_url = self._search_regex(
-            r'data-location="alacarta_videos"[^<]+url&quot;:&quot;(http://www\.rtve\.es/alacarta.+?)&',
-            webpage, 'alacarta url', default=None)
-        if alacarta_url is None:
-            raise ExtractorError(
-                'The webpage doesn\'t contain any video', expected=True)
+        play_url = self._html_search_meta('contentUrl', webpage)
+        if play_url is None:
+            raise ExtractorError('The webpage doesn\'t contain any video', expected=True)
 
-        return self.url_result(alacarta_url, ie=RTVEALaCartaIE.ie_key())
+        return self.url_result(play_url, ie=RTVEALaCartaIE.ie_key())


Reviews:
user: login: meGAmeS1
body: @bashonly Ready for review
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-07-23T08:14:56Z
user: login: seproDev
body: Sorry this took so long. Here an initial review.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T11:13:49Z
user: login: seproDev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-13T22:59:37Z
user: login: seproDev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-13T22:59:58Z
user: login: seproDev
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-13T23:13:15Z
user: login: bashonly
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-18T19:54:59Z
user: login: seproDev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T20:28:24Z
user: login: bashonly
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-18T20:41:55Z
user: login: seproDev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T20:49:10Z
user: login: seproDev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T20:56:44Z
user: login: bashonly
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-18T23:42:47Z

Review comments:
diff_hunk: @@ -38,18 +39,23 @@ class RTVEALaCartaIE(InfoExtractor):
             'ext': 'mp4',
             'title': 're:^24H LIVE [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
             'is_live': True,
+            'live_status': 'is_live',
+            'thumbnail': r're:https://img2\.rtve\.es/v/.*\.png',
         },
         'params': {
             'skip_download': 'live stream',
         },
+        'expected_warnings': ['Ignoring subtitle tracks found in the HLS manifest'],
path: yt_dlp/extractor/rtve.py
user: login: seproDev
body: Please change the format extraction to return both video and subtitle data. So using `self._extract_m3u8_formats_and_subtitles` and `self._extract_mpd_formats_and_subtitles`
created_at: 2025-03-31T10:15:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -58,6 +64,18 @@ class RTVEALaCartaIE(InfoExtractor):
     }, {
         'url': 'http://www.rtve.es/filmoteca/no-do/not-1-introduccion-primer-noticiario-espanol/1465256/',
         'only_matching': True,
+    }, {
+        'url': 'https://www.rtve.es/play/videos/saber-vivir/07-07-24/16177116/',
+        'md5': '9eebcf6e8d6306c3b7c46e86a0115f55',
+        'info_dict': {
+            'id': '16177116',
+            'ext': 'mp4',
+            'title': 'Saber vivir - 07/07/24',
+            'thumbnail': r're:https://img2\.rtve\.es/v/.*\.png',
+            'duration': 2162.68,
+            'series': 'Saber vivir',
+        },
+        'expected_warnings': ['Failed to download MPD manifest', 'Failed to download m3u8 information'],
     }]
 
     def _real_initialize(self):
path: yt_dlp/extractor/rtve.py
user: login: seproDev
body: This seems to always return `default`. Can we hard code it instead of making the extra request?
created_at: 2025-03-31T10:16:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -70,65 +88,86 @@ def _real_initialize(self):
     def _decrypt_url(png):
         encrypted_data = io.BytesIO(base64.b64decode(png)[8:])
         while True:
-            length = struct.unpack('!I', encrypted_data.read(4))[0]
+            length_data = encrypted_data.read(4)
+            length = struct.unpack('!I', length_data)[0]
             chunk_type = encrypted_data.read(4)
             if chunk_type == b'IEND':
                 break
             data = encrypted_data.read(length)
             if chunk_type == b'tEXt':
-                alphabet_data, text = data.split(b'\0')
-                quality, url_data = text.split(b'%%')
-                alphabet = []
-                e = 0
-                d = 0
-                for l in alphabet_data.decode('iso-8859-1'):
-                    if d == 0:
-                        alphabet.append(l)
-                        d = e = (e + 1) % 4
-                    else:
-                        d -= 1
-                url = ''
-                f = 0
-                e = 3
-                b = 1
-                for letter in url_data.decode('iso-8859-1'):
-                    if f == 0:
-                        l = int(letter) * 10
-                        f = 1
-                    else:
-                        if e == 0:
-                            l += int(letter)
-                            url += alphabet[l]
-                            e = (b + 3) % 4
-                            f = 0
-                            b += 1
-                        else:
-                            e -= 1
-
-                yield quality.decode(), url
+                if b'%%' in data:
+                    alphabet_data, text = data.split(b'\0')
+                    quality, url_data = text.split(b'%%')
+                    alphabet = RTVEALaCartaIE._get_alfabet(alphabet_data)
+                    url = RTVEALaCartaIE._get_url(alphabet, url_data)
+                    quality_str = quality.decode()
+                else:
+                    data = bytes(filter(None, data))
+                    alphabet_data, url_data = data.split(b'#')
+                    alphabet = RTVEALaCartaIE._get_alfabet(alphabet_data)
+
+                    url = RTVEALaCartaIE._get_url(alphabet, url_data)
+                    quality_str = ''
+                yield quality_str, url
             encrypted_data.read(4)  # CRC
 
+    @staticmethod
+    def _get_url(alphabet, url_data):
+        url = ''
+        f = 0
+        e = 3
+        b = 1
+        for letter in url_data.decode('iso-8859-1'):
+            if f == 0:
+                l = int(letter) * 10
+                f = 1
+            else:
+                if e == 0:
+                    l += int(letter)
+                    url += alphabet[l]
+                    e = (b + 3) % 4
+                    f = 0
+                    b += 1
+                else:
+                    e -= 1
+        return url
+
+    @staticmethod
+    def _get_alfabet(alphabet_data):
+        alphabet = []
+        e = 0
+        d = 0
+        for l in alphabet_data.decode('iso-8859-1'):
+            if d == 0:
+                alphabet.append(l)
+                d = e = (e + 1) % 4
+            else:
+                d -= 1
+        return alphabet
+
     def _extract_png_formats(self, video_id):
-        png = self._download_webpage(
-            f'http://www.rtve.es/ztnr/movil/thumbnail/{self._manager}/videos/{video_id}.png',
-            video_id, 'Downloading url information', query={'q': 'v2'})
-        q = qualities(['Media', 'Alta', 'HQ', 'HD_READY', 'HD_FULL'])
         formats = []
-        for quality, video_url in self._decrypt_url(png):
-            ext = determine_ext(video_url)
-            if ext == 'm3u8':
-                formats.extend(self._extract_m3u8_formats(
-                    video_url, video_id, 'mp4', 'm3u8_native',
-                    m3u8_id='hls', fatal=False))
-            elif ext == 'mpd':
-                formats.extend(self._extract_mpd_formats(
-                    video_url, video_id, 'dash', fatal=False))
-            else:
-                formats.append({
-                    'format_id': quality,
-                    'quality': q(quality),
-                    'url': video_url,
-                })
+        q = qualities(['Media', 'Alta', 'HQ', 'HD_READY', 'HD_FULL'])
+        for manager in (self._manager, 'rtveplayw'):
path: yt_dlp/extractor/rtve.py
user: login: seproDev
body: For what URLs is the additional variant required? This currently results in duplicate formats. Can we just always use `rtveplayw`?
created_at: 2025-03-31T10:17:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -141,10 +180,8 @@ def _real_extract(self, url):
         title = info['title'].strip()
path: yt_dlp/extractor/rtve.py
user: login: seproDev
body: We might as well fix this. Title does not need to be fatal. We can also in-line this along with some other info.
```py
        return {
            'id': video_id,
            'formats': formats,
            'subtitles': subtitles,
            **traverse_obj(info, {
                'title': ('title', {str.strip}),
                'alt_title': ('alt', {str.strip}),
                'description': ('description', {clean_html}),
                'timestamp': ('dateOfEmission', {parse_iso8601(delimiter=' ')}),
                'release_timestamp': ('publicationDate', {parse_iso8601(delimiter=' ')}),
                'modified_timestamp': ('modificationDate', {parse_iso8601(delimiter=' ')}),
                'thumbnail': (('thumbnail', 'image', 'imageSEO'), {url_or_none}, any),
                'duration': ('duration', {float_or_none(scale=1000)}),
                'is_live': ('live', {bool}),
                'series': ('programTitle', {clean_html}),
            }),
        }
```
Note: This requires you to merge master in to your branch for the partial application of `parse_iso8601`
created_at: 2025-03-31T10:26:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -161,7 +198,7 @@ def _real_extract(self, url):
 
     def _get_subtitles(self, video_id, sub_file):
         subs = self._download_json(
-            sub_file + '.json', video_id,
+            sub_file, video_id,
             'Downloading subtitles info')['page']['items']
path: yt_dlp/extractor/rtve.py
user: login: seproDev
body: Can you please change this to not be fatal.
created_at: 2025-03-31T10:27:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -70,65 +88,86 @@ def _real_initialize(self):
     def _decrypt_url(png):
         encrypted_data = io.BytesIO(base64.b64decode(png)[8:])
         while True:
-            length = struct.unpack('!I', encrypted_data.read(4))[0]
+            length_data = encrypted_data.read(4)
+            length = struct.unpack('!I', length_data)[0]
             chunk_type = encrypted_data.read(4)
             if chunk_type == b'IEND':
                 break
             data = encrypted_data.read(length)
             if chunk_type == b'tEXt':
-                alphabet_data, text = data.split(b'\0')
-                quality, url_data = text.split(b'%%')
-                alphabet = []
-                e = 0
-                d = 0
-                for l in alphabet_data.decode('iso-8859-1'):
-                    if d == 0:
-                        alphabet.append(l)
-                        d = e = (e + 1) % 4
-                    else:
-                        d -= 1
-                url = ''
-                f = 0
-                e = 3
-                b = 1
-                for letter in url_data.decode('iso-8859-1'):
-                    if f == 0:
-                        l = int(letter) * 10
-                        f = 1
-                    else:
-                        if e == 0:
-                            l += int(letter)
-                            url += alphabet[l]
-                            e = (b + 3) % 4
-                            f = 0
-                            b += 1
-                        else:
-                            e -= 1
-
-                yield quality.decode(), url
+                if b'%%' in data:
+                    alphabet_data, text = data.split(b'\0')
+                    quality, url_data = text.split(b'%%')
+                    alphabet = RTVEALaCartaIE._get_alfabet(alphabet_data)
+                    url = RTVEALaCartaIE._get_url(alphabet, url_data)
+                    quality_str = quality.decode()
+                else:
+                    data = bytes(filter(None, data))
+                    alphabet_data, url_data = data.split(b'#')
+                    alphabet = RTVEALaCartaIE._get_alfabet(alphabet_data)
+
+                    url = RTVEALaCartaIE._get_url(alphabet, url_data)
+                    quality_str = ''
+                yield quality_str, url
             encrypted_data.read(4)  # CRC
 
+    @staticmethod
path: yt_dlp/extractor/rtve.py
user: login: seproDev
body: Lets leave a comment.
```suggestion
    # Reimplementation of https://js2.rtve.es/pages/app-player/3.5.1/js/pf_video.js
    @staticmethod
```
created_at: 2025-03-31T10:39:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -8,28 +8,29 @@
     determine_ext,
     float_or_none,
     qualities,
-    remove_end,
-    remove_start,
     try_get,
 )
 
 
 class RTVEALaCartaIE(InfoExtractor):
     IE_NAME = 'rtve.es:alacarta'
-    IE_DESC = 'RTVE a la carta'
-    _VALID_URL = r'https?://(?:www\.)?rtve\.es/(m/)?(alacarta/videos|filmoteca)/[^/]+/[^/]+/(?P<id>\d+)'
+    IE_DESC = 'RTVE a la carta and Play'
+    _VALID_URL = [
+        r'https?://(?:www\.)?rtve\.es/(m/)?(alacarta/videos|filmoteca)/[^/]+/[^/]+/(?P<id>\d+)',
+        r'https?://(?:www\.)?rtve\.es/(m/)?play/videos/[^/]+/[^/]+/(?P<id>\d+)',
+    ]
path: yt_dlp/extractor/rtve.py
user: login: seproDev
body: Not sure why we need a separate regex
```suggestion
    _VALID_URL = r'https?://(?:www\.)?rtve\.es/(m/)?(?:(?:alacarta|play)/videos|filmoteca)/[^/?#]+/[^/?#]+/(?P<id>\d+)'
```
created_at: 2025-03-31T10:47:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -70,65 +88,86 @@ def _real_initialize(self):
     def _decrypt_url(png):
         encrypted_data = io.BytesIO(base64.b64decode(png)[8:])
         while True:
-            length = struct.unpack('!I', encrypted_data.read(4))[0]
+            length_data = encrypted_data.read(4)
+            length = struct.unpack('!I', length_data)[0]
             chunk_type = encrypted_data.read(4)
             if chunk_type == b'IEND':
                 break
             data = encrypted_data.read(length)
             if chunk_type == b'tEXt':
-                alphabet_data, text = data.split(b'\0')
-                quality, url_data = text.split(b'%%')
-                alphabet = []
-                e = 0
-                d = 0
-                for l in alphabet_data.decode('iso-8859-1'):
-                    if d == 0:
-                        alphabet.append(l)
-                        d = e = (e + 1) % 4
-                    else:
-                        d -= 1
-                url = ''
-                f = 0
-                e = 3
-                b = 1
-                for letter in url_data.decode('iso-8859-1'):
-                    if f == 0:
-                        l = int(letter) * 10
-                        f = 1
-                    else:
-                        if e == 0:
-                            l += int(letter)
-                            url += alphabet[l]
-                            e = (b + 3) % 4
-                            f = 0
-                            b += 1
-                        else:
-                            e -= 1
-
-                yield quality.decode(), url
+                if b'%%' in data:
+                    alphabet_data, text = data.split(b'\0')
+                    quality, url_data = text.split(b'%%')
+                    alphabet = RTVEALaCartaIE._get_alfabet(alphabet_data)
+                    url = RTVEALaCartaIE._get_url(alphabet, url_data)
+                    quality_str = quality.decode()
+                else:
+                    data = bytes(filter(None, data))
+                    alphabet_data, url_data = data.split(b'#')
+                    alphabet = RTVEALaCartaIE._get_alfabet(alphabet_data)
+
+                    url = RTVEALaCartaIE._get_url(alphabet, url_data)
+                    quality_str = ''
+                yield quality_str, url
             encrypted_data.read(4)  # CRC
 
+    @staticmethod
+    def _get_url(alphabet, url_data):
+        url = ''
+        f = 0
+        e = 3
+        b = 1
+        for letter in url_data.decode('iso-8859-1'):
+            if f == 0:
+                l = int(letter) * 10
+                f = 1
+            else:
+                if e == 0:
+                    l += int(letter)
+                    url += alphabet[l]
+                    e = (b + 3) % 4
+                    f = 0
+                    b += 1
+                else:
+                    e -= 1
+        return url
+
+    @staticmethod
+    def _get_alfabet(alphabet_data):
path: yt_dlp/extractor/rtve.py
user: login: seproDev
body: Let's keep the code english
```suggestion
    def _get_alphabet(alphabet_data):
```
created_at: 2025-03-31T10:57:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -70,65 +88,86 @@ def _real_initialize(self):
     def _decrypt_url(png):
         encrypted_data = io.BytesIO(base64.b64decode(png)[8:])
         while True:
-            length = struct.unpack('!I', encrypted_data.read(4))[0]
+            length_data = encrypted_data.read(4)
+            length = struct.unpack('!I', length_data)[0]
             chunk_type = encrypted_data.read(4)
             if chunk_type == b'IEND':
                 break
             data = encrypted_data.read(length)
             if chunk_type == b'tEXt':
-                alphabet_data, text = data.split(b'\0')
-                quality, url_data = text.split(b'%%')
-                alphabet = []
-                e = 0
-                d = 0
-                for l in alphabet_data.decode('iso-8859-1'):
-                    if d == 0:
-                        alphabet.append(l)
-                        d = e = (e + 1) % 4
-                    else:
-                        d -= 1
-                url = ''
-                f = 0
-                e = 3
-                b = 1
-                for letter in url_data.decode('iso-8859-1'):
-                    if f == 0:
-                        l = int(letter) * 10
-                        f = 1
-                    else:
-                        if e == 0:
-                            l += int(letter)
-                            url += alphabet[l]
-                            e = (b + 3) % 4
-                            f = 0
-                            b += 1
-                        else:
-                            e -= 1
-
-                yield quality.decode(), url
+                if b'%%' in data:
+                    alphabet_data, text = data.split(b'\0')
+                    quality, url_data = text.split(b'%%')
+                    alphabet = RTVEALaCartaIE._get_alfabet(alphabet_data)
+                    url = RTVEALaCartaIE._get_url(alphabet, url_data)
+                    quality_str = quality.decode()
+                else:
+                    data = bytes(filter(None, data))
+                    alphabet_data, url_data = data.split(b'#')
+                    alphabet = RTVEALaCartaIE._get_alfabet(alphabet_data)
+
+                    url = RTVEALaCartaIE._get_url(alphabet, url_data)
+                    quality_str = ''
+                yield quality_str, url
path: yt_dlp/extractor/rtve.py
user: login: seproDev
body: Looking at the sites JS I would have said this is a more accurate implementation:
```suggestion
                data = bytes(filter(None, data))
                alphabet_data, _, url_data = data.partition(b'#')
                quality_str, _, url_data = url_data.rpartition(b'%%')
                quality_str = quality_str.decode() or ''
                alphabet = RTVEALaCartaIE._get_alfabet(alphabet_data)
                url = RTVEALaCartaIE._get_url(alphabet, url_data)
                yield quality_str, url
```
Do you have any examples where `%%` is in the data?
created_at: 2025-03-31T11:11:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -282,31 +319,32 @@ class RTVELiveIE(RTVEALaCartaIE):  # XXX: Do not subclass from concrete IE
         'info_dict': {
             'id': 'la-1',
             'ext': 'mp4',
-            'title': 're:^La 1 [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
+            'title': str,
+            'live_status': 'is_live',
         },
         'params': {
             'skip_download': 'live stream',
         },
+        'expected_warnings': ['Ignoring subtitle tracks found in the HLS manifest'],
     }]
 
     def _real_extract(self, url):
         mobj = self._match_valid_url(url)
         video_id = mobj.group('id')
 
         webpage = self._download_webpage(url, video_id)
-        title = remove_end(self._og_search_title(webpage), ' en directo en RTVE.es')
-        title = remove_start(title, 'Estoy viendo ')
+        title = self._html_extract_title(webpage)
path: yt_dlp/extractor/rtve.py
user: login: seproDev
body: Inline below
created_at: 2025-03-31T11:13:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -58,6 +64,18 @@ class RTVEALaCartaIE(InfoExtractor):
     }, {
         'url': 'http://www.rtve.es/filmoteca/no-do/not-1-introduccion-primer-noticiario-espanol/1465256/',
         'only_matching': True,
+    }, {
+        'url': 'https://www.rtve.es/play/videos/saber-vivir/07-07-24/16177116/',
+        'md5': '9eebcf6e8d6306c3b7c46e86a0115f55',
+        'info_dict': {
+            'id': '16177116',
+            'ext': 'mp4',
+            'title': 'Saber vivir - 07/07/24',
+            'thumbnail': r're:https://img2\.rtve\.es/v/.*\.png',
+            'duration': 2162.68,
+            'series': 'Saber vivir',
+        },
+        'expected_warnings': ['Failed to download MPD manifest', 'Failed to download m3u8 information'],
     }]
 
     def _real_initialize(self):
path: yt_dlp/extractor/rtve.py
user: login: seproDev
body: I decided to hard code this, as it was only dependent on the user agent.
created_at: 2025-04-13T22:59:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -70,65 +88,86 @@ def _real_initialize(self):
     def _decrypt_url(png):
         encrypted_data = io.BytesIO(base64.b64decode(png)[8:])
         while True:
-            length = struct.unpack('!I', encrypted_data.read(4))[0]
+            length_data = encrypted_data.read(4)
+            length = struct.unpack('!I', length_data)[0]
             chunk_type = encrypted_data.read(4)
             if chunk_type == b'IEND':
                 break
             data = encrypted_data.read(length)
             if chunk_type == b'tEXt':
-                alphabet_data, text = data.split(b'\0')
-                quality, url_data = text.split(b'%%')
-                alphabet = []
-                e = 0
-                d = 0
-                for l in alphabet_data.decode('iso-8859-1'):
-                    if d == 0:
-                        alphabet.append(l)
-                        d = e = (e + 1) % 4
-                    else:
-                        d -= 1
-                url = ''
-                f = 0
-                e = 3
-                b = 1
-                for letter in url_data.decode('iso-8859-1'):
-                    if f == 0:
-                        l = int(letter) * 10
-                        f = 1
-                    else:
-                        if e == 0:
-                            l += int(letter)
-                            url += alphabet[l]
-                            e = (b + 3) % 4
-                            f = 0
-                            b += 1
-                        else:
-                            e -= 1
-
-                yield quality.decode(), url
+                if b'%%' in data:
+                    alphabet_data, text = data.split(b'\0')
+                    quality, url_data = text.split(b'%%')
+                    alphabet = RTVEALaCartaIE._get_alfabet(alphabet_data)
+                    url = RTVEALaCartaIE._get_url(alphabet, url_data)
+                    quality_str = quality.decode()
+                else:
+                    data = bytes(filter(None, data))
+                    alphabet_data, url_data = data.split(b'#')
+                    alphabet = RTVEALaCartaIE._get_alfabet(alphabet_data)
+
+                    url = RTVEALaCartaIE._get_url(alphabet, url_data)
+                    quality_str = ''
+                yield quality_str, url
             encrypted_data.read(4)  # CRC
 
+    @staticmethod
+    def _get_url(alphabet, url_data):
+        url = ''
+        f = 0
+        e = 3
+        b = 1
+        for letter in url_data.decode('iso-8859-1'):
+            if f == 0:
+                l = int(letter) * 10
+                f = 1
+            else:
+                if e == 0:
+                    l += int(letter)
+                    url += alphabet[l]
+                    e = (b + 3) % 4
+                    f = 0
+                    b += 1
+                else:
+                    e -= 1
+        return url
+
+    @staticmethod
+    def _get_alfabet(alphabet_data):
+        alphabet = []
+        e = 0
+        d = 0
+        for l in alphabet_data.decode('iso-8859-1'):
+            if d == 0:
+                alphabet.append(l)
+                d = e = (e + 1) % 4
+            else:
+                d -= 1
+        return alphabet
+
     def _extract_png_formats(self, video_id):
-        png = self._download_webpage(
-            f'http://www.rtve.es/ztnr/movil/thumbnail/{self._manager}/videos/{video_id}.png',
-            video_id, 'Downloading url information', query={'q': 'v2'})
-        q = qualities(['Media', 'Alta', 'HQ', 'HD_READY', 'HD_FULL'])
         formats = []
-        for quality, video_url in self._decrypt_url(png):
-            ext = determine_ext(video_url)
-            if ext == 'm3u8':
-                formats.extend(self._extract_m3u8_formats(
-                    video_url, video_id, 'mp4', 'm3u8_native',
-                    m3u8_id='hls', fatal=False))
-            elif ext == 'mpd':
-                formats.extend(self._extract_mpd_formats(
-                    video_url, video_id, 'dash', fatal=False))
-            else:
-                formats.append({
-                    'format_id': quality,
-                    'quality': q(quality),
-                    'url': video_url,
-                })
+        q = qualities(['Media', 'Alta', 'HQ', 'HD_READY', 'HD_FULL'])
+        for manager in (self._manager, 'rtveplayw'):
path: yt_dlp/extractor/rtve.py
user: login: seproDev
body: Ah for https://www.rtve.es/infantil/serie/agus-lui-churros-crafts/video/gusano/7048976/ `rtveplayw` return 1080p, while `default` returns 720p. Keeping both.
created_at: 2025-04-13T22:59:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,35 +1,141 @@
 import base64
 import io
 import struct
+import urllib.parse
 
 from .common import InfoExtractor
 from ..utils import (
     ExtractorError,
+    clean_html,
     determine_ext,
     float_or_none,
+    parse_iso8601,
     qualities,
-    remove_end,
-    remove_start,
-    try_get,
+    url_or_none,
 )
+from ..utils.traversal import subs_list_to_dict, traverse_obj
 
 
-class RTVEALaCartaIE(InfoExtractor):
+class RTVEBaseIE(InfoExtractor):
+    # Reimplementation of https://js2.rtve.es/pages/app-player/3.5.1/js/pf_video.js
+    @staticmethod
+    def _decrypt_url(png):
+        encrypted_data = io.BytesIO(base64.b64decode(png)[8:])
+        while True:
+            length_data = encrypted_data.read(4)
+            length = struct.unpack('!I', length_data)[0]
+            chunk_type = encrypted_data.read(4)
+            if chunk_type == b'IEND':
+                break
+            data = encrypted_data.read(length)
+            if chunk_type == b'tEXt':
+                data = bytes(filter(None, data))
+                alphabet_data, _, url_data = data.partition(b'#')
+                quality_str, _, url_data = url_data.rpartition(b'%%')
+                quality_str = quality_str.decode() or ''
+                alphabet = RTVEBaseIE._get_alphabet(alphabet_data)
+                url = RTVEBaseIE._get_url(alphabet, url_data)
+                yield quality_str, url
+            encrypted_data.read(4)  # CRC
+
+    @staticmethod
+    def _get_url(alphabet, url_data):
+        url = ''
+        f = 0
+        e = 3
+        b = 1
+        for char in url_data.decode('iso-8859-1'):
+            if f == 0:
+                l = int(char) * 10
+                f = 1
+            else:
+                if e == 0:
+                    l += int(char)
+                    url += alphabet[l]
+                    e = (b + 3) % 4
+                    f = 0
+                    b += 1
+                else:
+                    e -= 1
+        return url
+
+    @staticmethod
+    def _get_alphabet(alphabet_data):
+        alphabet = []
+        e = 0
+        d = 0
+        for char in alphabet_data.decode('iso-8859-1'):
+            if d == 0:
+                alphabet.append(char)
+                d = e = (e + 1) % 4
+            else:
+                d -= 1
+        return alphabet
+
+    def _extract_png_formats_and_subtitles(self, video_id, media_type='videos'):
+        formats, subtitles = [], {}
+        q = qualities(['Media', 'Alta', 'HQ', 'HD_READY', 'HD_FULL'])
+        for manager in ('rtveplayw', 'default'):
+            png = self._download_webpage(
+                f'http://www.rtve.es/ztnr/movil/thumbnail/{manager}/{media_type}/{video_id}.png',
+                video_id, 'Downloading url information', query={'q': 'v2'}, fatal=False)
+            if not png:
+                continue
+
+            for quality, video_url in self._decrypt_url(png):
+                ext = determine_ext(video_url)
+                if ext == 'm3u8':
+                    fmts, subs = self._extract_m3u8_formats_and_subtitles(
+                        video_url, video_id, 'mp4', m3u8_id='hls', fatal=False)
+                    formats.extend(fmts)
+                    self._merge_subtitles(subs, target=subtitles)
+                elif ext == 'mpd':
+                    fmts, subs = self._extract_mpd_formats_and_subtitles(
+                        video_url, video_id, 'dash', fatal=False)
+                    formats.extend(fmts)
+                    self._merge_subtitles(subs, target=subtitles)
+                else:
+                    formats.append({
+                        'format_id': quality,
+                        'quality': q(quality),
+                        'url': video_url,
+                    })
+        return formats, subtitles
+
+    def _parse_metadata(self, metadata):
+        return traverse_obj(metadata, {
+            'title': ('title', {str.strip}),
+            'alt_title': ('alt', {str.strip}),
+            'description': ('description', {clean_html}),
+            'timestamp': ('dateOfEmission', {parse_iso8601(delimiter=' ')}),
+            'release_timestamp': ('publicationDate', {parse_iso8601(delimiter=' ')}),
+            'modified_timestamp': ('modificationDate', {parse_iso8601(delimiter=' ')}),
+            'thumbnail': (('thumbnail', 'image', 'imageSEO'), {url_or_none}, any),
+            'duration': ('duration', {float_or_none(scale=1000)}),
+            'is_live': ('live', {bool}),
+            'series': (('programTitle', ('programInfo', 'title')), {clean_html}, any),
+        })
+
+
+class RTVEALaCartaIE(RTVEBaseIE):
     IE_NAME = 'rtve.es:alacarta'
-    IE_DESC = 'RTVE a la carta'
-    _VALID_URL = r'https?://(?:www\.)?rtve\.es/(m/)?(alacarta/videos|filmoteca)/[^/]+/[^/]+/(?P<id>\d+)'
+    IE_DESC = 'RTVE a la carta and Play'
+    _VALID_URL = [
+        r'https?://(?:www\.)?rtve\.es/(m/)?(?:(?:alacarta|play)/videos|filmoteca)/[^/?#]+/[^/?#]+/(?P<id>\d+)',
path: yt_dlp/extractor/rtve.py
user: login: bashonly
body: ```suggestion
        r'https?://(?:www\.)?rtve\.es/(?:m/)?(?:(?:alacarta|play)/videos|filmoteca)/(?:[^/?#]+/){2}(?P<id>\d+)',
```
created_at: 2025-04-17T23:48:39Z
author_association: MEMBER
diff_hunk: @@ -38,140 +144,88 @@ class RTVEALaCartaIE(InfoExtractor):
             'ext': 'mp4',
             'title': 're:^24H LIVE [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
             'is_live': True,
+            'live_status': 'is_live',
+            'thumbnail': r're:https://img2\.rtve\.es/v/.*\.png',
         },
         'params': {
             'skip_download': 'live stream',
         },
     }, {
         'url': 'http://www.rtve.es/alacarta/videos/servir-y-proteger/servir-proteger-capitulo-104/4236788/',
-        'md5': 'd850f3c8731ea53952ebab489cf81cbf',
+        'md5': 'f3cf0d1902d008c48c793e736706c174',
         'info_dict': {
             'id': '4236788',
             'ext': 'mp4',
-            'title': 'Servir y proteger - Capítulo 104',
-            'duration': 3222.0,
+            'title': 'Episodio 104',
+            'duration': 3222.8,
+            'thumbnail': r're:https://img2\.rtve\.es/v/.*\.png',
+            'series': 'Servir y proteger',
         },
-        'expected_warnings': ['Failed to download MPD manifest', 'Failed to download m3u8 information'],
     }, {
         'url': 'http://www.rtve.es/m/alacarta/videos/cuentame-como-paso/cuentame-como-paso-t16-ultimo-minuto-nuestra-vida-capitulo-276/2969138/?media=tve',
         'only_matching': True,
     }, {
         'url': 'http://www.rtve.es/filmoteca/no-do/not-1-introduccion-primer-noticiario-espanol/1465256/',
         'only_matching': True,
+    }, {
+        'url': 'https://www.rtve.es/play/videos/saber-vivir/07-07-24/16177116/',
+        'md5': 'a5b24fcdfa3ff5cb7908aba53d22d4b6',
+        'info_dict': {
+            'id': '16177116',
+            'ext': 'mp4',
+            'title': 'Saber vivir - 07/07/24',
+            'thumbnail': r're:https://img2\.rtve\.es/v/.*\.png',
+            'duration': 2162.68,
+            'series': 'Saber vivir',
+        },
+    }, {
+        'url': 'https://www.rtve.es/infantil/serie/agus-lui-churros-crafts/video/gusano/7048976/',
+        'info_dict': {
+            'id': '7048976',
+            'ext': 'mp4',
+            'title': 'Gusano',
+            'thumbnail': r're:https://img2\.rtve\.es/v/.*\.png',
+            'duration': 292.86,
+            'series': 'Agus & Lui: Churros y Crafts',
+            '_old_archive_ids': ['rtveinfantil 7048976'],
+        },
     }]
 
-    def _real_initialize(self):
-        user_agent_b64 = base64.b64encode(self.get_param('http_headers')['User-Agent'].encode()).decode('utf-8')
-        self._manager = self._download_json(
-            'http://www.rtve.es/odin/loki/' + user_agent_b64,
-            None, 'Fetching manager info')['manager']
-
-    @staticmethod
-    def _decrypt_url(png):
-        encrypted_data = io.BytesIO(base64.b64decode(png)[8:])
-        while True:
-            length = struct.unpack('!I', encrypted_data.read(4))[0]
-            chunk_type = encrypted_data.read(4)
-            if chunk_type == b'IEND':
-                break
-            data = encrypted_data.read(length)
-            if chunk_type == b'tEXt':
-                alphabet_data, text = data.split(b'\0')
-                quality, url_data = text.split(b'%%')
-                alphabet = []
-                e = 0
-                d = 0
-                for l in alphabet_data.decode('iso-8859-1'):
-                    if d == 0:
-                        alphabet.append(l)
-                        d = e = (e + 1) % 4
-                    else:
-                        d -= 1
-                url = ''
-                f = 0
-                e = 3
-                b = 1
-                for letter in url_data.decode('iso-8859-1'):
-                    if f == 0:
-                        l = int(letter) * 10
-                        f = 1
-                    else:
-                        if e == 0:
-                            l += int(letter)
-                            url += alphabet[l]
-                            e = (b + 3) % 4
-                            f = 0
-                            b += 1
-                        else:
-                            e -= 1
-
-                yield quality.decode(), url
-            encrypted_data.read(4)  # CRC
-
-    def _extract_png_formats(self, video_id):
-        png = self._download_webpage(
-            f'http://www.rtve.es/ztnr/movil/thumbnail/{self._manager}/videos/{video_id}.png',
-            video_id, 'Downloading url information', query={'q': 'v2'})
-        q = qualities(['Media', 'Alta', 'HQ', 'HD_READY', 'HD_FULL'])
-        formats = []
-        for quality, video_url in self._decrypt_url(png):
-            ext = determine_ext(video_url)
-            if ext == 'm3u8':
-                formats.extend(self._extract_m3u8_formats(
-                    video_url, video_id, 'mp4', 'm3u8_native',
-                    m3u8_id='hls', fatal=False))
-            elif ext == 'mpd':
-                formats.extend(self._extract_mpd_formats(
-                    video_url, video_id, 'dash', fatal=False))
-            else:
-                formats.append({
-                    'format_id': quality,
-                    'quality': q(quality),
-                    'url': video_url,
-                })
-        return formats
+    def _get_subtitles(self, video_id):
+        subtitle_data = self._download_json(
+            f'https://api2.rtve.es/api/videos/{video_id}/subtitulos.json', video_id,
+            'Downloading subtitles info')
+        return traverse_obj(subtitle_data, ('page', 'items', ..., {
+            'id': ('lang', {str}),
+            'url': ('src', {url_or_none}),
+        }, all, {subs_list_to_dict(lang='es')}))
 
     def _real_extract(self, url):
         video_id = self._match_id(url)
-        info = self._download_json(
+        metadata = self._download_json(
             f'http://www.rtve.es/api/videos/{video_id}/config/alacarta_videos.json',
             video_id)['page']['items'][0]
-        if info['state'] == 'DESPU':
+        if metadata['state'] == 'DESPU':
             raise ExtractorError('The video is no longer available', expected=True)
-        title = info['title'].strip()
-        formats = self._extract_png_formats(video_id)
+        formats, subtitles = self._extract_png_formats_and_subtitles(video_id)
 
-        subtitles = None
-        sbt_file = info.get('sbtFile')
-        if sbt_file:
-            subtitles = self.extract_subtitles(video_id, sbt_file)
+        self._merge_subtitles(self.extract_subtitles(video_id), target=subtitles)
 
-        is_live = info.get('live') is True
+        is_infantil = urllib.parse.urlparse(url).path.startswith('/infantil/')
 
         return {
             'id': video_id,
-            'title': title,
             'formats': formats,
-            'thumbnail': info.get('image'),
             'subtitles': subtitles,
-            'duration': float_or_none(info.get('duration'), 1000),
-            'is_live': is_live,
-            'series': info.get('programTitle'),
+            **self._parse_metadata(metadata),
+            '_old_archive_ids': [f'rtveinfantil {video_id}'] if is_infantil else None,
path: yt_dlp/extractor/rtve.py
user: login: bashonly
body: ```suggestion
            '_old_archive_ids': [make_archive_id('rtveinfantil', video_id)] if is_infantil else None,
```
created_at: 2025-04-17T23:49:43Z
author_association: MEMBER
diff_hunk: @@ -38,140 +144,88 @@ class RTVEALaCartaIE(InfoExtractor):
             'ext': 'mp4',
             'title': 're:^24H LIVE [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
             'is_live': True,
+            'live_status': 'is_live',
+            'thumbnail': r're:https://img2\.rtve\.es/v/.*\.png',
         },
         'params': {
             'skip_download': 'live stream',
         },
     }, {
         'url': 'http://www.rtve.es/alacarta/videos/servir-y-proteger/servir-proteger-capitulo-104/4236788/',
-        'md5': 'd850f3c8731ea53952ebab489cf81cbf',
+        'md5': 'f3cf0d1902d008c48c793e736706c174',
         'info_dict': {
             'id': '4236788',
             'ext': 'mp4',
-            'title': 'Servir y proteger - Capítulo 104',
-            'duration': 3222.0,
+            'title': 'Episodio 104',
+            'duration': 3222.8,
+            'thumbnail': r're:https://img2\.rtve\.es/v/.*\.png',
+            'series': 'Servir y proteger',
         },
-        'expected_warnings': ['Failed to download MPD manifest', 'Failed to download m3u8 information'],
     }, {
         'url': 'http://www.rtve.es/m/alacarta/videos/cuentame-como-paso/cuentame-como-paso-t16-ultimo-minuto-nuestra-vida-capitulo-276/2969138/?media=tve',
         'only_matching': True,
     }, {
         'url': 'http://www.rtve.es/filmoteca/no-do/not-1-introduccion-primer-noticiario-espanol/1465256/',
         'only_matching': True,
+    }, {
+        'url': 'https://www.rtve.es/play/videos/saber-vivir/07-07-24/16177116/',
+        'md5': 'a5b24fcdfa3ff5cb7908aba53d22d4b6',
+        'info_dict': {
+            'id': '16177116',
+            'ext': 'mp4',
+            'title': 'Saber vivir - 07/07/24',
+            'thumbnail': r're:https://img2\.rtve\.es/v/.*\.png',
+            'duration': 2162.68,
+            'series': 'Saber vivir',
+        },
+    }, {
+        'url': 'https://www.rtve.es/infantil/serie/agus-lui-churros-crafts/video/gusano/7048976/',
+        'info_dict': {
+            'id': '7048976',
+            'ext': 'mp4',
+            'title': 'Gusano',
+            'thumbnail': r're:https://img2\.rtve\.es/v/.*\.png',
+            'duration': 292.86,
+            'series': 'Agus & Lui: Churros y Crafts',
+            '_old_archive_ids': ['rtveinfantil 7048976'],
+        },
     }]
 
-    def _real_initialize(self):
-        user_agent_b64 = base64.b64encode(self.get_param('http_headers')['User-Agent'].encode()).decode('utf-8')
-        self._manager = self._download_json(
-            'http://www.rtve.es/odin/loki/' + user_agent_b64,
-            None, 'Fetching manager info')['manager']
-
-    @staticmethod
-    def _decrypt_url(png):
-        encrypted_data = io.BytesIO(base64.b64decode(png)[8:])
-        while True:
-            length = struct.unpack('!I', encrypted_data.read(4))[0]
-            chunk_type = encrypted_data.read(4)
-            if chunk_type == b'IEND':
-                break
-            data = encrypted_data.read(length)
-            if chunk_type == b'tEXt':
-                alphabet_data, text = data.split(b'\0')
-                quality, url_data = text.split(b'%%')
-                alphabet = []
-                e = 0
-                d = 0
-                for l in alphabet_data.decode('iso-8859-1'):
-                    if d == 0:
-                        alphabet.append(l)
-                        d = e = (e + 1) % 4
-                    else:
-                        d -= 1
-                url = ''
-                f = 0
-                e = 3
-                b = 1
-                for letter in url_data.decode('iso-8859-1'):
-                    if f == 0:
-                        l = int(letter) * 10
-                        f = 1
-                    else:
-                        if e == 0:
-                            l += int(letter)
-                            url += alphabet[l]
-                            e = (b + 3) % 4
-                            f = 0
-                            b += 1
-                        else:
-                            e -= 1
-
-                yield quality.decode(), url
-            encrypted_data.read(4)  # CRC
-
-    def _extract_png_formats(self, video_id):
-        png = self._download_webpage(
-            f'http://www.rtve.es/ztnr/movil/thumbnail/{self._manager}/videos/{video_id}.png',
-            video_id, 'Downloading url information', query={'q': 'v2'})
-        q = qualities(['Media', 'Alta', 'HQ', 'HD_READY', 'HD_FULL'])
-        formats = []
-        for quality, video_url in self._decrypt_url(png):
-            ext = determine_ext(video_url)
-            if ext == 'm3u8':
-                formats.extend(self._extract_m3u8_formats(
-                    video_url, video_id, 'mp4', 'm3u8_native',
-                    m3u8_id='hls', fatal=False))
-            elif ext == 'mpd':
-                formats.extend(self._extract_mpd_formats(
-                    video_url, video_id, 'dash', fatal=False))
-            else:
-                formats.append({
-                    'format_id': quality,
-                    'quality': q(quality),
-                    'url': video_url,
-                })
-        return formats
+    def _get_subtitles(self, video_id):
+        subtitle_data = self._download_json(
+            f'https://api2.rtve.es/api/videos/{video_id}/subtitulos.json', video_id,
+            'Downloading subtitles info')
+        return traverse_obj(subtitle_data, ('page', 'items', ..., {
+            'id': ('lang', {str}),
+            'url': ('src', {url_or_none}),
+        }, all, {subs_list_to_dict(lang='es')}))
 
     def _real_extract(self, url):
         video_id = self._match_id(url)
-        info = self._download_json(
+        metadata = self._download_json(
             f'http://www.rtve.es/api/videos/{video_id}/config/alacarta_videos.json',
             video_id)['page']['items'][0]
-        if info['state'] == 'DESPU':
+        if metadata['state'] == 'DESPU':
             raise ExtractorError('The video is no longer available', expected=True)
-        title = info['title'].strip()
-        formats = self._extract_png_formats(video_id)
+        formats, subtitles = self._extract_png_formats_and_subtitles(video_id)
 
-        subtitles = None
-        sbt_file = info.get('sbtFile')
-        if sbt_file:
-            subtitles = self.extract_subtitles(video_id, sbt_file)
+        self._merge_subtitles(self.extract_subtitles(video_id), target=subtitles)
 
-        is_live = info.get('live') is True
+        is_infantil = urllib.parse.urlparse(url).path.startswith('/infantil/')
 
         return {
             'id': video_id,
-            'title': title,
             'formats': formats,
-            'thumbnail': info.get('image'),
             'subtitles': subtitles,
-            'duration': float_or_none(info.get('duration'), 1000),
-            'is_live': is_live,
-            'series': info.get('programTitle'),
+            **self._parse_metadata(metadata),
+            '_old_archive_ids': [f'rtveinfantil {video_id}'] if is_infantil else None,
         }
 
-    def _get_subtitles(self, video_id, sub_file):
-        subs = self._download_json(
-            sub_file + '.json', video_id,
-            'Downloading subtitles info')['page']['items']
-        return dict(
-            (s['lang'], [{'ext': 'vtt', 'url': s['src']}])
-            for s in subs)
 
-
-class RTVEAudioIE(RTVEALaCartaIE):  # XXX: Do not subclass from concrete IE
+class RTVEAudioIE(RTVEBaseIE):
     IE_NAME = 'rtve.es:audio'
     IE_DESC = 'RTVE audio'
-    _VALID_URL = r'https?://(?:www\.)?rtve\.es/(alacarta|play)/audios/[^/]+/[^/]+/(?P<id>[0-9]+)'
+    _VALID_URL = r'https?://(?:www\.)?rtve\.es/(alacarta|play)/audios/[^/?#]+/[^/?#]+/(?P<id>\d+)'
path: yt_dlp/extractor/rtve.py
user: login: bashonly
body: ```suggestion
    _VALID_URL = r'https?://(?:www\.)?rtve\.es/(alacarta|play)/audios/(?:[^/?#]+/){2}(?P<id>\d+)'
```
created_at: 2025-04-17T23:50:07Z
author_association: MEMBER
diff_hunk: @@ -38,140 +144,88 @@ class RTVEALaCartaIE(InfoExtractor):
             'ext': 'mp4',
             'title': 're:^24H LIVE [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
             'is_live': True,
+            'live_status': 'is_live',
+            'thumbnail': r're:https://img2\.rtve\.es/v/.*\.png',
         },
         'params': {
             'skip_download': 'live stream',
         },
     }, {
         'url': 'http://www.rtve.es/alacarta/videos/servir-y-proteger/servir-proteger-capitulo-104/4236788/',
-        'md5': 'd850f3c8731ea53952ebab489cf81cbf',
+        'md5': 'f3cf0d1902d008c48c793e736706c174',
         'info_dict': {
             'id': '4236788',
             'ext': 'mp4',
-            'title': 'Servir y proteger - Capítulo 104',
-            'duration': 3222.0,
+            'title': 'Episodio 104',
+            'duration': 3222.8,
+            'thumbnail': r're:https://img2\.rtve\.es/v/.*\.png',
+            'series': 'Servir y proteger',
         },
-        'expected_warnings': ['Failed to download MPD manifest', 'Failed to download m3u8 information'],
     }, {
         'url': 'http://www.rtve.es/m/alacarta/videos/cuentame-como-paso/cuentame-como-paso-t16-ultimo-minuto-nuestra-vida-capitulo-276/2969138/?media=tve',
         'only_matching': True,
     }, {
         'url': 'http://www.rtve.es/filmoteca/no-do/not-1-introduccion-primer-noticiario-espanol/1465256/',
         'only_matching': True,
+    }, {
+        'url': 'https://www.rtve.es/play/videos/saber-vivir/07-07-24/16177116/',
+        'md5': 'a5b24fcdfa3ff5cb7908aba53d22d4b6',
+        'info_dict': {
+            'id': '16177116',
+            'ext': 'mp4',
+            'title': 'Saber vivir - 07/07/24',
+            'thumbnail': r're:https://img2\.rtve\.es/v/.*\.png',
+            'duration': 2162.68,
+            'series': 'Saber vivir',
+        },
+    }, {
+        'url': 'https://www.rtve.es/infantil/serie/agus-lui-churros-crafts/video/gusano/7048976/',
+        'info_dict': {
+            'id': '7048976',
+            'ext': 'mp4',
+            'title': 'Gusano',
+            'thumbnail': r're:https://img2\.rtve\.es/v/.*\.png',
+            'duration': 292.86,
+            'series': 'Agus & Lui: Churros y Crafts',
+            '_old_archive_ids': ['rtveinfantil 7048976'],
+        },
     }]
 
-    def _real_initialize(self):
-        user_agent_b64 = base64.b64encode(self.get_param('http_headers')['User-Agent'].encode()).decode('utf-8')
-        self._manager = self._download_json(
-            'http://www.rtve.es/odin/loki/' + user_agent_b64,
-            None, 'Fetching manager info')['manager']
-
-    @staticmethod
-    def _decrypt_url(png):
-        encrypted_data = io.BytesIO(base64.b64decode(png)[8:])
-        while True:
-            length = struct.unpack('!I', encrypted_data.read(4))[0]
-            chunk_type = encrypted_data.read(4)
-            if chunk_type == b'IEND':
-                break
-            data = encrypted_data.read(length)
-            if chunk_type == b'tEXt':
-                alphabet_data, text = data.split(b'\0')
-                quality, url_data = text.split(b'%%')
-                alphabet = []
-                e = 0
-                d = 0
-                for l in alphabet_data.decode('iso-8859-1'):
-                    if d == 0:
-                        alphabet.append(l)
-                        d = e = (e + 1) % 4
-                    else:
-                        d -= 1
-                url = ''
-                f = 0
-                e = 3
-                b = 1
-                for letter in url_data.decode('iso-8859-1'):
-                    if f == 0:
-                        l = int(letter) * 10
-                        f = 1
-                    else:
-                        if e == 0:
-                            l += int(letter)
-                            url += alphabet[l]
-                            e = (b + 3) % 4
-                            f = 0
-                            b += 1
-                        else:
-                            e -= 1
-
-                yield quality.decode(), url
-            encrypted_data.read(4)  # CRC
-
-    def _extract_png_formats(self, video_id):
-        png = self._download_webpage(
-            f'http://www.rtve.es/ztnr/movil/thumbnail/{self._manager}/videos/{video_id}.png',
-            video_id, 'Downloading url information', query={'q': 'v2'})
-        q = qualities(['Media', 'Alta', 'HQ', 'HD_READY', 'HD_FULL'])
-        formats = []
-        for quality, video_url in self._decrypt_url(png):
-            ext = determine_ext(video_url)
-            if ext == 'm3u8':
-                formats.extend(self._extract_m3u8_formats(
-                    video_url, video_id, 'mp4', 'm3u8_native',
-                    m3u8_id='hls', fatal=False))
-            elif ext == 'mpd':
-                formats.extend(self._extract_mpd_formats(
-                    video_url, video_id, 'dash', fatal=False))
-            else:
-                formats.append({
-                    'format_id': quality,
-                    'quality': q(quality),
-                    'url': video_url,
-                })
-        return formats
+    def _get_subtitles(self, video_id):
+        subtitle_data = self._download_json(
+            f'https://api2.rtve.es/api/videos/{video_id}/subtitulos.json', video_id,
+            'Downloading subtitles info')
+        return traverse_obj(subtitle_data, ('page', 'items', ..., {
+            'id': ('lang', {str}),
+            'url': ('src', {url_or_none}),
+        }, all, {subs_list_to_dict(lang='es')}))
 
     def _real_extract(self, url):
         video_id = self._match_id(url)
-        info = self._download_json(
+        metadata = self._download_json(
             f'http://www.rtve.es/api/videos/{video_id}/config/alacarta_videos.json',
             video_id)['page']['items'][0]
-        if info['state'] == 'DESPU':
+        if metadata['state'] == 'DESPU':
             raise ExtractorError('The video is no longer available', expected=True)
-        title = info['title'].strip()
-        formats = self._extract_png_formats(video_id)
+        formats, subtitles = self._extract_png_formats_and_subtitles(video_id)
 
-        subtitles = None
-        sbt_file = info.get('sbtFile')
-        if sbt_file:
-            subtitles = self.extract_subtitles(video_id, sbt_file)
+        self._merge_subtitles(self.extract_subtitles(video_id), target=subtitles)
path: yt_dlp/extractor/rtve.py
user: login: bashonly
body: Does this actually work? I would think we'd need to pass `subtitles` to `extract_subtitles` and merge them inside `_get_subtitles`
created_at: 2025-04-18T19:51:34Z
author_association: MEMBER
diff_hunk: @@ -38,140 +144,88 @@ class RTVEALaCartaIE(InfoExtractor):
             'ext': 'mp4',
             'title': 're:^24H LIVE [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
             'is_live': True,
+            'live_status': 'is_live',
+            'thumbnail': r're:https://img2\.rtve\.es/v/.*\.png',
         },
         'params': {
             'skip_download': 'live stream',
         },
     }, {
         'url': 'http://www.rtve.es/alacarta/videos/servir-y-proteger/servir-proteger-capitulo-104/4236788/',
-        'md5': 'd850f3c8731ea53952ebab489cf81cbf',
+        'md5': 'f3cf0d1902d008c48c793e736706c174',
         'info_dict': {
             'id': '4236788',
             'ext': 'mp4',
-            'title': 'Servir y proteger - Capítulo 104',
-            'duration': 3222.0,
+            'title': 'Episodio 104',
+            'duration': 3222.8,
+            'thumbnail': r're:https://img2\.rtve\.es/v/.*\.png',
+            'series': 'Servir y proteger',
         },
-        'expected_warnings': ['Failed to download MPD manifest', 'Failed to download m3u8 information'],
     }, {
         'url': 'http://www.rtve.es/m/alacarta/videos/cuentame-como-paso/cuentame-como-paso-t16-ultimo-minuto-nuestra-vida-capitulo-276/2969138/?media=tve',
         'only_matching': True,
     }, {
         'url': 'http://www.rtve.es/filmoteca/no-do/not-1-introduccion-primer-noticiario-espanol/1465256/',
         'only_matching': True,
+    }, {
+        'url': 'https://www.rtve.es/play/videos/saber-vivir/07-07-24/16177116/',
+        'md5': 'a5b24fcdfa3ff5cb7908aba53d22d4b6',
+        'info_dict': {
+            'id': '16177116',
+            'ext': 'mp4',
+            'title': 'Saber vivir - 07/07/24',
+            'thumbnail': r're:https://img2\.rtve\.es/v/.*\.png',
+            'duration': 2162.68,
+            'series': 'Saber vivir',
+        },
+    }, {
+        'url': 'https://www.rtve.es/infantil/serie/agus-lui-churros-crafts/video/gusano/7048976/',
+        'info_dict': {
+            'id': '7048976',
+            'ext': 'mp4',
+            'title': 'Gusano',
+            'thumbnail': r're:https://img2\.rtve\.es/v/.*\.png',
+            'duration': 292.86,
+            'series': 'Agus & Lui: Churros y Crafts',
+            '_old_archive_ids': ['rtveinfantil 7048976'],
+        },
     }]
 
-    def _real_initialize(self):
-        user_agent_b64 = base64.b64encode(self.get_param('http_headers')['User-Agent'].encode()).decode('utf-8')
-        self._manager = self._download_json(
-            'http://www.rtve.es/odin/loki/' + user_agent_b64,
-            None, 'Fetching manager info')['manager']
-
-    @staticmethod
-    def _decrypt_url(png):
-        encrypted_data = io.BytesIO(base64.b64decode(png)[8:])
-        while True:
-            length = struct.unpack('!I', encrypted_data.read(4))[0]
-            chunk_type = encrypted_data.read(4)
-            if chunk_type == b'IEND':
-                break
-            data = encrypted_data.read(length)
-            if chunk_type == b'tEXt':
-                alphabet_data, text = data.split(b'\0')
-                quality, url_data = text.split(b'%%')
-                alphabet = []
-                e = 0
-                d = 0
-                for l in alphabet_data.decode('iso-8859-1'):
-                    if d == 0:
-                        alphabet.append(l)
-                        d = e = (e + 1) % 4
-                    else:
-                        d -= 1
-                url = ''
-                f = 0
-                e = 3
-                b = 1
-                for letter in url_data.decode('iso-8859-1'):
-                    if f == 0:
-                        l = int(letter) * 10
-                        f = 1
-                    else:
-                        if e == 0:
-                            l += int(letter)
-                            url += alphabet[l]
-                            e = (b + 3) % 4
-                            f = 0
-                            b += 1
-                        else:
-                            e -= 1
-
-                yield quality.decode(), url
-            encrypted_data.read(4)  # CRC
-
-    def _extract_png_formats(self, video_id):
-        png = self._download_webpage(
-            f'http://www.rtve.es/ztnr/movil/thumbnail/{self._manager}/videos/{video_id}.png',
-            video_id, 'Downloading url information', query={'q': 'v2'})
-        q = qualities(['Media', 'Alta', 'HQ', 'HD_READY', 'HD_FULL'])
-        formats = []
-        for quality, video_url in self._decrypt_url(png):
-            ext = determine_ext(video_url)
-            if ext == 'm3u8':
-                formats.extend(self._extract_m3u8_formats(
-                    video_url, video_id, 'mp4', 'm3u8_native',
-                    m3u8_id='hls', fatal=False))
-            elif ext == 'mpd':
-                formats.extend(self._extract_mpd_formats(
-                    video_url, video_id, 'dash', fatal=False))
-            else:
-                formats.append({
-                    'format_id': quality,
-                    'quality': q(quality),
-                    'url': video_url,
-                })
-        return formats
+    def _get_subtitles(self, video_id):
+        subtitle_data = self._download_json(
+            f'https://api2.rtve.es/api/videos/{video_id}/subtitulos.json', video_id,
+            'Downloading subtitles info')
+        return traverse_obj(subtitle_data, ('page', 'items', ..., {
+            'id': ('lang', {str}),
+            'url': ('src', {url_or_none}),
+        }, all, {subs_list_to_dict(lang='es')}))
 
     def _real_extract(self, url):
         video_id = self._match_id(url)
-        info = self._download_json(
+        metadata = self._download_json(
             f'http://www.rtve.es/api/videos/{video_id}/config/alacarta_videos.json',
             video_id)['page']['items'][0]
-        if info['state'] == 'DESPU':
+        if metadata['state'] == 'DESPU':
             raise ExtractorError('The video is no longer available', expected=True)
-        title = info['title'].strip()
-        formats = self._extract_png_formats(video_id)
+        formats, subtitles = self._extract_png_formats_and_subtitles(video_id)
 
-        subtitles = None
-        sbt_file = info.get('sbtFile')
-        if sbt_file:
-            subtitles = self.extract_subtitles(video_id, sbt_file)
+        self._merge_subtitles(self.extract_subtitles(video_id), target=subtitles)
 
-        is_live = info.get('live') is True
+        is_infantil = urllib.parse.urlparse(url).path.startswith('/infantil/')
path: yt_dlp/extractor/rtve.py
user: login: bashonly
body: could also make this a capture group in `_VALID_URL` and set the var at the beginning of `_real_extract`
created_at: 2025-04-18T19:52:23Z
author_association: MEMBER
diff_hunk: @@ -282,46 +293,46 @@ class RTVELiveIE(RTVEALaCartaIE):  # XXX: Do not subclass from concrete IE
         'info_dict': {
             'id': 'la-1',
             'ext': 'mp4',
-            'title': 're:^La 1 [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
+            'title': str,
+            'live_status': 'is_live',
         },
         'params': {
             'skip_download': 'live stream',
         },
     }]
 
     def _real_extract(self, url):
-        mobj = self._match_valid_url(url)
-        video_id = mobj.group('id')
-
+        video_id = self._match_id(url)
         webpage = self._download_webpage(url, video_id)
-        title = remove_end(self._og_search_title(webpage), ' en directo en RTVE.es')
-        title = remove_start(title, 'Estoy viendo ')
 
-        vidplayer_id = self._search_regex(
-            (r'playerId=player([0-9]+)',
-             r'class=["\'].*?\blive_mod\b.*?["\'][^>]+data-assetid=["\'](\d+)',
-             r'data-id=["\'](\d+)'),
-            webpage, 'internal video ID')
+        data_setup = self._search_json(
+            r'<div[^>]+class="[^"]*videoPlayer[^"]*"[^>]*data-setup=\'',
+            webpage, 'data_setup', video_id)
+
+        formats, subtitles = self._extract_png_formats_and_subtitles(data_setup['idAsset'])
 
         return {
             'id': video_id,
-            'title': title,
-            'formats': self._extract_png_formats(vidplayer_id),
+            'title': self._html_extract_title(webpage),
path: yt_dlp/extractor/rtve.py
user: login: bashonly
body: what does the title look like? we don't need to perform any string surgery like the old code did?
created_at: 2025-04-18T19:53:45Z
author_association: MEMBER
diff_hunk: @@ -282,46 +293,46 @@ class RTVELiveIE(RTVEALaCartaIE):  # XXX: Do not subclass from concrete IE
         'info_dict': {
             'id': 'la-1',
             'ext': 'mp4',
-            'title': 're:^La 1 [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
+            'title': str,
+            'live_status': 'is_live',
         },
         'params': {
             'skip_download': 'live stream',
         },
     }]
 
     def _real_extract(self, url):
-        mobj = self._match_valid_url(url)
-        video_id = mobj.group('id')
-
+        video_id = self._match_id(url)
         webpage = self._download_webpage(url, video_id)
-        title = remove_end(self._og_search_title(webpage), ' en directo en RTVE.es')
-        title = remove_start(title, 'Estoy viendo ')
 
-        vidplayer_id = self._search_regex(
-            (r'playerId=player([0-9]+)',
-             r'class=["\'].*?\blive_mod\b.*?["\'][^>]+data-assetid=["\'](\d+)',
-             r'data-id=["\'](\d+)'),
-            webpage, 'internal video ID')
+        data_setup = self._search_json(
+            r'<div[^>]+class="[^"]*videoPlayer[^"]*"[^>]*data-setup=\'',
path: yt_dlp/extractor/rtve.py
user: login: bashonly
body: they single quote the json and don't escape html special chars (`"`=>`&quot;`)?
created_at: 2025-04-18T19:54:24Z
author_association: MEMBER
diff_hunk: @@ -38,140 +144,88 @@ class RTVEALaCartaIE(InfoExtractor):
             'ext': 'mp4',
             'title': 're:^24H LIVE [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
             'is_live': True,
+            'live_status': 'is_live',
+            'thumbnail': r're:https://img2\.rtve\.es/v/.*\.png',
         },
         'params': {
             'skip_download': 'live stream',
         },
     }, {
         'url': 'http://www.rtve.es/alacarta/videos/servir-y-proteger/servir-proteger-capitulo-104/4236788/',
-        'md5': 'd850f3c8731ea53952ebab489cf81cbf',
+        'md5': 'f3cf0d1902d008c48c793e736706c174',
         'info_dict': {
             'id': '4236788',
             'ext': 'mp4',
-            'title': 'Servir y proteger - Capítulo 104',
-            'duration': 3222.0,
+            'title': 'Episodio 104',
+            'duration': 3222.8,
+            'thumbnail': r're:https://img2\.rtve\.es/v/.*\.png',
+            'series': 'Servir y proteger',
         },
-        'expected_warnings': ['Failed to download MPD manifest', 'Failed to download m3u8 information'],
     }, {
         'url': 'http://www.rtve.es/m/alacarta/videos/cuentame-como-paso/cuentame-como-paso-t16-ultimo-minuto-nuestra-vida-capitulo-276/2969138/?media=tve',
         'only_matching': True,
     }, {
         'url': 'http://www.rtve.es/filmoteca/no-do/not-1-introduccion-primer-noticiario-espanol/1465256/',
         'only_matching': True,
+    }, {
+        'url': 'https://www.rtve.es/play/videos/saber-vivir/07-07-24/16177116/',
+        'md5': 'a5b24fcdfa3ff5cb7908aba53d22d4b6',
+        'info_dict': {
+            'id': '16177116',
+            'ext': 'mp4',
+            'title': 'Saber vivir - 07/07/24',
+            'thumbnail': r're:https://img2\.rtve\.es/v/.*\.png',
+            'duration': 2162.68,
+            'series': 'Saber vivir',
+        },
+    }, {
+        'url': 'https://www.rtve.es/infantil/serie/agus-lui-churros-crafts/video/gusano/7048976/',
+        'info_dict': {
+            'id': '7048976',
+            'ext': 'mp4',
+            'title': 'Gusano',
+            'thumbnail': r're:https://img2\.rtve\.es/v/.*\.png',
+            'duration': 292.86,
+            'series': 'Agus & Lui: Churros y Crafts',
+            '_old_archive_ids': ['rtveinfantil 7048976'],
+        },
     }]
 
-    def _real_initialize(self):
-        user_agent_b64 = base64.b64encode(self.get_param('http_headers')['User-Agent'].encode()).decode('utf-8')
-        self._manager = self._download_json(
-            'http://www.rtve.es/odin/loki/' + user_agent_b64,
-            None, 'Fetching manager info')['manager']
-
-    @staticmethod
-    def _decrypt_url(png):
-        encrypted_data = io.BytesIO(base64.b64decode(png)[8:])
-        while True:
-            length = struct.unpack('!I', encrypted_data.read(4))[0]
-            chunk_type = encrypted_data.read(4)
-            if chunk_type == b'IEND':
-                break
-            data = encrypted_data.read(length)
-            if chunk_type == b'tEXt':
-                alphabet_data, text = data.split(b'\0')
-                quality, url_data = text.split(b'%%')
-                alphabet = []
-                e = 0
-                d = 0
-                for l in alphabet_data.decode('iso-8859-1'):
-                    if d == 0:
-                        alphabet.append(l)
-                        d = e = (e + 1) % 4
-                    else:
-                        d -= 1
-                url = ''
-                f = 0
-                e = 3
-                b = 1
-                for letter in url_data.decode('iso-8859-1'):
-                    if f == 0:
-                        l = int(letter) * 10
-                        f = 1
-                    else:
-                        if e == 0:
-                            l += int(letter)
-                            url += alphabet[l]
-                            e = (b + 3) % 4
-                            f = 0
-                            b += 1
-                        else:
-                            e -= 1
-
-                yield quality.decode(), url
-            encrypted_data.read(4)  # CRC
-
-    def _extract_png_formats(self, video_id):
-        png = self._download_webpage(
-            f'http://www.rtve.es/ztnr/movil/thumbnail/{self._manager}/videos/{video_id}.png',
-            video_id, 'Downloading url information', query={'q': 'v2'})
-        q = qualities(['Media', 'Alta', 'HQ', 'HD_READY', 'HD_FULL'])
-        formats = []
-        for quality, video_url in self._decrypt_url(png):
-            ext = determine_ext(video_url)
-            if ext == 'm3u8':
-                formats.extend(self._extract_m3u8_formats(
-                    video_url, video_id, 'mp4', 'm3u8_native',
-                    m3u8_id='hls', fatal=False))
-            elif ext == 'mpd':
-                formats.extend(self._extract_mpd_formats(
-                    video_url, video_id, 'dash', fatal=False))
-            else:
-                formats.append({
-                    'format_id': quality,
-                    'quality': q(quality),
-                    'url': video_url,
-                })
-        return formats
+    def _get_subtitles(self, video_id):
+        subtitle_data = self._download_json(
+            f'https://api2.rtve.es/api/videos/{video_id}/subtitulos.json', video_id,
+            'Downloading subtitles info')
+        return traverse_obj(subtitle_data, ('page', 'items', ..., {
+            'id': ('lang', {str}),
+            'url': ('src', {url_or_none}),
+        }, all, {subs_list_to_dict(lang='es')}))
 
     def _real_extract(self, url):
         video_id = self._match_id(url)
-        info = self._download_json(
+        metadata = self._download_json(
             f'http://www.rtve.es/api/videos/{video_id}/config/alacarta_videos.json',
             video_id)['page']['items'][0]
-        if info['state'] == 'DESPU':
+        if metadata['state'] == 'DESPU':
             raise ExtractorError('The video is no longer available', expected=True)
-        title = info['title'].strip()
-        formats = self._extract_png_formats(video_id)
+        formats, subtitles = self._extract_png_formats_and_subtitles(video_id)
 
-        subtitles = None
-        sbt_file = info.get('sbtFile')
-        if sbt_file:
-            subtitles = self.extract_subtitles(video_id, sbt_file)
+        self._merge_subtitles(self.extract_subtitles(video_id), target=subtitles)
path: yt_dlp/extractor/rtve.py
user: login: seproDev
body: I don't see why not. We always extract the m3u8/mpd subs, but only call the dedicated api if subtitles were requested by the user.
created_at: 2025-04-18T20:28:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -38,140 +144,88 @@ class RTVEALaCartaIE(InfoExtractor):
             'ext': 'mp4',
             'title': 're:^24H LIVE [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
             'is_live': True,
+            'live_status': 'is_live',
+            'thumbnail': r're:https://img2\.rtve\.es/v/.*\.png',
         },
         'params': {
             'skip_download': 'live stream',
         },
     }, {
         'url': 'http://www.rtve.es/alacarta/videos/servir-y-proteger/servir-proteger-capitulo-104/4236788/',
-        'md5': 'd850f3c8731ea53952ebab489cf81cbf',
+        'md5': 'f3cf0d1902d008c48c793e736706c174',
         'info_dict': {
             'id': '4236788',
             'ext': 'mp4',
-            'title': 'Servir y proteger - Capítulo 104',
-            'duration': 3222.0,
+            'title': 'Episodio 104',
+            'duration': 3222.8,
+            'thumbnail': r're:https://img2\.rtve\.es/v/.*\.png',
+            'series': 'Servir y proteger',
         },
-        'expected_warnings': ['Failed to download MPD manifest', 'Failed to download m3u8 information'],
     }, {
         'url': 'http://www.rtve.es/m/alacarta/videos/cuentame-como-paso/cuentame-como-paso-t16-ultimo-minuto-nuestra-vida-capitulo-276/2969138/?media=tve',
         'only_matching': True,
     }, {
         'url': 'http://www.rtve.es/filmoteca/no-do/not-1-introduccion-primer-noticiario-espanol/1465256/',
         'only_matching': True,
+    }, {
+        'url': 'https://www.rtve.es/play/videos/saber-vivir/07-07-24/16177116/',
+        'md5': 'a5b24fcdfa3ff5cb7908aba53d22d4b6',
+        'info_dict': {
+            'id': '16177116',
+            'ext': 'mp4',
+            'title': 'Saber vivir - 07/07/24',
+            'thumbnail': r're:https://img2\.rtve\.es/v/.*\.png',
+            'duration': 2162.68,
+            'series': 'Saber vivir',
+        },
+    }, {
+        'url': 'https://www.rtve.es/infantil/serie/agus-lui-churros-crafts/video/gusano/7048976/',
+        'info_dict': {
+            'id': '7048976',
+            'ext': 'mp4',
+            'title': 'Gusano',
+            'thumbnail': r're:https://img2\.rtve\.es/v/.*\.png',
+            'duration': 292.86,
+            'series': 'Agus & Lui: Churros y Crafts',
+            '_old_archive_ids': ['rtveinfantil 7048976'],
+        },
     }]
 
-    def _real_initialize(self):
-        user_agent_b64 = base64.b64encode(self.get_param('http_headers')['User-Agent'].encode()).decode('utf-8')
-        self._manager = self._download_json(
-            'http://www.rtve.es/odin/loki/' + user_agent_b64,
-            None, 'Fetching manager info')['manager']
-
-    @staticmethod
-    def _decrypt_url(png):
-        encrypted_data = io.BytesIO(base64.b64decode(png)[8:])
-        while True:
-            length = struct.unpack('!I', encrypted_data.read(4))[0]
-            chunk_type = encrypted_data.read(4)
-            if chunk_type == b'IEND':
-                break
-            data = encrypted_data.read(length)
-            if chunk_type == b'tEXt':
-                alphabet_data, text = data.split(b'\0')
-                quality, url_data = text.split(b'%%')
-                alphabet = []
-                e = 0
-                d = 0
-                for l in alphabet_data.decode('iso-8859-1'):
-                    if d == 0:
-                        alphabet.append(l)
-                        d = e = (e + 1) % 4
-                    else:
-                        d -= 1
-                url = ''
-                f = 0
-                e = 3
-                b = 1
-                for letter in url_data.decode('iso-8859-1'):
-                    if f == 0:
-                        l = int(letter) * 10
-                        f = 1
-                    else:
-                        if e == 0:
-                            l += int(letter)
-                            url += alphabet[l]
-                            e = (b + 3) % 4
-                            f = 0
-                            b += 1
-                        else:
-                            e -= 1
-
-                yield quality.decode(), url
-            encrypted_data.read(4)  # CRC
-
-    def _extract_png_formats(self, video_id):
-        png = self._download_webpage(
-            f'http://www.rtve.es/ztnr/movil/thumbnail/{self._manager}/videos/{video_id}.png',
-            video_id, 'Downloading url information', query={'q': 'v2'})
-        q = qualities(['Media', 'Alta', 'HQ', 'HD_READY', 'HD_FULL'])
-        formats = []
-        for quality, video_url in self._decrypt_url(png):
-            ext = determine_ext(video_url)
-            if ext == 'm3u8':
-                formats.extend(self._extract_m3u8_formats(
-                    video_url, video_id, 'mp4', 'm3u8_native',
-                    m3u8_id='hls', fatal=False))
-            elif ext == 'mpd':
-                formats.extend(self._extract_mpd_formats(
-                    video_url, video_id, 'dash', fatal=False))
-            else:
-                formats.append({
-                    'format_id': quality,
-                    'quality': q(quality),
-                    'url': video_url,
-                })
-        return formats
+    def _get_subtitles(self, video_id):
+        subtitle_data = self._download_json(
+            f'https://api2.rtve.es/api/videos/{video_id}/subtitulos.json', video_id,
+            'Downloading subtitles info')
+        return traverse_obj(subtitle_data, ('page', 'items', ..., {
+            'id': ('lang', {str}),
+            'url': ('src', {url_or_none}),
+        }, all, {subs_list_to_dict(lang='es')}))
 
     def _real_extract(self, url):
         video_id = self._match_id(url)
-        info = self._download_json(
+        metadata = self._download_json(
             f'http://www.rtve.es/api/videos/{video_id}/config/alacarta_videos.json',
             video_id)['page']['items'][0]
-        if info['state'] == 'DESPU':
+        if metadata['state'] == 'DESPU':
             raise ExtractorError('The video is no longer available', expected=True)
-        title = info['title'].strip()
-        formats = self._extract_png_formats(video_id)
+        formats, subtitles = self._extract_png_formats_and_subtitles(video_id)
 
-        subtitles = None
-        sbt_file = info.get('sbtFile')
-        if sbt_file:
-            subtitles = self.extract_subtitles(video_id, sbt_file)
+        self._merge_subtitles(self.extract_subtitles(video_id), target=subtitles)
path: yt_dlp/extractor/rtve.py
user: login: bashonly
body: ah true, just looked at the `extract_subtitles` code and nvm
created_at: 2025-04-18T20:41:55Z
author_association: MEMBER
diff_hunk: @@ -282,46 +293,46 @@ class RTVELiveIE(RTVEALaCartaIE):  # XXX: Do not subclass from concrete IE
         'info_dict': {
             'id': 'la-1',
             'ext': 'mp4',
-            'title': 're:^La 1 [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
+            'title': str,
+            'live_status': 'is_live',
         },
         'params': {
             'skip_download': 'live stream',
         },
     }]
 
     def _real_extract(self, url):
-        mobj = self._match_valid_url(url)
-        video_id = mobj.group('id')
-
+        video_id = self._match_id(url)
         webpage = self._download_webpage(url, video_id)
-        title = remove_end(self._og_search_title(webpage), ' en directo en RTVE.es')
-        title = remove_start(title, 'Estoy viendo ')
 
-        vidplayer_id = self._search_regex(
-            (r'playerId=player([0-9]+)',
-             r'class=["\'].*?\blive_mod\b.*?["\'][^>]+data-assetid=["\'](\d+)',
-             r'data-id=["\'](\d+)'),
-            webpage, 'internal video ID')
+        data_setup = self._search_json(
+            r'<div[^>]+class="[^"]*videoPlayer[^"]*"[^>]*data-setup=\'',
+            webpage, 'data_setup', video_id)
+
+        formats, subtitles = self._extract_png_formats_and_subtitles(data_setup['idAsset'])
 
         return {
             'id': video_id,
-            'title': title,
-            'formats': self._extract_png_formats(vidplayer_id),
+            'title': self._html_extract_title(webpage),
path: yt_dlp/extractor/rtve.py
user: login: seproDev
body: Yes, they are just providing the full title now. I have improved the extractor a bit by adding more metadata and support the new type URLs
created_at: 2025-04-18T20:49:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -282,46 +293,46 @@ class RTVELiveIE(RTVEALaCartaIE):  # XXX: Do not subclass from concrete IE
         'info_dict': {
             'id': 'la-1',
             'ext': 'mp4',
-            'title': 're:^La 1 [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
+            'title': str,
+            'live_status': 'is_live',
         },
         'params': {
             'skip_download': 'live stream',
         },
     }]
 
     def _real_extract(self, url):
-        mobj = self._match_valid_url(url)
-        video_id = mobj.group('id')
-
+        video_id = self._match_id(url)
         webpage = self._download_webpage(url, video_id)
-        title = remove_end(self._og_search_title(webpage), ' en directo en RTVE.es')
-        title = remove_start(title, 'Estoy viendo ')
 
-        vidplayer_id = self._search_regex(
-            (r'playerId=player([0-9]+)',
-             r'class=["\'].*?\blive_mod\b.*?["\'][^>]+data-assetid=["\'](\d+)',
-             r'data-id=["\'](\d+)'),
-            webpage, 'internal video ID')
+        data_setup = self._search_json(
+            r'<div[^>]+class="[^"]*videoPlayer[^"]*"[^>]*data-setup=\'',
path: yt_dlp/extractor/rtve.py
user: login: seproDev
body: Yes
```html
      <div class="ima f16x9 videoPlayer" 
      data-setup='{
        "id":1,
        "idAsset":"1688877",
        "isLive": "true",
        "segment": false,
        "imagen": "https://img2.rtve.es/resources/thumbslive/H_1688877.jpg",
        "hasDRM": false,
        "idLicencia": null,
        "requireLogged": true
      }'>
```
created_at: 2025-04-18T20:56:43Z
author_association: CONTRIBUTOR

PR comments:
user: login: bashonly
created_at: 2024-07-08T18:54:58Z
body: Thanks for the PR.

Initial review:

- Don't subclass from a concrete IE class. RTVEPlay doesn't even need to be its own extractor if all it's doing is adding a `_VALID_URL` pattern; instead you can just add the pattern to `RTVEALaCartaIE._VALID_URL` (it can be a list of patterns).

- Don't replace tests. Add a new test for a play URL

- Check the code with ruff and autopep8; see the [developer instructions in the contributing guide](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#developer-instructions)
author_association: MEMBER
user: login: bashonly
created_at: 2024-07-08T18:55:57Z
body: Does this fix #1346 ?
author_association: MEMBER
user: login: meGAmeS1
created_at: 2024-07-08T19:00:17Z
body: > Thanks for the PR.
> 
> Initial review:
> 
> * Don't subclass from a concrete IE class. RTVEPlay doesn't even need to be its own extractor if all it's doing is adding a `_VALID_URL` pattern; instead you can just add the pattern to `RTVEALaCartaIE._VALID_URL` (it can be a list of patterns).
> * Don't replace tests. Add a new test for a play URL
> * Check the code with ruff and autopep8; see the [developer instructions in the contributing guide](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#developer-instructions)

Okay thank you for the suggestions and the developer guide. I'll fix that and follow the rules





> Does this fix #1346 ?

Actually, yes! I just tested with the first video from https://www.rtve.es/play/videos/comando-actualidad/ (linked in the OP issue)
author_association: CONTRIBUTOR
user: login: meGAmeS1
created_at: 2024-07-10T11:21:17Z
body: So I managed to follow your advices, I removed an old IE subclass that was only here to add regex and did the same for the subclass I previously created.

I also edited every tests from this extractor in order to fix and pass them (some previously linked videos were now unavailable so I found other ones)

And I reformatted the code to match the desired style
author_association: CONTRIBUTOR
user: login: bashonly
created_at: 2024-07-10T16:01:43Z
body: Why did you remove `RTVEInfantilIE` ?
author_association: MEMBER
user: login: meGAmeS1
created_at: 2024-07-10T19:22:08Z
body: Because, as it was a subclass only defined to add a regex, I grouped it in the main class. Should I revert that?
author_association: CONTRIBUTOR
user: login: bashonly
created_at: 2024-07-10T19:25:12Z
body: Yes please. I was only referring to the class you were adding.
author_association: MEMBER
user: login: meGAmeS1
created_at: 2024-07-10T21:17:36Z
body: Ok sorry about that then, I reverted it back (but with the change of test to make it work)
author_association: CONTRIBUTOR
user: login: meGAmeS1
created_at: 2024-07-16T21:23:30Z
body: @bashonly Hi, do you consider the PR as ready to be merged or should I add something else? Thanks in advance :)
author_association: CONTRIBUTOR
user: login: oowowaee
created_at: 2024-07-22T04:38:59Z
body: Does this only work with old shows as per this comment? https://github.com/yt-dlp/yt-dlp/issues/1346#issuecomment-951204695. I had tried with a newer show (https://www.rtve.es/play/videos/la-promesa/temporada-3-episodio-384/16160419) and got stuck in an endless redirect loop.
author_association: NONE
user: login: meGAmeS1
created_at: 2024-07-22T05:56:44Z
body: > Does this only work with old shows as per this comment? [#1346 (comment)](https://github.com/yt-dlp/yt-dlp/issues/1346#issuecomment-951204695). I had tried with a newer show (https://www.rtve.es/play/videos/la-promesa/temporada-3-episodio-384/16160419) and got stuck in an endless redirect loop.

Here is the log of this content with my PR :
```
[rtve.es:alacarta] Fetching manager info
[rtve.es:alacarta] Extracting URL: https://www.rtve.es/play/videos/la-promesa/temporada-3-episodio-384/16160419
[rtve.es:alacarta] 16160419: Downloading JSON metadata
[rtve.es:alacarta] 16160419: Downloading url information
[rtve.es:alacarta] 16160419: Downloading m3u8 information
WARNING: [rtve.es:alacarta] Failed to download m3u8 information: HTTP Error 403: Forbidden
[rtve.es:alacarta] 16160419: Downloading MPD manifest
WARNING: [rtve.es:alacarta] Failed to download MPD manifest: HTTP Error 403: Forbidden
[rtve.es:alacarta] 16160419: Downloading subtitles info
[info] 16160419: Downloading subtitles: es
[info] 16160419: Downloading 1 format(s): 0
[info] Writing video subtitles to: Episodio 384 [16160419].es.vtt
[download] Destination: Episodio 384 [16160419].es.vtt
[download] 100% of   77.12KiB in 00:00:00 at 822.03KiB/s 
[SubtitlesConvertor] Converting subtitles
Deleting original file Episodio 384 [16160419].es.vtt (pass -k to keep)
ERROR: unable to download video data: HTTP Error 403: Forbidden

Process finished with exit code 1
```

It doesn't manage to download it (because from what I can see, this content is only available with RTVE PLAY+ and I don't have it) but at least it doesn't lock itself under an infinite loop. If someone gives me access to an account with RTVE PLAY+ I could try to make the extractor works with the `--username` and `--password` parameters.


Edit: I see there is a 7 days trial period, I might give it a try. But this could be done in an other PR, this one is just to make the extractor works again.
author_association: CONTRIBUTOR
user: login: oowowaee
created_at: 2024-07-22T06:12:09Z
body: @meGAmeS1 I'm not sure if I'm doing something wrong then, I don't have a play+ account but get stuck in

```
[redirect] Following redirect to https://www.rtve.es/play/videos/la-promesa/temporada-3-episodio-384/16160419/
```
author_association: NONE
user: login: meGAmeS1
created_at: 2024-07-22T06:16:44Z
body: > @meGAmeS1 I'm not sure if I'm doing something wrong then, I don't have a play+ account but get stuck in
> 
> ```
> [redirect] Following redirect to https://www.rtve.es/play/videos/la-promesa/temporada-3-episodio-384/16160419/
> ```

And when you are accessing the page from your browser, what do you see? I see that for me:
![image](https://github.com/user-attachments/assets/cced6c1c-fc7c-45fd-961b-9e842961e96a)

Also, just to check, in what country are you? I'm in France, so maybe I have a different behaviour

author_association: CONTRIBUTOR
user: login: oowowaee
created_at: 2024-07-22T06:29:33Z
body: @meGAmeS1 I see what you see without a Spanish VPN, the content isn't restricted from inside Spain, there's a modal that pops up that you can close to watch the episode.  I can try again without using a VPN, the other prs I was testing earlier needed that.
author_association: NONE
user: login: meGAmeS1
created_at: 2024-07-22T07:24:22Z
body: > @meGAmeS1 I see what you see without a Spanish VPN, the content isn't restricted from inside Spain, there's a modal that pops up that you can close to watch the episode. I can try again without using a VPN, the other prs I was testing earlier needed that.

Oh I see, so indeed the content is freely available with a VPN in Spain. But it completely works with my PR:
```
[rtve.es:alacarta] Fetching manager info
[rtve.es:alacarta] Extracting URL: https://www.rtve.es/play/videos/la-promesa/temporada-3-episodio-384/16160419
[rtve.es:alacarta] 16160419: Downloading JSON metadata
[rtve.es:alacarta] 16160419: Downloading url information
[rtve.es:alacarta] 16160419: Downloading m3u8 information
[rtve.es:alacarta] 16160419: Downloading MPD manifest
[rtve.es:alacarta] 16160419: Downloading subtitles info
[info] 16160419: Downloading subtitles: es
[info] 16160419: Downloading 1 format(s): hls-1211
[info] Writing video subtitles to: Episodio 384 [16160419].es.vtt
[download] Destination: Episodio 384 [16160419].es.vtt
[download] 100% of   77.12KiB in 00:00:00 at 358.40KiB/s 
[SubtitlesConvertor] Converting subtitles
Deleting original file Episodio 384 [16160419].es.vtt (pass -k to keep)
[hlsnative] Downloading m3u8 manifest
[hlsnative] Total fragments: 648
[download] Destination: Episodio 384 [16160419].mp4
[download]  33.0% of ~ 468.59MiB at    4.51MiB/s ETA 01:03 (frag 215/648)[download] Got error: <urllib3.connection.HTTPSConnection object at 0x1070db2e0>: Failed to establish a new connection: [Errno 61] Connection refused. Retrying (1/10)...
[download] Got error: <urllib3.connection.HTTPSConnection object at 0x1070a8070>: Failed to establish a new connection: [Errno 61] Connection refused. Retrying (2/10)...
[download] Got error: <urllib3.connection.HTTPSConnection object at 0x1070abc10>: Failed to establish a new connection: [Errno 61] Connection refused. Retrying (3/10)...
[download] Got error: <urllib3.connection.HTTPSConnection object at 0x1070a8e50>: Failed to establish a new connection: [Errno 61] Connection refused. Retrying (4/10)...
[download] 100% of  464.76MiB in 00:01:28 at 5.24MiB/s                   
[FixupM3u8] Fixing MPEG-TS in MP4 container of "Episodio 384 [16160419].mp4"
[VideoConvertor] Not converting media file "Episodio 384 [16160419].mp4"; already is in target format mp4
[EmbedSubtitle] Embedding subtitles in "Episodio 384 [16160419].mp4"

Process finished with exit code 0
```

Are you sure you are using the code of my PR? Because indeed, if I use the current release of yt-dlp, I get an infinite loop:
```
[generic] Extracting URL: https://www.rtve.es/play/videos/la-promesa/temporada-3-episodio-384/16160419
[generic] 16160419: Downloading webpage
[redirect] Following redirect to https://www.rtve.es/play/videos/la-promesa/temporada-3-episodio-384/16160419/
[generic] Extracting URL: https://www.rtve.es/play/videos/la-promesa/temporada-3-episodio-384/16160419/
[generic] 16160419: Downloading webpage
WARNING: [generic] Falling back on generic information extractor
[generic] 16160419: Extracting information
[generic] Extracting URL: https://www.rtve.es/play/videos/la-promesa/episodio-384/16160419/#__youtubedl_smuggle=%7B%22force...%2F16160419%2F%22%7D
[generic] 16160419: Downloading webpage
[redirect] Following redirect to https://www.rtve.es/play/videos/la-promesa/temporada-3-episodio-384/16160419/
[generic] Extracting URL: https://www.rtve.es/play/videos/la-promesa/temporada-3-episodio-384/16160419/#__youtubedl_smuggle...3A+%2216160419%22%7D
[generic] 16160419: Downloading webpage
WARNING: [generic] Falling back on generic information extractor
[generic] 16160419: Extracting information
[generic] Extracting URL: https://www.rtve.es/play/videos/la-promesa/episodio-384/16160419/#__youtubedl_smuggle=%7B%22force...%2F16160419%2F%22%7D
[generic] 16160419: Downloading webpage
[redirect] Following redirect to https://www.rtve.es/play/videos/la-promesa/temporada-3-episodio-384/16160419/
[generic] Extracting URL: https://www.rtve.es/play/videos/la-promesa/temporada-3-episodio-384/16160419/#__youtubedl_smuggle...3A+%2216160419%22%7D
[generic] 16160419: Downloading webpage
WARNING: [generic] Falling back on generic information extractor
```
author_association: CONTRIBUTOR
user: login: oowowaee
created_at: 2024-07-22T13:16:20Z
body: @meGAmeS1 Yes, I'm sorry I had something weird with my env yesterday and it wasn't picking up the changes at first.  I tested after and it works, and with the VPN everything works.  Thank you so much for fixing this, I was about to give up yesterday on this and then saw your pr at the last moment :)
author_association: NONE
user: login: meGAmeS1
created_at: 2024-08-06T09:28:21Z
body: I noticed that the current extractor couldn't download the best quality, so I worked it out to add this as well in this MR with that last change!
author_association: CONTRIBUTOR
user: login: wydengyre
created_at: 2024-10-13T13:27:22Z
body: I checked out this branch and rebased it on `master`. Verified that it works with this very recent (yesterday) RTVE page: https://www.rtve.es/play/videos/somos-cine/colon-adn-verdadero-origen/16281015/

Nice work @meGAmeS1 ! Much appreciated.

```
mleon@Matthews-MacBook-Air yt-dlp % ./yt-dlp.sh 'https://www.rtve.es/play/videos/somos-cine/colon-adn-verdadero-origen/16281015/'
[rtve.es:alacarta] Fetching manager info
[rtve.es:alacarta] Extracting URL: https://www.rtve.es/play/videos/somos-cine/colon-adn-verdadero-origen/16281015/
[rtve.es:alacarta] 16281015: Downloading JSON metadata
[rtve.es:alacarta] 16281015: Downloading url information
[rtve.es:alacarta] 16281015: Downloading m3u8 information
[rtve.es:alacarta] 16281015: Downloading MPD manifest
[rtve.es:alacarta] 16281015: Downloading url information
[rtve.es:alacarta] 16281015: Downloading m3u8 information
[rtve.es:alacarta] 16281015: Downloading MPD manifest
[info] 16281015: Downloading 1 format(s): hls-4976
[hlsnative] Downloading m3u8 manifest
[hlsnative] Total fragments: 1274
[download] Destination: Colón ADN. Su verdadero origen [16281015].mp4
```

Note that I am located within Spain.
author_association: NONE
user: login: meGAmeS1
created_at: 2024-10-13T15:36:18Z
body: Glad it could help someone else again @wydengyre, thanks for me letting me know.

Just to keep that branch up-to-date to the master branch I did the rebase too here

@bashonly Could you please remove the pending-fixes tag ?
author_association: CONTRIBUTOR
user: login: nerviZida
created_at: 2024-10-13T19:36:08Z
body: Wonderful, Great, Unbelievable, .....

Many, many, many... THANK YOU
Really.
Thank you.

Dank je, 谢谢, Grazie, Ευχαριστώ, Danke, ممنون, gracias, Takk, ..........

:) :] 
author_association: NONE
user: login: pachulo
created_at: 2024-11-13T23:09:05Z
body: I'm being hitted by this problem; how can I help so that this is merged?
author_association: NONE
user: login: mixpc
created_at: 2025-02-28T16:39:52Z
body: Hi,

I  just cloned, compiled and run your version. I don't think there was an error while compiling.

```
user@ubuntu-server-development:~/yt-dlp$ ./yt-dlp --version
2024.07.07
```

Endless loop:
```
user@ubuntu-server-development:~/yt-dlp$ ./yt-dlp.sh https://www.rtve.es/play/videos/el-comisario-montalbano/perro-terracota/6641981/
[generic] Extracting URL: https://www.rtve.es/play/videos/el-comisario-montalbano/perro-terracota/6641981/
[generic] 6641981: Downloading webpage
WARNING: [generic] Falling back on generic information extractor
[generic] 6641981: Extracting information
[generic] Extracting URL: https://www.rtve.es/play/videos/el-comisario-montalbano/el-perro-de-terracota/6641981/#__youtubed...a%2F6641981%2F%22%7D
[generic] 6641981: Downloading webpage
[redirect] Following redirect to https://www.rtve.es/play/videos/el-comisario-montalbano/perro-terracota/6641981/
[generic] Extracting URL: https://www.rtve.es/play/videos/el-comisario-montalbano/perro-terracota/6641981/#__youtubedl_smug...%3A+%226641981%22%7D
[generic] 6641981: Downloading webpage
WARNING: [generic] Falling back on generic information extractor
```

Also, the video has dual audio, would yt-dlp pick the two audio tracks? Thank you in advance!
author_association: NONE
user: login: meGAmeS1
created_at: 2025-03-31T09:58:18Z
body: Hello @mixpc, sorry for the late reply

> Hi,
> 
> I just cloned, compiled and run your version. I don't think there was an error while compiling.
> 
> ```
> user@ubuntu-server-development:~/yt-dlp$ ./yt-dlp --version
> 2024.07.07
> ```
> 
> Endless loop:
> 
> ```
> user@ubuntu-server-development:~/yt-dlp$ ./yt-dlp.sh https://www.rtve.es/play/videos/el-comisario-montalbano/perro-terracota/6641981/
> [generic] Extracting URL: https://www.rtve.es/play/videos/el-comisario-montalbano/perro-terracota/6641981/
> [generic] 6641981: Downloading webpage
> WARNING: [generic] Falling back on generic information extractor
> [generic] 6641981: Extracting information
> [generic] Extracting URL: https://www.rtve.es/play/videos/el-comisario-montalbano/el-perro-de-terracota/6641981/#__youtubed...a%2F6641981%2F%22%7D
> [generic] 6641981: Downloading webpage
> [redirect] Following redirect to https://www.rtve.es/play/videos/el-comisario-montalbano/perro-terracota/6641981/
> [generic] Extracting URL: https://www.rtve.es/play/videos/el-comisario-montalbano/perro-terracota/6641981/#__youtubedl_smug...%3A+%226641981%22%7D
> [generic] 6641981: Downloading webpage
> WARNING: [generic] Falling back on generic information extractor
> ```

Are you sure you cloned my repo ? ([git@github.com:meGAmeS1/yt-dlp.git](https://github.com/meGAmeS1/yt-dlp.git)) And are you on the right branch (`add-rtve-play`) ? Because the error `Falling back on generic information extractor` means your URL doesn't match the RTVE Extractor you provided, but when I test on my machine it does match the URL and download the file correctly

> Also, the video has dual audio, would yt-dlp pick the two audio tracks? Thank you in advance!

With yt-dlp you can list available formats (audio and video tracks) with the `-F` argument:
```
yt_dlp -F https://www.rtve.es/play/videos/el-comisario-montalbano/perro-terracota/6641981/ 
[rtve.es:alacarta] Fetching manager info
[rtve.es:alacarta] Extracting URL: https://www.rtve.es/play/videos/el-comisario-montalbano/perro-terracota/6641981/
[rtve.es:alacarta] 6641981: Downloading JSON metadata
[rtve.es:alacarta] 6641981: Downloading url information
[rtve.es:alacarta] 6641981: Downloading m3u8 information
[rtve.es:alacarta] 6641981: Downloading MPD manifest
[rtve.es:alacarta] 6641981: Downloading url information
[rtve.es:alacarta] 6641981: Downloading m3u8 information
[rtve.es:alacarta] 6641981: Downloading MPD manifest
[info] Available formats for 6641981:
ID                  EXT RESOLUTION FPS |   FILESIZE   TBR PROTO | VCODEC        VBR ACODEC      ABR ASR MORE INFO
----------------------------------------------------------------------------------------------------------------------------
dash-audio=194477-0 m4a audio only     | ~142.46MiB  194k dash  | audio only        mp4a.40.2  194k 48k DASH audio, m4a_dash
dash-audio=194477-1 m4a audio only     | ~142.46MiB  194k dash  | audio only        mp4a.40.2  194k 48k DASH audio, m4a_dash
2                   mp4 unknown        |                  http  | unknown           unknown
3                   mp4 unknown        |                  http  | unknown           unknown
dash-video=2766000  mp4 1280x720    25 | ~  1.98GiB 2766k dash  | avc1.640029 2766k video only          DASH video, mp4_dash
hls-3139            mp4 1280x720    25 | ~  2.25GiB 3139k m3u8  | avc1.640029 2766k mp4a.40.2  194k
dash-video=4520000  mp4 1920x1080   25 | ~  3.23GiB 4520k dash  | avc1.640029 4520k video only          DASH video, mp4_dash
hls-4998            mp4 1920x1080   25 | ~  3.58GiB 4998k m3u8  | avc1.640029 4520k mp4a.40.2  194k
```

As you can see, both of the audio tracks are listed. I don't know which one it will download by default but yes with yt-dlp you can tell it to download both tracks by doing something like this : `yt_dlp --audio-multistreams -f bestvideo+dash-audio=194477-0+dash-audio=194477-1 https://www.rtve.es/play/videos/el-comisario-montalbano/perro-terracota/6641981/` (note how I selected the bestvideo tracks and both audio tracks with their IDs : `bestvideo+dash-audio=194477-0+dash-audio=194477-1`)
author_association: CONTRIBUTOR
user: login: meGAmeS1
created_at: 2025-03-31T11:47:33Z
body: > Sorry this took so long. Here an initial review.

Hi, and thank you for your full review. I'll look into it this weekend as it has been a while since I didn't look at this code.
author_association: CONTRIBUTOR
user: login: mixpc
created_at: 2025-03-31T20:15:46Z
body: @meGAmeS1 Thank you! I downloaded from your repo but missed the branch...

Please, note the two audios are the same audio in Spanish. So the Italian audio is missing. Strange.

Take
`https://www.rtve.es/play/videos/el-comisario-montalbano/forma-agua/6641872/`

If you watch it online, you can select and listen to the two audio streams in Spanish and Italian.

But I have downloaded the two audio streams:

```
me@ubuntu-server:~/rtve/yt-dlp-add-rtve-play$ ./yt-dlp.sh -F  https://www.rtve.es/play/videos/el-comisario-montalbano/forma-agua/6641872/
[rtve.es:alacarta] Fetching manager info
[rtve.es:alacarta] Extracting URL: https://www.rtve.es/play/videos/el-comisario-montalbano/forma-agua/6641872/
[rtve.es:alacarta] 6641872: Downloading JSON metadata
[rtve.es:alacarta] 6641872: Downloading url information
[rtve.es:alacarta] 6641872: Downloading m3u8 information
[rtve.es:alacarta] 6641872: Downloading MPD manifest
[rtve.es:alacarta] 6641872: Downloading url information
[rtve.es:alacarta] 6641872: Downloading m3u8 information
[rtve.es:alacarta] 6641872: Downloading MPD manifest
[info] Available formats for 6641872:
ID                  EXT RESOLUTION FPS │   FILESIZE   TBR PROTO │ VCODEC        VBR ACODEC      ABR ASR MORE INFO
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
dash-audio=193464-0 m4a audio only     │ ~154.36MiB  193k dash  │ audio only        mp4a.40.2  193k 48k DASH audio, m4a_dash
dash-audio=193464-1 m4a audio only     │ ~154.36MiB  193k dash  │ audio only        mp4a.40.2  193k 48k DASH audio, m4a_dash
2                   mp4 unknown        │                  http  │ unknown           unknown
3                   mp4 unknown        │                  http  │ unknown           unknown
dash-video=2766000  mp4 1280x720    25 │ ~  2.16GiB 2766k dash  │ avc1.640029 2766k video only          DASH video, mp4_dash
hls-3138            mp4 1280x720    25 │ ~  2.44GiB 3138k m3u8  │ avc1.640029 2766k mp4a.40.2  193k
dash-video=4519000  mp4 1920x1080   25 │ ~  3.52GiB 4519k dash  │ avc1.640029 4519k video only          DASH video, mp4_dash
hls-4996            mp4 1920x1080   25 │ ~  3.89GiB 4996k m3u8  │ avc1.640029 4519k mp4a.40.2  193k
me@ubuntu-server:~/rtve/yt-dlp-add-rtve-play$ ./yt-dlp.sh -f dash-audio=193464-0 https://www.rtve.es/play/videos/el-comisario-montalbano/forma-agua/6641872/
[rtve.es:alacarta] Fetching manager info
[rtve.es:alacarta] Extracting URL: https://www.rtve.es/play/videos/el-comisario-montalbano/forma-agua/6641872/
[rtve.es:alacarta] 6641872: Downloading JSON metadata
[rtve.es:alacarta] 6641872: Downloading url information
[rtve.es:alacarta] 6641872: Downloading m3u8 information
[rtve.es:alacarta] 6641872: Downloading MPD manifest
[rtve.es:alacarta] 6641872: Downloading url information
[rtve.es:alacarta] 6641872: Downloading m3u8 information
[rtve.es:alacarta] 6641872: Downloading MPD manifest
[info] 6641872: Downloading 1 format(s): dash-audio=193464-0
[dashsegments] Total fragments: 3348
[download] Destination: La forma del agua [6641872].m4a
[download] 100% of  156.24MiB in 00:02:03 at 1.26MiB/s
[FixupM4a] Correcting container of "La forma del agua [6641872].m4a"
[FixupDuplicateMoov] Fixing duplicate MOOV atoms of "La forma del agua [6641872].m4a"
me@ubuntu-server:~/rtve/yt-dlp-add-rtve-play$ mv La\ forma\ del\ agua\ \[6641872\].m4a audio0.m4a
me@ubuntu-server:~/rtve/yt-dlp-add-rtve-play$ ./yt-dlp.sh -f dash-audio=193464-1 https://www.rtve.es/play/videos/el-comisario-montalbano/forma-agua/6641872/
[rtve.es:alacarta] Fetching manager info
[rtve.es:alacarta] Extracting URL: https://www.rtve.es/play/videos/el-comisario-montalbano/forma-agua/6641872/
[rtve.es:alacarta] 6641872: Downloading JSON metadata
[rtve.es:alacarta] 6641872: Downloading url information
[rtve.es:alacarta] 6641872: Downloading m3u8 information
[rtve.es:alacarta] 6641872: Downloading MPD manifest
[rtve.es:alacarta] 6641872: Downloading url information
[rtve.es:alacarta] 6641872: Downloading m3u8 information
[rtve.es:alacarta] 6641872: Downloading MPD manifest
[info] 6641872: Downloading 1 format(s): dash-audio=193464-1
[dashsegments] Total fragments: 3348
[download] Destination: La forma del agua [6641872].m4a
[download] 100% of  156.24MiB in 00:03:51 at 692.20KiB/s
[FixupM4a] Correcting container of "La forma del agua [6641872].m4a"
[FixupDuplicateMoov] Fixing duplicate MOOV atoms of "La forma del agua [6641872].m4a"
me@ubuntu-server:~/rtve/yt-dlp-add-rtve-play$ mv La\ forma\ del\ agua\ \[6641872\].m4a audio1.m4a

```
and audio0.mp4 and audio1.m4a are both in Spanish. So maybe code in rtve has changed?


author_association: NONE
user: login: meGAmeS1
created_at: 2025-04-14T16:31:31Z
body: @seproDev Well done for the cleanup. As suggested in a comment on an other issue, I said I could look on how to add playlist support (to be able to download a whole directly instead of doing video by video manually, so for exemple pass this URL `https://www.rtve.es/play/videos/masterchef/` to download each video of the show).

Your work motivated me and I've done it, should I push it here or should it be on its own PR after this one is merged ? (the code isn't very long, it's about 40 lines of code)
author_association: CONTRIBUTOR
user: login: seproDev
created_at: 2025-04-14T16:34:55Z
body: Let's do that in a separate PR after this one is merged.
author_association: CONTRIBUTOR
user: login: graphic-parse
created_at: 2025-05-27T20:48:13Z
body: Hello, thank you for the project. I would like to point out some small bugs.

> yt_dlp https://www.rtve.es/play/videos/cine-de-siempre/matrimonios-separados/16535882/ --get-title
> Matrimonios separados
> yt_dlp https://www.rtve.es/play/videos/cine-de-siempre/matrimonios-separados/16535882/ --get-thumbnail
> https://img2.rtve.es/v/matrimonios-separados_16535882.png
> yt_dlp https://www.rtve.es/play/videos/cine-de-siempre/matrimonios-separados/16535882/ --get-url
> https://rtvehlsvodlote7.rtve.es/mediavodv2/resources/TE_SCN7PRE/mp4/0/6/1744114593860.mp4/1744114593860-audio=191759-video=4518881.m3u8?hls_client_manifest_version=3&idasset=16535882
> yt_dlp https://www.rtve.es/play/videos/cine-de-siempre/matrimonios-separados/16535882/ --list-subs
> [rtve.es:alacarta] Extracting URL: https://www.rtve.es/play/videos/cine-de-siempre/matrimonios-separados/16535882/
> [rtve.es:alacarta] 16535882: Downloading JSON metadata
> [rtve.es:alacarta] 16535882: Downloading url information
> [rtve.es:alacarta] 16535882: Downloading m3u8 information
> [rtve.es:alacarta] 16535882: Downloading MPD manifest
> [rtve.es:alacarta] 16535882: Downloading url information
> [rtve.es:alacarta] 16535882: Downloading m3u8 information
> [rtve.es:alacarta] 16535882: Downloading MPD manifest
> [rtve.es:alacarta] 16535882: Downloading subtitles info
> [info] Available subtitles for 16535882:
> Language Formats
> es       vtt

If instead of using the `'http://www.rtve.es/api/videos/%s/config/alacarta_videos.json'` invocation we would simply use `'http://www.rtve.es/api/videos/%s.json'` it would extract more complete information.

The note `subtitleRef` includes the address to the subtitle page `https://api2.rtve.es/api/videos/{video_id}/subtitulos.json`.

For example the title could be preferably the longTitle, the image could give the full address with formatting and the subtitle, which is not shown now, could link to the address. In the url the characters from mp4/ to idasset could be replaced by ? separator.

The results could be the following instead of the above.

> Cine - Matrimonios separados
> https://img2.rtve.es/imagenes/cine-matrimonios-separados/01744296899637.jpg
> https://rtvehlsvodlote7.rtve.es/mediavodv2/resources/TE_SCN7PRE/mp4/0/6/1744114593860.mp4?idasset=16535882
> [info] Available subtitles for 16535882:
> es       https://www.rtve.es/resources/vtt/4/4/5/6/01744287016544.vtt

Some strings to include in the extractor would be these, including the call to the page with the subtitle address included in the new shorter .json.

`        subtitles = None
        if info.get('subtitleRef') is not None:
            subtitles = self.extract_subtitles(video_id, info['subtitleRef'])

        return {
            'id': video_id,
            'title': info['longTitle'].strip(),
            'formats': formats,
            'thumbnail': info.get('thumbnail'),
            'page_url': url,
            'subtitles': subtitles,
            'duration': float_or_none(info.get('duration'), scale=1000),
        }`
But currently the call `info` and `info.get` do not work in this context.

Thank you very much for your help.


author_association: NONE
user: login: bashonly
created_at: 2025-05-27T20:51:13Z
body: @graphic-parse Thanks for the info, but could you please open a new issue? Comments on closed PRs are out-of-sight and out-of-mind, and this info will be lost and forgotten by tomorrow if you don't open a new issue to track the proposed improvements to the extractor.
author_association: MEMBER
user: login: graphic-parse
created_at: 2025-05-27T21:25:02Z
body: > @graphic-parse Thanks for the info, but could you please open a new issue? Comments on closed PRs are out-of-sight and out-of-mind, and this info will be lost and forgotten by tomorrow if you don't open a new issue to track the proposed improvements to the extractor.

Thanks, I had thought about it but I had understood that to open a new topic and not saturate it you had to fill in a template with all the items that don't work, and apart from the fact that I don't trust to do it well and they reject it, at least the project works partially. Besides I guess they alert by email the participants subscribed to the thread, who have already participated in the topic and know about it. As long as it works I don't mind if they manage to fix these details.
author_association: NONE