[ie/mtv] Overhaul extractors

Authored by: bashonly
pull/14052/head
bashonly 1 day ago
parent 404bd889d0
commit 9d9d9c55be
No known key found for this signature in database
GPG Key ID: 783F096F253D15B0

@ -14,7 +14,6 @@ from yt_dlp.extractor import (
NRKTVIE, NRKTVIE,
PBSIE, PBSIE,
CeskaTelevizeIE, CeskaTelevizeIE,
ComedyCentralIE,
DailymotionIE, DailymotionIE,
DemocracynowIE, DemocracynowIE,
LyndaIE, LyndaIE,
@ -279,23 +278,6 @@ class TestNPOSubtitles(BaseTestSubtitles):
self.assertEqual(md5(subtitles['nl']), 'fc6435027572b63fb4ab143abd5ad3f4') self.assertEqual(md5(subtitles['nl']), 'fc6435027572b63fb4ab143abd5ad3f4')
@is_download_test
@unittest.skip('IE broken')
class TestMTVSubtitles(BaseTestSubtitles):
url = 'http://www.cc.com/video-clips/p63lk0/adam-devine-s-house-party-chasing-white-swans'
IE = ComedyCentralIE
def getInfoDict(self):
return super().getInfoDict()['entries'][0]
def test_allsubtitles(self):
self.DL.params['writesubtitles'] = True
self.DL.params['allsubtitles'] = True
subtitles = self.getSubtitles()
self.assertEqual(set(subtitles.keys()), {'en'})
self.assertEqual(md5(subtitles['en']), '78206b8d8a0cfa9da64dc026eea48961')
@is_download_test @is_download_test
class TestNRKSubtitles(BaseTestSubtitles): class TestNRKSubtitles(BaseTestSubtitles):
url = 'http://tv.nrk.no/serie/ikke-gjoer-dette-hjemme/DMPV73000411/sesong-2/episode-1' url = 'http://tv.nrk.no/serie/ikke-gjoer-dette-hjemme/DMPV73000411/sesong-2/episode-1'

@ -411,10 +411,7 @@ from .cnn import (
CNNIE, CNNIE,
CNNIndonesiaIE, CNNIndonesiaIE,
) )
from .comedycentral import ( from .comedycentral import ComedyCentralIE
ComedyCentralIE,
ComedyCentralTVIE,
)
from .commonmistakes import ( from .commonmistakes import (
BlobIE, BlobIE,
CommonMistakesIE, CommonMistakesIE,
@ -1187,15 +1184,7 @@ from .moview import MoviewPlayIE
from .moviezine import MoviezineIE from .moviezine import MoviezineIE
from .movingimage import MovingImageIE from .movingimage import MovingImageIE
from .msn import MSNIE from .msn import MSNIE
from .mtv import ( from .mtv import MTVIE
MTVDEIE,
MTVIE,
MTVItaliaIE,
MTVItaliaProgrammaIE,
MTVJapanIE,
MTVServicesEmbeddedIE,
MTVVideoIE,
)
from .muenchentv import MuenchenTVIE from .muenchentv import MuenchenTVIE
from .murrtube import ( from .murrtube import (
MurrtubeIE, MurrtubeIE,

@ -1,79 +1,47 @@
from .mtv import MTVServicesInfoExtractor from .mtv import MTVServicesBaseIE
from ..utils import unified_strdate
class BetIE(MTVServicesBaseIE):
class BetIE(MTVServicesInfoExtractor): _VALID_URL = r'https?://(?:www\.)?bet\.com/(?:video-clips|episodes)/(?P<id>[\da-z]{6})'
_WORKING = False _TESTS = [{
_VALID_URL = r'https?://(?:www\.)?bet\.com/(?:[^/]+/)+(?P<id>.+?)\.html' 'url': 'https://www.bet.com/video-clips/w9mk7v',
_TESTS = [ 'info_dict': {
{ 'id': '3022d121-d191-43fd-b5fb-b2c26f335497',
'url': 'http://www.bet.com/news/politics/2014/12/08/in-bet-exclusive-obama-talks-race-and-racism.html', 'ext': 'mp4',
'info_dict': { 'display_id': 'w9mk7v',
'id': '07e96bd3-8850-3051-b856-271b457f0ab8', 'title': 'New Normal',
'display_id': 'in-bet-exclusive-obama-talks-race-and-racism', 'description': 'md5:d7898c124713b4646cecad9d16ff01f3',
'ext': 'flv', 'duration': 30.08,
'title': 'A Conversation With President Obama', 'series': 'Tyler Perry\'s Sistas',
'description': 'President Obama urges persistence in confronting racism and bias.', 'season': 'Season 0',
'duration': 1534, 'season_number': 0,
'upload_date': '20141208', 'episode': 'Episode 0',
'thumbnail': r're:(?i)^https?://.*\.jpg$', 'episode_number': 0,
'subtitles': { 'timestamp': 1755269073,
'en': 'mincount:2', 'upload_date': '20250815',
},
},
'params': {
# rtmp download
'skip_download': True,
},
}, },
{ 'params': {'skip_download': 'm3u8'},
'url': 'http://www.bet.com/video/news/national/2014/justice-for-ferguson-a-community-reacts.html', }, {
'info_dict': { 'url': 'https://www.bet.com/episodes/nmce72/tyler-perry-s-sistas-heavy-is-the-crown-season-9-ep-5',
'id': '9f516bf1-7543-39c4-8076-dd441b459ba9', 'info_dict': {
'display_id': 'justice-for-ferguson-a-community-reacts', 'id': '6427562b-3029-11f0-b405-16fff45bc035',
'ext': 'flv', 'ext': 'mp4',
'title': 'Justice for Ferguson: A Community Reacts', 'display_id': 'nmce72',
'description': 'A BET News special.', 'title': 'Heavy Is the Crown',
'duration': 1696, 'description': 'md5:1ed345d3157a50572d2464afcc7a652a',
'upload_date': '20141125', 'channel': 'BET',
'thumbnail': r're:(?i)^https?://.*\.jpg$', 'duration': 2550.0,
'subtitles': { 'thumbnail': r're:https://images\.paramount\.tech/uri/mgid:arc:imageassetref',
'en': 'mincount:2', 'series': 'Tyler Perry\'s Sistas',
}, 'season': 'Season 9',
}, 'season_number': 9,
'params': { 'episode': 'Episode 5',
# rtmp download 'episode_number': 5,
'skip_download': True, 'timestamp': 1755165600,
}, 'upload_date': '20250814',
'release_timestamp': 1755129600,
'release_date': '20250814',
}, },
] 'params': {'skip_download': 'm3u8'},
'skip': 'Requires provider sign-in',
_FEED_URL = 'http://feeds.mtvnservices.com/od/feed/bet-mrss-player' }]
def _get_feed_query(self, uri):
return {
'uuid': uri,
}
def _extract_mgid(self, webpage):
return self._search_regex(r'data-uri="([^"]+)', webpage, 'mgid')
def _real_extract(self, url):
display_id = self._match_id(url)
webpage = self._download_webpage(url, display_id)
mgid = self._extract_mgid(webpage)
videos_info = self._get_videos_info(mgid)
info_dict = videos_info['entries'][0]
upload_date = unified_strdate(self._html_search_meta('date', webpage))
description = self._html_search_meta('description', webpage)
info_dict.update({
'display_id': display_id,
'description': description,
'upload_date': upload_date,
})
return info_dict

@ -1,55 +1,27 @@
from .mtv import MTVServicesInfoExtractor from .mtv import MTVServicesBaseIE
class ComedyCentralIE(MTVServicesInfoExtractor): class ComedyCentralIE(MTVServicesBaseIE):
_VALID_URL = r'https?://(?:www\.)?cc\.com/(?:episodes|video(?:-clips)?|collection-playlist|movies)/(?P<id>[0-9a-z]{6})' _VALID_URL = r'https?://(?:www\.)?cc\.com/video-clips/(?P<id>[\da-z]{6})'
_FEED_URL = 'http://comedycentral.com/feeds/mrss/'
_TESTS = [{ _TESTS = [{
'url': 'http://www.cc.com/video-clips/5ke9v2/the-daily-show-with-trevor-noah-doc-rivers-and-steve-ballmer---the-nba-player-strike', 'url': 'https://www.cc.com/video-clips/wl12cx',
'md5': 'b8acb347177c680ff18a292aa2166f80',
'info_dict': { 'info_dict': {
'id': '89ccc86e-1b02-4f83-b0c9-1d9592ecd025', 'id': 'dec6953e-80c8-43b3-96cd-05e9230e704d',
'ext': 'mp4', 'ext': 'mp4',
'title': 'The Daily Show with Trevor Noah|August 28, 2020|25|25149|Doc Rivers and Steve Ballmer - The NBA Player Strike', 'display_id': 'wl12cx',
'description': 'md5:5334307c433892b85f4f5e5ac9ef7498', 'title': 'Alison Brie and Dave Franco -"Together"- Extended Interview',
'timestamp': 1598670000, 'description': 'md5:ec68e38d3282f863de9cde0ce5cd231c',
'upload_date': '20200829', 'duration': 516.76,
'thumbnail': r're:https://images\.paramount\.tech/uri/mgid:arc:imageassetref:',
'series': 'The Daily Show',
'season': 'Season 30',
'season_number': 30,
'episode': 'Episode 0',
'episode_number': 0,
'timestamp': 1753973314,
'upload_date': '20250731',
'release_timestamp': 1753977914,
'release_date': '20250731',
}, },
}, { 'params': {'skip_download': 'm3u8'},
'url': 'http://www.cc.com/episodes/pnzzci/drawn-together--american-idol--parody-clip-show-season-3-ep-314',
'only_matching': True,
}, {
'url': 'https://www.cc.com/video/k3sdvm/the-daily-show-with-jon-stewart-exclusive-the-fourth-estate',
'only_matching': True,
}, {
'url': 'https://www.cc.com/collection-playlist/cosnej/stand-up-specials/t6vtjb',
'only_matching': True,
}, {
'url': 'https://www.cc.com/movies/tkp406/a-cluesterfuenke-christmas',
'only_matching': True,
}] }]
class ComedyCentralTVIE(MTVServicesInfoExtractor):
_VALID_URL = r'https?://(?:www\.)?comedycentral\.tv/folgen/(?P<id>[0-9a-z]{6})'
_TESTS = [{
'url': 'https://www.comedycentral.tv/folgen/pxdpec/josh-investigates-klimawandel-staffel-1-ep-1',
'info_dict': {
'id': '15907dc3-ec3c-11e8-a442-0e40cf2fc285',
'ext': 'mp4',
'title': 'Josh Investigates',
'description': 'Steht uns das Ende der Welt bevor?',
},
}]
_FEED_URL = 'http://feeds.mtvnservices.com/od/feed/intl-mrss-player-feed'
_GEO_COUNTRIES = ['DE']
def _get_feed_query(self, uri):
return {
'accountOverride': 'intl.mtvi.com',
'arcEp': 'web.cc.tv',
'ep': 'b9032c3a',
'imageEp': 'web.cc.tv',
'mgid': uri,
}

@ -1,652 +1,251 @@
import re import base64
import xml.etree.ElementTree import json
import time
import urllib.parse
from .common import InfoExtractor from .common import InfoExtractor
from ..networking import HEADRequest, Request
from ..utils import ( from ..utils import (
ExtractorError, ExtractorError,
RegexNotFoundError,
find_xpath_attr,
fix_xml_ampersands,
float_or_none, float_or_none,
int_or_none, int_or_none,
join_nonempty, js_to_json,
strip_or_none, jwt_decode_hs256,
timeconvert, parse_iso8601,
try_get, parse_qs,
unescapeHTML, update_url,
update_url_query, update_url_query,
url_basename, url_or_none,
xpath_text,
) )
from ..utils.traversal import require, traverse_obj
def _media_xml_tag(tag): class MTVServicesBaseIE(InfoExtractor):
return f'{{http://search.yahoo.com/mrss/}}{tag}' _CACHE_SECTION = 'mtvservices'
_ACCESS_TOKEN_KEY = 'access'
_REFRESH_TOKEN_KEY = 'refresh'
class MTVServicesInfoExtractor(InfoExtractor): _MEDIA_TOKEN_KEY = 'media'
_MOBILE_TEMPLATE = None _token_cache = {}
_LANG = None
@staticmethod @staticmethod
def _id_from_uri(uri): def _jwt_is_expired(token):
return uri.split(':')[-1] return jwt_decode_hs256(token)['exp'] - time.time() < 120
@staticmethod @staticmethod
def _remove_template_parameter(url): def _get_auth_suite_data(config):
# Remove the templates, like &device={device} return traverse_obj(config, {
return re.sub(r'&[^=]*?={.*?}(?=(&|$))', '', url) 'clientId': ('clientId', {str}),
'countryCode': ('countryCode', {str}),
def _get_feed_url(self, uri, url=None): })
return self._FEED_URL
def _call_auth_api(self, path, config, display_id=None, note=None, data=None, headers=None, query=None):
def _get_thumbnail_url(self, uri, itemdoc): headers = {
search_path = '{}/{}'.format(_media_xml_tag('group'), _media_xml_tag('thumbnail')) 'Accept': 'application/json',
thumb_node = itemdoc.find(search_path) 'Client-Description': 'deviceName=Chrome Windows;deviceType=desktop;system=Windows NT 10.0',
if thumb_node is None: 'Api-Version': '2025-07-09',
return None **(headers or {}),
return thumb_node.get('url') or thumb_node.text or None
def _extract_mobile_video_formats(self, mtvn_id):
webpage_url = self._MOBILE_TEMPLATE % mtvn_id
req = Request(webpage_url)
# Otherwise we get a webpage that would execute some javascript
req.headers['User-Agent'] = 'curl/7'
webpage = self._download_webpage(req, mtvn_id,
'Downloading mobile page')
metrics_url = unescapeHTML(self._search_regex(r'<a href="(http://metrics.+?)"', webpage, 'url'))
req = HEADRequest(metrics_url)
response = self._request_webpage(req, mtvn_id, 'Resolving url')
url = response.url
# Transform the url to get the best quality:
url = re.sub(r'.+pxE=mp4', 'http://mtvnmobile.vo.llnwd.net/kip0/_pxn=0+_pxK=18639+_pxE=mp4', url, count=1)
return [{'url': url, 'ext': 'mp4'}]
def _extract_video_formats(self, mdoc, mtvn_id, video_id):
if re.match(r'.*/(error_country_block\.swf|geoblock\.mp4|copyright_error\.flv(?:\?geo\b.+?)?)$', mdoc.find('.//src').text) is not None:
if mtvn_id is not None and self._MOBILE_TEMPLATE is not None:
self.to_screen('The normal version is not available from your '
'country, trying with the mobile version')
return self._extract_mobile_video_formats(mtvn_id)
raise ExtractorError('This video is not available from your country.',
expected=True)
formats = []
for rendition in mdoc.findall('.//rendition'):
if rendition.get('method') == 'hls':
hls_url = rendition.find('./src').text
formats.extend(self._extract_m3u8_formats(
hls_url, video_id, ext='mp4', entry_protocol='m3u8_native',
m3u8_id='hls', fatal=False))
else:
# fms
try:
_, _, ext = rendition.attrib['type'].partition('/')
rtmp_video_url = rendition.find('./src').text
if 'error_not_available.swf' in rtmp_video_url:
raise ExtractorError(
f'{self.IE_NAME} said: video is not available',
expected=True)
if rtmp_video_url.endswith('siteunavail.png'):
continue
formats.extend([{
'ext': 'flv' if rtmp_video_url.startswith('rtmp') else ext,
'url': rtmp_video_url,
'format_id': join_nonempty(
'rtmp' if rtmp_video_url.startswith('rtmp') else None,
rendition.get('bitrate')),
'width': int(rendition.get('width')),
'height': int(rendition.get('height')),
}])
except (KeyError, TypeError):
raise ExtractorError('Invalid rendition field.')
return formats
def _extract_subtitles(self, mdoc, mtvn_id):
subtitles = {}
for transcript in mdoc.findall('.//transcript'):
if transcript.get('kind') != 'captions':
continue
lang = transcript.get('srclang')
for typographic in transcript.findall('./typographic'):
sub_src = typographic.get('src')
if not sub_src:
continue
ext = typographic.get('format')
if ext == 'cea-608':
ext = 'scc'
subtitles.setdefault(lang, []).append({
'url': str(sub_src),
'ext': ext,
})
return subtitles
def _get_video_info(self, itemdoc, use_hls=True):
uri = itemdoc.find('guid').text
video_id = self._id_from_uri(uri)
self.report_extraction(video_id)
content_el = itemdoc.find('{}/{}'.format(_media_xml_tag('group'), _media_xml_tag('content')))
mediagen_url = self._remove_template_parameter(content_el.attrib['url'])
mediagen_url = mediagen_url.replace('device={device}', '')
if 'acceptMethods' not in mediagen_url:
mediagen_url += '&' if '?' in mediagen_url else '?'
mediagen_url += 'acceptMethods='
mediagen_url += 'hls' if use_hls else 'fms'
mediagen_doc = self._download_xml(
mediagen_url, video_id, 'Downloading video urls', fatal=False)
if not isinstance(mediagen_doc, xml.etree.ElementTree.Element):
return None
item = mediagen_doc.find('./video/item')
if item is not None and item.get('type') == 'text':
message = f'{self.IE_NAME} returned error: '
if item.get('code') is not None:
message += '{} - '.format(item.get('code'))
message += item.text
raise ExtractorError(message, expected=True)
description = strip_or_none(xpath_text(itemdoc, 'description'))
timestamp = timeconvert(xpath_text(itemdoc, 'pubDate'))
title_el = None
if title_el is None:
title_el = find_xpath_attr(
itemdoc, './/{http://search.yahoo.com/mrss/}category',
'scheme', 'urn:mtvn:video_title')
if title_el is None:
title_el = itemdoc.find('.//{http://search.yahoo.com/mrss/}title')
if title_el is None:
title_el = itemdoc.find('.//title')
if title_el.text is None:
title_el = None
title = title_el.text
if title is None:
raise ExtractorError('Could not find video title')
title = title.strip()
series = find_xpath_attr(
itemdoc, './/{http://search.yahoo.com/mrss/}category',
'scheme', 'urn:mtvn:franchise')
season = find_xpath_attr(
itemdoc, './/{http://search.yahoo.com/mrss/}category',
'scheme', 'urn:mtvn:seasonN')
episode = find_xpath_attr(
itemdoc, './/{http://search.yahoo.com/mrss/}category',
'scheme', 'urn:mtvn:episodeN')
series = series.text if series is not None else None
season = season.text if season is not None else None
episode = episode.text if episode is not None else None
if season and episode:
# episode number includes season, so remove it
episode = re.sub(rf'^{season}', '', episode)
# This a short id that's used in the webpage urls
mtvn_id = None
mtvn_id_node = find_xpath_attr(itemdoc, './/{http://search.yahoo.com/mrss/}category',
'scheme', 'urn:mtvn:id')
if mtvn_id_node is not None:
mtvn_id = mtvn_id_node.text
formats = self._extract_video_formats(mediagen_doc, mtvn_id, video_id)
# Some parts of complete video may be missing (e.g. missing Act 3 in
# http://www.southpark.de/alle-episoden/s14e01-sexual-healing)
if not formats:
return None
return {
'title': title,
'formats': formats,
'subtitles': self._extract_subtitles(mediagen_doc, mtvn_id),
'id': video_id,
'thumbnail': self._get_thumbnail_url(uri, itemdoc),
'description': description,
'duration': float_or_none(content_el.attrib.get('duration')),
'timestamp': timestamp,
'series': series,
'season_number': int_or_none(season),
'episode_number': int_or_none(episode),
} }
if data is not None:
def _get_feed_query(self, uri): headers['Content-Type'] = 'application/json'
data = {'uri': uri} if isinstance(data, dict):
if self._LANG: data = json.dumps(data, separators=(',', ':')).encode()
data['lang'] = self._LANG
return data return self._download_json(
f'https://auth.mtvnservices.com/{path}', display_id,
def _get_videos_info(self, uri, use_hls=True, url=None): note=note or 'Calling authentication API', data=data,
video_id = self._id_from_uri(uri) headers=headers, query={**self._get_auth_suite_data(config), **(query or {})})
feed_url = self._get_feed_url(uri, url)
info_url = update_url_query(feed_url, self._get_feed_query(uri)) def _get_fresh_access_token(self, config, display_id=None, force_refresh=False):
return self._get_videos_info_from_url(info_url, video_id, use_hls) resource_id = config['resourceId']
# resource_id should already be in _token_cache since _get_media_token is the caller
def _get_videos_info_from_url(self, url, video_id, use_hls=True): tokens = self._token_cache[resource_id]
idoc = self._download_xml(
url, video_id, access_token = tokens.get(self._ACCESS_TOKEN_KEY)
'Downloading info', transform_source=fix_xml_ampersands) if not force_refresh and access_token and not self._jwt_is_expired(access_token):
return access_token
title = xpath_text(idoc, './channel/title')
description = xpath_text(idoc, './channel/description') if self._REFRESH_TOKEN_KEY not in tokens:
response = self._call_auth_api(
entries = [] 'accessToken', config, display_id, 'Retrieving auth tokens', data=b'')
for item in idoc.findall('.//item'): else:
info = self._get_video_info(item, use_hls) response = self._call_auth_api(
if info: 'accessToken/refresh', config, display_id, 'Refreshing auth tokens',
entries.append(info) data={'refreshToken': tokens[self._REFRESH_TOKEN_KEY]},
headers={'Authorization': f'Bearer {access_token}'})
# TODO: should be multi-video
return self.playlist_result( tokens[self._ACCESS_TOKEN_KEY] = response['applicationAccessToken']
entries, playlist_title=title, playlist_description=description) tokens[self._REFRESH_TOKEN_KEY] = response['deviceRefreshToken']
self.cache.store(self._CACHE_SECTION, resource_id, tokens)
def _extract_triforce_mgid(self, webpage, data_zone=None, video_id=None):
triforce_feed = self._parse_json(self._search_regex( return tokens[self._ACCESS_TOKEN_KEY]
r'triforceManifestFeed\s*=\s*({.+?})\s*;\s*\n', webpage,
'triforce feed', default='{}'), video_id, fatal=False) def _get_media_token(self, video_config, config, display_id=None):
resource_id = config['resourceId']
data_zone = self._search_regex( if resource_id in self._token_cache:
r'data-zone=(["\'])(?P<zone>.+?_lc_promo.*?)\1', webpage, tokens = self._token_cache[resource_id]
'data zone', default=data_zone, group='zone') else:
tokens = self._token_cache[resource_id] = self.cache.load(self._CACHE_SECTION, resource_id) or {}
feed_url = try_get(
triforce_feed, lambda x: x['manifest']['zones'][data_zone]['feed'], media_token = tokens.get(self._MEDIA_TOKEN_KEY)
str) if media_token and not self._jwt_is_expired(media_token):
if not feed_url: return media_token
return
access_token = self._get_fresh_access_token(config, display_id)
feed = self._download_json(feed_url, video_id, fatal=False) if not jwt_decode_hs256(access_token).get('accessMethods'):
if not feed: mso_id = self.get_param('ap_mso')
return if not mso_id:
raise ExtractorError(
return try_get(feed, lambda x: x['result']['data']['id'], str) 'This video is only available for users of participating TV providers. '
'Use --ap-mso to specify Adobe Pass Multiple-system operator Identifier and pass '
@staticmethod 'cookies from a browser session where you are signed-in to your provider.', expected=True)
def _extract_child_with_type(parent, t):
for c in parent['children']: auth_suite_data = json.dumps(
if c.get('type') == t: self._get_auth_suite_data(config), separators=(',', ':')).encode()
return c callback_url = update_url_query(config['callbackURL'], {
'authSuiteData': urllib.parse.quote(base64.b64encode(auth_suite_data).decode()),
def _extract_mgid(self, webpage): 'mvpdCode': mso_id,
try: })
# the url can be http://media.mtvnservices.com/fb/{mgid}.swf auth_url = self._call_auth_api(
# or http://media.mtvnservices.com/{mgid} f'mvpd/{mso_id}/login', config, display_id,
og_url = self._og_search_video_url(webpage) 'Retrieving provider authentication URL',
mgid = url_basename(og_url) query={'callbackUrl': callback_url},
if mgid.endswith('.swf'): headers={'Authorization': f'Bearer {access_token}'})['authenticationUrl']
mgid = mgid[:-4] res = self._download_webpage_handle(auth_url, display_id, 'Downloading provider auth page')
except RegexNotFoundError: # BEGIN provider-specific code
mgid = None redirect_url = self._search_json(
r'initInterstitialRedirect\(', res[0], 'redirect JSON',
if mgid is None or ':' not in mgid: display_id, transform_source=js_to_json)['continue']
mgid = self._search_regex( urlh = self._request_webpage(redirect_url, display_id, 'Requesting provider redirect page')
[r'data-mgid="(.*?)"', r'swfobject\.embedSWF\(".*?(mgid:.*?)"'], authorization_code = parse_qs(urlh.url)['authorizationCode'][-1]
webpage, 'mgid', default=None) # END provider-specific code
self._call_auth_api(
if not mgid: f'access/mvpd/{mso_id}', config, display_id,
sm4_embed = self._html_search_meta( 'Submitting authorization code to MTVNServices',
'sm4:video:embed', webpage, 'sm4 embed', default='') query={'authorizationCode': authorization_code}, data=b'',
mgid = self._search_regex( headers={'Authorization': f'Bearer {access_token}'})
r'embed/(mgid:.+?)["\'&?/]', sm4_embed, 'mgid', default=None) access_token = self._get_fresh_access_token(config, display_id, force_refresh=True)
if not mgid: tokens[self._MEDIA_TOKEN_KEY] = self._call_auth_api(
mgid = self._extract_triforce_mgid(webpage) 'mediaToken', config, display_id, 'Fetching media token', data={
'content': {('id' if k == 'videoId' else k): v for k, v in video_config.items()},
if not mgid: 'resourceId': resource_id,
data = self._parse_json(self._search_regex( }, headers={'Authorization': f'Bearer {access_token}'})['mediaToken']
r'__DATA__\s*=\s*({.+?});', webpage, 'data'), None)
main_container = self._extract_child_with_type(data, 'MainContainer') self.cache.store(self._CACHE_SECTION, resource_id, tokens)
ab_testing = self._extract_child_with_type(main_container, 'ABTesting') return tokens[self._MEDIA_TOKEN_KEY]
video_player = self._extract_child_with_type(ab_testing or main_container, 'VideoPlayer')
if video_player:
mgid = try_get(video_player, lambda x: x['props']['media']['video']['config']['uri'])
else:
flex_wrapper = self._extract_child_with_type(ab_testing or main_container, 'FlexWrapper')
auth_suite_wrapper = self._extract_child_with_type(flex_wrapper, 'AuthSuiteWrapper')
player = self._extract_child_with_type(auth_suite_wrapper or flex_wrapper, 'Player')
if player:
mgid = try_get(player, lambda x: x['props']['videoDetail']['mgid'])
if not mgid:
raise ExtractorError('Could not extract mgid')
return mgid
def _real_extract(self, url):
title = url_basename(url)
webpage = self._download_webpage(url, title)
mgid = self._extract_mgid(webpage)
return self._get_videos_info(mgid, url=url)
class MTVServicesEmbeddedIE(MTVServicesInfoExtractor):
IE_NAME = 'mtvservices:embedded'
_VALID_URL = r'https?://media\.mtvnservices\.com/embed/(?P<mgid>.+?)(\?|/|$)'
_EMBED_REGEX = [r'<iframe[^>]+?src=(["\'])(?P<url>(?:https?:)?//media\.mtvnservices\.com/embed/.+?)\1']
_TEST = {
# From http://www.thewrap.com/peter-dinklage-sums-up-game-of-thrones-in-45-seconds-video/
'url': 'http://media.mtvnservices.com/embed/mgid:uma:video:mtv.com:1043906/cp~vid%3D1043906%26uri%3Dmgid%3Auma%3Avideo%3Amtv.com%3A1043906',
'md5': 'cb349b21a7897164cede95bd7bf3fbb9',
'info_dict': {
'id': '1043906',
'ext': 'mp4',
'title': 'Peter Dinklage Sums Up \'Game Of Thrones\' In 45 Seconds',
'description': '"Sexy sexy sexy, stabby stabby stabby, beautiful language," says Peter Dinklage as he tries summarizing "Game of Thrones" in under a minute.',
'timestamp': 1400126400,
'upload_date': '20140515',
},
}
def _get_feed_url(self, uri, url=None):
video_id = self._id_from_uri(uri)
config = self._download_json(
f'http://media.mtvnservices.com/pmt/e1/access/index.html?uri={uri}&configtype=edge', video_id)
return self._remove_template_parameter(config['feedWithQueryParams'])
def _real_extract(self, url): def _real_extract(self, url):
mobj = self._match_valid_url(url) display_id = self._match_id(url)
mgid = mobj.group('mgid')
return self._get_videos_info(mgid) data = self._download_json(update_url(url, query=None), display_id, query={'json': 'true'})
flex_wrapper = traverse_obj(data, (
'children', lambda _, v: v['type'] == 'MainContainer',
class MTVIE(MTVServicesInfoExtractor): (None, ('children', lambda _, v: v['type'] == 'AviaWrapper')),
IE_NAME = 'mtv' 'children', lambda _, v: v['type'] == 'FlexWrapper', {dict}, any))
_VALID_URL = r'https?://(?:www\.)?mtv\.com/(?:video-clips|(?:full-)?episodes)/(?P<id>[^/?#.]+)' video_detail = traverse_obj(flex_wrapper, (
_FEED_URL = 'http://www.mtv.com/feeds/mrss/' (None, ('children', lambda _, v: v['type'] == 'AuthSuiteWrapper')),
'children', lambda _, v: v['type'] == 'Player',
'props', 'videoDetail', {dict}, any))
if not video_detail:
video_detail = traverse_obj(data, (
'children', ..., ('handleTVEAuthRedirection', None),
'videoDetail', {dict}, any, {require('video detail')}))
mgid = video_detail['mgid']
video_id = mgid.rpartition(':')[2]
service_url = traverse_obj(video_detail, ('videoServiceUrl', {url_or_none}, {update_url(query=None)}))
headers = {}
if video_detail.get('authRequired'):
video_config = traverse_obj(flex_wrapper, (
'children', lambda _, v: v['type'] == 'AuthSuiteWrapper',
'props', 'videoConfig', {dict}, any, {require('video config')}))
config = traverse_obj(data, (
'props', 'authSuiteConfig', {dict}, {require('auth suite config')}))
headers['X-VIA-TVE-MEDIATOKEN'] = self._get_media_token(video_config, config, display_id)
stream_info = self._download_json(
service_url or f'https://topaz.paramount.tech/topaz/api/{mgid}/mica.json',
video_id, 'Downloading API JSON', 'Unable to download API JSON',
query={'clientPlatform': 'desktop'}, headers=headers)['stitchedstream']
manifest_type = stream_info['manifesttype']
if manifest_type == 'hls':
formats, subtitles = self._extract_m3u8_formats_and_subtitles(
stream_info['source'], video_id, 'mp4', m3u8_id=manifest_type)
elif manifest_type == 'dash':
formats, subtitles = self._extract_mpd_formats_and_subtitles(
stream_info['source'], video_id, mpd_id=manifest_type)
else:
self.raise_no_formats(f'Unsupported manifest type "{manifest_type}"')
formats, subtitles = [], {}
_TESTS = [{
'url': 'http://www.mtv.com/video-clips/vl8qof/unlocking-the-truth-trailer',
'md5': '1edbcdf1e7628e414a8c5dcebca3d32b',
'info_dict': {
'id': '5e14040d-18a4-47c4-a582-43ff602de88e',
'ext': 'mp4',
'title': 'Unlocking The Truth|July 18, 2016|1|101|Trailer',
'description': '"Unlocking the Truth" premieres August 17th at 11/10c.',
'timestamp': 1468846800,
'upload_date': '20160718',
},
}, {
'url': 'http://www.mtv.com/full-episodes/94tujl/unlocking-the-truth-gates-of-hell-season-1-ep-101',
'only_matching': True,
}, {
'url': 'http://www.mtv.com/episodes/g8xu7q/teen-mom-2-breaking-the-wall-season-7-ep-713',
'only_matching': True,
}]
class MTVJapanIE(MTVServicesInfoExtractor):
IE_NAME = 'mtvjapan'
_VALID_URL = r'https?://(?:www\.)?mtvjapan\.com/videos/(?P<id>[0-9a-z]+)'
_TEST = {
'url': 'http://www.mtvjapan.com/videos/prayht/fresh-info-cadillac-escalade',
'info_dict': {
'id': 'bc01da03-6fe5-4284-8880-f291f4e368f5',
'ext': 'mp4',
'title': '【Fresh Info】Cadillac ESCALADE Sport Edition',
},
'params': {
'skip_download': True,
},
}
_GEO_COUNTRIES = ['JP']
_FEED_URL = 'http://feeds.mtvnservices.com/od/feed/intl-mrss-player-feed'
def _get_feed_query(self, uri):
return { return {
'arcEp': 'mtvjapan.com', **traverse_obj(video_detail, {
'mgid': uri, 'title': ('title', {str}),
'channel': ('channel', 'name', {str}),
'thumbnails': ('images', ..., {'url': ('url', {url_or_none})}),
'description': (('fullDescription', 'description'), {str}, any),
'series': ('parentEntity', 'title', {str}),
'season_number': ('seasonNumber', {int_or_none}),
'episode_number': ('episodeAiringOrder', {int_or_none}),
'duration': ('duration', 'milliseconds', {float_or_none(scale=1000)}),
'timestamp': ((
('originalPublishDate', {parse_iso8601}),
('publishDate', 'timestamp', {int_or_none})), any),
'release_timestamp': ((
('originalAirDate', {parse_iso8601}),
('airDate', 'timestamp', {int_or_none})), any),
}),
'id': video_id,
'display_id': display_id,
'formats': formats,
'subtitles': subtitles,
} }
class MTVVideoIE(MTVServicesInfoExtractor): class MTVIE(MTVServicesBaseIE):
IE_NAME = 'mtv:video' IE_NAME = 'mtv'
_VALID_URL = r'''(?x)^https?:// _VALID_URL = r'https?://(?:www\.)?mtv\.com/(?:video-clips|episodes)/(?P<id>[\da-z]{6})'
(?:(?:www\.)?mtv\.com/videos/.+?/(?P<videoid>[0-9]+)/[^/]+$|
m\.mtv\.com/videos/video\.rbml\?.*?id=(?P<mgid>[^&]+))'''
_FEED_URL = 'http://www.mtv.com/player/embed/AS3/rss/'
_TESTS = [
{
'url': 'http://www.mtv.com/videos/misc/853555/ours-vh1-storytellers.jhtml',
'md5': '850f3f143316b1e71fa56a4edfd6e0f8',
'info_dict': {
'id': '853555',
'ext': 'mp4',
'title': 'Taylor Swift - "Ours (VH1 Storytellers)"',
'description': 'Album: Taylor Swift performs "Ours" for VH1 Storytellers at Harvey Mudd College.',
'timestamp': 1352610000,
'upload_date': '20121111',
},
},
]
def _get_thumbnail_url(self, uri, itemdoc):
return 'http://mtv.mtvnimages.com/uri/' + uri
def _real_extract(self, url):
mobj = self._match_valid_url(url)
video_id = mobj.group('videoid')
uri = mobj.groupdict().get('mgid')
if uri is None:
webpage = self._download_webpage(url, video_id)
# Some videos come from Vevo.com
m_vevo = re.search(
r'(?s)isVevoVideo = true;.*?vevoVideoId = "(.*?)";', webpage)
if m_vevo:
vevo_id = m_vevo.group(1)
self.to_screen(f'Vevo video detected: {vevo_id}')
return self.url_result(f'vevo:{vevo_id}', ie='Vevo')
uri = self._html_search_regex(r'/uri/(.*?)\?', webpage, 'uri')
return self._get_videos_info(uri)
class MTVDEIE(MTVServicesInfoExtractor):
_WORKING = False
IE_NAME = 'mtv.de'
_VALID_URL = r'https?://(?:www\.)?mtv\.de/(?:musik/videoclips|folgen|news)/(?P<id>[0-9a-z]+)'
_TESTS = [{ _TESTS = [{
'url': 'http://www.mtv.de/musik/videoclips/2gpnv7/Traum', 'url': 'https://www.mtv.com/video-clips/syolsj',
'info_dict': {
'id': 'd5d472bc-f5b7-11e5-bffd-a4badb20dab5',
'ext': 'mp4',
'title': 'Traum',
'description': 'Traum',
},
'params': {
# rtmp download
'skip_download': True,
},
'skip': 'Blocked at Travis CI',
}, {
# mediagen URL without query (e.g. http://videos.mtvnn.com/mediagen/e865da714c166d18d6f80893195fcb97)
'url': 'http://www.mtv.de/folgen/6b1ylu/teen-mom-2-enthuellungen-S5-F1',
'info_dict': { 'info_dict': {
'id': '1e5a878b-31c5-11e7-a442-0e40cf2fc285', 'id': '213ea7f8-bac7-4a43-8cd5-8d8cb8c8160f',
'ext': 'mp4', 'ext': 'mp4',
'title': 'Teen Mom 2', 'display_id': 'syolsj',
'description': 'md5:dc65e357ef7e1085ed53e9e9d83146a7', 'title': 'The Challenge: Vets & New Threats',
}, 'description': 'md5:c4d2e90a5fff6463740fbf96b2bb6a41',
'params': { 'duration': 95.0,
# rtmp download 'thumbnail': r're:https://images\.paramount\.tech/uri/mgid:arc:imageassetref',
'skip_download': True, 'series': 'The Challenge',
}, 'season': 'Season 41',
'skip': 'Blocked at Travis CI', 'season_number': 41,
'episode': 'Episode 0',
'episode_number': 0,
'timestamp': 1753945200,
'upload_date': '20250731',
'release_timestamp': 1753945200,
'release_date': '20250731',
},
'params': {'skip_download': 'm3u8'},
}, { }, {
'url': 'http://www.mtv.de/news/glolix/77491-mtv-movies-spotlight--pixels--teil-3', 'url': 'https://www.mtv.com/episodes/uzvigh',
'info_dict': { 'info_dict': {
'id': 'local_playlist-4e760566473c4c8c5344', 'id': '364e8b9e-e415-11ef-b405-16fff45bc035',
'ext': 'mp4', 'ext': 'mp4',
'title': 'Article_mtv-movies-spotlight-pixels-teil-3_short-clips_part1', 'display_id': 'uzvigh',
'description': 'MTV Movies Supercut', 'title': 'CT Tamburello and Johnny Bananas',
}, 'description': 'md5:364cea52001e9c13f92784e3365c6606',
'params': { 'channel': 'MTV',
# rtmp download 'duration': 1260.0,
'skip_download': True, 'thumbnail': r're:https://images\.paramount\.tech/uri/mgid:arc:imageassetref',
}, 'series': 'Ridiculousness',
'skip': 'Das Video kann zur Zeit nicht abgespielt werden.', 'season': 'Season 47',
'season_number': 47,
'episode': 'Episode 19',
'episode_number': 19,
'timestamp': 1753318800,
'upload_date': '20250724',
'release_timestamp': 1753318800,
'release_date': '20250724',
},
'params': {'skip_download': 'm3u8'},
}] }]
_GEO_COUNTRIES = ['DE']
_FEED_URL = 'http://feeds.mtvnservices.com/od/feed/intl-mrss-player-feed'
def _get_feed_query(self, uri):
return {
'arcEp': 'mtv.de',
'mgid': uri,
}
class MTVItaliaIE(MTVServicesInfoExtractor):
IE_NAME = 'mtv.it'
_VALID_URL = r'https?://(?:www\.)?mtv\.it/(?:episodi|video|musica)/(?P<id>[0-9a-z]+)'
_TESTS = [{
'url': 'http://www.mtv.it/episodi/24bqab/mario-una-serie-di-maccio-capatonda-cavoli-amario-episodio-completo-S1-E1',
'info_dict': {
'id': '0f0fc78e-45fc-4cce-8f24-971c25477530',
'ext': 'mp4',
'title': 'Cavoli amario (episodio completo)',
'description': 'md5:4962bccea8fed5b7c03b295ae1340660',
'series': 'Mario - Una Serie Di Maccio Capatonda',
'season_number': 1,
'episode_number': 1,
},
'params': {
'skip_download': True,
},
}]
_GEO_COUNTRIES = ['IT']
_FEED_URL = 'http://feeds.mtvnservices.com/od/feed/intl-mrss-player-feed'
def _get_feed_query(self, uri):
return {
'arcEp': 'mtv.it',
'mgid': uri,
}
class MTVItaliaProgrammaIE(MTVItaliaIE): # XXX: Do not subclass from concrete IE
IE_NAME = 'mtv.it:programma'
_VALID_URL = r'https?://(?:www\.)?mtv\.it/(?:programmi|playlist)/(?P<id>[0-9a-z]+)'
_TESTS = [{
# program page: general
'url': 'http://www.mtv.it/programmi/s2rppv/mario-una-serie-di-maccio-capatonda',
'info_dict': {
'id': 'a6f155bc-8220-4640-aa43-9b95f64ffa3d',
'title': 'Mario - Una Serie Di Maccio Capatonda',
'description': 'md5:72fbffe1f77ccf4e90757dd4e3216153',
},
'playlist_count': 2,
'params': {
'skip_download': True,
},
}, {
# program page: specific season
'url': 'http://www.mtv.it/programmi/d9ncjf/mario-una-serie-di-maccio-capatonda-S2',
'info_dict': {
'id': '4deeb5d8-f272-490c-bde2-ff8d261c6dd1',
'title': 'Mario - Una Serie Di Maccio Capatonda - Stagione 2',
},
'playlist_count': 34,
'params': {
'skip_download': True,
},
}, {
# playlist page + redirect
'url': 'http://www.mtv.it/playlist/sexy-videos/ilctal',
'info_dict': {
'id': 'dee8f9ee-756d-493b-bf37-16d1d2783359',
'title': 'Sexy Videos',
},
'playlist_mincount': 145,
'params': {
'skip_download': True,
},
}]
_GEO_COUNTRIES = ['IT']
_FEED_URL = 'http://www.mtv.it/feeds/triforce/manifest/v8'
def _get_entries(self, title, url):
while True:
pg = self._search_regex(r'/(\d+)$', url, 'entries', '1')
entries = self._download_json(url, title, f'page {pg}')
url = try_get(
entries, lambda x: x['result']['nextPageURL'], str)
entries = try_get(
entries, (
lambda x: x['result']['data']['items'],
lambda x: x['result']['data']['seasons']),
list)
for entry in entries or []:
if entry.get('canonicalURL'):
yield self.url_result(entry['canonicalURL'])
if not url:
break
def _real_extract(self, url):
query = {'url': url}
info_url = update_url_query(self._FEED_URL, query)
video_id = self._match_id(url)
info = self._download_json(info_url, video_id).get('manifest')
redirect = try_get(
info, lambda x: x['newLocation']['url'], str)
if redirect:
return self.url_result(redirect)
title = info.get('title')
video_id = try_get(
info, lambda x: x['reporting']['itemId'], str)
parent_id = try_get(
info, lambda x: x['reporting']['parentId'], str)
playlist_url = current_url = None
for z in (info.get('zones') or {}).values():
if z.get('moduleName') in ('INTL_M304', 'INTL_M209'):
info_url = z.get('feed')
if z.get('moduleName') in ('INTL_M308', 'INTL_M317'):
playlist_url = playlist_url or z.get('feed')
if z.get('moduleName') in ('INTL_M300',):
current_url = current_url or z.get('feed')
if not info_url:
raise ExtractorError('No info found')
if video_id == parent_id:
video_id = self._search_regex(
r'([^\/]+)/[^\/]+$', info_url, 'video_id')
info = self._download_json(info_url, video_id, 'Show infos')
info = try_get(info, lambda x: x['result']['data'], dict)
title = title or try_get(
info, (
lambda x: x['title'],
lambda x: x['headline']),
str)
description = try_get(info, lambda x: x['content'], str)
if current_url:
season = try_get(
self._download_json(playlist_url, video_id, 'Seasons info'),
lambda x: x['result']['data'], dict)
current = try_get(
season, lambda x: x['currentSeason'], str)
seasons = try_get(
season, lambda x: x['seasons'], list) or []
if current in [s.get('eTitle') for s in seasons]:
playlist_url = current_url
title = re.sub(
r'[-|]\s*(?:mtv\s*italia|programma|playlist)',
'', title, flags=re.IGNORECASE).strip()
return self.playlist_result(
self._get_entries(title, playlist_url),
video_id, title, description)

@ -1,91 +1,57 @@
from .mtv import MTVServicesInfoExtractor from .mtv import MTVServicesBaseIE
from ..utils import update_url_query
class NickIE(MTVServicesInfoExtractor): class NickIE(MTVServicesBaseIE):
IE_NAME = 'nick.com' IE_NAME = 'nick.com'
_VALID_URL = r'https?://(?P<domain>(?:www\.)?nick(?:jr)?\.com)/(?:[^/]+/)?(?P<type>videos/clip|[^/]+/videos|episodes/[^/]+)/(?P<id>[^/?#.]+)' _VALID_URL = r'https?://(?:www\.)?nick\.com/(?:video-clips|episodes)/(?P<id>[\da-z]{6})'
_FEED_URL = 'http://udat.mtvnservices.com/service1/dispatch.htm'
_GEO_COUNTRIES = ['US'] _GEO_COUNTRIES = ['US']
_TESTS = [{ _TESTS = [{
'url': 'https://www.nick.com/episodes/sq47rw/spongebob-squarepants-a-place-for-pets-lockdown-for-love-season-13-ep-1', 'url': 'https://www.nick.com/episodes/u3smw8/wylde-pak-best-summer-ever-season-1-ep-1',
'info_dict': { 'info_dict': {
'description': 'md5:0650a9eb88955609d5c1d1c79292e234', 'id': 'eb9d4db0-274a-11ef-a913-0e37995d42c9',
'title': 'A Place for Pets/Lockdown for Love', 'ext': 'mp4',
'display_id': 'u3smw8',
'title': 'Best Summer Ever?',
'description': 'md5:c737a0ade3fbc09d569c3b3d029a7792',
'channel': 'Nickelodeon',
'duration': 1296.0,
'thumbnail': r're:https://assets\.nick\.com/uri/mgid:arc:imageassetref:',
'series': 'Wylde Pak',
'season': 'Season 1',
'season_number': 1,
'episode': 'Episode 1',
'episode_number': 1,
'timestamp': 1746100800,
'upload_date': '20250501',
'release_timestamp': 1746100800,
'release_date': '20250501',
}, },
'playlist': [ 'params': {'skip_download': 'm3u8'},
{
'md5': 'cb8a2afeafb7ae154aca5a64815ec9d6',
'info_dict': {
'id': '85ee8177-d6ce-48f8-9eee-a65364f8a6df',
'ext': 'mp4',
'title': 'SpongeBob SquarePants: "A Place for Pets/Lockdown for Love" S1',
'description': 'A Place for Pets/Lockdown for Love: When customers bring pets into the Krusty Krab, Mr. Krabs realizes pets are more profitable than owners. Plankton ruins another date with Karen, so she puts the Chum Bucket on lockdown until he proves his affection.',
},
},
{
'md5': '839a04f49900a1fcbf517020d94e0737',
'info_dict': {
'id': '2e2a9960-8fd4-411d-868b-28eb1beb7fae',
'ext': 'mp4',
'title': 'SpongeBob SquarePants: "A Place for Pets/Lockdown for Love" S2',
'description': 'A Place for Pets/Lockdown for Love: When customers bring pets into the Krusty Krab, Mr. Krabs realizes pets are more profitable than owners. Plankton ruins another date with Karen, so she puts the Chum Bucket on lockdown until he proves his affection.',
},
},
{
'md5': 'f1145699f199770e2919ee8646955d46',
'info_dict': {
'id': 'dc91c304-6876-40f7-84a6-7aece7baa9d0',
'ext': 'mp4',
'title': 'SpongeBob SquarePants: "A Place for Pets/Lockdown for Love" S3',
'description': 'A Place for Pets/Lockdown for Love: When customers bring pets into the Krusty Krab, Mr. Krabs realizes pets are more profitable than owners. Plankton ruins another date with Karen, so she puts the Chum Bucket on lockdown until he proves his affection.',
},
},
{
'md5': 'd463116875aee2585ee58de3b12caebd',
'info_dict': {
'id': '5d929486-cf4c-42a1-889a-6e0d183a101a',
'ext': 'mp4',
'title': 'SpongeBob SquarePants: "A Place for Pets/Lockdown for Love" S4',
'description': 'A Place for Pets/Lockdown for Love: When customers bring pets into the Krusty Krab, Mr. Krabs realizes pets are more profitable than owners. Plankton ruins another date with Karen, so she puts the Chum Bucket on lockdown until he proves his affection.',
},
},
],
}, { }, {
'url': 'http://www.nickjr.com/blues-clues-and-you/videos/blues-clues-and-you-original-209-imagination-station/', 'url': 'https://www.nick.com/video-clips/0p4706/spongebob-squarepants-spongebob-loving-the-krusty-krab-for-7-minutes',
'info_dict': { 'info_dict': {
'id': '31631529-2fc5-430b-b2ef-6a74b4609abd', 'id': '4aac2228-5295-4076-b986-159513cf4ce4',
'ext': 'mp4', 'ext': 'mp4',
'description': 'md5:9d65a66df38e02254852794b2809d1cf', 'display_id': '0p4706',
'title': 'Blue\'s Imagination Station', 'title': 'SpongeBob Loving the Krusty Krab for 7 Minutes!',
'description': 'md5:72bf59babdf4e6d642187502864e111d',
'duration': 423.423,
'thumbnail': r're:https://assets\.nick\.com/uri/mgid:arc:imageassetref:',
'series': 'SpongeBob SquarePants',
'season': 'Season 0',
'season_number': 0,
'episode': 'Episode 0',
'episode_number': 0,
'timestamp': 1663819200,
'upload_date': '20220922',
}, },
'skip': 'Not accessible?', 'params': {'skip_download': 'm3u8'},
}] }]
def _get_feed_query(self, uri):
return {
'feed': 'nick_arc_player_prime',
'mgid': uri,
}
def _real_extract(self, url): class NickBrIE(MTVServicesBaseIE):
domain, video_type, display_id = self._match_valid_url(url).groups()
if video_type.startswith('episodes'):
return super()._real_extract(url)
video_data = self._download_json(
f'http://{domain}/data/video.endLevel.json',
display_id, query={
'urlKey': display_id,
})
return self._get_videos_info(video_data['player'] + video_data['id'])
class NickBrIE(MTVServicesInfoExtractor):
IE_NAME = 'nickelodeon:br' IE_NAME = 'nickelodeon:br'
_WORKING = False
_VALID_URL = r'''(?x) _VALID_URL = r'''(?x)
https?:// https?://
(?: (?:
@ -112,42 +78,10 @@ class NickBrIE(MTVServicesInfoExtractor):
'only_matching': True, 'only_matching': True,
}] }]
def _real_extract(self, url):
domain, display_id = self._match_valid_url(url).groups()
webpage = self._download_webpage(url, display_id)
uri = self._search_regex(
r'data-(?:contenturi|mgid)="([^"]+)', webpage, 'mgid')
video_id = self._id_from_uri(uri)
config = self._download_json(
'http://media.mtvnservices.com/pmt/e1/access/index.html',
video_id, query={
'uri': uri,
'configtype': 'edge',
}, headers={
'Referer': url,
})
info_url = self._remove_template_parameter(config['feedWithQueryParams'])
if info_url == 'None':
if domain.startswith('www.'):
domain = domain[4:]
content_domain = {
'mundonick.uol': 'mundonick.com.br',
'nickjr': 'br.nickelodeonjunior.tv',
}[domain]
query = {
'mgid': uri,
'imageEp': content_domain,
'arcEp': content_domain,
}
if domain == 'nickjr.com.br':
query['ep'] = 'c4b16088'
info_url = update_url_query(
'http://feeds.mtvnservices.com/od/feed/intl-mrss-player-feed', query)
return self._get_videos_info_from_url(info_url, video_id)
class NickDeIE(MTVServicesInfoExtractor): class NickDeIE(MTVServicesBaseIE):
IE_NAME = 'nick.de' IE_NAME = 'nick.de'
_WORKING = False
_VALID_URL = r'https?://(?:www\.)?(?P<host>nick\.(?:de|com\.pl|ch)|nickelodeon\.(?:nl|be|at|dk|no|se))/[^/]+/(?:[^/]+/)*(?P<id>[^/?#&]+)' _VALID_URL = r'https?://(?:www\.)?(?P<host>nick\.(?:de|com\.pl|ch)|nickelodeon\.(?:nl|be|at|dk|no|se))/[^/]+/(?:[^/]+/)*(?P<id>[^/?#&]+)'
_TESTS = [{ _TESTS = [{
'url': 'http://www.nick.de/playlist/3773-top-videos/videos/episode/17306-zu-wasser-und-zu-land-rauchende-erdnusse', 'url': 'http://www.nick.de/playlist/3773-top-videos/videos/episode/17306-zu-wasser-und-zu-land-rauchende-erdnusse',
@ -181,15 +115,10 @@ class NickDeIE(MTVServicesInfoExtractor):
'only_matching': True, 'only_matching': True,
}] }]
def _get_feed_url(self, uri, url=None):
video_id = self._id_from_uri(uri)
config = self._download_json(
f'http://media.mtvnservices.com/pmt/e1/access/index.html?uri={uri}&configtype=edge&ref={url}', video_id)
return self._remove_template_parameter(config['feedWithQueryParams'])
class NickRuIE(MTVServicesBaseIE):
class NickRuIE(MTVServicesInfoExtractor):
IE_NAME = 'nickelodeonru' IE_NAME = 'nickelodeonru'
_WORKING = False
_VALID_URL = r'https?://(?:www\.)nickelodeon\.(?:ru|fr|es|pt|ro|hu|com\.tr)/[^/]+/(?:[^/]+/)*(?P<id>[^/?#&]+)' _VALID_URL = r'https?://(?:www\.)nickelodeon\.(?:ru|fr|es|pt|ro|hu|com\.tr)/[^/]+/(?:[^/]+/)*(?P<id>[^/?#&]+)'
_TESTS = [{ _TESTS = [{
'url': 'http://www.nickelodeon.ru/shows/henrydanger/videos/episodes/3-sezon-15-seriya-licenziya-na-polyot/pmomfb#playlist/7airc6', 'url': 'http://www.nickelodeon.ru/shows/henrydanger/videos/episodes/3-sezon-15-seriya-licenziya-na-polyot/pmomfb#playlist/7airc6',
@ -216,9 +145,3 @@ class NickRuIE(MTVServicesInfoExtractor):
'url': 'http://www.nickelodeon.com.tr/programlar/sunger-bob/videolar/kayip-yatak/mgqbjy', 'url': 'http://www.nickelodeon.com.tr/programlar/sunger-bob/videolar/kayip-yatak/mgqbjy',
'only_matching': True, 'only_matching': True,
}] }]
def _real_extract(self, url):
video_id = self._match_id(url)
webpage = self._download_webpage(url, video_id)
mgid = self._extract_mgid(webpage, url)
return self.url_result(f'http://media.mtvnservices.com/embed/{mgid}')

@ -1,44 +1,64 @@
from .mtv import MTVServicesInfoExtractor from .mtv import MTVServicesBaseIE
class SouthParkIE(MTVServicesInfoExtractor): class SouthParkIE(MTVServicesBaseIE):
IE_NAME = 'southpark.cc.com' IE_NAME = 'southpark.cc.com'
_VALID_URL = r'https?://(?:www\.)?(?P<url>southpark(?:\.cc|studios)\.com/((?:video-)?clips|(?:full-)?episodes|collections)/(?P<id>.+?)(\?|#|$))' _VALID_URL = r'https?://(?:www\.)?southpark(?:\.cc|studios)\.com/(?:video-clips|episodes|collections)/(?P<id>[\da-z]{6})'
_FEED_URL = 'http://feeds.mtvnservices.com/od/feed/intl-mrss-player-feed'
_TESTS = [{ _TESTS = [{
'url': 'https://southpark.cc.com/video-clips/d7wr06/south-park-you-all-agreed-to-counseling', 'url': 'https://southpark.cc.com/video-clips/d7wr06/south-park-you-all-agreed-to-counseling',
'info_dict': { 'info_dict': {
'id': '31929ad5-8269-11eb-8774-70df2f866ace',
'ext': 'mp4', 'ext': 'mp4',
'display_id': 'd7wr06',
'title': 'You All Agreed to Counseling', 'title': 'You All Agreed to Counseling',
'description': 'Kenny, Cartman, Stan, and Kyle visit Mr. Mackey and ask for his help getting Mrs. Nelson to come back. Mr. Mackey reveals the only way to get things back to normal is to get the teachers vaccinated.', 'description': 'md5:01f78fb306c7042f3f05f3c78edfc212',
'duration': 134.552,
'thumbnail': r're:https://images\.paramount\.tech/uri/mgid:arc:imageassetref:',
'series': 'South Park',
'season': 'Season 24',
'season_number': 24,
'episode': 'Episode 2',
'episode_number': 2,
'timestamp': 1615352400, 'timestamp': 1615352400,
'upload_date': '20210310', 'upload_date': '20210310',
'release_timestamp': 1615352400,
'release_date': '20210310',
}, },
'params': {'skip_download': 'm3u8'},
}, { }, {
'url': 'http://southpark.cc.com/collections/7758/fan-favorites/1', 'url': 'https://southpark.cc.com/episodes/940f8z/south-park-cartman-gets-an-anal-probe-season-1-ep-1',
'info_dict': {
'id': '5fb8887e-ecfd-11e0-aca6-0026b9414f30',
'ext': 'mp4',
'display_id': '940f8z',
'title': 'Cartman Gets An Anal Probe',
'description': 'md5:964e1968c468545752feef102b140300',
'channel': 'Comedy Central',
'duration': 1319.0,
'thumbnail': r're:https://images\.paramount\.tech/uri/mgid:arc:imageassetref:',
'series': 'South Park',
'season': 'Season 1',
'season_number': 1,
'episode': 'Episode 1',
'episode_number': 1,
'timestamp': 871473600,
'upload_date': '19970813',
'release_timestamp': 871473600,
'release_date': '19970813',
},
'params': {'skip_download': 'm3u8'},
}, {
'url': 'https://southpark.cc.com/collections/dejukt/south-park-best-of-mr-mackey/tphx9j',
'only_matching': True, 'only_matching': True,
}, { }, {
'url': 'https://www.southparkstudios.com/episodes/h4o269/south-park-stunning-and-brave-season-19-ep-1', 'url': 'https://www.southparkstudios.com/episodes/h4o269/south-park-stunning-and-brave-season-19-ep-1',
'only_matching': True, 'only_matching': True,
}] }]
def _get_feed_query(self, uri):
return {
'accountOverride': 'intl.mtvi.com',
'arcEp': 'shared.southpark.global',
'ep': '90877963',
'imageEp': 'shared.southpark.global',
'mgid': uri,
}
class SouthParkEsIE(SouthParkIE): # XXX: Do not subclass from concrete IE class SouthParkEsIE(MTVServicesBaseIE):
IE_NAME = 'southpark.cc.com:español' IE_NAME = 'southpark.cc.com:español'
_VALID_URL = r'https?://(?:www\.)?(?P<url>southpark\.cc\.com/es/episodios/(?P<id>.+?)(\?|#|$))' _VALID_URL = r'https?://(?:www\.)?(?P<url>southpark\.cc\.com/es/episodios/(?P<id>.+?)(\?|#|$))'
_LANG = 'es'
_TESTS = [{ _TESTS = [{
'url': 'http://southpark.cc.com/es/episodios/s01e01-cartman-consigue-una-sonda-anal#source=351c1323-0b96-402d-a8b9-40d01b2e9bde&position=1&sort=!airdate', 'url': 'http://southpark.cc.com/es/episodios/s01e01-cartman-consigue-una-sonda-anal#source=351c1323-0b96-402d-a8b9-40d01b2e9bde&position=1&sort=!airdate',
'info_dict': { 'info_dict': {
@ -50,9 +70,10 @@ class SouthParkEsIE(SouthParkIE): # XXX: Do not subclass from concrete IE
}] }]
class SouthParkDeIE(SouthParkIE): # XXX: Do not subclass from concrete IE class SouthParkDeIE(MTVServicesBaseIE):
IE_NAME = 'southpark.de' IE_NAME = 'southpark.de'
_VALID_URL = r'https?://(?:www\.)?(?P<url>southpark\.de/(?:(en/(videoclip|collections|episodes|video-clips))|(videoclip|collections|folgen))/(?P<id>(?P<unique_id>.+?)/.+?)(?:\?|#|$))' _VALID_URL = r'https?://(?:www\.)?(?P<url>southpark\.de/(?:(en/(videoclip|collections|episodes|video-clips))|(videoclip|collections|folgen))/(?P<id>(?P<unique_id>.+?)/.+?)(?:\?|#|$))'
_GEO_COUNTRIES = ['DE']
_TESTS = [{ _TESTS = [{
'url': 'https://www.southpark.de/videoclip/rsribv/south-park-rueckzug-zum-gummibonbon-wald', 'url': 'https://www.southpark.de/videoclip/rsribv/south-park-rueckzug-zum-gummibonbon-wald',
'only_matching': True, 'only_matching': True,
@ -99,19 +120,11 @@ class SouthParkDeIE(SouthParkIE): # XXX: Do not subclass from concrete IE
}, },
}] }]
def _get_feed_url(self, uri, url=None):
video_id = self._id_from_uri(uri)
config = self._download_json(
f'http://media.mtvnservices.com/pmt/e1/access/index.html?uri={uri}&configtype=edge&ref={url}', video_id)
return self._remove_template_parameter(config['feedWithQueryParams'])
def _get_feed_query(self, uri):
return
class SouthParkLatIE(MTVServicesBaseIE):
class SouthParkLatIE(SouthParkIE): # XXX: Do not subclass from concrete IE
IE_NAME = 'southpark.lat' IE_NAME = 'southpark.lat'
_VALID_URL = r'https?://(?:www\.)?southpark\.lat/(?:en/)?(?:video-?clips?|collections|episod(?:e|io)s)/(?P<id>[^/?#&]+)' _VALID_URL = r'https?://(?:www\.)?southpark\.lat/(?:en/)?(?:video-?clips?|collections|episod(?:e|io)s)/(?P<id>[^/?#&]+)'
_GEO_COUNTRIES = ['BR']
_TESTS = [{ _TESTS = [{
'url': 'https://www.southpark.lat/en/video-clips/ct46op/south-park-tooth-fairy-cartman', 'url': 'https://www.southpark.lat/en/video-clips/ct46op/south-park-tooth-fairy-cartman',
'only_matching': True, 'only_matching': True,
@ -141,22 +154,11 @@ class SouthParkLatIE(SouthParkIE): # XXX: Do not subclass from concrete IE
}, },
}] }]
def _get_feed_url(self, uri, url=None):
video_id = self._id_from_uri(uri)
config = self._download_json(
f'http://media.mtvnservices.com/pmt/e1/access/index.html?uri={uri}&configtype=edge&ref={url}',
video_id)
return self._remove_template_parameter(config['feedWithQueryParams'])
def _get_feed_query(self, uri):
return
class SouthParkNlIE(MTVServicesBaseIE):
class SouthParkNlIE(SouthParkIE): # XXX: Do not subclass from concrete IE
IE_NAME = 'southpark.nl' IE_NAME = 'southpark.nl'
_VALID_URL = r'https?://(?:www\.)?(?P<url>southpark\.nl/(?:clips|(?:full-)?episodes|collections)/(?P<id>.+?)(\?|#|$))' _VALID_URL = r'https?://(?:www\.)?(?P<url>southpark\.nl/(?:clips|(?:full-)?episodes|collections)/(?P<id>.+?)(\?|#|$))'
_FEED_URL = 'http://www.southpark.nl/feeds/video-player/mrss/' _GEO_COUNTRIES = ['NL']
_TESTS = [{ _TESTS = [{
'url': 'http://www.southpark.nl/full-episodes/s18e06-freemium-isnt-free', 'url': 'http://www.southpark.nl/full-episodes/s18e06-freemium-isnt-free',
'info_dict': { 'info_dict': {
@ -167,11 +169,10 @@ class SouthParkNlIE(SouthParkIE): # XXX: Do not subclass from concrete IE
}] }]
class SouthParkDkIE(SouthParkIE): # XXX: Do not subclass from concrete IE class SouthParkDkIE(MTVServicesBaseIE):
IE_NAME = 'southparkstudios.dk' IE_NAME = 'southparkstudios.dk'
_VALID_URL = r'https?://(?:www\.)?(?P<url>southparkstudios\.(?:dk|nu)/(?:clips|full-episodes|collections)/(?P<id>.+?)(\?|#|$))' _VALID_URL = r'https?://(?:www\.)?(?P<url>southparkstudios\.(?:dk|nu)/(?:clips|full-episodes|collections)/(?P<id>.+?)(\?|#|$))'
_FEED_URL = 'http://www.southparkstudios.dk/feeds/video-player/mrss/' _GEO_COUNTRIES = ['DK']
_TESTS = [{ _TESTS = [{
'url': 'http://www.southparkstudios.dk/full-episodes/s18e07-grounded-vindaloop', 'url': 'http://www.southparkstudios.dk/full-episodes/s18e07-grounded-vindaloop',
'info_dict': { 'info_dict': {

@ -1,7 +1,7 @@
from .mtv import MTVServicesInfoExtractor from .mtv import MTVServicesBaseIE
class BellatorIE(MTVServicesInfoExtractor): class BellatorIE(MTVServicesBaseIE):
_VALID_URL = r'https?://(?:www\.)?bellator\.com/[^/]+/[\da-z]{6}(?:[/?#&]|$)' _VALID_URL = r'https?://(?:www\.)?bellator\.com/[^/]+/[\da-z]{6}(?:[/?#&]|$)'
_TESTS = [{ _TESTS = [{
'url': 'http://www.bellator.com/fight/atwr7k/bellator-158-michael-page-vs-evangelista-cyborg', 'url': 'http://www.bellator.com/fight/atwr7k/bellator-158-michael-page-vs-evangelista-cyborg',
@ -19,7 +19,7 @@ class BellatorIE(MTVServicesInfoExtractor):
_GEO_COUNTRIES = ['US'] _GEO_COUNTRIES = ['US']
class ParamountNetworkIE(MTVServicesInfoExtractor): class ParamountNetworkIE(MTVServicesBaseIE):
_VALID_URL = r'https?://(?:www\.)?paramountnetwork\.com/[^/]+/[\da-z]{6}(?:[/?#&]|$)' _VALID_URL = r'https?://(?:www\.)?paramountnetwork\.com/[^/]+/[\da-z]{6}(?:[/?#&]|$)'
_TESTS = [{ _TESTS = [{
'url': 'http://www.paramountnetwork.com/episodes/j830qm/lip-sync-battle-joel-mchale-vs-jim-rash-season-2-ep-13', 'url': 'http://www.paramountnetwork.com/episodes/j830qm/lip-sync-battle-joel-mchale-vs-jim-rash-season-2-ep-13',

@ -1,9 +1,9 @@
from .mtv import MTVServicesInfoExtractor from .mtv import MTVServicesBaseIE
# TODO: Remove - Reason not used anymore - Service moved to youtube # TODO: Remove - Reason not used anymore - Service moved to youtube
class TVLandIE(MTVServicesInfoExtractor): class TVLandIE(MTVServicesBaseIE):
IE_NAME = 'tvland.com' IE_NAME = 'tvland.com'
_VALID_URL = r'https?://(?:www\.)?tvland\.com/(?:video-clips|(?:full-)?episodes)/(?P<id>[^/?#.]+)' _VALID_URL = r'https?://(?:www\.)?tvland\.com/(?:video-clips|(?:full-)?episodes)/(?P<id>[^/?#.]+)'
_FEED_URL = 'http://www.tvland.com/feeds/mrss/' _FEED_URL = 'http://www.tvland.com/feeds/mrss/'

@ -1,33 +1,50 @@
from .mtv import MTVServicesInfoExtractor from .mtv import MTVServicesBaseIE
# TODO: Remove - Reason: Outdated Site
class VH1IE(MTVServicesBaseIE):
class VH1IE(MTVServicesInfoExtractor):
IE_NAME = 'vh1.com' IE_NAME = 'vh1.com'
_FEED_URL = 'http://www.vh1.com/feeds/mrss/' _VALID_URL = r'https?://(?:www\.)?vh1\.com/(?:video-clips|episodes)/(?P<id>[\da-z]{6})'
_TESTS = [{ _TESTS = [{
'url': 'https://www.vh1.com/episodes/0aqivv/nick-cannon-presents-wild-n-out-foushee-season-16-ep-12', 'url': 'https://www.vh1.com/episodes/d06ta1/barely-famous-barely-famous-season-1-ep-1',
'info_dict': { 'info_dict': {
'title': 'Fousheé', 'id': '4af4cf2c-a854-11e4-9596-0026b9414f30',
'description': 'Fousheé joins Team Evolutions fight against Nick and Team Revolution in Baby Daddy, Baby Mama; Kick Em Out the Classroom; Backseat of My Ride and Wildstyle; and Fousheé performs.', 'ext': 'mp4',
'display_id': 'd06ta1',
'title': 'Barely Famous',
'description': 'md5:6da5c9d88012eba0a80fc731c99b5fed',
'channel': 'VH1',
'duration': 1280.0,
'thumbnail': r're:https://images\.paramount\.tech/uri/mgid:arc:imageassetref:',
'series': 'Barely Famous',
'season': 'Season 1',
'season_number': 1,
'episode': 'Episode 1',
'episode_number': 1,
'timestamp': 1426680000,
'upload_date': '20150318',
'release_timestamp': 1426680000,
'release_date': '20150318',
}, },
'playlist_mincount': 4, 'params': {'skip_download': 'm3u8'},
'skip': '404 Not found',
}, { }, {
# Clip 'url': 'https://www.vh1.com/video-clips/ryzt2n/love-hip-hop-miami-love-hip-hop-miami-season-5-recap',
'url': 'https://www.vh1.com/video-clips/e0sja0/nick-cannon-presents-wild-n-out-foushee-clap-for-him',
'info_dict': { 'info_dict': {
'id': 'a07563f7-a37b-4e7f-af68-85855c2c7cc3', 'id': '59e62974-4a5c-4417-91c3-5044cb2f4ce2',
'ext': 'mp4', 'ext': 'mp4',
'title': 'Fousheé - "clap for him"', 'display_id': 'ryzt2n',
'description': 'Singer Fousheé hits the Wild N Out: In the Dark stage with a performance of the tongue-in-cheek track "clap for him" from her 2021 album "time machine."', 'title': 'Love & Hip Hop Miami - Season 5 Recap',
'upload_date': '20210826', 'description': 'md5:4e49c65d0007bfc8d06db555a6b76ef0',
}, 'duration': 792.083,
'params': { 'thumbnail': r're:https://images\.paramount\.tech/uri/mgid:arc:imageassetref:',
# m3u8 download 'series': 'Love & Hip Hop Miami',
'skip_download': True, 'season': 'Season 6',
'season_number': 6,
'episode': 'Episode 0',
'episode_number': 0,
'timestamp': 1732597200,
'upload_date': '20241126',
'release_timestamp': 1732597200,
'release_date': '20241126',
}, },
'params': {'skip_download': 'm3u8'},
}] }]
_VALID_URL = r'https?://(?:www\.)?vh1\.com/(?:video-clips|episodes)/(?P<id>[^/?#.]+)'

Loading…
Cancel
Save