1
0
mirror of https://github.com/yt-dlp/yt-dlp.git synced 2025-08-22 03:11:41 +00:00

[ie/mtv] Overhaul extractors (#14052)

Adds SouthParkComBrIE and SouthParkCoUkIE

Removes these extractors:
 - CMTIE: migrated to Paramount+
 - ComedyCentralTVIE: migrated to Paramount+
 - MTVDEIE: migrated to Paramount+
 - MTVItaliaIE: migrated to Paramount+
 - MTVItaliaProgrammaIE: migrated to Paramount+
 - MTVJapanIE: migrated to JP Services
 - MTVServicesEmbeddedIE: dead domain
 - MTVVideoIE: migrated to Paramount+
 - NickBrIE: redirects to landing page w/o any videos
 - NickDeIE: redirects to landing page w/o any videos
 - NickRuIE: redirects to landing page w/o any videos
 - BellatorIE: migrated to PFL
 - ParamountNetworkIE: migrated to Paramount+
 - SouthParkNlIE: site no longer exists
 - TVLandIE: migrated to Paramount+

Closes #169, Closes #1711, Closes #1712, Closes #2621, Closes #3167, Closes #3893, Closes #4552, Closes #4702, Closes #4928, Closes #5249, Closes #6156, Closes #8722, Closes #9896, Closes #10168, Closes #12765, Closes #13446, Closes #14009

Authored by: bashonly, doe1080, Randalix, seproDev

Co-authored-by: doe1080 <98906116+doe1080@users.noreply.github.com>
Co-authored-by: Randalix <23729538+Randalix@users.noreply.github.com>
Co-authored-by: sepro <sepro@sepr0.com>
This commit is contained in:
bashonly 2025-08-19 15:46:11 -05:00 committed by GitHub
parent 471a2b60e0
commit 8df121ba59
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 639 additions and 1253 deletions

View File

@ -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'

View File

@ -398,16 +398,12 @@ from .cloudflarestream import CloudflareStreamIE
from .cloudycdn import CloudyCDNIE from .cloudycdn import CloudyCDNIE
from .clubic import ClubicIE from .clubic import ClubicIE
from .clyp import ClypIE from .clyp import ClypIE
from .cmt import CMTIE
from .cnbc import CNBCVideoIE from .cnbc import CNBCVideoIE
from .cnn import ( 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,
@ -1180,15 +1176,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,
@ -1330,12 +1318,7 @@ from .nhk import (
NhkVodProgramIE, NhkVodProgramIE,
) )
from .nhl import NHLIE from .nhl import NHLIE
from .nick import ( from .nick import NickIE
NickBrIE,
NickDeIE,
NickIE,
NickRuIE,
)
from .niconico import ( from .niconico import (
NiconicoHistoryIE, NiconicoHistoryIE,
NiconicoIE, NiconicoIE,
@ -1923,12 +1906,13 @@ from .soundgasm import (
SoundgasmProfileIE, SoundgasmProfileIE,
) )
from .southpark import ( from .southpark import (
SouthParkComBrIE,
SouthParkCoUkIE,
SouthParkDeIE, SouthParkDeIE,
SouthParkDkIE, SouthParkDkIE,
SouthParkEsIE, SouthParkEsIE,
SouthParkIE, SouthParkIE,
SouthParkLatIE, SouthParkLatIE,
SouthParkNlIE,
) )
from .sovietscloset import ( from .sovietscloset import (
SovietsClosetIE, SovietsClosetIE,
@ -1939,10 +1923,6 @@ from .spankbang import (
SpankBangPlaylistIE, SpankBangPlaylistIE,
) )
from .spiegel import SpiegelIE from .spiegel import SpiegelIE
from .spike import (
BellatorIE,
ParamountNetworkIE,
)
from .sport5 import Sport5IE from .sport5 import Sport5IE
from .sportbox import SportBoxIE from .sportbox import SportBoxIE
from .sportdeutschland import SportDeutschlandIE from .sportdeutschland import SportDeutschlandIE
@ -2207,7 +2187,6 @@ from .tvc import (
from .tver import TVerIE from .tver import TVerIE
from .tvigle import TvigleIE from .tvigle import TvigleIE
from .tviplayer import TVIPlayerIE from .tviplayer import TVIPlayerIE
from .tvland import TVLandIE
from .tvn24 import TVN24IE from .tvn24 import TVN24IE
from .tvnoe import TVNoeIE from .tvnoe import TVNoeIE
from .tvopengr import ( from .tvopengr import (

View File

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

View File

@ -1,55 +0,0 @@
from .mtv import MTVIE
# TODO: Remove - Reason: Outdated Site
class CMTIE(MTVIE): # XXX: Do not subclass from concrete IE
_WORKING = False
IE_NAME = 'cmt.com'
_VALID_URL = r'https?://(?:www\.)?cmt\.com/(?:videos|shows|(?:full-)?episodes|video-clips)/(?P<id>[^/]+)'
_TESTS = [{
'url': 'http://www.cmt.com/videos/garth-brooks/989124/the-call-featuring-trisha-yearwood.jhtml#artist=30061',
'md5': 'e6b7ef3c4c45bbfae88061799bbba6c2',
'info_dict': {
'id': '989124',
'ext': 'mp4',
'title': 'Garth Brooks - "The Call (featuring Trisha Yearwood)"',
'description': 'Blame It All On My Roots',
},
'skip': 'Video not available',
}, {
'url': 'http://www.cmt.com/videos/misc/1504699/still-the-king-ep-109-in-3-minutes.jhtml#id=1739908',
'md5': 'e61a801ca4a183a466c08bd98dccbb1c',
'info_dict': {
'id': '1504699',
'ext': 'mp4',
'title': 'Still The King Ep. 109 in 3 Minutes',
'description': 'Relive or catch up with Still The King by watching this recap of season 1, episode 9.',
'timestamp': 1469421000.0,
'upload_date': '20160725',
},
}, {
'url': 'http://www.cmt.com/shows/party-down-south/party-down-south-ep-407-gone-girl/1738172/playlist/#id=1738172',
'only_matching': True,
}, {
'url': 'http://www.cmt.com/full-episodes/537qb3/nashville-the-wayfaring-stranger-season-5-ep-501',
'only_matching': True,
}, {
'url': 'http://www.cmt.com/video-clips/t9e4ci/nashville-juliette-in-2-minutes',
'only_matching': True,
}]
def _extract_mgid(self, webpage, url):
mgid = self._search_regex(
r'MTVN\.VIDEO\.contentUri\s*=\s*([\'"])(?P<mgid>.+?)\1',
webpage, 'mgid', group='mgid', default=None)
if not mgid:
mgid = self._extract_triforce_mgid(webpage)
return mgid
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}')

View File

@ -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,
}

View File

@ -1,652 +1,268 @@
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 ..networking.exceptions import HTTPError
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}' _GEO_BYPASS = False
_GEO_COUNTRIES = ['US']
_CACHE_SECTION = 'mtvservices'
class MTVServicesInfoExtractor(InfoExtractor): _ACCESS_TOKEN_KEY = 'access'
_MOBILE_TEMPLATE = None _REFRESH_TOKEN_KEY = 'refresh'
_LANG = None _MEDIA_TOKEN_KEY = 'media'
_token_cache = {}
@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 _get_thumbnail_url(self, uri, itemdoc):
search_path = '{}/{}'.format(_media_xml_tag('group'), _media_xml_tag('thumbnail'))
thumb_node = itemdoc.find(search_path)
if thumb_node is None:
return None
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): def _call_auth_api(self, path, config, display_id=None, note=None, data=None, headers=None, query=None):
uri = itemdoc.find('guid').text headers = {
video_id = self._id_from_uri(uri) 'Accept': 'application/json',
self.report_extraction(video_id) 'Client-Description': 'deviceName=Chrome Windows;deviceType=desktop;system=Windows NT 10.0',
content_el = itemdoc.find('{}/{}'.format(_media_xml_tag('group'), _media_xml_tag('content'))) 'Api-Version': '2025-07-09',
mediagen_url = self._remove_template_parameter(content_el.attrib['url']) **(headers or {}),
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:
headers['Content-Type'] = 'application/json'
if isinstance(data, dict):
data = json.dumps(data, separators=(',', ':')).encode()
def _get_feed_query(self, uri): return self._download_json(
data = {'uri': uri} f'https://auth.mtvnservices.com/{path}', display_id,
if self._LANG: note=note or 'Calling authentication API', data=data,
data['lang'] = self._LANG headers=headers, query={**self._get_auth_suite_data(config), **(query or {})})
return data
def _get_videos_info(self, uri, use_hls=True, url=None): def _get_fresh_access_token(self, config, display_id=None, force_refresh=False):
video_id = self._id_from_uri(uri) resource_id = config['resourceId']
feed_url = self._get_feed_url(uri, url) # resource_id should already be in _token_cache since _get_media_token is the caller
info_url = update_url_query(feed_url, self._get_feed_query(uri)) tokens = self._token_cache[resource_id]
return self._get_videos_info_from_url(info_url, video_id, use_hls)
def _get_videos_info_from_url(self, url, video_id, use_hls=True): access_token = tokens.get(self._ACCESS_TOKEN_KEY)
idoc = self._download_xml( if not force_refresh and access_token and not self._jwt_is_expired(access_token):
url, video_id, return access_token
'Downloading info', transform_source=fix_xml_ampersands)
title = xpath_text(idoc, './channel/title') if self._REFRESH_TOKEN_KEY not in tokens:
description = xpath_text(idoc, './channel/description') response = self._call_auth_api(
'accessToken', config, display_id, 'Retrieving auth tokens', data=b'')
entries = []
for item in idoc.findall('.//item'):
info = self._get_video_info(item, use_hls)
if info:
entries.append(info)
# TODO: should be multi-video
return self.playlist_result(
entries, playlist_title=title, playlist_description=description)
def _extract_triforce_mgid(self, webpage, data_zone=None, video_id=None):
triforce_feed = self._parse_json(self._search_regex(
r'triforceManifestFeed\s*=\s*({.+?})\s*;\s*\n', webpage,
'triforce feed', default='{}'), video_id, fatal=False)
data_zone = self._search_regex(
r'data-zone=(["\'])(?P<zone>.+?_lc_promo.*?)\1', webpage,
'data zone', default=data_zone, group='zone')
feed_url = try_get(
triforce_feed, lambda x: x['manifest']['zones'][data_zone]['feed'],
str)
if not feed_url:
return
feed = self._download_json(feed_url, video_id, fatal=False)
if not feed:
return
return try_get(feed, lambda x: x['result']['data']['id'], str)
@staticmethod
def _extract_child_with_type(parent, t):
for c in parent['children']:
if c.get('type') == t:
return c
def _extract_mgid(self, webpage):
try:
# the url can be http://media.mtvnservices.com/fb/{mgid}.swf
# or http://media.mtvnservices.com/{mgid}
og_url = self._og_search_video_url(webpage)
mgid = url_basename(og_url)
if mgid.endswith('.swf'):
mgid = mgid[:-4]
except RegexNotFoundError:
mgid = None
if mgid is None or ':' not in mgid:
mgid = self._search_regex(
[r'data-mgid="(.*?)"', r'swfobject\.embedSWF\(".*?(mgid:.*?)"'],
webpage, 'mgid', default=None)
if not mgid:
sm4_embed = self._html_search_meta(
'sm4:video:embed', webpage, 'sm4 embed', default='')
mgid = self._search_regex(
r'embed/(mgid:.+?)["\'&?/]', sm4_embed, 'mgid', default=None)
if not mgid:
mgid = self._extract_triforce_mgid(webpage)
if not mgid:
data = self._parse_json(self._search_regex(
r'__DATA__\s*=\s*({.+?});', webpage, 'data'), None)
main_container = self._extract_child_with_type(data, 'MainContainer')
ab_testing = self._extract_child_with_type(main_container, 'ABTesting')
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: else:
flex_wrapper = self._extract_child_with_type(ab_testing or main_container, 'FlexWrapper') response = self._call_auth_api(
auth_suite_wrapper = self._extract_child_with_type(flex_wrapper, 'AuthSuiteWrapper') 'accessToken/refresh', config, display_id, 'Refreshing auth tokens',
player = self._extract_child_with_type(auth_suite_wrapper or flex_wrapper, 'Player') data={'refreshToken': tokens[self._REFRESH_TOKEN_KEY]},
if player: headers={'Authorization': f'Bearer {access_token}'})
mgid = try_get(player, lambda x: x['props']['videoDetail']['mgid'])
if not mgid: tokens[self._ACCESS_TOKEN_KEY] = response['applicationAccessToken']
raise ExtractorError('Could not extract mgid') tokens[self._REFRESH_TOKEN_KEY] = response['deviceRefreshToken']
self.cache.store(self._CACHE_SECTION, resource_id, tokens)
return mgid return tokens[self._ACCESS_TOKEN_KEY]
def _get_media_token(self, video_config, config, display_id=None):
resource_id = config['resourceId']
if resource_id in self._token_cache:
tokens = self._token_cache[resource_id]
else:
tokens = self._token_cache[resource_id] = self.cache.load(self._CACHE_SECTION, resource_id) or {}
media_token = tokens.get(self._MEDIA_TOKEN_KEY)
if media_token and not self._jwt_is_expired(media_token):
return media_token
access_token = self._get_fresh_access_token(config, display_id)
if not jwt_decode_hs256(access_token).get('accessMethods'):
# MTVServices uses a custom AdobePass oauth flow which is incompatible with AdobePassIE
mso_id = self.get_param('ap_mso')
if not mso_id:
raise ExtractorError(
'This video is only available for users of participating TV providers. '
'Use --ap-mso to specify Adobe Pass Multiple-system operator Identifier and pass '
'cookies from a browser session where you are signed-in to your provider.', expected=True)
auth_suite_data = json.dumps(
self._get_auth_suite_data(config), separators=(',', ':')).encode()
callback_url = update_url_query(config['callbackURL'], {
'authSuiteData': urllib.parse.quote(base64.b64encode(auth_suite_data).decode()),
'mvpdCode': mso_id,
})
auth_url = self._call_auth_api(
f'mvpd/{mso_id}/login', config, display_id,
'Retrieving provider authentication URL',
query={'callbackUrl': callback_url},
headers={'Authorization': f'Bearer {access_token}'})['authenticationUrl']
res = self._download_webpage_handle(auth_url, display_id, 'Downloading provider auth page')
# XXX: The following "provider-specific code" likely only works if mso_id == Comcast_SSO
# BEGIN provider-specific code
redirect_url = self._search_json(
r'initInterstitialRedirect\(', res[0], 'redirect JSON',
display_id, transform_source=js_to_json)['continue']
urlh = self._request_webpage(redirect_url, display_id, 'Requesting provider redirect page')
authorization_code = parse_qs(urlh.url)['authorizationCode'][-1]
# END provider-specific code
self._call_auth_api(
f'access/mvpd/{mso_id}', config, display_id,
'Submitting authorization code to MTVNServices',
query={'authorizationCode': authorization_code}, data=b'',
headers={'Authorization': f'Bearer {access_token}'})
access_token = self._get_fresh_access_token(config, display_id, force_refresh=True)
tokens[self._MEDIA_TOKEN_KEY] = self._call_auth_api(
'mediaToken', config, display_id, 'Fetching media token', data={
'content': {('id' if k == 'videoId' else k): v for k, v in video_config.items()},
'resourceId': resource_id,
}, headers={'Authorization': f'Bearer {access_token}'})['mediaToken']
self.cache.store(self._CACHE_SECTION, resource_id, tokens)
return tokens[self._MEDIA_TOKEN_KEY]
def _real_extract(self, url): def _real_extract(self, url):
title = url_basename(url) display_id = self._match_id(url)
webpage = self._download_webpage(url, title)
mgid = self._extract_mgid(webpage)
return self._get_videos_info(mgid, url=url)
try:
data = self._download_json(
update_url(url, query=None), display_id,
query={'json': 'true'})
except ExtractorError as e:
if isinstance(e.cause, HTTPError) and e.cause.status == 404 and not self.suitable(e.cause.response.url):
self.raise_geo_restricted(countries=self._GEO_COUNTRIES)
raise
class MTVServicesEmbeddedIE(MTVServicesInfoExtractor): flex_wrapper = traverse_obj(data, (
IE_NAME = 'mtvservices:embedded' 'children', lambda _, v: v['type'] == 'MainContainer',
_VALID_URL = r'https?://media\.mtvnservices\.com/embed/(?P<mgid>.+?)(\?|/|$)' (None, ('children', lambda _, v: v['type'] == 'AviaWrapper')),
_EMBED_REGEX = [r'<iframe[^>]+?src=(["\'])(?P<url>(?:https?:)?//media\.mtvnservices\.com/embed/.+?)\1'] 'children', lambda _, v: v['type'] == 'FlexWrapper', {dict}, any))
video_detail = traverse_obj(flex_wrapper, (
(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')}))
_TEST = { mgid = video_detail['mgid']
# From http://www.thewrap.com/peter-dinklage-sums-up-game-of-thrones-in-45-seconds-video/ video_id = mgid.rpartition(':')[2]
'url': 'http://media.mtvnservices.com/embed/mgid:uma:video:mtv.com:1043906/cp~vid%3D1043906%26uri%3Dmgid%3Auma%3Avideo%3Amtv.com%3A1043906', service_url = traverse_obj(video_detail, ('videoServiceUrl', {url_or_none}, {update_url(query=None)}))
'md5': 'cb349b21a7897164cede95bd7bf3fbb9', if not service_url:
'info_dict': { raise ExtractorError('This content is no longer available', expected=True)
'id': '1043906',
'ext': 'mp4', headers = {}
'title': 'Peter Dinklage Sums Up \'Game Of Thrones\' In 45 Seconds', if video_detail.get('authRequired'):
'description': '"Sexy sexy sexy, stabby stabby stabby, beautiful language," says Peter Dinklage as he tries summarizing "Game of Thrones" in under a minute.', # The vast majority of provider-locked content has been moved to Paramount+
'timestamp': 1400126400, # BetIE is the only extractor that is currently known to reach this code path
'upload_date': '20140515', 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, 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 = [], {}
return {
**traverse_obj(video_detail, {
'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,
} }
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): class MTVIE(MTVServicesBaseIE):
mobj = self._match_valid_url(url)
mgid = mobj.group('mgid')
return self._get_videos_info(mgid)
class MTVIE(MTVServicesInfoExtractor):
IE_NAME = 'mtv' IE_NAME = 'mtv'
_VALID_URL = r'https?://(?:www\.)?mtv\.com/(?:video-clips|(?:full-)?episodes)/(?P<id>[^/?#.]+)' _VALID_URL = r'https?://(?:www\.)?mtv\.com/(?:video-clips|episodes)/(?P<id>[\da-z]{6})'
_FEED_URL = 'http://www.mtv.com/feeds/mrss/'
_TESTS = [{ _TESTS = [{
'url': 'http://www.mtv.com/video-clips/vl8qof/unlocking-the-truth-trailer', 'url': 'https://www.mtv.com/video-clips/syolsj',
'md5': '1edbcdf1e7628e414a8c5dcebca3d32b',
'info_dict': { 'info_dict': {
'id': '5e14040d-18a4-47c4-a582-43ff602de88e', 'id': '213ea7f8-bac7-4a43-8cd5-8d8cb8c8160f',
'ext': 'mp4', 'ext': 'mp4',
'title': 'Unlocking The Truth|July 18, 2016|1|101|Trailer', 'display_id': 'syolsj',
'description': '"Unlocking the Truth" premieres August 17th at 11/10c.', 'title': 'The Challenge: Vets & New Threats',
'timestamp': 1468846800, 'description': 'md5:c4d2e90a5fff6463740fbf96b2bb6a41',
'upload_date': '20160718', 'duration': 95.0,
'thumbnail': r're:https://images\.paramount\.tech/uri/mgid:arc:imageassetref',
'series': 'The Challenge',
'season': 'Season 41',
'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.com/full-episodes/94tujl/unlocking-the-truth-gates-of-hell-season-1-ep-101', 'url': 'https://www.mtv.com/episodes/uzvigh',
'only_matching': True, 'info_dict': {
}, { 'id': '364e8b9e-e415-11ef-b405-16fff45bc035',
'url': 'http://www.mtv.com/episodes/g8xu7q/teen-mom-2-breaking-the-wall-season-7-ep-713', 'ext': 'mp4',
'only_matching': True, 'display_id': 'uzvigh',
'title': 'CT Tamburello and Johnny Bananas',
'description': 'md5:364cea52001e9c13f92784e3365c6606',
'channel': 'MTV',
'duration': 1260.0,
'thumbnail': r're:https://images\.paramount\.tech/uri/mgid:arc:imageassetref',
'series': 'Ridiculousness',
'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'},
}] }]
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 {
'arcEp': 'mtvjapan.com',
'mgid': uri,
}
class MTVVideoIE(MTVServicesInfoExtractor):
IE_NAME = 'mtv:video'
_VALID_URL = r'''(?x)^https?://
(?:(?: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 = [{
'url': 'http://www.mtv.de/musik/videoclips/2gpnv7/Traum',
'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': {
'id': '1e5a878b-31c5-11e7-a442-0e40cf2fc285',
'ext': 'mp4',
'title': 'Teen Mom 2',
'description': 'md5:dc65e357ef7e1085ed53e9e9d83146a7',
},
'params': {
# rtmp download
'skip_download': True,
},
'skip': 'Blocked at Travis CI',
}, {
'url': 'http://www.mtv.de/news/glolix/77491-mtv-movies-spotlight--pixels--teil-3',
'info_dict': {
'id': 'local_playlist-4e760566473c4c8c5344',
'ext': 'mp4',
'title': 'Article_mtv-movies-spotlight-pixels-teil-3_short-clips_part1',
'description': 'MTV Movies Supercut',
},
'params': {
# rtmp download
'skip_download': True,
},
'skip': 'Das Video kann zur Zeit nicht abgespielt werden.',
}]
_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)

View File

@ -1,224 +1,48 @@
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']
_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',
},
'playlist': [
{
'md5': 'cb8a2afeafb7ae154aca5a64815ec9d6',
'info_dict': {
'id': '85ee8177-d6ce-48f8-9eee-a65364f8a6df',
'ext': 'mp4', 'ext': 'mp4',
'title': 'SpongeBob SquarePants: "A Place for Pets/Lockdown for Love" S1', 'display_id': 'u3smw8',
'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.', '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',
}, },
}, 'params': {'skip_download': 'm3u8'},
{
'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):
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'
_VALID_URL = r'''(?x)
https?://
(?:
(?P<domain>(?:www\.)?nickjr|mundonick\.uol)\.com\.br|
(?:www\.)?nickjr\.[a-z]{2}|
(?:www\.)?nickelodeonjunior\.fr
)
/(?:programas/)?[^/]+/videos/(?:episodios/)?(?P<id>[^/?\#.]+)
'''
_TESTS = [{
'url': 'http://www.nickjr.com.br/patrulha-canina/videos/210-labirinto-de-pipoca/',
'only_matching': True,
}, {
'url': 'http://mundonick.uol.com.br/programas/the-loud-house/videos/muitas-irmas/7ljo9j',
'only_matching': True,
}, {
'url': 'http://www.nickjr.nl/paw-patrol/videos/311-ge-wol-dig-om-terug-te-zijn/',
'only_matching': True,
}, {
'url': 'http://www.nickjr.de/blaze-und-die-monster-maschinen/videos/f6caaf8f-e4e8-4cc1-b489-9380d6dcd059/',
'only_matching': True,
}, {
'url': 'http://www.nickelodeonjunior.fr/paw-patrol-la-pat-patrouille/videos/episode-401-entier-paw-patrol/',
'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):
IE_NAME = 'nick.de'
_VALID_URL = r'https?://(?:www\.)?(?P<host>nick\.(?:de|com\.pl|ch)|nickelodeon\.(?:nl|be|at|dk|no|se))/[^/]+/(?:[^/]+/)*(?P<id>[^/?#&]+)'
_TESTS = [{
'url': 'http://www.nick.de/playlist/3773-top-videos/videos/episode/17306-zu-wasser-und-zu-land-rauchende-erdnusse',
'only_matching': True,
}, {
'url': 'http://www.nick.de/shows/342-icarly',
'only_matching': True,
}, {
'url': 'http://www.nickelodeon.nl/shows/474-spongebob/videos/17403-een-kijkje-in-de-keuken-met-sandy-van-binnenuit',
'only_matching': True,
}, {
'url': 'http://www.nickelodeon.at/playlist/3773-top-videos/videos/episode/77993-das-letzte-gefecht',
'only_matching': True,
}, {
'url': 'http://www.nick.com.pl/seriale/474-spongebob-kanciastoporty/wideo/17412-teatr-to-jest-to-rodeo-oszolom',
'only_matching': True,
}, {
'url': 'http://www.nickelodeon.no/program/2626-bulderhuset/videoer/90947-femteklasse-veronica-vs-vanzilla',
'only_matching': True,
}, {
'url': 'http://www.nickelodeon.dk/serier/2626-hojs-hus/videoer/761-tissepause',
'only_matching': True,
}, {
'url': 'http://www.nickelodeon.se/serier/2626-lugn-i-stormen/videos/998-',
'only_matching': True,
}, {
'url': 'http://www.nick.ch/shows/2304-adventure-time-abenteuerzeit-mit-finn-und-jake',
'only_matching': True,
}, {
'url': 'http://www.nickelodeon.be/afspeellijst/4530-top-videos/videos/episode/73917-inval-broodschapper-lariekoek-arie',
'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(MTVServicesInfoExtractor):
IE_NAME = 'nickelodeonru'
_VALID_URL = r'https?://(?:www\.)nickelodeon\.(?:ru|fr|es|pt|ro|hu|com\.tr)/[^/]+/(?:[^/]+/)*(?P<id>[^/?#&]+)'
_TESTS = [{
'url': 'http://www.nickelodeon.ru/shows/henrydanger/videos/episodes/3-sezon-15-seriya-licenziya-na-polyot/pmomfb#playlist/7airc6',
'only_matching': True,
}, {
'url': 'http://www.nickelodeon.ru/videos/smotri-na-nickelodeon-v-iyule/g9hvh7',
'only_matching': True,
}, {
'url': 'http://www.nickelodeon.fr/programmes/bob-l-eponge/videos/le-marathon-de-booh-kini-bottom-mardi-31-octobre/nfn7z0',
'only_matching': True,
}, {
'url': 'http://www.nickelodeon.es/videos/nickelodeon-consejos-tortitas/f7w7xy',
'only_matching': True,
}, {
'url': 'http://www.nickelodeon.pt/series/spongebob-squarepants/videos/a-bolha-de-tinta-gigante/xutq1b',
'only_matching': True,
}, {
'url': 'http://www.nickelodeon.ro/emisiuni/shimmer-si-shine/video/nahal-din-bomboane/uw5u2k',
'only_matching': True,
}, {
'url': 'http://www.nickelodeon.hu/musorok/spongyabob-kockanadrag/videok/episodes/buborekfujas-az-elszakadt-nadrag/q57iob#playlist/k6te4y',
'only_matching': True,
}, {
'url': 'http://www.nickelodeon.com.tr/programlar/sunger-bob/videolar/kayip-yatak/mgqbjy',
'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}')

View File

@ -1,58 +1,94 @@
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>[^?#]+)'
_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/south-park-you-all-agreed-to-counseling',
'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/south-park-cartman-gets-an-anal-probe-season-1-ep-1',
'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(MTVServicesBaseIE):
class SouthParkEsIE(SouthParkIE): # XXX: Do not subclass from concrete IE
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\.)?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': 'https://southpark.cc.com/es/episodios/er4a32/south-park-aumento-de-peso-4000-temporada-1-ep-2',
'info_dict': { 'info_dict': {
'title': 'Cartman Consigue Una Sonda Anal', 'id': '5fb94f0c-ecfd-11e0-aca6-0026b9414f30',
'description': 'Cartman Consigue Una Sonda Anal', 'ext': 'mp4',
'display_id': 'er4a32/south-park-aumento-de-peso-4000-temporada-1-ep-2',
'title': 'Aumento de peso 4000',
'description': 'md5:a939b4819ea74c245a0cde180de418c0',
'channel': 'Comedy Central',
'duration': 1320.0,
'thumbnail': r're:https://images\.paramount\.tech/uri/mgid:arc:imageassetref:',
'series': 'South Park',
'season': 'Season 1',
'season_number': 1,
'episode': 'Episode 2',
'episode_number': 2,
'timestamp': 872078400,
'upload_date': '19970820',
'release_timestamp': 872078400,
'release_date': '19970820',
}, },
'playlist_count': 4, 'params': {'skip_download': 'm3u8'},
'skip': 'Geo-restricted',
}] }]
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\.)?southpark\.de/(?:en/)?(?:videoclip|collections|episodes|video-clips|folgen)/(?P<id>[^?#]+)'
_GEO_COUNTRIES = ['DE']
_GEO_BYPASS = True
_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,
@ -66,123 +102,253 @@ class SouthParkDeIE(SouthParkIE): # XXX: Do not subclass from concrete IE
# clip # clip
'url': 'https://www.southpark.de/en/video-clips/ct46op/south-park-tooth-fairy-cartman', 'url': 'https://www.southpark.de/en/video-clips/ct46op/south-park-tooth-fairy-cartman',
'info_dict': { 'info_dict': {
'id': 'e99d45ea-ed00-11e0-aca6-0026b9414f30',
'ext': 'mp4', 'ext': 'mp4',
'id': 'e99d45ea-ed00-11e0-aca6-0026b9414f30',
'display_id': 'ct46op/south-park-tooth-fairy-cartman',
'title': 'Tooth Fairy Cartman', 'title': 'Tooth Fairy Cartman',
'description': 'md5:db02e23818b4dc9cb5f0c5a7e8833a68', 'description': 'Cartman steals Butters\' tooth and gets four dollars for it.',
'duration': 93.26,
'thumbnail': r're:https://images\.paramount\.tech/uri/mgid:arc:imageassetref:',
'series': 'South Park',
'season': 'Season 4',
'season_number': 4,
'episode': 'Episode 1',
'episode_number': 1,
'timestamp': 954990360,
'upload_date': '20000406',
'release_timestamp': 954990360,
'release_date': '20000406',
}, },
'params': {'skip_download': 'm3u8'},
}, { }, {
# episode # episode
'url': 'https://www.southpark.de/en/episodes/yy0vjs/south-park-the-pandemic-special-season-24-ep-1', 'url': 'https://www.southpark.de/en/episodes/yy0vjs/south-park-the-pandemic-special-season-24-ep-1',
'info_dict': { 'info_dict': {
'id': 'f5fbd823-04bc-11eb-9b1b-0e40cf2fc285',
'ext': 'mp4', 'ext': 'mp4',
'title': 'South Park', 'id': '230a4f02-f583-11ea-834d-70df2f866ace',
'display_id': 'yy0vjs/south-park-the-pandemic-special-season-24-ep-1',
'title': 'The Pandemic Special',
'description': 'md5:ae0d875eff169dcbed16b21531857ac1', 'description': 'md5:ae0d875eff169dcbed16b21531857ac1',
'channel': 'Comedy Central',
'duration': 2724.0,
'thumbnail': r're:https://images\.paramount\.tech/uri/mgid:arc:imageassetref:',
'series': 'South Park',
'season': 'Season 24',
'season_number': 24,
'episode': 'Episode 1',
'episode_number': 1,
'timestamp': 1601932260,
'upload_date': '20201005',
'release_timestamp': 1601932270,
'release_date': '20201005',
}, },
'params': {'skip_download': 'm3u8'},
}, { }, {
# clip # clip
'url': 'https://www.southpark.de/videoclip/ct46op/south-park-zahnfee-cartman', 'url': 'https://www.southpark.de/videoclip/ct46op/south-park-zahnfee-cartman',
'info_dict': { 'info_dict': {
'id': 'e99d45ea-ed00-11e0-aca6-0026b9414f30',
'ext': 'mp4', 'ext': 'mp4',
'id': 'e99d45ea-ed00-11e0-aca6-0026b9414f30',
'display_id': 'ct46op/south-park-zahnfee-cartman',
'title': 'Zahnfee Cartman', 'title': 'Zahnfee Cartman',
'description': 'md5:b917eec991d388811d911fd1377671ac', 'description': 'md5:b917eec991d388811d911fd1377671ac',
'duration': 93.26,
'thumbnail': r're:https://images\.paramount\.tech/uri/mgid:arc:imageassetref:',
'series': 'South Park',
'season': 'Season 4',
'season_number': 4,
'episode': 'Episode 1',
'episode_number': 1,
'timestamp': 954990360,
'upload_date': '20000406',
'release_timestamp': 954990360,
'release_date': '20000406',
}, },
'params': {'skip_download': 'm3u8'},
}, { }, {
# episode # episode
'url': 'https://www.southpark.de/folgen/242csn/south-park-her-mit-dem-hirn-staffel-1-ep-7', 'url': 'https://www.southpark.de/folgen/4r4367/south-park-katerstimmung-staffel-12-ep-3',
'info_dict': { 'info_dict': {
'id': '607115f3-496f-40c3-8647-2b0bcff486c0',
'ext': 'mp4', 'ext': 'mp4',
'title': 'md5:South Park | Pink Eye | E 0107 | HDSS0107X deu | Version: 634312 | Comedy Central S1', 'id': '68c79aa4-ecfd-11e0-aca6-0026b9414f30',
'display_id': '4r4367/south-park-katerstimmung-staffel-12-ep-3',
'title': 'Katerstimmung',
'description': 'md5:94e0e2cd568ffa635e0725518bb4b180',
'channel': 'Comedy Central',
'duration': 1320.0,
'thumbnail': r're:https://images\.paramount\.tech/uri/mgid:arc:imageassetref:',
'series': 'South Park',
'season': 'Season 12',
'season_number': 12,
'episode': 'Episode 3',
'episode_number': 3,
'timestamp': 1206504000,
'upload_date': '20080326',
'release_timestamp': 1206504000,
'release_date': '20080326',
}, },
'params': {'skip_download': 'm3u8'},
}] }]
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): class SouthParkLatIE(MTVServicesBaseIE):
return
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 = ['MX']
_GEO_BYPASS = True
_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, 'info_dict': {
'ext': 'mp4',
'id': 'e99d45ea-ed00-11e0-aca6-0026b9414f30',
'display_id': 'ct46op/south-park-tooth-fairy-cartman',
'title': 'Tooth Fairy Cartman',
'description': 'Cartman steals Butters\' tooth and gets four dollars for it.',
'duration': 93.26,
'thumbnail': r're:https://images\.paramount\.tech/uri/mgid:arc:imageassetref:',
'series': 'South Park',
'season': 'Season 4',
'season_number': 4,
'episode': 'Episode 1',
'episode_number': 1,
'timestamp': 954990360,
'upload_date': '20000406',
'release_timestamp': 954990360,
'release_date': '20000406',
},
'params': {'skip_download': 'm3u8'},
}, { }, {
'url': 'https://www.southpark.lat/episodios/9h0qbg/south-park-orgia-gatuna-temporada-3-ep-7', 'url': 'https://www.southpark.lat/episodios/9h0qbg/south-park-orgia-gatuna-temporada-3-ep-7',
'only_matching': True, 'info_dict': {
'ext': 'mp4',
'id': '600d273a-ecfd-11e0-aca6-0026b9414f30',
'display_id': '9h0qbg/south-park-orgia-gatuna-temporada-3-ep-7',
'title': 'Orgía Gatuna ',
'description': 'md5:73c6648413f5977026abb792a25c65d5',
'channel': 'Comedy Central',
'duration': 1319.0,
'thumbnail': r're:https://images\.paramount\.tech/uri/mgid:arc:imageassetref:',
'series': 'South Park',
'season': 'Season 3',
'season_number': 3,
'episode': 'Episode 7',
'episode_number': 7,
'timestamp': 931924800,
'upload_date': '19990714',
'release_timestamp': 931924800,
'release_date': '19990714',
},
'params': {'skip_download': 'm3u8'},
}, { }, {
'url': 'https://www.southpark.lat/en/collections/29ve08/south-park-heating-up/lydbrc', 'url': 'https://www.southpark.lat/en/collections/29ve08/south-park-heating-up/lydbrc',
'only_matching': True, 'only_matching': True,
}, {
# clip
'url': 'https://www.southpark.lat/en/video-clips/ct46op/south-park-tooth-fairy-cartman',
'info_dict': {
'id': 'e99d45ea-ed00-11e0-aca6-0026b9414f30',
'ext': 'mp4',
'title': 'Tooth Fairy Cartman',
'description': 'md5:db02e23818b4dc9cb5f0c5a7e8833a68',
},
}, {
# episode
'url': 'https://www.southpark.lat/episodios/9h0qbg/south-park-orgia-gatuna-temporada-3-ep-7',
'info_dict': {
'id': 'f5fbd823-04bc-11eb-9b1b-0e40cf2fc285',
'ext': 'mp4',
'title': 'South Park',
'description': 'md5:ae0d875eff169dcbed16b21531857ac1',
},
}]
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(SouthParkIE): # XXX: Do not subclass from concrete IE
IE_NAME = 'southpark.nl'
_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/'
_TESTS = [{
'url': 'http://www.southpark.nl/full-episodes/s18e06-freemium-isnt-free',
'info_dict': {
'title': 'Freemium Isn\'t Free',
'description': 'Stan is addicted to the new Terrance and Phillip mobile game.',
},
'playlist_mincount': 3,
}] }]
class SouthParkDkIE(SouthParkIE): # XXX: Do not subclass from concrete IE class SouthParkDkIE(MTVServicesBaseIE):
IE_NAME = 'southparkstudios.dk' IE_NAME = 'southparkstudios.nu'
_VALID_URL = r'https?://(?:www\.)?(?P<url>southparkstudios\.(?:dk|nu)/(?:clips|full-episodes|collections)/(?P<id>.+?)(\?|#|$))' _VALID_URL = r'https?://(?:www\.)?southparkstudios\.nu/(?:video-clips|episodes|collections)/(?P<id>[^?#]+)'
_FEED_URL = 'http://www.southparkstudios.dk/feeds/video-player/mrss/' _GEO_COUNTRIES = ['DK']
_GEO_BYPASS = True
_TESTS = [{ _TESTS = [{
'url': 'http://www.southparkstudios.dk/full-episodes/s18e07-grounded-vindaloop', 'url': 'https://www.southparkstudios.nu/episodes/y3uvvc/south-park-grounded-vindaloop-season-18-ep-7',
'info_dict': { 'info_dict': {
'ext': 'mp4',
'id': 'f60690a7-21a7-4ee7-8834-d7099a8707ab',
'display_id': 'y3uvvc/south-park-grounded-vindaloop-season-18-ep-7',
'title': 'Grounded Vindaloop', 'title': 'Grounded Vindaloop',
'description': 'Butters is convinced he\'s living in a virtual reality.', 'description': 'Butters is convinced he\'s living in a virtual reality.',
'channel': 'Comedy Central',
'duration': 1319.0,
'thumbnail': r're:https://images\.paramount\.tech/uri/mgid:arc:imageassetref:',
'series': 'South Park',
'season': 'Season 18',
'season_number': 18,
'episode': 'Episode 7',
'episode_number': 7,
'timestamp': 1415847600,
'upload_date': '20141113',
'release_timestamp': 1415768400,
'release_date': '20141112',
}, },
'playlist_mincount': 3, 'params': {'skip_download': 'm3u8'},
}, { }, {
'url': 'http://www.southparkstudios.dk/collections/2476/superhero-showdown/1', 'url': 'https://www.southparkstudios.nu/collections/8dk7kr/south-park-best-of-south-park/sd5ean',
'only_matching': True, 'only_matching': True,
}, { }, {
'url': 'http://www.southparkstudios.nu/collections/2476/superhero-showdown/1', 'url': 'https://www.southparkstudios.nu/video-clips/k42mrf/south-park-kick-the-baby',
'only_matching': True,
}]
class SouthParkComBrIE(MTVServicesBaseIE):
IE_NAME = 'southparkstudios.com.br'
_VALID_URL = r'https?://(?:www\.)?southparkstudios\.com\.br/(?:en/)?(?:video-clips|episodios|collections|episodes)/(?P<id>[^?#]+)'
_GEO_COUNTRIES = ['BR']
_GEO_BYPASS = True
_TESTS = [{
'url': 'https://www.southparkstudios.com.br/video-clips/3vifo0/south-park-welcome-to-mar-a-lago7',
'info_dict': {
'ext': 'mp4',
'id': 'ccc3e952-7352-11f0-b405-16fff45bc035',
'display_id': '3vifo0/south-park-welcome-to-mar-a-lago7',
'title': 'Welcome to Mar-a-Lago',
'description': 'The President welcomes Mr. Mackey to Mar-a-Lago, a magical place where anything can happen.',
'duration': 139.223,
'thumbnail': r're:https://images\.paramount\.tech/uri/mgid:arc:imageassetref:',
'series': 'South Park',
'season': 'Season 27',
'season_number': 27,
'episode': 'Episode 2',
'episode_number': 2,
'timestamp': 1754546400,
'upload_date': '20250807',
'release_timestamp': 1754546400,
'release_date': '20250807',
},
'params': {'skip_download': 'm3u8'},
}, {
'url': 'https://www.southparkstudios.com.br/episodios/940f8z/south-park-cartman-ganha-uma-sonda-anal-temporada-1-ep-1',
'only_matching': True,
}, {
'url': 'https://www.southparkstudios.com.br/collections/8dk7kr/south-park-best-of-south-park/sd5ean',
'only_matching': True,
}, {
'url': 'https://www.southparkstudios.com.br/en/episodes/5v0oap/south-park-south-park-the-25th-anniversary-concert-ep-1',
'only_matching': True,
}]
class SouthParkCoUkIE(MTVServicesBaseIE):
IE_NAME = 'southparkstudios.co.uk'
_VALID_URL = r'https?://(?:www\.)?southparkstudios\.co\.uk/(?:video-clips|collections|episodes)/(?P<id>[^?#]+)'
_GEO_COUNTRIES = ['UK']
_GEO_BYPASS = True
_TESTS = [{
'url': 'https://www.southparkstudios.co.uk/video-clips/8kabfr/south-park-respectclydesauthority',
'info_dict': {
'ext': 'mp4',
'id': 'f6d9af23-734e-11f0-b405-16fff45bc035',
'display_id': '8kabfr/south-park-respectclydesauthority',
'title': '#RespectClydesAuthority',
'description': 'After learning about Clyde\'s Podcast, Cartman needs to see it for himself.',
'duration': 45.045,
'thumbnail': r're:https://images\.paramount\.tech/uri/mgid:arc:imageassetref:',
'series': 'South Park',
'season': 'Season 27',
'season_number': 27,
'episode': 'Episode 2',
'episode_number': 2,
'timestamp': 1754546400,
'upload_date': '20250807',
'release_timestamp': 1754546400,
'release_date': '20250807',
},
'params': {'skip_download': 'm3u8'},
}, {
'url': 'https://www.southparkstudios.co.uk/episodes/e1yoxn/south-park-imaginationland-season-11-ep-10',
'only_matching': True,
}, {
'url': 'https://www.southparkstudios.co.uk/collections/8dk7kr/south-park-best-of-south-park/sd5ean',
'only_matching': True, 'only_matching': True,
}] }]

View File

@ -1,46 +0,0 @@
from .mtv import MTVServicesInfoExtractor
class BellatorIE(MTVServicesInfoExtractor):
_VALID_URL = r'https?://(?:www\.)?bellator\.com/[^/]+/[\da-z]{6}(?:[/?#&]|$)'
_TESTS = [{
'url': 'http://www.bellator.com/fight/atwr7k/bellator-158-michael-page-vs-evangelista-cyborg',
'info_dict': {
'title': 'Michael Page vs. Evangelista Cyborg',
'description': 'md5:0d917fc00ffd72dd92814963fc6cbb05',
},
'playlist_count': 3,
}, {
'url': 'http://www.bellator.com/video-clips/bw6k7n/bellator-158-foundations-michael-venom-page',
'only_matching': True,
}]
_FEED_URL = 'http://www.bellator.com/feeds/mrss/'
_GEO_COUNTRIES = ['US']
class ParamountNetworkIE(MTVServicesInfoExtractor):
_VALID_URL = r'https?://(?:www\.)?paramountnetwork\.com/[^/]+/[\da-z]{6}(?:[/?#&]|$)'
_TESTS = [{
'url': 'http://www.paramountnetwork.com/episodes/j830qm/lip-sync-battle-joel-mchale-vs-jim-rash-season-2-ep-13',
'info_dict': {
'id': '37ace3a8-1df6-48be-85b8-38df8229e241',
'ext': 'mp4',
'title': 'Lip Sync Battle|April 28, 2016|2|209|Joel McHale Vs. Jim Rash|Act 1',
'description': 'md5:a739ca8f978a7802f67f8016d27ce114',
},
'params': {
# m3u8 download
'skip_download': True,
},
}]
_FEED_URL = 'http://feeds.mtvnservices.com/od/feed/intl-mrss-player-feed'
_GEO_COUNTRIES = ['US']
def _get_feed_query(self, uri):
return {
'arcEp': 'paramountnetwork.com',
'imageEp': 'paramountnetwork.com',
'mgid': uri,
}

View File

@ -1,37 +0,0 @@
from .mtv import MTVServicesInfoExtractor
# TODO: Remove - Reason not used anymore - Service moved to youtube
class TVLandIE(MTVServicesInfoExtractor):
IE_NAME = 'tvland.com'
_VALID_URL = r'https?://(?:www\.)?tvland\.com/(?:video-clips|(?:full-)?episodes)/(?P<id>[^/?#.]+)'
_FEED_URL = 'http://www.tvland.com/feeds/mrss/'
_TESTS = [{
# Geo-restricted. Without a proxy metadata are still there. With a
# proxy it redirects to http://m.tvland.com/app/
'url': 'https://www.tvland.com/episodes/s04pzf/everybody-loves-raymond-the-dog-season-1-ep-19',
'info_dict': {
'description': 'md5:84928e7a8ad6649371fbf5da5e1ad75a',
'title': 'The Dog',
},
'playlist_mincount': 5,
'skip': '404 Not found',
}, {
'url': 'https://www.tvland.com/video-clips/4n87f2/younger-a-first-look-at-younger-season-6',
'md5': 'e2c6389401cf485df26c79c247b08713',
'info_dict': {
'id': '891f7d3c-5b5b-4753-b879-b7ba1a601757',
'ext': 'mp4',
'title': 'Younger|April 30, 2019|6|NO-EPISODE#|A First Look at Younger Season 6',
'description': 'md5:595ea74578d3a888ae878dfd1c7d4ab2',
'upload_date': '20190430',
'timestamp': 1556658000,
},
'params': {
'skip_download': True,
},
}, {
'url': 'http://www.tvland.com/full-episodes/iu0hz6/younger-a-kiss-is-just-a-kiss-season-3-ep-301',
'only_matching': True,
}]

View File

@ -1,33 +1,50 @@
from .mtv import MTVServicesInfoExtractor from .mtv import MTVServicesBaseIE
# TODO: Remove - Reason: Outdated Site
class VH1IE(MTVServicesInfoExtractor): class VH1IE(MTVServicesBaseIE):
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.',
},
'playlist_mincount': 4,
'skip': '404 Not found',
}, {
# Clip
'url': 'https://www.vh1.com/video-clips/e0sja0/nick-cannon-presents-wild-n-out-foushee-clap-for-him',
'info_dict': {
'id': 'a07563f7-a37b-4e7f-af68-85855c2c7cc3',
'ext': 'mp4', 'ext': 'mp4',
'title': 'Fousheé - "clap for him"', 'display_id': 'd06ta1',
'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': 'Barely Famous',
'upload_date': '20210826', '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',
}, },
'params': { 'params': {'skip_download': 'm3u8'},
# m3u8 download }, {
'skip_download': True, 'url': 'https://www.vh1.com/video-clips/ryzt2n/love-hip-hop-miami-love-hip-hop-miami-season-5-recap',
'info_dict': {
'id': '59e62974-4a5c-4417-91c3-5044cb2f4ce2',
'ext': 'mp4',
'display_id': 'ryzt2n',
'title': 'Love & Hip Hop Miami - Season 5 Recap',
'description': 'md5:4e49c65d0007bfc8d06db555a6b76ef0',
'duration': 792.083,
'thumbnail': r're:https://images\.paramount\.tech/uri/mgid:arc:imageassetref:',
'series': 'Love & Hip Hop Miami',
'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>[^/?#.]+)'