[RadioFrance] Fix podcast and person playlist downloads

This commit is contained in:
lonm 2024-05-15 16:39:56 +01:00
parent 351dc0bc33
commit e2243c2033

View File

@ -267,10 +267,10 @@ def _real_extract(self, url):
class RadioFrancePlaylistBaseIE(RadioFranceBaseIE): class RadioFrancePlaylistBaseIE(RadioFranceBaseIE):
"""Subclasses must set _METADATA_KEY""" """Subclasses must set _METADATA_KEY"""
def _call_api(self, content_id, cursor, page_num): def _call_api(self, station, content_id, cursor):
raise NotImplementedError('This method must be implemented by subclasses') raise NotImplementedError('This method must be implemented by subclasses')
def _generate_playlist_entries(self, content_id, content_response): def _generate_playlist_entries(self, station, content_id, content_response):
for page_num in itertools.count(2): for page_num in itertools.count(2):
for entry in content_response['items']: for entry in content_response['items']:
yield self.url_result( yield self.url_result(
@ -281,28 +281,39 @@ def _generate_playlist_entries(self, content_id, content_response):
'thumbnail': ('visual', 'src'), 'thumbnail': ('visual', 'src'),
})) }))
next_cursor = traverse_obj(content_response, (('pagination', None), 'next'), get_all=False) if not content_response["next"]:
if not next_cursor:
break break
content_response = self._call_api(content_id, next_cursor, page_num) content_response = self._call_api(station, content_id, content_response["next"])
def _real_extract(self, url): def _real_extract(self, url):
display_id = self._match_id(url) playlist_id = self._match_id(url)
# If it is a podcast playlist, get the name of the station it is on
# profile page playlists are not attached to a station currently
station = self._match_valid_url(url).group('station') if isinstance(self, RadioFrancePodcastIE) else None
metadata = self._download_json( # Check if user started request from a page other than 1
'https://www.radiofrance.fr/api/v2.1/path', display_id, startpage = 1
query={'value': urllib.parse.urlparse(url).path})['content'] parsedurl = urllib.parse.urlparse(url)
if parsedurl.query:
startpagequery = urllib.parse.parse_qs(parsedurl.query)
if 'p' in startpagequery:
startpage = int(startpagequery['p'][0])
content_id = metadata['id'] # Get data for the first page, and the uuid for the playlist
metadata = self._call_api(station, playlist_id, startpage)
uuid = traverse_obj(metadata, ('metadata', 'id'))
# This method should return the final playlist metadata which yt-dlp can then use to download everything
return self.playlist_result( return self.playlist_result(
self._generate_playlist_entries(content_id, metadata[self._METADATA_KEY]), content_id, self._generate_playlist_entries(station, playlist_id, metadata),
display_id=display_id, **{**traverse_obj(metadata, { uuid,
display_id=playlist_id,
**{**traverse_obj(metadata['metadata'], {
'title': 'title', 'title': 'title',
'description': 'standFirst', 'description': 'standFirst',
'thumbnail': ('visual', 'src'), 'thumbnail': ('visual', 'src'),
}), **traverse_obj(metadata, { }), **traverse_obj(metadata['metadata'], {
'title': 'name', 'title': 'name',
'description': 'role', 'description': 'role',
})}) })})
@ -311,7 +322,7 @@ def _real_extract(self, url):
class RadioFrancePodcastIE(RadioFrancePlaylistBaseIE): class RadioFrancePodcastIE(RadioFrancePlaylistBaseIE):
_VALID_URL = rf'''(?x) _VALID_URL = rf'''(?x)
{RadioFranceBaseIE._VALID_URL_BASE} {RadioFranceBaseIE._VALID_URL_BASE}
/(?:{RadioFranceBaseIE._STATIONS_RE}) /(?P<station>{RadioFranceBaseIE._STATIONS_RE})
/podcasts/(?P<id>[\w-]+)/?(?:[?#]|$) /podcasts/(?P<id>[\w-]+)/?(?:[?#]|$)
''' '''
@ -363,10 +374,27 @@ class RadioFrancePodcastIE(RadioFrancePlaylistBaseIE):
_METADATA_KEY = 'expressions' _METADATA_KEY = 'expressions'
def _call_api(self, podcast_id, cursor, page_num): def _call_api(self, station, podcast_id, cursor):
return self._download_json( # The data is stored in the last <script> tag on a page
f'https://www.radiofrance.fr/api/v2.1/concepts/{podcast_id}/expressions', podcast_id, url = 'https://www.radiofrance.fr/' + station + '/podcasts/' + podcast_id + '?p=' + str(cursor)
note=f'Downloading page {page_num}', query={'pageCursor': cursor}) webpage = self._download_webpage(url, podcast_id, note=f'Downloading {podcast_id} page {cursor}')
resp = dict()
# _search_json cannot parse the data as it contains javascript
# Therefore, parse the episodes objects array separately
resp['items'] = self._search_json(r'a.items\s*=\s*', webpage, podcast_id, podcast_id,
contains_pattern=r'\[.+\]', transform_source=js_to_json)
# the pagination data is stored in a javascript object 'a'
lastPage = int(re.search(r'a\.lastPage\s*=\s*(\d+);', webpage).group(1))
hasMorePages = cursor < lastPage
resp['next'] = cursor + 1 if hasMorePages else None
resp['metadata'] = self._search_json(r'content:\s*', webpage, podcast_id, podcast_id,
transform_source=js_to_json)
return resp
class RadioFranceProfileIE(RadioFrancePlaylistBaseIE): class RadioFranceProfileIE(RadioFrancePlaylistBaseIE):
@ -380,7 +408,7 @@ class RadioFranceProfileIE(RadioFrancePlaylistBaseIE):
'title': 'Thomas Pesquet', 'title': 'Thomas Pesquet',
'description': 'Astronaute à l\'agence spatiale européenne', 'description': 'Astronaute à l\'agence spatiale européenne',
}, },
'playlist_mincount': 212, 'playlist_mincount': 158,
}, { }, {
'url': 'https://www.radiofrance.fr/personnes/eugenie-bastie', 'url': 'https://www.radiofrance.fr/personnes/eugenie-bastie',
'info_dict': { 'info_dict': {
@ -398,15 +426,37 @@ class RadioFranceProfileIE(RadioFrancePlaylistBaseIE):
_METADATA_KEY = 'documents' _METADATA_KEY = 'documents'
def _call_api(self, profile_id, cursor, page_num): def _call_api(self, station, profile_id, cursor):
resp = self._download_json( url = 'https://www.radiofrance.fr/personnes/' + profile_id + '?p=' + str(cursor)
f'https://www.radiofrance.fr/api/v2.1/taxonomy/{profile_id}/documents', profile_id, webpage = self._download_webpage(url, profile_id, note=f'Downloading {profile_id} page {cursor}')
note=f'Downloading page {page_num}', query={
'relation': 'personality', resp = dict()
'cursor': cursor,
}) # On profile pages, the data is stored in a javascript array in the final <script>
# Each episode is stored as
# a[0] = { id: ... }; a[1] = [ id: ... ]; etc.
# Annoyingly, sometimes it is delivered using 'b', with 'a' holding metadata
resp['items'] = []
podcastindex = 0
nextmatch = True
while nextmatch:
nextmatch = self._search_json(r'\w+\[' + str(podcastindex) + r'\]\s*=\s*', webpage, profile_id,
profile_id, transform_source=js_to_json, fatal=False, default=None)
podcastindex += 1
if nextmatch is not None:
resp['items'].append(nextmatch)
# There is more than one pagination key in the final <script>
# We should use pick the pagination object which is within a documents object
pagedata = self._search_json(r'documents\s*:\s*', webpage, profile_id, profile_id,
transform_source=js_to_json)
lastPage = traverse_obj(pagedata, ('pagination', 'lastPage'))
hasMorePages = cursor < lastPage
resp['next'] = cursor + 1 if hasMorePages else None
resp['metadata'] = self._search_json(r'content:\s*', webpage, profile_id, profile_id,
transform_source=js_to_json)
resp['next'] = traverse_obj(resp, ('pagination', 'next'))
return resp return resp