mirror of
https://github.com/yt-dlp/yt-dlp.git
synced 2025-08-17 09:58:29 +00:00
Merge branch 'yt-dlp:master' into generic_tests
This commit is contained in:
commit
bb6b063f45
@ -2195,7 +2195,7 @@ def _filter(f):
|
|||||||
return op(actual_value, comparison_value)
|
return op(actual_value, comparison_value)
|
||||||
return _filter
|
return _filter
|
||||||
|
|
||||||
def _check_formats(self, formats):
|
def _check_formats(self, formats, warning=True):
|
||||||
for f in formats:
|
for f in formats:
|
||||||
working = f.get('__working')
|
working = f.get('__working')
|
||||||
if working is not None:
|
if working is not None:
|
||||||
@ -2208,6 +2208,9 @@ def _check_formats(self, formats):
|
|||||||
continue
|
continue
|
||||||
temp_file = tempfile.NamedTemporaryFile(suffix='.tmp', delete=False, dir=path or None)
|
temp_file = tempfile.NamedTemporaryFile(suffix='.tmp', delete=False, dir=path or None)
|
||||||
temp_file.close()
|
temp_file.close()
|
||||||
|
# If FragmentFD fails when testing a fragment, it will wrongly set a non-zero return code.
|
||||||
|
# Save the actual return code for later. See https://github.com/yt-dlp/yt-dlp/issues/13750
|
||||||
|
original_retcode = self._download_retcode
|
||||||
try:
|
try:
|
||||||
success, _ = self.dl(temp_file.name, f, test=True)
|
success, _ = self.dl(temp_file.name, f, test=True)
|
||||||
except (DownloadError, OSError, ValueError, *network_exceptions):
|
except (DownloadError, OSError, ValueError, *network_exceptions):
|
||||||
@ -2218,12 +2221,18 @@ def _check_formats(self, formats):
|
|||||||
os.remove(temp_file.name)
|
os.remove(temp_file.name)
|
||||||
except OSError:
|
except OSError:
|
||||||
self.report_warning(f'Unable to delete temporary file "{temp_file.name}"')
|
self.report_warning(f'Unable to delete temporary file "{temp_file.name}"')
|
||||||
|
# Restore the actual return code
|
||||||
|
self._download_retcode = original_retcode
|
||||||
f['__working'] = success
|
f['__working'] = success
|
||||||
if success:
|
if success:
|
||||||
f.pop('__needs_testing', None)
|
f.pop('__needs_testing', None)
|
||||||
yield f
|
yield f
|
||||||
else:
|
else:
|
||||||
self.to_screen('[info] Unable to download format {}. Skipping...'.format(f['format_id']))
|
msg = f'Unable to download format {f["format_id"]}. Skipping...'
|
||||||
|
if warning:
|
||||||
|
self.report_warning(msg)
|
||||||
|
else:
|
||||||
|
self.to_screen(f'[info] {msg}')
|
||||||
|
|
||||||
def _select_formats(self, formats, selector):
|
def _select_formats(self, formats, selector):
|
||||||
return list(selector({
|
return list(selector({
|
||||||
@ -2949,7 +2958,7 @@ def is_wellformed(f):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if self.params.get('check_formats') is True:
|
if self.params.get('check_formats') is True:
|
||||||
formats = LazyList(self._check_formats(formats[::-1]), reverse=True)
|
formats = LazyList(self._check_formats(formats[::-1], warning=False), reverse=True)
|
||||||
|
|
||||||
if not formats or formats[0] is not info_dict:
|
if not formats or formats[0] is not info_dict:
|
||||||
# only set the 'formats' fields if the original info_dict list them
|
# only set the 'formats' fields if the original info_dict list them
|
||||||
|
@ -1557,6 +1557,7 @@
|
|||||||
PlatziCourseIE,
|
PlatziCourseIE,
|
||||||
PlatziIE,
|
PlatziIE,
|
||||||
)
|
)
|
||||||
|
from .playerfm import PlayerFmIE
|
||||||
from .playplustv import PlayPlusTVIE
|
from .playplustv import PlayPlusTVIE
|
||||||
from .playsuisse import PlaySuisseIE
|
from .playsuisse import PlaySuisseIE
|
||||||
from .playtvak import PlaytvakIE
|
from .playtvak import PlaytvakIE
|
||||||
|
@ -111,11 +111,9 @@ class AENetworksIE(AENetworksBaseIE):
|
|||||||
IE_NAME = 'aenetworks'
|
IE_NAME = 'aenetworks'
|
||||||
IE_DESC = 'A+E Networks: A&E, Lifetime, History.com, FYI Network and History Vault'
|
IE_DESC = 'A+E Networks: A&E, Lifetime, History.com, FYI Network and History Vault'
|
||||||
_VALID_URL = AENetworksBaseIE._BASE_URL_REGEX + r'''(?P<id>
|
_VALID_URL = AENetworksBaseIE._BASE_URL_REGEX + r'''(?P<id>
|
||||||
shows/[^/]+/season-\d+/episode-\d+|
|
shows/[^/?#]+/season-\d+/episode-\d+|
|
||||||
(?:
|
(?P<type>movie|special)s/[^/?#]+(?P<extra>/[^/?#]+)?|
|
||||||
(?:movie|special)s/[^/]+|
|
(?:shows/[^/?#]+/)?videos/[^/?#]+
|
||||||
(?:shows/[^/]+/)?videos
|
|
||||||
)/[^/?#&]+
|
|
||||||
)'''
|
)'''
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.history.com/shows/mountain-men/season-1/episode-1',
|
'url': 'http://www.history.com/shows/mountain-men/season-1/episode-1',
|
||||||
@ -128,7 +126,7 @@ class AENetworksIE(AENetworksBaseIE):
|
|||||||
'upload_date': '20120529',
|
'upload_date': '20120529',
|
||||||
'uploader': 'AENE-NEW',
|
'uploader': 'AENE-NEW',
|
||||||
'duration': 2592.0,
|
'duration': 2592.0,
|
||||||
'thumbnail': r're:^https?://.*\.jpe?g$',
|
'thumbnail': r're:https?://.+/.+\.jpg',
|
||||||
'chapters': 'count:5',
|
'chapters': 'count:5',
|
||||||
'tags': 'count:14',
|
'tags': 'count:14',
|
||||||
'categories': ['Mountain Men'],
|
'categories': ['Mountain Men'],
|
||||||
@ -139,10 +137,7 @@ class AENetworksIE(AENetworksBaseIE):
|
|||||||
'series': 'Mountain Men',
|
'series': 'Mountain Men',
|
||||||
'age_limit': 0,
|
'age_limit': 0,
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {'skip_download': 'm3u8'},
|
||||||
# m3u8 download
|
|
||||||
'skip_download': True,
|
|
||||||
},
|
|
||||||
'add_ie': ['ThePlatform'],
|
'add_ie': ['ThePlatform'],
|
||||||
'skip': 'Geo-restricted - This content is not available in your location.',
|
'skip': 'Geo-restricted - This content is not available in your location.',
|
||||||
}, {
|
}, {
|
||||||
@ -156,7 +151,7 @@ class AENetworksIE(AENetworksBaseIE):
|
|||||||
'upload_date': '20160112',
|
'upload_date': '20160112',
|
||||||
'uploader': 'AENE-NEW',
|
'uploader': 'AENE-NEW',
|
||||||
'duration': 1277.695,
|
'duration': 1277.695,
|
||||||
'thumbnail': r're:^https?://.*\.jpe?g$',
|
'thumbnail': r're:https?://.+/.+\.jpg',
|
||||||
'chapters': 'count:4',
|
'chapters': 'count:4',
|
||||||
'tags': 'count:23',
|
'tags': 'count:23',
|
||||||
'episode': 'Inlawful Entry',
|
'episode': 'Inlawful Entry',
|
||||||
@ -166,10 +161,53 @@ class AENetworksIE(AENetworksBaseIE):
|
|||||||
'series': 'Duck Dynasty',
|
'series': 'Duck Dynasty',
|
||||||
'age_limit': 0,
|
'age_limit': 0,
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {'skip_download': 'm3u8'},
|
||||||
# m3u8 download
|
'add_ie': ['ThePlatform'],
|
||||||
'skip_download': True,
|
}, {
|
||||||
|
'url': 'https://play.mylifetime.com/movies/v-c-andrews-web-of-dreams',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '1590627395981',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'VC Andrews\' Web of Dreams',
|
||||||
|
'description': 'md5:2a8ba13ae64271c79eb65c0577d312ce',
|
||||||
|
'uploader': 'AENE-NEW',
|
||||||
|
'age_limit': 14,
|
||||||
|
'duration': 5253.665,
|
||||||
|
'thumbnail': r're:https?://.+/.+\.jpg',
|
||||||
|
'chapters': 'count:8',
|
||||||
|
'tags': ['lifetime', 'mylifetime', 'lifetime channel', "VC Andrews' Web of Dreams"],
|
||||||
|
'series': '',
|
||||||
|
'season': 'Season 0',
|
||||||
|
'season_number': 0,
|
||||||
|
'episode': 'VC Andrews\' Web of Dreams',
|
||||||
|
'episode_number': 0,
|
||||||
|
'timestamp': 1566489703.0,
|
||||||
|
'upload_date': '20190822',
|
||||||
},
|
},
|
||||||
|
'params': {'skip_download': 'm3u8'},
|
||||||
|
'add_ie': ['ThePlatform'],
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.aetv.com/specials/hunting-jonbenets-killer-the-untold-story',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '1488235587551',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Hunting JonBenet\'s Killer: The Untold Story',
|
||||||
|
'description': 'md5:209869425ee392d74fe29201821e48b4',
|
||||||
|
'uploader': 'AENE-NEW',
|
||||||
|
'age_limit': 14,
|
||||||
|
'duration': 5003.903,
|
||||||
|
'thumbnail': r're:https?://.+/.+\.jpg',
|
||||||
|
'chapters': 'count:10',
|
||||||
|
'tags': 'count:11',
|
||||||
|
'series': '',
|
||||||
|
'season': 'Season 0',
|
||||||
|
'season_number': 0,
|
||||||
|
'episode': 'Hunting JonBenet\'s Killer: The Untold Story',
|
||||||
|
'episode_number': 0,
|
||||||
|
'timestamp': 1554987697.0,
|
||||||
|
'upload_date': '20190411',
|
||||||
|
},
|
||||||
|
'params': {'skip_download': 'm3u8'},
|
||||||
'add_ie': ['ThePlatform'],
|
'add_ie': ['ThePlatform'],
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://www.fyi.tv/shows/tiny-house-nation/season-1/episode-8',
|
'url': 'http://www.fyi.tv/shows/tiny-house-nation/season-1/episode-8',
|
||||||
@ -198,7 +236,9 @@ class AENetworksIE(AENetworksBaseIE):
|
|||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
domain, canonical = self._match_valid_url(url).groups()
|
domain, canonical, url_type, extra = self._match_valid_url(url).group('domain', 'id', 'type', 'extra')
|
||||||
|
if url_type in ('movie', 'special') and not extra:
|
||||||
|
canonical += f'/full-{url_type}'
|
||||||
return self._extract_aetn_info(domain, 'canonical', '/' + canonical, url)
|
return self._extract_aetn_info(domain, 'canonical', '/' + canonical, url)
|
||||||
|
|
||||||
|
|
||||||
|
@ -11,8 +11,14 @@
|
|||||||
|
|
||||||
class DangalPlayBaseIE(InfoExtractor):
|
class DangalPlayBaseIE(InfoExtractor):
|
||||||
_NETRC_MACHINE = 'dangalplay'
|
_NETRC_MACHINE = 'dangalplay'
|
||||||
|
_REGION = 'IN'
|
||||||
_OTV_USER_ID = None
|
_OTV_USER_ID = None
|
||||||
_LOGIN_HINT = 'Pass credentials as -u "token" -p "USER_ID" where USER_ID is the `otv_user_id` in browser local storage'
|
_LOGIN_HINT = (
|
||||||
|
'Pass credentials as -u "token" -p "USER_ID" '
|
||||||
|
'(where USER_ID is the value of "otv_user_id" in your browser local storage). '
|
||||||
|
'Your login region can be optionally suffixed to the username as @REGION '
|
||||||
|
'(where REGION is the two-letter "region" code found in your browser local storage), '
|
||||||
|
'e.g.: -u "token@IN" -p "USER_ID"')
|
||||||
_API_BASE = 'https://ottapi.dangalplay.com'
|
_API_BASE = 'https://ottapi.dangalplay.com'
|
||||||
_AUTH_TOKEN = 'jqeGWxRKK7FK5zEk3xCM' # from https://www.dangalplay.com/main.48ad19e24eb46acccef3.js
|
_AUTH_TOKEN = 'jqeGWxRKK7FK5zEk3xCM' # from https://www.dangalplay.com/main.48ad19e24eb46acccef3.js
|
||||||
_SECRET_KEY = 'f53d31a4377e4ef31fa0' # same as above
|
_SECRET_KEY = 'f53d31a4377e4ef31fa0' # same as above
|
||||||
@ -20,8 +26,12 @@ class DangalPlayBaseIE(InfoExtractor):
|
|||||||
def _perform_login(self, username, password):
|
def _perform_login(self, username, password):
|
||||||
if self._OTV_USER_ID:
|
if self._OTV_USER_ID:
|
||||||
return
|
return
|
||||||
if username != 'token' or not re.fullmatch(r'[\da-f]{32}', password):
|
mobj = re.fullmatch(r'token(?:@(?P<region>[A-Z]{2}))?', username)
|
||||||
|
if not mobj or not re.fullmatch(r'[\da-f]{32}', password):
|
||||||
raise ExtractorError(self._LOGIN_HINT, expected=True)
|
raise ExtractorError(self._LOGIN_HINT, expected=True)
|
||||||
|
if region := mobj.group('region'):
|
||||||
|
self._REGION = region
|
||||||
|
self.write_debug(f'Setting login region to "{self._REGION}"')
|
||||||
self._OTV_USER_ID = password
|
self._OTV_USER_ID = password
|
||||||
|
|
||||||
def _real_initialize(self):
|
def _real_initialize(self):
|
||||||
@ -52,7 +62,7 @@ def _call_api(self, path, display_id, note='Downloading JSON metadata', fatal=Tr
|
|||||||
f'{self._API_BASE}/{path}', display_id, note, fatal=fatal,
|
f'{self._API_BASE}/{path}', display_id, note, fatal=fatal,
|
||||||
headers={'Accept': 'application/json'}, query={
|
headers={'Accept': 'application/json'}, query={
|
||||||
'auth_token': self._AUTH_TOKEN,
|
'auth_token': self._AUTH_TOKEN,
|
||||||
'region': 'IN',
|
'region': self._REGION,
|
||||||
**query,
|
**query,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -106,7 +116,7 @@ def _generate_api_data(self, data):
|
|||||||
'catalog_id': catalog_id,
|
'catalog_id': catalog_id,
|
||||||
'content_id': content_id,
|
'content_id': content_id,
|
||||||
'category': '',
|
'category': '',
|
||||||
'region': 'IN',
|
'region': self._REGION,
|
||||||
'auth_token': self._AUTH_TOKEN,
|
'auth_token': self._AUTH_TOKEN,
|
||||||
'id': self._OTV_USER_ID,
|
'id': self._OTV_USER_ID,
|
||||||
'md5': hashlib.md5(unhashed.encode()).hexdigest(),
|
'md5': hashlib.md5(unhashed.encode()).hexdigest(),
|
||||||
@ -129,11 +139,14 @@ def _real_extract(self, url):
|
|||||||
except ExtractorError as e:
|
except ExtractorError as e:
|
||||||
if isinstance(e.cause, HTTPError) and e.cause.status == 422:
|
if isinstance(e.cause, HTTPError) and e.cause.status == 422:
|
||||||
error_info = traverse_obj(e.cause.response.read().decode(), ({json.loads}, 'error', {dict})) or {}
|
error_info = traverse_obj(e.cause.response.read().decode(), ({json.loads}, 'error', {dict})) or {}
|
||||||
if error_info.get('code') == '1016':
|
error_code = error_info.get('code')
|
||||||
|
if error_code == '1016':
|
||||||
self.raise_login_required(
|
self.raise_login_required(
|
||||||
f'Your token has expired or is invalid. {self._LOGIN_HINT}', method=None)
|
f'Your token has expired or is invalid. {self._LOGIN_HINT}', method=None)
|
||||||
elif msg := error_info.get('message'):
|
elif error_code == '4028':
|
||||||
raise ExtractorError(msg)
|
self.raise_login_required(
|
||||||
|
f'Your login region is unspecified or incorrect. {self._LOGIN_HINT}', method=None)
|
||||||
|
raise ExtractorError(join_nonempty(error_code, error_info.get('message'), delim=': '))
|
||||||
raise
|
raise
|
||||||
|
|
||||||
m3u8_url = traverse_obj(details, (
|
m3u8_url = traverse_obj(details, (
|
||||||
|
70
yt_dlp/extractor/playerfm.py
Normal file
70
yt_dlp/extractor/playerfm.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import clean_html, clean_podcast_url, int_or_none, str_or_none, url_or_none
|
||||||
|
from ..utils.traversal import traverse_obj
|
||||||
|
|
||||||
|
|
||||||
|
class PlayerFmIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'(?P<url>https?://(?:www\.)?player\.fm/(?:series/)?[\w-]+/(?P<id>[\w-]+))'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://player.fm/series/chapo-trap-house/movie-mindset-33-casino-feat-felix',
|
||||||
|
'info_dict': {
|
||||||
|
'ext': 'mp3',
|
||||||
|
'id': '478606546',
|
||||||
|
'display_id': 'movie-mindset-33-casino-feat-felix',
|
||||||
|
'thumbnail': r're:^https://.*\.(jpg|png)',
|
||||||
|
'title': 'Movie Mindset 33 - Casino feat. Felix',
|
||||||
|
'creators': ['Chapo Trap House'],
|
||||||
|
'description': r're:The first episode of this season of Movie Mindset is free .+ we feel about it\.',
|
||||||
|
'duration': 6830,
|
||||||
|
'timestamp': 1745406000,
|
||||||
|
'upload_date': '20250423',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://player.fm/series/nbc-nightly-news-with-tom-llamas/thursday-april-17-2025',
|
||||||
|
'info_dict': {
|
||||||
|
'ext': 'mp3',
|
||||||
|
'id': '477635490',
|
||||||
|
'display_id': 'thursday-april-17-2025',
|
||||||
|
'title': 'Thursday, April 17, 2025',
|
||||||
|
'thumbnail': r're:^https://.*\.(jpg|png)',
|
||||||
|
'duration': 1143,
|
||||||
|
'description': 'md5:4890b8cf9a55a787561cd5d59dfcda82',
|
||||||
|
'creators': ['NBC News'],
|
||||||
|
'timestamp': 1744941374,
|
||||||
|
'upload_date': '20250418',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://player.fm/series/soccer-101/ep-109-its-kicking-off-how-have-the-rules-for-kickoff-changed-what-are-the-best-approaches-to-getting-the-game-underway-and-how-could-we-improve-on-the-present-system-ack3NzL3yibvs4pf',
|
||||||
|
'info_dict': {
|
||||||
|
'ext': 'mp3',
|
||||||
|
'id': '481418710',
|
||||||
|
'thumbnail': r're:^https://.*\.(jpg|png)',
|
||||||
|
'title': r're:#109 It\'s kicking off! How have the rules for kickoff changed, .+ the present system\?',
|
||||||
|
'creators': ['TSS'],
|
||||||
|
'duration': 1510,
|
||||||
|
'display_id': 'md5:b52ecacaefab891b59db69721bfd9b13',
|
||||||
|
'description': 'md5:52a39e36d08d8919527454f152ad3c25',
|
||||||
|
'timestamp': 1659102055,
|
||||||
|
'upload_date': '20220729',
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
display_id, url = self._match_valid_url(url).group('id', 'url')
|
||||||
|
data = self._download_json(f'{url}.json', display_id)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'display_id': display_id,
|
||||||
|
'vcodec': 'none',
|
||||||
|
**traverse_obj(data, {
|
||||||
|
'id': ('id', {int}, {str_or_none}),
|
||||||
|
'url': ('url', {clean_podcast_url}),
|
||||||
|
'title': ('title', {str}),
|
||||||
|
'description': ('description', {clean_html}),
|
||||||
|
'duration': ('duration', {int_or_none}),
|
||||||
|
'thumbnail': (('image', ('series', 'image')), 'url', {url_or_none}, any),
|
||||||
|
'filesize': ('size', {int_or_none}),
|
||||||
|
'timestamp': ('publishedAt', {int_or_none}),
|
||||||
|
'creators': ('series', 'author', {str}, filter, all, filter),
|
||||||
|
}),
|
||||||
|
}
|
@ -81,7 +81,7 @@ def fix_cdata(s):
|
|||||||
# geo flag is a bit unreliable and not properly set all the time
|
# geo flag is a bit unreliable and not properly set all the time
|
||||||
geoprotection = xpath_text(relinker, './geoprotection', default='N') == 'Y'
|
geoprotection = xpath_text(relinker, './geoprotection', default='N') == 'Y'
|
||||||
|
|
||||||
ext = determine_ext(media_url)
|
ext = determine_ext(media_url).lower()
|
||||||
formats = []
|
formats = []
|
||||||
|
|
||||||
if ext == 'mp3':
|
if ext == 'mp3':
|
||||||
@ -108,7 +108,7 @@ def fix_cdata(s):
|
|||||||
'format_id': join_nonempty('https', bitrate, delim='-'),
|
'format_id': join_nonempty('https', bitrate, delim='-'),
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
raise ExtractorError('Unrecognized media file found')
|
raise ExtractorError(f'Unrecognized media extension "{ext}"')
|
||||||
|
|
||||||
if (not formats and geoprotection is True) or '/video_no_available.mp4' in media_url:
|
if (not formats and geoprotection is True) or '/video_no_available.mp4' in media_url:
|
||||||
self.raise_geo_restricted(countries=self._GEO_COUNTRIES, metadata_available=True)
|
self.raise_geo_restricted(countries=self._GEO_COUNTRIES, metadata_available=True)
|
||||||
@ -503,6 +503,28 @@ class RaiPlaySoundIE(RaiBaseIE):
|
|||||||
'upload_date': '20211201',
|
'upload_date': '20211201',
|
||||||
},
|
},
|
||||||
'params': {'skip_download': True},
|
'params': {'skip_download': True},
|
||||||
|
}, {
|
||||||
|
# case-sensitivity test for uppercase extension
|
||||||
|
'url': 'https://www.raiplaysound.it/audio/2020/05/Storia--Lunita-dItalia-e-lunificazione-della-Germania-b4c16390-7f3f-4282-b353-d94897dacb7c.html',
|
||||||
|
'md5': 'c69ebd69282f0effd7ef67b7e2f6c7d8',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'b4c16390-7f3f-4282-b353-d94897dacb7c',
|
||||||
|
'ext': 'mp3',
|
||||||
|
'title': "Storia | 01 L'unità d'Italia e l'unificazione della Germania",
|
||||||
|
'alt_title': 'md5:ed4ed82585c52057b71b43994a59b705',
|
||||||
|
'description': 'md5:92818b6f31b2c150567d56b75db2ea7f',
|
||||||
|
'uploader': 'rai radio 3',
|
||||||
|
'duration': 2439.0,
|
||||||
|
'thumbnail': 'https://www.raiplaysound.it/dl/img/2023/09/07/1694084898279_Maturadio-LOGO-2048x1152.jpg',
|
||||||
|
'creators': ['rai radio 3'],
|
||||||
|
'series': 'Maturadio',
|
||||||
|
'season': 'Season 9',
|
||||||
|
'season_number': 9,
|
||||||
|
'episode': "01. L'unità d'Italia e l'unificazione della Germania",
|
||||||
|
'episode_number': 1,
|
||||||
|
'timestamp': 1590400740,
|
||||||
|
'upload_date': '20200525',
|
||||||
|
},
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
|
Loading…
Reference in New Issue
Block a user