Compare commits

..

No commits in common. "0eed3fe530d6ff4b668494c5b1d4d6fc1ade96f7" and "854fded114f3b7b33693c2d3418575d04014aa4b" have entirely different histories.

4 changed files with 142 additions and 107 deletions

View File

@ -457,8 +457,6 @@ class FFmpegFD(ExternalFD):
@classmethod @classmethod
def available(cls, path=None): def available(cls, path=None):
# TODO: Fix path for ffmpeg
# Fixme: This may be wrong when --ffmpeg-location is used
return FFmpegPostProcessor().available return FFmpegPostProcessor().available
def on_process_started(self, proc, stdin): def on_process_started(self, proc, stdin):

View File

@ -1,9 +1,14 @@
import re
from .common import InfoExtractor from .common import InfoExtractor
from ..utils import ( from ..utils import (
ExtractorError,
float_or_none,
format_field,
int_or_none, int_or_none,
url_or_none, str_or_none,
traverse_obj,
) )
from ..utils.traversal import traverse_obj
class MedalTVIE(InfoExtractor): class MedalTVIE(InfoExtractor):
@ -25,8 +30,25 @@ class MedalTVIE(InfoExtractor):
'view_count': int, 'view_count': int,
'like_count': int, 'like_count': int,
'duration': 13, 'duration': 13,
'thumbnail': r're:https://cdn\.medal\.tv/ugcp/content-thumbnail/.*\.jpg', },
'tags': ['headshot', 'valorant', '4k', 'clutch', 'mornu'], }, {
'url': 'https://medal.tv/games/cod-cold-war/clips/2mA60jWAGQCBH',
'md5': 'fc7a3e4552ae8993c1c4006db46be447',
'info_dict': {
'id': '2mA60jWAGQCBH',
'ext': 'mp4',
'title': 'Quad Cold',
'description': 'Medal,https://medal.tv/desktop/',
'uploader': 'MowgliSB',
'timestamp': 1603165266,
'upload_date': '20201020',
'uploader_id': '10619174',
'thumbnail': 'https://cdn.medal.tv/10619174/thumbnail-34934644-720p.jpg?t=1080p&c=202042&missing',
'uploader_url': 'https://medal.tv/users/10619174',
'comment_count': int,
'view_count': int,
'like_count': int,
'duration': 23,
}, },
}, { }, {
'url': 'https://medal.tv/games/cod-cold-war/clips/2um24TWdty0NA', 'url': 'https://medal.tv/games/cod-cold-war/clips/2um24TWdty0NA',
@ -35,90 +57,104 @@ class MedalTVIE(InfoExtractor):
'id': '2um24TWdty0NA', 'id': '2um24TWdty0NA',
'ext': 'mp4', 'ext': 'mp4',
'title': 'u tk me i tk u bigger', 'title': 'u tk me i tk u bigger',
'description': '', 'description': 'Medal,https://medal.tv/desktop/',
'uploader': 'zahl', 'uploader': 'Mimicc',
'timestamp': 1605580939, 'timestamp': 1605580939,
'upload_date': '20201117', 'upload_date': '20201117',
'uploader_id': '5156321', 'uploader_id': '5156321',
'thumbnail': r're:https://cdn\.medal\.tv/source/.*\.png', 'thumbnail': 'https://cdn.medal.tv/5156321/thumbnail-36787208-360p.jpg?t=1080p&c=202046&missing',
'uploader_url': 'https://medal.tv/users/5156321', 'uploader_url': 'https://medal.tv/users/5156321',
'comment_count': int, 'comment_count': int,
'view_count': int, 'view_count': int,
'like_count': int, 'like_count': int,
'duration': 9, 'duration': 9,
}, },
}, {
# API requires auth
'url': 'https://medal.tv/games/valorant/clips/2WRj40tpY_EU9',
'md5': '6c6bb6569777fd8b4ef7b33c09de8dcf',
'info_dict': {
'id': '2WRj40tpY_EU9',
'ext': 'mp4',
'title': '1v5 clutch',
'description': '',
'uploader': 'adny',
'uploader_id': '6256941',
'uploader_url': 'https://medal.tv/users/6256941',
'comment_count': int,
'view_count': int,
'like_count': int,
'duration': 25,
'thumbnail': r're:https://cdn\.medal\.tv/source/.*\.jpg',
'timestamp': 1612896680,
'upload_date': '20210209',
},
'expected_warnings': ['Video formats are not available through API'],
}, { }, {
'url': 'https://medal.tv/games/valorant/clips/37rMeFpryCC-9', 'url': 'https://medal.tv/games/valorant/clips/37rMeFpryCC-9',
'only_matching': True, 'only_matching': True,
}, {
'url': 'https://medal.tv/games/valorant/clips/2WRj40tpY_EU9',
'only_matching': True,
}] }]
def _real_extract(self, url): def _real_extract(self, url):
video_id = self._match_id(url) video_id = self._match_id(url)
content_data = self._download_json( webpage = self._download_webpage(url, video_id, query={'mobilebypass': 'true'})
f'https://medal.tv/api/content/{video_id}', video_id,
headers={'Accept': 'application/json'}) hydration_data = self._search_json(
r'<script[^>]*>[^<]*\bhydrationData\s*=', webpage,
'next data', video_id, end_pattern='</script>', fatal=False)
clip = traverse_obj(hydration_data, ('clips', ...), get_all=False)
if not clip:
raise ExtractorError(
'Could not find video information.', video_id=video_id)
title = clip['contentTitle']
source_width = int_or_none(clip.get('sourceWidth'))
source_height = int_or_none(clip.get('sourceHeight'))
aspect_ratio = source_width / source_height if source_width and source_height else 16 / 9
def add_item(container, item_url, height, id_key='format_id', item_id=None):
item_id = item_id or '%dp' % height
if item_id not in item_url:
return
container.append({
'url': item_url,
id_key: item_id,
'width': round(aspect_ratio * height),
'height': height,
})
formats = [] formats = []
if m3u8_url := url_or_none(content_data.get('contentUrlHls')): thumbnails = []
formats.extend(self._extract_m3u8_formats(m3u8_url, video_id, 'mp4', m3u8_id='hls')) for k, v in clip.items():
if http_url := url_or_none(content_data.get('contentUrl')): if not (v and isinstance(v, str)):
formats.append({ continue
'url': http_url, mobj = re.match(r'(contentUrl|thumbnail)(?:(\d+)p)?$', k)
'format_id': 'http-source', if not mobj:
'ext': 'mp4', continue
'quality': 1, prefix = mobj.group(1)
}) height = int_or_none(mobj.group(2))
formats = [fmt for fmt in formats if 'video/privacy-protected-guest' not in fmt['url']] if prefix == 'contentUrl':
if not formats: add_item(
# Fallback, does not require auth formats, v, height or source_height,
self.report_warning('Video formats are not available through API, falling back to social video URL') item_id=None if height else 'source')
urlh = self._request_webpage( elif prefix == 'thumbnail':
f'https://medal.tv/api/content/{video_id}/socialVideoUrl', video_id, add_item(thumbnails, v, height, 'id')
note='Checking social video URL')
formats.append({ error = clip.get('error')
'url': urlh.url, if not formats and error:
'format_id': 'social-video', if error == 404:
'ext': 'mp4', self.raise_no_formats(
'quality': -1, 'That clip does not exist.',
}) expected=True, video_id=video_id)
else:
self.raise_no_formats(
f'An unknown error occurred ({error}).',
video_id=video_id)
# Necessary because the id of the author is not known in advance.
# Won't raise an issue if no profile can be found as this is optional.
author = traverse_obj(hydration_data, ('profiles', ...), get_all=False) or {}
author_id = str_or_none(author.get('userId'))
author_url = format_field(author_id, None, 'https://medal.tv/users/%s')
return { return {
'id': video_id, 'id': video_id,
'title': title,
'formats': formats, 'formats': formats,
**traverse_obj(content_data, { 'thumbnails': thumbnails,
'title': ('contentTitle', {str}), 'description': clip.get('contentDescription'),
'description': ('contentDescription', {str}), 'uploader': author.get('displayName'),
'timestamp': ('created', {int_or_none(scale=1000)}), 'timestamp': float_or_none(clip.get('created'), 1000),
'duration': ('videoLengthSeconds', {int_or_none}), 'uploader_id': author_id,
'view_count': ('views', {int_or_none}), 'uploader_url': author_url,
'like_count': ('likes', {int_or_none}), 'duration': int_or_none(clip.get('videoLengthSeconds')),
'comment_count': ('comments', {int_or_none}), 'view_count': int_or_none(clip.get('views')),
'uploader': ('poster', 'displayName', {str}), 'like_count': int_or_none(clip.get('likes')),
'uploader_id': ('poster', 'userId', {str}), 'comment_count': int_or_none(clip.get('comments')),
'uploader_url': ('poster', 'userId', {str}, filter, {lambda x: x and f'https://medal.tv/users/{x}'}),
'tags': ('tags', ..., {str}),
'thumbnail': ('thumbnailUrl', {url_or_none}),
}),
} }

View File

@ -1,17 +1,18 @@
import urllib.parse import json
from .brightcove import BrightcoveNewIE from .brightcove import BrightcoveNewIE
from .common import InfoExtractor from .common import InfoExtractor
from .zype import ZypeIE from .zype import ZypeIE
from ..networking import HEADRequest from ..networking import HEADRequest
from ..networking.exceptions import HTTPError
from ..utils import ( from ..utils import (
ExtractorError, ExtractorError,
filter_dict, filter_dict,
parse_qs, parse_qs,
smuggle_url, smuggle_url,
try_call,
urlencode_postdata, urlencode_postdata,
) )
from ..utils.traversal import traverse_obj
class ThisOldHouseIE(InfoExtractor): class ThisOldHouseIE(InfoExtractor):
@ -76,43 +77,46 @@ class ThisOldHouseIE(InfoExtractor):
'only_matching': True, 'only_matching': True,
}] }]
def _perform_login(self, username, password): _LOGIN_URL = 'https://login.thisoldhouse.com/usernamepassword/login'
login_page = self._download_webpage(
'https://www.thisoldhouse.com/insider-login', None, 'Downloading login page')
hidden_inputs = self._hidden_inputs(login_page)
response = self._download_json(
'https://www.thisoldhouse.com/wp-admin/admin-ajax.php', None, 'Logging in',
headers={
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
}, data=urlencode_postdata(filter_dict({
'action': 'onebill_subscriber_login',
'email': username,
'password': password,
'pricingPlanTerm': hidden_inputs['pricing_plan_term'],
'utm_parameters': hidden_inputs.get('utm_parameters'),
'nonce': hidden_inputs['mdcr_onebill_login_nonce'],
})))
message = traverse_obj(response, ('data', 'message', {str})) def _perform_login(self, username, password):
if not response['success']: self._request_webpage(
if message and 'Something went wrong' in message: HEADRequest('https://www.thisoldhouse.com/insider'), None, 'Requesting session cookies')
urlh = self._request_webpage(
'https://www.thisoldhouse.com/wp-login.php', None, 'Requesting login info',
errnote='Unable to login', query={'redirect_to': 'https://www.thisoldhouse.com/insider'})
try:
auth_form = self._download_webpage(
self._LOGIN_URL, None, 'Submitting credentials', headers={
'Content-Type': 'application/json',
'Referer': urlh.url,
}, data=json.dumps(filter_dict({
**{('client_id' if k == 'client' else k): v[0] for k, v in parse_qs(urlh.url).items()},
'tenant': 'thisoldhouse',
'username': username,
'password': password,
'popup_options': {},
'sso': True,
'_csrf': try_call(lambda: self._get_cookies(self._LOGIN_URL)['_csrf'].value),
'_intstate': 'deprecated',
}), separators=(',', ':')).encode())
except ExtractorError as e:
if isinstance(e.cause, HTTPError) and e.cause.status == 401:
raise ExtractorError('Invalid username or password', expected=True) raise ExtractorError('Invalid username or password', expected=True)
raise ExtractorError(message or 'Login was unsuccessful') raise
if message and 'Your subscription is not active' in message:
self.report_warning( self._request_webpage(
f'{self.IE_NAME} said your subscription is not active. ' 'https://login.thisoldhouse.com/login/callback', None, 'Completing login',
f'If your subscription is active, this could be caused by too many sign-ins, ' data=urlencode_postdata(self._hidden_inputs(auth_form)))
f'and you should instead try using {self._login_hint(method="cookies")[4:]}')
else:
self.write_debug(f'{self.IE_NAME} said: {message}')
def _real_extract(self, url): def _real_extract(self, url):
display_id = self._match_id(url) display_id = self._match_id(url)
webpage, urlh = self._download_webpage_handle(url, display_id) webpage = self._download_webpage(url, display_id)
# If login response says inactive subscription, site redirects to frontpage for Insider content if 'To Unlock This content' in webpage:
if 'To Unlock This content' in webpage or urllib.parse.urlparse(urlh.url).path in ('', '/'): self.raise_login_required(
self.raise_login_required('This video is only available for subscribers') 'This video is only available for subscribers. '
'Note that --cookies-from-browser may not work due to this site using session cookies')
video_url, video_id = self._search_regex( video_url, video_id = self._search_regex(
r'<iframe[^>]+src=[\'"]((?:https?:)?//(?:www\.)?thisoldhouse\.(?:chorus\.build|com)/videos/zype/([0-9a-f]{24})[^\'"]*)[\'"]', r'<iframe[^>]+src=[\'"]((?:https?:)?//(?:www\.)?thisoldhouse\.(?:chorus\.build|com)/videos/zype/([0-9a-f]{24})[^\'"]*)[\'"]',

View File

@ -192,10 +192,7 @@ class FFmpegPostProcessor(PostProcessor):
@property @property
def available(self): def available(self):
# If we return that ffmpeg is available, then the basename property *must* be run return bool(self._ffmpeg_location.get()) or self.basename is not None
# (as doing so has side effects), and its value can never be None
# See: https://github.com/yt-dlp/yt-dlp/issues/12829
return self.basename is not None
@property @property
def executable(self): def executable(self):