1
0
mirror of https://github.com/yt-dlp/yt-dlp synced 2025-12-16 14:15:41 +07:00

Compare commits

...

3 Commits

Author SHA1 Message Date
bashonly
0eed3fe530 [pp/ffmpeg] Fix uncaught error if bad --ffmpeg-location is given (#15104)
Revert 9f77e04c76

Closes #12829
Authored by: bashonly
2025-11-19 00:23:00 +00:00
sepro
a4c72acc46 [ie/MedalTV] Rework extractor (#15103)
Closes #15102
Authored by: seproDev
2025-11-19 00:52:55 +01:00
bashonly
9daba4f442 [ie/thisoldhouse] Fix login support (#15097)
Closes #14931
Authored by: bashonly
2025-11-18 23:08:21 +00:00
4 changed files with 106 additions and 141 deletions

View File

@@ -457,6 +457,8 @@ 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,14 +1,9 @@
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,
str_or_none, url_or_none,
traverse_obj,
) )
from ..utils.traversal import traverse_obj
class MedalTVIE(InfoExtractor): class MedalTVIE(InfoExtractor):
@@ -30,25 +25,8 @@ 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',
@@ -57,12 +35,12 @@ 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': 'Medal,https://medal.tv/desktop/', 'description': '',
'uploader': 'Mimicc', 'uploader': 'zahl',
'timestamp': 1605580939, 'timestamp': 1605580939,
'upload_date': '20201117', 'upload_date': '20201117',
'uploader_id': '5156321', 'uploader_id': '5156321',
'thumbnail': 'https://cdn.medal.tv/5156321/thumbnail-36787208-360p.jpg?t=1080p&c=202046&missing', 'thumbnail': r're:https://cdn\.medal\.tv/source/.*\.png',
'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,
@@ -70,91 +48,77 @@ class MedalTVIE(InfoExtractor):
'duration': 9, 'duration': 9,
}, },
}, { }, {
'url': 'https://medal.tv/games/valorant/clips/37rMeFpryCC-9', # API requires auth
'only_matching': True,
}, {
'url': 'https://medal.tv/games/valorant/clips/2WRj40tpY_EU9', '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',
'only_matching': True, '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)
webpage = self._download_webpage(url, video_id, query={'mobilebypass': 'true'}) content_data = self._download_json(
f'https://medal.tv/api/content/{video_id}', video_id,
hydration_data = self._search_json( headers={'Accept': 'application/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 = []
thumbnails = [] if m3u8_url := url_or_none(content_data.get('contentUrlHls')):
for k, v in clip.items(): formats.extend(self._extract_m3u8_formats(m3u8_url, video_id, 'mp4', m3u8_id='hls'))
if not (v and isinstance(v, str)): if http_url := url_or_none(content_data.get('contentUrl')):
continue formats.append({
mobj = re.match(r'(contentUrl|thumbnail)(?:(\d+)p)?$', k) 'url': http_url,
if not mobj: 'format_id': 'http-source',
continue 'ext': 'mp4',
prefix = mobj.group(1) 'quality': 1,
height = int_or_none(mobj.group(2)) })
if prefix == 'contentUrl': formats = [fmt for fmt in formats if 'video/privacy-protected-guest' not in fmt['url']]
add_item( if not formats:
formats, v, height or source_height, # Fallback, does not require auth
item_id=None if height else 'source') self.report_warning('Video formats are not available through API, falling back to social video URL')
elif prefix == 'thumbnail': urlh = self._request_webpage(
add_item(thumbnails, v, height, 'id') f'https://medal.tv/api/content/{video_id}/socialVideoUrl', video_id,
note='Checking social video URL')
error = clip.get('error') formats.append({
if not formats and error: 'url': urlh.url,
if error == 404: 'format_id': 'social-video',
self.raise_no_formats( 'ext': 'mp4',
'That clip does not exist.', 'quality': -1,
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,
'thumbnails': thumbnails, **traverse_obj(content_data, {
'description': clip.get('contentDescription'), 'title': ('contentTitle', {str}),
'uploader': author.get('displayName'), 'description': ('contentDescription', {str}),
'timestamp': float_or_none(clip.get('created'), 1000), 'timestamp': ('created', {int_or_none(scale=1000)}),
'uploader_id': author_id, 'duration': ('videoLengthSeconds', {int_or_none}),
'uploader_url': author_url, 'view_count': ('views', {int_or_none}),
'duration': int_or_none(clip.get('videoLengthSeconds')), 'like_count': ('likes', {int_or_none}),
'view_count': int_or_none(clip.get('views')), 'comment_count': ('comments', {int_or_none}),
'like_count': int_or_none(clip.get('likes')), 'uploader': ('poster', 'displayName', {str}),
'comment_count': int_or_none(clip.get('comments')), 'uploader_id': ('poster', 'userId', {str}),
'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,18 +1,17 @@
import json import urllib.parse
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):
@@ -77,46 +76,43 @@ class ThisOldHouseIE(InfoExtractor):
'only_matching': True, 'only_matching': True,
}] }]
_LOGIN_URL = 'https://login.thisoldhouse.com/usernamepassword/login'
def _perform_login(self, username, password): def _perform_login(self, username, password):
self._request_webpage( login_page = self._download_webpage(
HEADRequest('https://www.thisoldhouse.com/insider'), None, 'Requesting session cookies') 'https://www.thisoldhouse.com/insider-login', None, 'Downloading login page')
urlh = self._request_webpage( hidden_inputs = self._hidden_inputs(login_page)
'https://www.thisoldhouse.com/wp-login.php', None, 'Requesting login info', response = self._download_json(
errnote='Unable to login', query={'redirect_to': 'https://www.thisoldhouse.com/insider'}) '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'],
})))
try: message = traverse_obj(response, ('data', 'message', {str}))
auth_form = self._download_webpage( if not response['success']:
self._LOGIN_URL, None, 'Submitting credentials', headers={ if message and 'Something went wrong' in message:
'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 raise ExtractorError(message or 'Login was unsuccessful')
if message and 'Your subscription is not active' in message:
self._request_webpage( self.report_warning(
'https://login.thisoldhouse.com/login/callback', None, 'Completing login', f'{self.IE_NAME} said your subscription is not active. '
data=urlencode_postdata(self._hidden_inputs(auth_form))) f'If your subscription is active, this could be caused by too many sign-ins, '
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 = self._download_webpage(url, display_id) webpage, urlh = self._download_webpage_handle(url, display_id)
if 'To Unlock This content' in webpage: # If login response says inactive subscription, site redirects to frontpage for Insider content
self.raise_login_required( if 'To Unlock This content' in webpage or urllib.parse.urlparse(urlh.url).path in ('', '/'):
'This video is only available for subscribers. ' self.raise_login_required('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,7 +192,10 @@ def _probe_version(self):
@property @property
def available(self): def available(self):
return bool(self._ffmpeg_location.get()) or self.basename is not None # If we return that ffmpeg is available, then the basename property *must* be run
# (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):