Compare commits

...

5 Commits

Author SHA1 Message Date
Stefan Borer
751c52e668
Merge 68ad5cb00d into 1036478d13 2025-01-07 12:44:56 +03:00
dirkf
1036478d13 [YouTube] Endure subtitle URLs are complete
* WEB URLs are, MWEB not
* resolves #33017
2025-01-06 01:39:04 +00:00
dirkf
00ad2b8ca1 [YouTube] Refactor subtitle processing
* move to internal function
* use `traverse-obj()`
2025-01-06 01:24:30 +00:00
dirkf
ab7c61ca29 [YouTube] Apply code style changes, trailing commas, etc 2025-01-06 01:22:16 +00:00
Stefan Borer
68ad5cb00d [playsuisse] Add new extractor 2021-08-15 22:03:44 +02:00
3 changed files with 356 additions and 51 deletions

View File

@ -954,6 +954,7 @@ from .playfm import PlayFMIE
from .playplustv import PlayPlusTVIE
from .plays import PlaysTVIE
from .playstuff import PlayStuffIE
from .playsuisse import PlaySuisseIE
from .playtvak import PlaytvakIE
from .playvid import PlayvidIE
from .playwire import PlaywireIE

View File

@ -0,0 +1,299 @@
# coding: utf-8
from __future__ import unicode_literals
import json
from .common import InfoExtractor
from ..utils import std_headers
class PlaySuisseIE(InfoExtractor):
_MEDIA_URL = 'https://4bbepzm4ef.execute-api.eu-central-1.amazonaws.com/prod/graphql'
_VALID_URL = r'https?://(?:www\.)?playsuisse\.ch/watch/(?P<id1>[0-9]+)'
_TESTS = [
{
'url': 'https://www.playsuisse.ch/watch/763211/0',
'md5': '0d716b7a16c3e6ab784ef817ee9a20c1',
'info_dict': {
'id': '763211',
'ext': 'mp4',
'title': 'Wilder S01E01 - Knochen',
'description': 'md5:8ea7a8076ba000cd9e8bc132fd0afdd8'
}
},
{
'url': 'https://www.playsuisse.ch/watch/808675/0',
'md5': '7aa043e69fea5044db2da8bb58bca239',
'info_dict': {
'id': '808675',
'ext': 'mp4',
'title': 'Der Läufer',
'description': 'md5:'
}
},
{
'url': 'https://www.playsuisse.ch/watch/817913/0',
'md5': '50721c46ca0b3a9836eb61ecb0ed7097',
'info_dict': {
'id': '42',
'ext': 'mp4',
'title': 'Nr. 47 S01E01 - Die Einweihungsparty',
'description': 'md5:'
}
}
]
def _get_media_data(self, media_id):
locale = std_headers.get('locale', 'de').strip()
# TODO find out why the locale has no effect in request
response = self._download_json(
self._MEDIA_URL,
media_id,
data=json.dumps({
'operationName': 'AssetWatch',
'query': self._GRAPHQL_QUERY,
'variables': {
"assetId": media_id
}
}).encode('utf-8'),
headers={'Content-Type': 'application/json', 'locale': locale})
return response['data']['asset']
def _real_extract(self, url):
media_id, = self._VALID_URL_RE.match(url).groups()
media_data = self._get_media_data(media_id)
def _get_title(data):
if data.get('seriesName'):
return '{} S{:02}E{:02} - {}'.format(
data['seriesName'],
int(data['seasonNumber']),
int(data['episodeNumber']),
data['name'])
else:
return data['name']
title = _get_title(media_data)
description = media_data['description']
thumbnails = [
{
'id': thumb['id'],
'url': thumb['url']
}
for key, thumb in media_data.items()
if key.startswith('thumbnail') and thumb is not None
]
formats = []
for media in media_data['medias']:
# NOTE subtitle info is included in the m3u8 file, but it's not supported by ytdl
# https://github.com/ytdl-org/youtube-dl/issues/6106
if media['type'] == 'HLS':
formats.extend(self._extract_m3u8_formats(
media['url'],
media_id,
'mp4',
'm3u8_native',
m3u8_id="HLS",
fatal=False))
# NOTE seems to be 404 for all tested media
elif media['type'] == 'DASH' and False:
formats.extend(self._extract_mpd_formats(
media['url'],
media_id,
mpd_id='dash',
fatal=False
))
return {
'id': media_id,
'title': title,
'description': description,
'thumbnails': thumbnails,
'formats': formats,
}
_GRAPHQL_QUERY = '''\
query AssetWatch($assetId: ID!) {
asset(assetId: $assetId) {
...Asset
__typename
}
}
fragment Asset on Asset {
...AssetDetails
episodes {
...AssetDetails
__typename
}
__typename
}
fragment AssetDetails on Asset {
audioLanguages
awards
bu
contentCategories
contentCodes
contentTypes
contractType
countries
creators
creditsTimeInSecs
description
descriptionLong
directors
downloadable
duration
editorialContentCategoriesDatalab {
id
title
__typename
}
editorialContentMetaCategoriesDatalab {
id
title
__typename
}
endDate
episodeNumber
episodesInSequence
externalId
firstEpisodeDuration
id
image16x9 {
...ImageDetails
__typename
}
image2x3 {
...ImageDetails
__typename
}
image16x9WithTitle {
...ImageDetails
__typename
}
image2x3WithTitle {
...ImageDetails
__typename
}
mainCast
name
nextEpisode {
id
episodeNumber
seasonNumber
numberOfEpisodesInSeason
image16x9 {
...ImageDetails
__typename
}
__typename
}
numberOfSeasons
otherKeyPeople
parentalRating
popularity
premium
presenters
primaryLanguage
productionCompanies
productionCountries
provider
ratings
regions
restrictions
seasons {
seasonNumber
assetIds
__typename
}
seasonNumber
seriesId
seriesName
nextEpisode {
...NextEpisodeDetails
__typename
}
parentId
startDate
subtitleLanguages
tagline
targetAudience
themes
thumbnail16x9 {
...ImageDetails
__typename
}
thumbnail2x3 {
...ImageDetails
__typename
}
thumbnail16x9WithTitle {
...ImageDetails
__typename
}
thumbnail2x3WithTitle {
...ImageDetails
__typename
}
type
writers
year
medias {
...MediaDetails
__typename
}
trailerMedias {
...MediaDetails
__typename
}
sponsors {
...SponsorDetails
__typename
}
sponsorEndDate
__typename
}
fragment ImageDetails on Image {
id
url
alt
__typename
}
fragment MediaDetails on Media {
id
type
url
duration
__typename
}
fragment SponsorDetails on Sponsor {
id
name
description
type
externalId
image16x9 {
...ImageDetails
__typename
}
thumbnail16x9 {
...ImageDetails
__typename
}
__typename
}
fragment NextEpisodeDetails on NextEpisode {
id
episodeNumber
seasonNumber
numberOfEpisodesInSeason
image16x9 {
...ImageDetails
__typename
}
__typename
}
'''

View File

@ -9,6 +9,7 @@ import json
import os.path
import random
import re
import string
import time
import traceback
@ -67,6 +68,7 @@ from ..utils import (
class YoutubeBaseInfoExtractor(InfoExtractor):
"""Provide base functions for Youtube extractors"""
_LOGIN_URL = 'https://accounts.google.com/ServiceLogin'
_TWOFACTOR_URL = 'https://accounts.google.com/signin/challenge'
@ -138,7 +140,7 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
[2, 1, None, 1,
'https://accounts.google.com/ServiceLogin?passive=true&continue=https%3A%2F%2Fwww.youtube.com%2Fsignin%3Fnext%3D%252F%26action_handle_signin%3Dtrue%26hl%3Den%26app%3Ddesktop%26feature%3Dsign_in_button&hl=en&service=youtube&uilel=3&requestPath=%2FServiceLogin&Page=PasswordSeparationSignIn',
None, [], 4],
1, [None, None, []], None, None, None, True
1, [None, None, []], None, None, None, True,
],
username,
]
@ -160,7 +162,7 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
None, 1, None, [1, None, None, None, [password, None, True]],
[
None, None, [2, 1, None, 1, 'https://accounts.google.com/ServiceLogin?passive=true&continue=https%3A%2F%2Fwww.youtube.com%2Fsignin%3Fnext%3D%252F%26action_handle_signin%3Dtrue%26hl%3Den%26app%3Ddesktop%26feature%3Dsign_in_button&hl=en&service=youtube&uilel=3&requestPath=%2FServiceLogin&Page=PasswordSeparationSignIn', None, [], 4],
1, [None, None, []], None, None, None, True
1, [None, None, []], None, None, None, True,
]]
challenge_results = req(
@ -213,7 +215,7 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
user_hash, None, 2, None,
[
9, None, None, None, None, None, None, None,
[None, tfa_code, True, 2]
[None, tfa_code, True, 2],
]]
tfa_results = req(
@ -284,7 +286,7 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
'client': {
'clientName': 'WEB',
'clientVersion': '2.20201021.03.00',
}
},
},
}
@ -385,7 +387,7 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
'client': {
'clientName': 'WEB',
'clientVersion': '2.20201021.03.00',
}
},
},
'query': query,
}
@ -462,7 +464,7 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
# (HTML, videodetails, metadata, renderers)
'name': ('content', 'author', (('ownerChannelName', None), 'title'), ['text']),
'url': ('href', 'ownerProfileUrl', 'vanityChannelUrl',
['navigationEndpoint', 'browseEndpoint', 'canonicalBaseUrl'])
['navigationEndpoint', 'browseEndpoint', 'canonicalBaseUrl']),
}
if any((videodetails, metadata, renderers)):
result = (
@ -671,7 +673,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/8KVIDEO',
'description': '',
'uploader': '8KVIDEO',
'title': 'UHDTV TEST 8K VIDEO.mp4'
'title': 'UHDTV TEST 8K VIDEO.mp4',
},
'params': {
'youtube_include_dash_manifest': True,
@ -711,7 +713,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
'uploader_url': r're:https?://(?:www\.)?youtube\.com/@theamazingatheist',
'title': 'Burning Everyone\'s Koran',
'description': 'SUBSCRIBE: http://www.youtube.com/saturninefilms \r\n\r\nEven Obama has taken a stand against freedom on this issue: http://www.huffingtonpost.com/2010/09/09/obama-gma-interview-quran_n_710282.html',
}
},
},
# Age-gated videos
{
@ -839,7 +841,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
},
'expected_warnings': [
'DASH manifest missing',
]
],
},
# Olympics (https://github.com/ytdl-org/youtube-dl/issues/4431)
{
@ -1820,8 +1822,8 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
# cpn generation algorithm is reverse engineered from base.js.
# In fact it works even with dummy cpn.
CPN_ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_'
cpn = ''.join(CPN_ALPHABET[random.randint(0, 256) & 63] for _ in range(0, 16))
CPN_ALPHABET = string.ascii_letters + string.digits + '-_'
cpn = ''.join(CPN_ALPHABET[random.randint(0, 256) & 63] for _ in range(16))
# more consistent results setting it to right before the end
qs = parse_qs(playback_url)
@ -1881,8 +1883,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
mobj = re.match(cls._VALID_URL, url, re.VERBOSE)
if mobj is None:
raise ExtractorError('Invalid URL: %s' % url)
video_id = mobj.group(2)
return video_id
return mobj.group(2)
def _extract_chapters_from_json(self, data, video_id, duration):
chapters_list = try_get(
@ -2035,7 +2036,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
headers = {
'X-YouTube-Client-Name': '85',
'X-YouTube-Client-Version': '2.0',
'Origin': 'https://www.youtube.com'
'Origin': 'https://www.youtube.com',
}
video_info = self._call_api('player', query, video_id, fatal=False, headers=headers)
@ -2064,8 +2065,8 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
return ''.join([r['text'] for r in runs if isinstance(r.get('text'), compat_str)])
search_meta = (
lambda x: self._html_search_meta(x, webpage, default=None)) \
if webpage else lambda x: None
(lambda x: self._html_search_meta(x, webpage, default=None))
if webpage else lambda _: None)
video_details = player_response.get('videoDetails') or {}
microformat = try_get(
@ -2137,7 +2138,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
def build_fragments(f):
return LazyList({
'url': update_url_query(f['url'], {
'range': '{0}-{1}'.format(range_start, min(range_start + CHUNK_SIZE - 1, f['filesize']))
'range': '{0}-{1}'.format(range_start, min(range_start + CHUNK_SIZE - 1, f['filesize'])),
})
} for range_start in range(0, f['filesize'], CHUNK_SIZE))
@ -2236,7 +2237,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
'protocol': 'http_dash_segments',
'fragments': build_fragments(dct),
} if dct['filesize'] else {
'downloader_options': {'http_chunk_size': CHUNK_SIZE} # No longer useful?
'downloader_options': {'http_chunk_size': CHUNK_SIZE}, # No longer useful?
})
formats.append(dct)
@ -2414,9 +2415,9 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
'is_live': is_live,
}
pctr = try_get(
pctr = traverse_obj(
player_response,
lambda x: x['captions']['playerCaptionsTracklistRenderer'], dict)
('captions', 'playerCaptionsTracklistRenderer', T(dict)))
if pctr:
def process_language(container, base_url, lang_code, query):
lang_subs = []
@ -2430,9 +2431,11 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
})
container[lang_code] = lang_subs
def process_subtitles():
subtitles = {}
for caption_track in (pctr.get('captionTracks') or []):
base_url = caption_track.get('baseUrl')
for caption_track in traverse_obj(pctr, (
'captionTracks', lambda _, v: v.get('baseUrl'))):
base_url = self._yt_urljoin(caption_track['baseUrl'])
if not base_url:
continue
if caption_track.get('kind') != 'asr':
@ -2443,18 +2446,19 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
subtitles, base_url, lang_code, {})
continue
automatic_captions = {}
for translation_language in (pctr.get('translationLanguages') or []):
translation_language_code = translation_language.get('languageCode')
if not translation_language_code:
continue
for translation_language in traverse_obj(pctr, (
'translationLanguages', lambda _, v: v.get('languageCode'))):
translation_language_code = translation_language['languageCode']
process_language(
automatic_captions, base_url, translation_language_code,
{'tlang': translation_language_code})
info['automatic_captions'] = automatic_captions
info['subtitles'] = subtitles
process_subtitles()
parsed_url = compat_urllib_parse_urlparse(url)
for component in [parsed_url.fragment, parsed_url.query]:
for component in (parsed_url.fragment, parsed_url.query):
query = compat_parse_qs(component)
for k, v in query.items():
for d_k, s_ks in [('start', ('start', 't')), ('end', ('end',))]:
@ -2684,7 +2688,7 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
'title': 'Super Cooper Shorts - Shorts',
'uploader': 'Super Cooper Shorts',
'uploader_id': '@SuperCooperShorts',
}
},
}, {
# Channel that does not have a Shorts tab. Test should just download videos on Home tab instead
'url': 'https://www.youtube.com/@emergencyawesome/shorts',
@ -2738,7 +2742,7 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
'description': 'md5:609399d937ea957b0f53cbffb747a14c',
'uploader': 'ThirstForScience',
'uploader_id': '@ThirstForScience',
}
},
}, {
'url': 'https://www.youtube.com/c/ChristophLaimer/playlists',
'only_matching': True,
@ -3037,7 +3041,7 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
'uploader': '3Blue1Brown',
'uploader_id': '@3blue1brown',
'channel_id': 'UCYO_jab_esuFRV4b17AJtAw',
}
},
}]
@classmethod
@ -3335,7 +3339,7 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
'client': {
'clientName': 'WEB',
'clientVersion': client_version,
}
},
}
visitor_data = try_get(context, lambda x: x['client']['visitorData'], compat_str)
@ -3354,7 +3358,7 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
headers['x-goog-visitor-id'] = visitor_data
data['continuation'] = continuation['continuation']
data['clickTracking'] = {
'clickTrackingParams': continuation['itct']
'clickTrackingParams': continuation['itct'],
}
count = 0
retries = 3
@ -3613,7 +3617,7 @@ class YoutubePlaylistIE(InfoExtractor):
'uploader': 'milan',
'uploader_id': '@milan5503',
'channel_id': 'UCEI1-PVPcYXjB73Hfelbmaw',
}
},
}, {
'url': 'http://www.youtube.com/embed/_xDOZElKyNU?list=PLsyOSbh5bs16vubvKePAQ1x3PhKavfBIl',
'playlist_mincount': 455,
@ -3623,7 +3627,7 @@ class YoutubePlaylistIE(InfoExtractor):
'uploader': 'LBK',
'uploader_id': '@music_king',
'channel_id': 'UC21nz3_MesPLqtDqwdvnoxA',
}
},
}, {
'url': 'TLGGrESM50VT6acwMjAyMjAxNw',
'only_matching': True,
@ -3734,7 +3738,7 @@ class YoutubeSearchIE(SearchInfoExtractor, YoutubeBaseInfoExtractor):
'info_dict': {
'id': 'youtube-dl test video',
'title': 'youtube-dl test video',
}
},
}]
def _get_n_results(self, query, n):
@ -3754,7 +3758,7 @@ class YoutubeSearchDateIE(YoutubeSearchIE):
'info_dict': {
'id': 'youtube-dl test video',
'title': 'youtube-dl test video',
}
},
}]
@ -3769,7 +3773,7 @@ class YoutubeSearchURLIE(YoutubeBaseInfoExtractor):
'id': 'youtube-dl test video',
'title': 'youtube-dl test video',
},
'params': {'playlistend': 5}
'params': {'playlistend': 5},
}, {
'url': 'https://www.youtube.com/results?q=test&sp=EgQIBBgB',
'only_matching': True,
@ -3785,6 +3789,7 @@ class YoutubeSearchURLIE(YoutubeBaseInfoExtractor):
class YoutubeFeedsInfoExtractor(YoutubeTabIE):
"""
Base class for feed extractors
Subclasses must define the _FEED_NAME property.
"""
_LOGIN_REQUIRED = True