[^>]+>[^>]+>([^<]+)',
- webpage, 'description', fatal=False),
- 'uploader': self._html_search_regex(
- r'[^>]+>Channel:[^>]+>([^<]+)', media_info, 'channel', fatal=False),
- 'thumbnail': media.get('staticImage'),
+ 'subtitles': subtitles,
+ **traverse_obj(video_details, {
+ 'title': (('parlViewTitle', 'title'), {str}, any),
+ 'description': ('parlViewDescription', {str}),
+ 'duration': ('files', 'file', 'duration', {DURATION_RE.fullmatch}, 'duration', {parse_duration}),
+ 'timestamp': ('recordingFrom', {parse_iso8601}),
+ 'thumbnail': ('thumbUrl', {url_or_none}),
+ }),
}
diff --git a/yt_dlp/extractor/patreon.py b/yt_dlp/extractor/patreon.py
index 2c1436cac..9038b4a7f 100644
--- a/yt_dlp/extractor/patreon.py
+++ b/yt_dlp/extractor/patreon.py
@@ -19,7 +19,7 @@ from ..utils import (
url_or_none,
urljoin,
)
-from ..utils.traversal import traverse_obj, value
+from ..utils.traversal import require, traverse_obj, value
class PatreonBaseIE(InfoExtractor):
@@ -462,7 +462,7 @@ class PatreonCampaignIE(PatreonBaseIE):
_VALID_URL = r'''(?x)
https?://(?:www\.)?patreon\.com/(?:
(?:m|api/campaigns)/(?P\d+)|
- (?:c/)?(?P(?!creation[?/]|posts/|rss[?/])[\w-]+)
+ (?:cw?/)?(?P(?!creation[?/]|posts/|rss[?/])[\w-]+)
)(?:/posts)?/?(?:$|[?#])'''
_TESTS = [{
'url': 'https://www.patreon.com/dissonancepod/',
@@ -531,6 +531,28 @@ class PatreonCampaignIE(PatreonBaseIE):
'age_limit': 0,
},
'playlist_mincount': 331,
+ 'skip': 'Channel removed',
+ }, {
+ # next.js v13 data, see https://github.com/yt-dlp/yt-dlp/issues/13622
+ 'url': 'https://www.patreon.com/c/anythingelse/posts',
+ 'info_dict': {
+ 'id': '9631148',
+ 'title': 'Anything Else?',
+ 'description': 'md5:2ee1db4aed2f9460c2b295825a24aa08',
+ 'uploader': 'dan ',
+ 'uploader_id': '13852412',
+ 'uploader_url': 'https://www.patreon.com/anythingelse',
+ 'channel': 'Anything Else?',
+ 'channel_id': '9631148',
+ 'channel_url': 'https://www.patreon.com/anythingelse',
+ 'channel_follower_count': int,
+ 'age_limit': 0,
+ 'thumbnail': r're:https?://.+/.+',
+ },
+ 'playlist_mincount': 151,
+ }, {
+ 'url': 'https://www.patreon.com/cw/anythingelse',
+ 'only_matching': True,
}, {
'url': 'https://www.patreon.com/c/OgSog/posts',
'only_matching': True,
@@ -572,8 +594,11 @@ class PatreonCampaignIE(PatreonBaseIE):
campaign_id, vanity = self._match_valid_url(url).group('campaign_id', 'vanity')
if campaign_id is None:
webpage = self._download_webpage(url, vanity, headers={'User-Agent': self.patreon_user_agent})
- campaign_id = self._search_nextjs_data(
- webpage, vanity)['props']['pageProps']['bootstrapEnvelope']['pageBootstrap']['campaign']['data']['id']
+ campaign_id = traverse_obj(self._search_nextjs_data(webpage, vanity, default=None), (
+ 'props', 'pageProps', 'bootstrapEnvelope', 'pageBootstrap', 'campaign', 'data', 'id', {str}))
+ if not campaign_id:
+ campaign_id = traverse_obj(self._search_nextjs_v13_data(webpage, vanity), (
+ lambda _, v: v['type'] == 'campaign', 'id', {str}, any, {require('campaign ID')}))
params = {
'json-api-use-default-includes': 'false',
diff --git a/yt_dlp/extractor/playerfm.py b/yt_dlp/extractor/playerfm.py
new file mode 100644
index 000000000..d59d651a3
--- /dev/null
+++ b/yt_dlp/extractor/playerfm.py
@@ -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'(?Phttps?://(?:www\.)?player\.fm/(?:series/)?[\w-]+/(?P[\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),
+ }),
+ }
diff --git a/yt_dlp/extractor/plyr.py b/yt_dlp/extractor/plyr.py
new file mode 100644
index 000000000..c5f27cfd9
--- /dev/null
+++ b/yt_dlp/extractor/plyr.py
@@ -0,0 +1,104 @@
+import re
+
+from .common import InfoExtractor
+from .vimeo import VimeoIE
+
+
+class PlyrEmbedIE(InfoExtractor):
+ _VALID_URL = False
+ _WEBPAGE_TESTS = [{
+ # data-plyr-embed-id="https://player.vimeo.com/video/522319456/90e5c96063?dnt=1"
+ 'url': 'https://www.dhm.de/zeughauskino/filmreihen/online-filmreihen/filme-des-marshall-plans/200000000-mouths/',
+ 'info_dict': {
+ 'id': '522319456',
+ 'ext': 'mp4',
+ 'title': '200.000.000 Mouths (1950–51)',
+ 'uploader': 'Zeughauskino',
+ 'uploader_url': '',
+ 'comment_count': int,
+ 'like_count': int,
+ 'duration': 963,
+ 'thumbnail': 'https://i.vimeocdn.com/video/1081797161-9f09ddb4b7faa86e834e006b8e4b9c2cbaa0baa7da493211bf0796ae133a5ab8-d',
+ 'timestamp': 1615467405,
+ 'upload_date': '20210311',
+ 'release_timestamp': 1615467405,
+ 'release_date': '20210311',
+ },
+ 'params': {'skip_download': 'm3u8'},
+ 'expected_warnings': ['Failed to parse XML: not well-formed'],
+ }, {
+ # data-plyr-provider="vimeo" data-plyr-embed-id="803435276"
+ 'url': 'https://www.inarcassa.it/',
+ 'info_dict': {
+ 'id': '803435276',
+ 'ext': 'mp4',
+ 'title': 'HOME_Moto_Perpetuo',
+ 'uploader': 'Inarcassa',
+ 'uploader_url': '',
+ 'duration': 38,
+ 'thumbnail': 'https://i.vimeocdn.com/video/1663734769-945ad7ffabb16dbca009c023fd1d7b36bdb426a3dbae8345ed758136fe28f89a-d',
+ },
+ 'params': {'skip_download': 'm3u8'},
+ 'expected_warnings': ['Failed to parse XML: not well-formed'],
+ }, {
+ # data-plyr-embed-id="https://youtu.be/GF-BjYKoAqI"
+ 'url': 'https://www.profile.nl',
+ 'info_dict': {
+ 'id': 'GF-BjYKoAqI',
+ 'ext': 'mp4',
+ 'title': 'PROFILE: Recruitment Profile',
+ 'description': '',
+ 'media_type': 'video',
+ 'uploader': 'Profile Nederland',
+ 'uploader_id': '@profilenederland',
+ 'uploader_url': 'https://www.youtube.com/@profilenederland',
+ 'channel': 'Profile Nederland',
+ 'channel_id': 'UC9AUkB0Tv39-TBYjs05n3vg',
+ 'channel_url': 'https://www.youtube.com/channel/UC9AUkB0Tv39-TBYjs05n3vg',
+ 'channel_follower_count': int,
+ 'view_count': int,
+ 'like_count': int,
+ 'age_limit': 0,
+ 'duration': 39,
+ 'thumbnail': 'https://i.ytimg.com/vi/GF-BjYKoAqI/maxresdefault.jpg',
+ 'categories': ['Autos & Vehicles'],
+ 'tags': [],
+ 'timestamp': 1675692990,
+ 'upload_date': '20230206',
+ 'playable_in_embed': True,
+ 'availability': 'public',
+ 'live_status': 'not_live',
+ },
+ }, {
+ # data-plyr-embed-id="B1TZV8rNZoc" data-plyr-provider="youtube"
+ 'url': 'https://www.vnis.edu.vn',
+ 'info_dict': {
+ 'id': 'vnis.edu',
+ 'title': 'VNIS Education - Master Agent các Trường hàng đầu Bắc Mỹ',
+ 'description': 'md5:4dafcf7335bb018780e4426da8ab8e4e',
+ 'age_limit': 0,
+ 'thumbnail': 'https://vnis.edu.vn/wp-content/uploads/2021/05/ve-welcome-en.png',
+ 'timestamp': 1753233356,
+ 'upload_date': '20250723',
+ },
+ 'playlist_count': 3,
+ }]
+
+ @classmethod
+ def _extract_embed_urls(cls, url, webpage):
+ plyr_embeds = re.finditer(r'''(?x)
+ ]+(?:
+ data-plyr-embed-id="(?P [^"]+)"[^>]+data-plyr-provider="(?P[^"]+)"|
+ data-plyr-provider="(?P[^"]+)"[^>]+data-plyr-embed-id="(?P[^"]+)"
+ )[^>]*>''', webpage)
+ for mobj in plyr_embeds:
+ embed_id = mobj.group('id1') or mobj.group('id2')
+ provider = mobj.group('provider1') or mobj.group('provider2')
+ if provider == 'vimeo':
+ if not re.match(r'https?://', embed_id):
+ embed_id = f'https://player.vimeo.com/video/{embed_id}'
+ yield VimeoIE._smuggle_referrer(embed_id, url)
+ elif provider == 'youtube':
+ if not re.match(r'https?://', embed_id):
+ embed_id = f'https://youtube.com/watch?v={embed_id}'
+ yield embed_id
diff --git a/yt_dlp/extractor/rai.py b/yt_dlp/extractor/rai.py
index c489dc731..d1a4d4c37 100644
--- a/yt_dlp/extractor/rai.py
+++ b/yt_dlp/extractor/rai.py
@@ -81,7 +81,7 @@ class RaiBaseIE(InfoExtractor):
# geo flag is a bit unreliable and not properly set all the time
geoprotection = xpath_text(relinker, './geoprotection', default='N') == 'Y'
- ext = determine_ext(media_url)
+ ext = determine_ext(media_url).lower()
formats = []
if ext == 'mp3':
@@ -108,7 +108,7 @@ class RaiBaseIE(InfoExtractor):
'format_id': join_nonempty('https', bitrate, delim='-'),
})
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:
self.raise_geo_restricted(countries=self._GEO_COUNTRIES, metadata_available=True)
@@ -503,6 +503,28 @@ class RaiPlaySoundIE(RaiBaseIE):
'upload_date': '20211201',
},
'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):
@@ -765,7 +787,7 @@ class RaiCulturaIE(RaiNewsIE): # XXX: Do not subclass from concrete IE
class RaiSudtirolIE(RaiBaseIE):
- _VALID_URL = r'https?://raisudtirol\.rai\.it/.+media=(?P\w+)'
+ _VALID_URL = r'https?://rai(?:bz|sudtirol)\.rai\.it/.+media=(?P\w+)'
_TESTS = [{
# mp4 file
'url': 'https://raisudtirol.rai.it/la/index.php?media=Ptv1619729460',
@@ -791,6 +813,9 @@ class RaiSudtirolIE(RaiBaseIE):
'formats': 'count:6',
},
'params': {'skip_download': True},
+ }, {
+ 'url': 'https://raibz.rai.it/de/index.php?media=Ptv1751660400',
+ 'only_matching': True,
}]
def _real_extract(self, url):
diff --git a/yt_dlp/extractor/rtve.py b/yt_dlp/extractor/rtve.py
index 2812d9305..c2ccf73dd 100644
--- a/yt_dlp/extractor/rtve.py
+++ b/yt_dlp/extractor/rtve.py
@@ -6,9 +6,11 @@ import urllib.parse
from .common import InfoExtractor
from ..utils import (
ExtractorError,
+ InAdvancePagedList,
clean_html,
determine_ext,
float_or_none,
+ int_or_none,
make_archive_id,
parse_iso8601,
qualities,
@@ -371,3 +373,62 @@ class RTVETelevisionIE(InfoExtractor):
raise ExtractorError('The webpage doesn\'t contain any video', expected=True)
return self.url_result(play_url, ie=RTVEALaCartaIE.ie_key())
+
+
+class RTVEProgramIE(RTVEBaseIE):
+ IE_NAME = 'rtve.es:program'
+ IE_DESC = 'RTVE.es programs'
+ _VALID_URL = r'https?://(?:www\.)?rtve\.es/play/videos/(?P[\w-]+)/?(?:[?#]|$)'
+ _TESTS = [{
+ 'url': 'https://www.rtve.es/play/videos/saber-vivir/',
+ 'info_dict': {
+ 'id': '111570',
+ 'title': 'Saber vivir - Programa de ciencia y futuro en RTVE Play',
+ },
+ 'playlist_mincount': 400,
+ }]
+ _PAGE_SIZE = 60
+
+ def _fetch_page(self, program_id, page_num):
+ return self._download_json(
+ f'https://www.rtve.es/api/programas/{program_id}/videos',
+ program_id, note=f'Downloading page {page_num}',
+ query={
+ 'type': 39816,
+ 'page': page_num,
+ 'size': 60,
+ })
+
+ def _entries(self, page_data):
+ for video in traverse_obj(page_data, ('page', 'items', lambda _, v: url_or_none(v['htmlUrl']))):
+ yield self.url_result(
+ video['htmlUrl'], RTVEALaCartaIE, url_transparent=True,
+ **traverse_obj(video, {
+ 'id': ('id', {str}),
+ 'title': ('longTitle', {str}),
+ 'description': ('shortDescription', {str}),
+ 'duration': ('duration', {float_or_none(scale=1000)}),
+ 'series': (('programInfo', 'title'), {str}, any),
+ 'season_number': ('temporadaOrden', {int_or_none}),
+ 'season_id': ('temporadaId', {str}),
+ 'season': ('temporada', {str}),
+ 'episode_number': ('episode', {int_or_none}),
+ 'episode': ('title', {str}),
+ 'thumbnail': ('thumbnail', {url_or_none}),
+ }),
+ )
+
+ def _real_extract(self, url):
+ program_slug = self._match_id(url)
+ program_page = self._download_webpage(url, program_slug)
+
+ program_id = self._html_search_meta('DC.identifier', program_page, 'Program ID', fatal=True)
+
+ first_page = self._fetch_page(program_id, 1)
+ page_count = traverse_obj(first_page, ('page', 'totalPages', {int})) or 1
+
+ entries = InAdvancePagedList(
+ lambda idx: self._entries(self._fetch_page(program_id, idx + 1) if idx else first_page),
+ page_count, self._PAGE_SIZE)
+
+ return self.playlist_result(entries, program_id, self._html_extract_title(program_page))
diff --git a/yt_dlp/extractor/sauceplus.py b/yt_dlp/extractor/sauceplus.py
new file mode 100644
index 000000000..75d7022d3
--- /dev/null
+++ b/yt_dlp/extractor/sauceplus.py
@@ -0,0 +1,41 @@
+from .floatplane import FloatplaneBaseIE
+
+
+class SaucePlusIE(FloatplaneBaseIE):
+ IE_DESC = 'Sauce+'
+ _VALID_URL = r'https?://(?:(?:www|beta)\.)?sauceplus\.com/post/(?P\w+)'
+ _BASE_URL = 'https://www.sauceplus.com'
+ _HEADERS = {
+ 'Origin': _BASE_URL,
+ 'Referer': f'{_BASE_URL}/',
+ }
+ _IMPERSONATE_TARGET = True
+ _TESTS = [{
+ 'url': 'https://www.sauceplus.com/post/YbBwIa2A5g',
+ 'info_dict': {
+ 'id': 'eit4Ugu5TL',
+ 'ext': 'mp4',
+ 'display_id': 'YbBwIa2A5g',
+ 'title': 'Scare the Coyote - Episode 3',
+ 'description': '',
+ 'thumbnail': r're:^https?://.*\.jpe?g$',
+ 'duration': 2975,
+ 'comment_count': int,
+ 'like_count': int,
+ 'dislike_count': int,
+ 'release_date': '20250627',
+ 'release_timestamp': 1750993500,
+ 'uploader': 'Scare The Coyote',
+ 'uploader_id': '683e0a3269688656a5a49a44',
+ 'uploader_url': 'https://www.sauceplus.com/channel/ScareTheCoyote/home',
+ 'channel': 'Scare The Coyote',
+ 'channel_id': '683e0a326968866ceba49a45',
+ 'channel_url': 'https://www.sauceplus.com/channel/ScareTheCoyote/home/main',
+ 'availability': 'subscriber_only',
+ },
+ 'params': {'skip_download': 'm3u8'},
+ }]
+
+ def _real_initialize(self):
+ if not self._get_cookies(self._BASE_URL).get('__Host-sp-sess'):
+ self.raise_login_required()
diff --git a/yt_dlp/extractor/skeb.py b/yt_dlp/extractor/skeb.py
index bc5ec3da7..70111d094 100644
--- a/yt_dlp/extractor/skeb.py
+++ b/yt_dlp/extractor/skeb.py
@@ -1,140 +1,118 @@
from .common import InfoExtractor
-from ..utils import ExtractorError, determine_ext, parse_qs, traverse_obj
+from ..networking.exceptions import HTTPError
+from ..utils import (
+ ExtractorError,
+ clean_html,
+ int_or_none,
+ str_or_none,
+ url_or_none,
+)
+from ..utils.traversal import traverse_obj
class SkebIE(InfoExtractor):
- _VALID_URL = r'https?://skeb\.jp/@[^/]+/works/(?P\d+)'
-
+ _VALID_URL = r'https?://skeb\.jp/@(?P[^/?#]+)/works/(?P\d+)'
_TESTS = [{
'url': 'https://skeb.jp/@riiru_wm/works/10',
'info_dict': {
'id': '466853',
- 'title': '内容はおまかせします! by 姫ノ森りぃる@一周年',
+ 'ext': 'mp4',
+ 'title': '10-1',
'description': 'md5:1ec50901efc3437cfbfe3790468d532d',
- 'uploader': '姫ノ森りぃる@一周年',
- 'uploader_id': 'riiru_wm',
- 'age_limit': 0,
- 'tags': [],
- 'url': r're:https://skeb.+',
- 'thumbnail': r're:https://skeb.+',
- 'subtitles': {
- 'jpn': [{
- 'url': r're:https://skeb.+',
- 'ext': 'vtt',
- }],
- },
- 'width': 720,
- 'height': 405,
'duration': 313,
- 'fps': 30,
- 'ext': 'mp4',
+ 'genres': ['video'],
+ 'thumbnail': r're:https?://.+',
+ 'uploader': '姫ノ森りぃる@ひとづま',
+ 'uploader_id': 'riiru_wm',
},
}, {
'url': 'https://skeb.jp/@furukawa_nob/works/3',
'info_dict': {
'id': '489408',
- 'title': 'いつもお世話になってお... by 古川ノブ@音楽とVlo...',
- 'description': 'md5:5adc2e41d06d33b558bf7b1faeb7b9c2',
- 'uploader': '古川ノブ@音楽とVlogのVtuber',
- 'uploader_id': 'furukawa_nob',
- 'age_limit': 0,
- 'tags': [
- 'よろしく', '大丈夫', 'お願い', 'でした',
- '是非', 'O', 'バー', '遊び', 'おはよう',
- 'オーバ', 'ボイス',
- ],
- 'url': r're:https://skeb.+',
- 'thumbnail': r're:https://skeb.+',
- 'subtitles': {
- 'jpn': [{
- 'url': r're:https://skeb.+',
- 'ext': 'vtt',
- }],
- },
- 'duration': 98,
'ext': 'mp3',
- 'vcodec': 'none',
- 'abr': 128,
+ 'title': '3-1',
+ 'description': 'md5:6de1f8f876426a6ac321c123848176a8',
+ 'duration': 98,
+ 'genres': ['voice'],
+ 'tags': 'count:11',
+ 'thumbnail': r're:https?://.+',
+ 'uploader': '古川ノブ@宮城の動画勢Vtuber',
+ 'uploader_id': 'furukawa_nob',
},
}, {
- 'url': 'https://skeb.jp/@mollowmollow/works/6',
+ 'url': 'https://skeb.jp/@Rizu_panda_cube/works/626',
'info_dict': {
- 'id': '6',
- 'title': 'ヒロ。\n\n私のキャラク... by 諸々',
- 'description': 'md5:aa6cbf2ba320b50bce219632de195f07',
- '_type': 'playlist',
- 'entries': [{
- 'id': '486430',
- 'title': 'ヒロ。\n\n私のキャラク... by 諸々',
- 'description': 'md5:aa6cbf2ba320b50bce219632de195f07',
- }, {
- 'id': '486431',
- 'title': 'ヒロ。\n\n私のキャラク... by 諸々',
- }],
+ 'id': '626',
+ 'description': 'md5:834557b39ca56960c5f77dd6ddabe775',
+ 'uploader': 'りづ100億%',
+ 'uploader_id': 'Rizu_panda_cube',
+ 'tags': 'count:57',
+ 'genres': ['video'],
},
+ 'playlist_count': 2,
+ 'expected_warnings': ['Skipping unsupported extension'],
}]
+ def _call_api(self, uploader_id, work_id):
+ return self._download_json(
+ f'https://skeb.jp/api/users/{uploader_id}/works/{work_id}', work_id, headers={
+ 'Accept': 'application/json',
+ 'Authorization': 'Bearer null',
+ })
+
def _real_extract(self, url):
- video_id = self._match_id(url)
- nuxt_data = self._search_nuxt_data(self._download_webpage(url, video_id), video_id)
+ uploader_id, work_id = self._match_valid_url(url).group('uploader_id', 'id')
+ try:
+ works = self._call_api(uploader_id, work_id)
+ except ExtractorError as e:
+ if not isinstance(e.cause, HTTPError) or e.cause.status != 429:
+ raise
+ webpage = e.cause.response.read().decode()
+ value = self._search_regex(
+ r'document\.cookie\s*=\s*["\']request_key=([^;"\']+)', webpage, 'request key')
+ self._set_cookie('skeb.jp', 'request_key', value)
+ works = self._call_api(uploader_id, work_id)
- parent = {
- 'id': video_id,
- 'title': nuxt_data.get('title'),
- 'description': nuxt_data.get('description'),
- 'uploader': traverse_obj(nuxt_data, ('creator', 'name')),
- 'uploader_id': traverse_obj(nuxt_data, ('creator', 'screen_name')),
- 'age_limit': 18 if nuxt_data.get('nsfw') else 0,
- 'tags': nuxt_data.get('tag_list'),
+ info = {
+ 'uploader_id': uploader_id,
+ **traverse_obj(works, {
+ 'age_limit': ('nsfw', {bool}, {lambda x: 18 if x else None}),
+ 'description': (('source_body', 'body'), {clean_html}, filter, any),
+ 'genres': ('genre', {str}, filter, all, filter),
+ 'tags': ('tag_list', ..., {str}, filter, all, filter),
+ 'uploader': ('creator', 'name', {str}),
+ }),
}
entries = []
- for item in nuxt_data.get('previews') or []:
- vid_url = item.get('url')
- given_ext = traverse_obj(item, ('information', 'extension'))
- preview_ext = determine_ext(vid_url, default_ext=None)
- if not preview_ext:
- content_disposition = parse_qs(vid_url)['response-content-disposition'][0]
- preview_ext = self._search_regex(
- r'filename="[^"]+\.([^\.]+?)"', content_disposition,
- 'preview file extension', fatal=False, group=1)
- if preview_ext not in ('mp4', 'mp3'):
+ for idx, preview in enumerate(traverse_obj(works, ('previews', lambda _, v: url_or_none(v['url']))), 1):
+ ext = traverse_obj(preview, ('information', 'extension', {str}))
+ if ext not in ('mp3', 'mp4'):
+ self.report_warning(f'Skipping unsupported extension "{ext}"')
continue
- if not vid_url or not item.get('id'):
- continue
- width, height = traverse_obj(item, ('information', 'width')), traverse_obj(item, ('information', 'height'))
- if width is not None and height is not None:
- # the longest side is at most 720px for non-client viewers
- max_size = max(width, height)
- width, height = (x * 720 // max_size for x in (width, height))
+
entries.append({
- **parent,
- 'id': str(item['id']),
- 'url': vid_url,
- 'thumbnail': item.get('poster_url'),
+ 'ext': ext,
+ 'title': f'{work_id}-{idx}',
'subtitles': {
- 'jpn': [{
- 'url': item.get('vtt_url'),
+ 'ja': [{
'ext': 'vtt',
+ 'url': preview['vtt_url'],
}],
- } if item.get('vtt_url') else None,
- 'width': width,
- 'height': height,
- 'duration': traverse_obj(item, ('information', 'duration')),
- 'fps': traverse_obj(item, ('information', 'frame_rate')),
- 'ext': preview_ext or given_ext,
- 'vcodec': 'none' if preview_ext == 'mp3' else None,
- # you'll always get 128kbps MP3 for non-client viewers
- 'abr': 128 if preview_ext == 'mp3' else None,
+ } if url_or_none(preview.get('vtt_url')) else None,
+ 'vcodec': 'none' if ext == 'mp3' else None,
+ **info,
+ **traverse_obj(preview, {
+ 'id': ('id', {str_or_none}),
+ 'thumbnail': ('poster_url', {url_or_none}),
+ 'url': ('url', {url_or_none}),
+ }),
+ **traverse_obj(preview, ('information', {
+ 'duration': ('duration', {int_or_none}),
+ 'fps': ('frame_rate', {int_or_none}),
+ 'height': ('height', {int_or_none}),
+ 'width': ('width', {int_or_none}),
+ })),
})
- if not entries:
- raise ExtractorError('No video/audio attachment found in this commission.', expected=True)
- elif len(entries) == 1:
- return entries[0]
- else:
- parent.update({
- '_type': 'playlist',
- 'entries': entries,
- })
- return parent
+ return self.playlist_result(entries, work_id, **info)
diff --git a/yt_dlp/extractor/skyit.py b/yt_dlp/extractor/skyit.py
index 0013d2621..fe45be774 100644
--- a/yt_dlp/extractor/skyit.py
+++ b/yt_dlp/extractor/skyit.py
@@ -213,7 +213,7 @@ class CieloTVItIE(SkyItIE): # XXX: Do not subclass from concrete IE
class TV8ItIE(SkyItVideoIE): # XXX: Do not subclass from concrete IE
IE_NAME = 'tv8.it'
- _VALID_URL = r'https?://(?:www\.)?tv8\.it/(?:show)?video/[0-9a-z-]+-(?P\d+)'
+ _VALID_URL = r'https?://(?:www\.)?tv8\.it/(?:show)?video/(?:[0-9a-z-]+-)?(?P\d+)'
_TESTS = [{
'url': 'https://www.tv8.it/video/ogni-mattina-ucciso-asino-di-andrea-lo-cicero-630529',
'md5': '9ab906a3f75ea342ed928442f9dabd21',
@@ -227,6 +227,19 @@ class TV8ItIE(SkyItVideoIE): # XXX: Do not subclass from concrete IE
'thumbnail': 'https://videoplatform.sky.it/still/2020/11/18/1605717753954_ogni-mattina-ucciso-asino-di-andrea-lo-cicero_videostill_1.jpg',
},
'params': {'skip_download': 'm3u8'},
+ }, {
+ 'url': 'https://www.tv8.it/video/964361',
+ 'md5': '1e58e807154658a16edc29e45be38107',
+ 'info_dict': {
+ 'id': '964361',
+ 'ext': 'mp4',
+ 'title': 'GialappaShow - S.4 Ep.2',
+ 'description': 'md5:60bb4ff5af18bbeeaedabc1de5f9e1e2',
+ 'duration': 8030,
+ 'thumbnail': 'https://videoplatform.sky.it/captures/494/2024/11/06/964361/964361_1730888412914_thumb_494.jpg',
+ 'timestamp': 1730821499,
+ 'upload_date': '20241105',
+ },
}]
_DOMAIN = 'mtv8'
diff --git a/yt_dlp/extractor/soundcloud.py b/yt_dlp/extractor/soundcloud.py
index 3496a08ef..404e29897 100644
--- a/yt_dlp/extractor/soundcloud.py
+++ b/yt_dlp/extractor/soundcloud.py
@@ -242,7 +242,7 @@ class SoundcloudBaseIE(InfoExtractor):
format_urls.add(format_url)
formats.append({
'format_id': 'download',
- 'ext': urlhandle_detect_ext(urlh, default='mp3'),
+ 'ext': urlhandle_detect_ext(urlh),
'filesize': int_or_none(urlh.headers.get('Content-Length')),
'url': format_url,
'quality': 10,
diff --git a/yt_dlp/extractor/sportdeutschland.py b/yt_dlp/extractor/sportdeutschland.py
index 2d6acb876..0b7d90a07 100644
--- a/yt_dlp/extractor/sportdeutschland.py
+++ b/yt_dlp/extractor/sportdeutschland.py
@@ -8,67 +8,86 @@ from ..utils import (
class SportDeutschlandIE(InfoExtractor):
- _VALID_URL = r'https?://sportdeutschland\.tv/(?P(?:[^/]+/)?[^?#/&]+)'
+ _VALID_URL = r'https?://(?:player\.)?sportdeutschland\.tv/(?P(?:[^/?#]+/)?[^?#/&]+)'
_TESTS = [{
- 'url': 'https://sportdeutschland.tv/blauweissbuchholztanzsport/buchholzer-formationswochenende-2023-samstag-1-bundesliga-landesliga',
+ # Single-part video, direct link
+ 'url': 'https://sportdeutschland.tv/rostock-griffins/gfl2-rostock-griffins-vs-elmshorn-fighting-pirates',
+ 'md5': '35c11a19395c938cdd076b93bda54cde',
'info_dict': {
- 'id': '9839a5c7-0dbb-48a8-ab63-3b408adc7b54',
+ 'id': '9f27a97d-1544-4d0b-aa03-48d92d17a03a',
'ext': 'mp4',
- 'title': 'Buchholzer Formationswochenende 2023 - Samstag - 1. Bundesliga / Landesliga',
- 'display_id': 'blauweissbuchholztanzsport/buchholzer-formationswochenende-2023-samstag-1-bundesliga-landesliga',
- 'description': 'md5:a288c794a5ee69e200d8f12982f81a87',
+ 'title': 'GFL2: Rostock Griffins vs. Elmshorn Fighting Pirates',
+ 'display_id': 'rostock-griffins/gfl2-rostock-griffins-vs-elmshorn-fighting-pirates',
+ 'channel': 'Rostock Griffins',
+ 'channel_url': 'https://sportdeutschland.tv/rostock-griffins',
'live_status': 'was_live',
- 'channel': 'Blau-Weiss Buchholz Tanzsport',
- 'channel_url': 'https://sportdeutschland.tv/blauweissbuchholztanzsport',
- 'channel_id': '93ec33c9-48be-43b6-b404-e016b64fdfa3',
- 'duration': 32447,
- 'upload_date': '20230114',
- 'timestamp': 1673733618,
+ 'description': 'md5:60cb00067e55dafa27b0933a43d72862',
+ 'channel_id': '9635f21c-3f67-4584-9ce4-796e9a47276b',
+ 'timestamp': 1749913117,
+ 'upload_date': '20250614',
+ 'duration': 12287.0,
},
}, {
- 'url': 'https://sportdeutschland.tv/deutscherbadmintonverband/bwf-tour-1-runde-feld-1-yonex-gainward-german-open-2022-0',
+ # Single-part video, embedded player link
+ 'url': 'https://player.sportdeutschland.tv/9e9619c4-7d77-43c4-926d-49fb57dc06dc',
'info_dict': {
- 'id': '95c80c52-6b9a-4ae9-9197-984145adfced',
+ 'id': '9f27a97d-1544-4d0b-aa03-48d92d17a03a',
'ext': 'mp4',
- 'title': 'BWF Tour: 1. Runde Feld 1 - YONEX GAINWARD German Open 2022',
- 'display_id': 'deutscherbadmintonverband/bwf-tour-1-runde-feld-1-yonex-gainward-german-open-2022-0',
- 'description': 'md5:2afb5996ceb9ac0b2ac81f563d3a883e',
+ 'title': 'GFL2: Rostock Griffins vs. Elmshorn Fighting Pirates',
+ 'display_id': '9e9619c4-7d77-43c4-926d-49fb57dc06dc',
+ 'channel': 'Rostock Griffins',
+ 'channel_url': 'https://sportdeutschland.tv/rostock-griffins',
'live_status': 'was_live',
- 'channel': 'Deutscher Badminton Verband',
- 'channel_url': 'https://sportdeutschland.tv/deutscherbadmintonverband',
- 'channel_id': '93ca5866-2551-49fc-8424-6db35af58920',
- 'duration': 41097,
- 'upload_date': '20220309',
- 'timestamp': 1646860727.0,
+ 'description': 'md5:60cb00067e55dafa27b0933a43d72862',
+ 'channel_id': '9635f21c-3f67-4584-9ce4-796e9a47276b',
+ 'timestamp': 1749913117,
+ 'upload_date': '20250614',
+ 'duration': 12287.0,
},
+ 'params': {'skip_download': True},
}, {
- 'url': 'https://sportdeutschland.tv/ggcbremen/formationswochenende-latein-2023',
+ # Multi-part video
+ 'url': 'https://sportdeutschland.tv/rhine-ruhr-2025-fisu-world-university-games/volleyball-w-japan-vs-brasilien-halbfinale-2',
'info_dict': {
- 'id': '9889785e-55b0-4d97-a72a-ce9a9f157cce',
- 'title': 'Formationswochenende Latein 2023 - Samstag',
- 'display_id': 'ggcbremen/formationswochenende-latein-2023',
- 'description': 'md5:6e4060d40ff6a8f8eeb471b51a8f08b2',
+ 'id': '9f63d737-2444-4e3a-a1ea-840df73fd481',
+ 'display_id': 'rhine-ruhr-2025-fisu-world-university-games/volleyball-w-japan-vs-brasilien-halbfinale-2',
+ 'title': 'Volleyball w: Japan vs. Braslien - Halbfinale 2',
+ 'description': 'md5:0a17da15e48a687e6019639c3452572b',
+ 'channel': 'Rhine-Ruhr 2025 FISU World University Games',
+ 'channel_id': '9f5216be-a49d-470b-9a30-4fe9df993334',
+ 'channel_url': 'https://sportdeutschland.tv/rhine-ruhr-2025-fisu-world-university-games',
'live_status': 'was_live',
- 'channel': 'Grün-Gold-Club Bremen e.V.',
- 'channel_id': '9888f04e-bb46-4c7f-be47-df960a4167bb',
- 'channel_url': 'https://sportdeutschland.tv/ggcbremen',
},
- 'playlist_count': 3,
+ 'playlist_count': 2,
'playlist': [{
'info_dict': {
- 'id': '988e1fea-9d44-4fab-8c72-3085fb667547',
+ 'id': '9f725a94-d43e-40ff-859d-13da3081bb04',
'ext': 'mp4',
- 'channel_url': 'https://sportdeutschland.tv/ggcbremen',
- 'channel_id': '9888f04e-bb46-4c7f-be47-df960a4167bb',
- 'channel': 'Grün-Gold-Club Bremen e.V.',
- 'duration': 86,
- 'title': 'Formationswochenende Latein 2023 - Samstag Part 1',
- 'upload_date': '20230225',
- 'timestamp': 1677349909,
+ 'title': 'Volleyball w: Japan vs. Braslien - Halbfinale 2 Part 1',
+ 'channel': 'Rhine-Ruhr 2025 FISU World University Games',
+ 'channel_id': '9f5216be-a49d-470b-9a30-4fe9df993334',
+ 'channel_url': 'https://sportdeutschland.tv/rhine-ruhr-2025-fisu-world-university-games',
+ 'duration': 14773.0,
+ 'timestamp': 1753085197,
+ 'upload_date': '20250721',
+ 'live_status': 'was_live',
+ },
+ }, {
+ 'info_dict': {
+ 'id': '9f725a94-370e-4477-89ac-1751098e3217',
+ 'ext': 'mp4',
+ 'title': 'Volleyball w: Japan vs. Braslien - Halbfinale 2 Part 2',
+ 'channel': 'Rhine-Ruhr 2025 FISU World University Games',
+ 'channel_id': '9f5216be-a49d-470b-9a30-4fe9df993334',
+ 'channel_url': 'https://sportdeutschland.tv/rhine-ruhr-2025-fisu-world-university-games',
+ 'duration': 14773.0,
+ 'timestamp': 1753128421,
+ 'upload_date': '20250721',
'live_status': 'was_live',
},
}],
}, {
+ # Livestream
'url': 'https://sportdeutschland.tv/dtb/gymnastik-international-tag-1',
'info_dict': {
'id': '95d71b8a-370a-4b87-ad16-94680da18528',
@@ -87,8 +106,9 @@ class SportDeutschlandIE(InfoExtractor):
def _process_video(self, asset_id, video):
is_live = video['type'] == 'mux_live'
token = self._download_json(
- f'https://api.sportdeutschland.tv/api/frontend/asset-token/{asset_id}',
- video['id'], query={'type': video['type'], 'playback_id': video['src']})['token']
+ f'https://api.sportdeutschland.tv/api/web/personal/asset-token/{asset_id}',
+ video['id'], query={'type': video['type'], 'playback_id': video['src']},
+ headers={'Referer': 'https://sportdeutschland.tv/'})['token']
formats, subtitles = self._extract_m3u8_formats_and_subtitles(
f'https://stream.mux.com/{video["src"]}.m3u8?token={token}', video['id'], live=is_live)
diff --git a/yt_dlp/extractor/sproutvideo.py b/yt_dlp/extractor/sproutvideo.py
index c0923594e..ff9dc7dee 100644
--- a/yt_dlp/extractor/sproutvideo.py
+++ b/yt_dlp/extractor/sproutvideo.py
@@ -41,6 +41,7 @@ class SproutVideoIE(InfoExtractor):
'duration': 703,
'thumbnail': r're:https?://images\.sproutvideo\.com/.+\.jpg',
},
+ 'skip': 'Account Disabled',
}, {
# http formats 'sd' and 'hd' are available
'url': 'https://videos.sproutvideo.com/embed/119cd6bc1a18e6cd98/30751a1761ae5b90',
@@ -100,8 +101,15 @@ class SproutVideoIE(InfoExtractor):
webpage = self._download_webpage(
url, video_id, headers=traverse_obj(smuggled_data, {'Referer': 'referer'}))
data = self._search_json(
- r'var\s+dat\s*=\s*["\']', webpage, 'data', video_id, contains_pattern=r'[A-Za-z0-9+/=]+',
- end_pattern=r'["\'];', transform_source=lambda x: base64.b64decode(x).decode())
+ r'(?:var|const|let)\s+(?:dat|(?:player|video)Info|)\s*=\s*["\']', webpage, 'player info',
+ video_id, contains_pattern=r'[A-Za-z0-9+/=]+', end_pattern=r'["\'];',
+ transform_source=lambda x: base64.b64decode(x).decode())
+
+ # SproutVideo may send player info for 'SMPTE Color Monitor Test' [a791d7b71b12ecc52e]
+ # e.g. if the user-agent we used with the webpage request is too old
+ video_uid = data['videoUid']
+ if video_id != video_uid:
+ raise ExtractorError(f'{self.IE_NAME} sent the wrong video data ({video_uid})')
formats, subtitles = [], {}
headers = {
diff --git a/yt_dlp/extractor/tbs.py b/yt_dlp/extractor/tbs.py
index 80534731e..f8891671f 100644
--- a/yt_dlp/extractor/tbs.py
+++ b/yt_dlp/extractor/tbs.py
@@ -5,45 +5,110 @@ from .turner import TurnerBaseIE
from ..utils import (
float_or_none,
int_or_none,
+ make_archive_id,
strip_or_none,
)
+from ..utils.traversal import traverse_obj
class TBSIE(TurnerBaseIE):
- _VALID_URL = r'https?://(?:www\.)?(?Ptbs|tntdrama)\.com(?P/(?:movies|watchtnt|watchtbs|shows/[^/]+/(?:clips|season-\d+/episode-\d+))/(?P[^/?#]+))'
+ _SITE_INFO = {
+ 'tbs': ('TBS', 'eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJkZTA0NTYxZS1iMTFhLTRlYTgtYTg5NC01NjI3MGM1NmM2MWIiLCJuYmYiOjE1MzcxODkzOTAsImlzcyI6ImF1dGguYWRvYmUuY29tIiwiaWF0IjoxNTM3MTg5MzkwfQ.Z7ny66kaqNDdCHf9Y9KsV12LrBxrLkGGxlYe2XGm6qsw2T-k1OCKC1TMzeqiZP735292MMRAQkcJDKrMIzNbAuf9nCdIcv4kE1E2nqUnjPMBduC1bHffZp8zlllyrN2ElDwM8Vhwv_5nElLRwWGEt0Kaq6KJAMZA__WDxKWC18T-wVtsOZWXQpDqO7nByhfj2t-Z8c3TUNVsA_wHgNXlkzJCZ16F2b7yGLT5ZhLPupOScd3MXC5iPh19HSVIok22h8_F_noTmGzmMnIRQi6bWYWK2zC7TQ_MsYHfv7V6EaG5m1RKZTV6JAwwoJQF_9ByzarLV1DGwZxD9-eQdqswvg'),
+ 'tntdrama': ('TNT', 'eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIwOTMxYTU4OS1jZjEzLTRmNjMtYTJmYy03MzhjMjE1NWU5NjEiLCJuYmYiOjE1MzcxOTA4MjcsImlzcyI6ImF1dGguYWRvYmUuY29tIiwiaWF0IjoxNTM3MTkwODI3fQ.AucKvtws7oekTXi80_zX4-BlgJD9GLvlOI9FlBCjdlx7Pa3eJ0AqbogynKMiatMbnLOTMHGjd7tTiq422unmZjBz70dhePAe9BbW0dIo7oQ57vZ-VBYw_tWYRPmON61MwAbLVlqROD3n_zURs85S8TlkQx9aNx9x_riGGELjd8l05CVa_pOluNhYvuIFn6wmrASOKI1hNEblBDWh468UWP571-fe4zzi0rlYeeHd-cjvtWvOB3bQsWrUVbK4pRmqvzEH59j0vNF-ihJF9HncmUicYONe47Mib3elfMok23v4dB1_UAlQY_oawfNcynmEnJQCcqFmbHdEwTW6gMiYsA'),
+ 'trutv': ('truTV', 'eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhYzQyOTkwMi0xMDYzLTQyNTQtYWJlYS1iZTY2ODM4MTVmZGIiLCJuYmYiOjE1MzcxOTA4NjgsImlzcyI6ImF1dGguYWRvYmUuY29tIiwiaWF0IjoxNTM3MTkwODY4fQ.ewXl5LDMDvvx3nDXV4jCdSwUq_sOluKoOVsIjznAo6Zo4zrGe9rjlZ9DOmQKW66g6VRMexJsJ5vM1EkY8TC5-YcQw_BclK1FPGO1rH3Wf7tX_l0b1BVbSJQKIj9UgqDp_QbGcBXz24kN4So3U22mhs6di9PYyyfG68ccKL2iRprcVKWCslIHwUF-T7FaEqb0K57auilxeW1PONG2m-lIAcZ62DUwqXDWvw0CRoWI08aVVqkkhnXaSsQfLs5Ph1Pfh9Oq3g_epUm9Ss45mq6XM7gbOb5omTcKLADRKK-PJVB_JXnZnlsXbG0ttKE1cTKJ738qu7j4aipYTf-W0nKF5Q'),
+ }
+ _VALID_URL = fr'''(?x)
+ https?://(?:www\.)?(?P{"|".join(map(re.escape, _SITE_INFO))})\.com
+ (?P/(?:
+ (?Pwatch(?:tnt|tbs|trutv))|
+ movies|shows/[^/?#]+/(?:clips|season-\d+/episode-\d+)
+ )/(?P[^/?#]+))
+ '''
_TESTS = [{
- 'url': 'http://www.tntdrama.com/shows/the-alienist/clips/monster',
+ 'url': 'https://www.tbs.com/shows/american-dad/season-6/episode-12/you-debt-your-life',
'info_dict': {
- 'id': '8d384cde33b89f3a43ce5329de42903ed5099887',
+ 'id': '984bdcd8db0cc00dc699927f2a411c8c6e0e48f3',
'ext': 'mp4',
- 'title': 'Monster',
- 'description': 'Get a first look at the theatrical trailer for TNT’s highly anticipated new psychological thriller The Alienist, which premieres January 22 on TNT.',
- 'timestamp': 1508175329,
- 'upload_date': '20171016',
+ 'title': 'You Debt Your Life',
+ 'description': 'md5:f211cfeb9187fd3cdb53eb0e8930d499',
+ 'duration': 1231.0,
+ 'thumbnail': r're:https://images\.tbs\.com/tbs/.+\.(?:jpe?g|png)',
+ 'chapters': 'count:4',
+ 'season': 'Season 6',
+ 'season_number': 6,
+ 'episode': 'Episode 12',
+ 'episode_number': 12,
+ 'timestamp': 1478276239,
+ 'upload_date': '20161104',
},
- 'params': {
- # m3u8 download
- 'skip_download': True,
+ 'params': {'skip_download': 'm3u8'},
+ }, {
+ 'url': 'https://www.tntdrama.com/shows/the-librarians-the-next-chapter/season-1/episode-10/and-going-medieval',
+ 'info_dict': {
+ 'id': 'e487b31b663a8001864f62fd20907782f7b8ccb8',
+ 'ext': 'mp4',
+ 'title': 'And Going Medieval',
+ 'description': 'md5:5aed0ae23a6cf148a02fe3c1be8359fa',
+ 'duration': 2528.0,
+ 'thumbnail': r're:https://images\.tntdrama\.com/tnt/.+\.(?:jpe?g|png)',
+ 'chapters': 'count:7',
+ 'season': 'Season 1',
+ 'season_number': 1,
+ 'episode': 'Episode 10',
+ 'episode_number': 10,
+ 'timestamp': 1743107520,
+ 'upload_date': '20250327',
},
+ 'params': {'skip_download': 'm3u8'},
+ }, {
+ 'url': 'https://www.trutv.com/shows/the-carbonaro-effect/season-1/episode-1/got-the-bug-out',
+ 'info_dict': {
+ 'id': 'b457dd7458fd9e64b596355950b13a1ca799dc39',
+ 'ext': 'mp4',
+ 'title': 'Got the Bug Out',
+ 'description': 'md5:9eeddf6248f73517b0e5969b8a43c025',
+ 'duration': 1283.0,
+ 'thumbnail': r're:https://images\.trutv\.com/tru/.+\.(?:jpe?g|png)',
+ 'chapters': 'count:4',
+ 'season': 'Season 1',
+ 'season_number': 1,
+ 'episode': 'Episode 1',
+ 'episode_number': 1,
+ 'timestamp': 1570040829,
+ 'upload_date': '20191002',
+ '_old_archive_ids': ['trutv b457dd7458fd9e64b596355950b13a1ca799dc39'],
+ },
+ 'params': {'skip_download': 'm3u8'},
+ }, {
+ 'url': 'http://www.tntdrama.com/shows/the-alienist/clips/monster',
+ 'only_matching': True,
}, {
'url': 'http://www.tbs.com/shows/search-party/season-1/episode-1/explicit-the-mysterious-disappearance-of-the-girl-no-one-knew',
'only_matching': True,
}, {
'url': 'http://www.tntdrama.com/movies/star-wars-a-new-hope',
'only_matching': True,
+ }, {
+ 'url': 'https://www.trutv.com/shows/impractical-jokers/season-9/episode-1/you-dirty-dog',
+ 'only_matching': True,
+ }, {
+ 'url': 'https://www.trutv.com/watchtrutv/east',
+ 'only_matching': True,
+ }, {
+ 'url': 'https://www.tbs.com/watchtbs/east',
+ 'only_matching': True,
+ }, {
+ 'url': 'https://www.tntdrama.com/watchtnt/east',
+ 'only_matching': True,
}]
- _SOFTWARE_STATEMENT_MAP = {
- 'tbs': 'eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJkZTA0NTYxZS1iMTFhLTRlYTgtYTg5NC01NjI3MGM1NmM2MWIiLCJuYmYiOjE1MzcxODkzOTAsImlzcyI6ImF1dGguYWRvYmUuY29tIiwiaWF0IjoxNTM3MTg5MzkwfQ.Z7ny66kaqNDdCHf9Y9KsV12LrBxrLkGGxlYe2XGm6qsw2T-k1OCKC1TMzeqiZP735292MMRAQkcJDKrMIzNbAuf9nCdIcv4kE1E2nqUnjPMBduC1bHffZp8zlllyrN2ElDwM8Vhwv_5nElLRwWGEt0Kaq6KJAMZA__WDxKWC18T-wVtsOZWXQpDqO7nByhfj2t-Z8c3TUNVsA_wHgNXlkzJCZ16F2b7yGLT5ZhLPupOScd3MXC5iPh19HSVIok22h8_F_noTmGzmMnIRQi6bWYWK2zC7TQ_MsYHfv7V6EaG5m1RKZTV6JAwwoJQF_9ByzarLV1DGwZxD9-eQdqswvg',
- 'tntdrama': 'eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIwOTMxYTU4OS1jZjEzLTRmNjMtYTJmYy03MzhjMjE1NWU5NjEiLCJuYmYiOjE1MzcxOTA4MjcsImlzcyI6ImF1dGguYWRvYmUuY29tIiwiaWF0IjoxNTM3MTkwODI3fQ.AucKvtws7oekTXi80_zX4-BlgJD9GLvlOI9FlBCjdlx7Pa3eJ0AqbogynKMiatMbnLOTMHGjd7tTiq422unmZjBz70dhePAe9BbW0dIo7oQ57vZ-VBYw_tWYRPmON61MwAbLVlqROD3n_zURs85S8TlkQx9aNx9x_riGGELjd8l05CVa_pOluNhYvuIFn6wmrASOKI1hNEblBDWh468UWP571-fe4zzi0rlYeeHd-cjvtWvOB3bQsWrUVbK4pRmqvzEH59j0vNF-ihJF9HncmUicYONe47Mib3elfMok23v4dB1_UAlQY_oawfNcynmEnJQCcqFmbHdEwTW6gMiYsA',
- }
def _real_extract(self, url):
- site, path, display_id = self._match_valid_url(url).groups()
+ site, path, display_id, watch = self._match_valid_url(url).group('site', 'path', 'id', 'watch')
+ is_live = bool(watch)
webpage = self._download_webpage(url, display_id)
- drupal_settings = self._parse_json(self._search_regex(
- r'',
- webpage, 'drupal setting'), display_id)
- is_live = 'watchtnt' in path or 'watchtbs' in path
+ drupal_settings = self._search_json(
+ r' |