Compare commits

..

No commits in common. "master" and "2026.06.09" have entirely different histories.

8 changed files with 20 additions and 102 deletions

View File

@ -327,12 +327,6 @@ class TestUtil(unittest.TestCase):
with self.assertRaises(_UnsafeExtensionError): with self.assertRaises(_UnsafeExtensionError):
prepend_extension('abc.unexpected_ext', ext, 'ext') prepend_extension('abc.unexpected_ext', ext, 'ext')
# Test allow-unsafe-ext compat option
_UnsafeExtensionError._enabled = False
self.assertEqual(prepend_extension('abc.ext', 'un/safe'), 'abc.un/safe.ext')
# Re-enable sanitization for other tests
_UnsafeExtensionError._enabled = True
def test_replace_extension(self): def test_replace_extension(self):
self.assertEqual(replace_extension('abc.ext', 'temp'), 'abc.temp') self.assertEqual(replace_extension('abc.ext', 'temp'), 'abc.temp')
self.assertEqual(replace_extension('abc.ext', 'temp', 'ext'), 'abc.temp') self.assertEqual(replace_extension('abc.ext', 'temp', 'ext'), 'abc.temp')
@ -351,12 +345,6 @@ class TestUtil(unittest.TestCase):
with self.assertRaises(_UnsafeExtensionError): with self.assertRaises(_UnsafeExtensionError):
replace_extension('abc.unexpected_ext', ext, 'ext') replace_extension('abc.unexpected_ext', ext, 'ext')
# Test allow-unsafe-ext compat option
_UnsafeExtensionError._enabled = False
self.assertEqual(replace_extension('abc.ext', 'bin'), 'abc.bin')
# Re-enable sanitization for other tests
_UnsafeExtensionError._enabled = True
def test_subtitles_filename(self): def test_subtitles_filename(self):
self.assertEqual(subtitles_filename('abc.ext', 'en', 'vtt'), 'abc.en.vtt') self.assertEqual(subtitles_filename('abc.ext', 'en', 'vtt'), 'abc.en.vtt')
self.assertEqual(subtitles_filename('abc.ext', 'en', 'vtt', 'ext'), 'abc.en.vtt') self.assertEqual(subtitles_filename('abc.ext', 'en', 'vtt', 'ext'), 'abc.en.vtt')
@ -2172,10 +2160,6 @@ Line 1
headers6 = HTTPHeaderDict(a=1, b=2) headers6 = HTTPHeaderDict(a=1, b=2)
self.assertEqual(pickle.loads(pickle.dumps(headers6)), headers6) self.assertEqual(pickle.loads(pickle.dumps(headers6)), headers6)
headers7 = HTTPHeaderDict()
headers7 |= {'X-dlp': 'data'}
self.assertEqual(headers7.sensitive(), {'X-dlp': 'data'})
def test_extract_basic_auth(self): def test_extract_basic_auth(self):
assert extract_basic_auth('http://:foo.bar') == ('http://:foo.bar', None) assert extract_basic_auth('http://:foo.bar') == ('http://:foo.bar', None)
assert extract_basic_auth('http://foo.bar') == ('http://foo.bar', None) assert extract_basic_auth('http://foo.bar') == ('http://foo.bar', None)

View File

@ -619,7 +619,7 @@ def validate_options(opts):
warnings.append( warnings.append(
'Using allow-unsafe-ext opens you up to potential attacks. ' 'Using allow-unsafe-ext opens you up to potential attacks. '
'Use with great care!') 'Use with great care!')
_UnsafeExtensionError._enabled = False _UnsafeExtensionError.sanitize_extension = lambda x, prepend=False: x
return warnings, deprecation_warnings return warnings, deprecation_warnings

View File

@ -209,7 +209,7 @@ class CurlFD(ExternalFD):
return False return False
cls.exe = path cls.exe = path
cls._curl_version = version_tuple(parts[1], lenient=True) cls._curl_version = version_tuple(parts[1])
return path return path
def _make_cmd(self, tmpfilename, info_dict): def _make_cmd(self, tmpfilename, info_dict):

View File

@ -420,10 +420,10 @@ class BandcampWeeklyIE(BandcampIE): # XXX: Do not subclass from concrete IE
'info_dict': { 'info_dict': {
'id': '224', 'id': '224',
'ext': 'mp3', 'ext': 'mp3',
'title': 'Magic Moments, 2017-04-04', 'title': 'Bandcamp Weekly, 2017-04-04',
'description': 'md5:5d48150916e8e02d030623a48512c874', 'description': 'md5:5d48150916e8e02d030623a48512c874',
'thumbnail': 'https://f4.bcbits.com/img/9982549_0.jpg', 'thumbnail': 'https://f4.bcbits.com/img/9982549_0.jpg',
'series': 'Magic Moments', 'series': 'Bandcamp Weekly',
'episode_id': '224', 'episode_id': '224',
'release_timestamp': 1491264000, 'release_timestamp': 1491264000,
'release_date': '20170404', 'release_date': '20170404',
@ -440,10 +440,10 @@ class BandcampWeeklyIE(BandcampIE): # XXX: Do not subclass from concrete IE
def _real_extract(self, url): def _real_extract(self, url):
show_id = self._match_id(url) show_id = self._match_id(url)
show_data = self._download_json( show_data = self._download_json(
'https://bandcamp.com/api/player/2/player_data_web', 'https://bandcamp.com/api/bcradio_api/1/get_show',
show_id, 'Downloading radio show JSON', show_id, 'Downloading radio show JSON',
data=json.dumps({'item_id': int(show_id), 'item_type': 'radio'}).encode(), data=json.dumps({'id': show_id}).encode(),
headers={'Content-Type': 'application/json'})['tracklist'] headers={'Content-Type': 'application/json'})
audio_data = show_data['compiledTrack'] audio_data = show_data['compiledTrack']
stream_url = audio_data['streamUrl'] stream_url = audio_data['streamUrl']

View File

@ -65,9 +65,8 @@ class PatreonBaseIE(InfoExtractor):
class PatreonIE(PatreonBaseIE): class PatreonIE(PatreonBaseIE):
IE_NAME = 'patreon' IE_NAME = 'patreon'
_VALID_URL = r'https?://(?:www\.)?patreon\.com/(?:creation\?hid=|(?:[^/?#]+/)?posts/(?:[\w-]+-)?)(?P<id>\d+)' _VALID_URL = r'https?://(?:www\.)?patreon\.com/(?:creation\?hid=|posts/(?:[\w-]+-)?)(?P<id>\d+)'
_TESTS = [{ _TESTS = [{
# FIXME: Fails due to no description extracted
'url': 'http://www.patreon.com/creation?hid=743933', 'url': 'http://www.patreon.com/creation?hid=743933',
'md5': 'e25505eec1053a6e6813b8ed369875cc', 'md5': 'e25505eec1053a6e6813b8ed369875cc',
'info_dict': { 'info_dict': {
@ -108,17 +107,17 @@ class PatreonIE(PatreonBaseIE):
'id': 'SU4fj_aEMVw', 'id': 'SU4fj_aEMVw',
'ext': 'mp4', 'ext': 'mp4',
'title': 'I\'m on Patreon!', 'title': 'I\'m on Patreon!',
'uploader': 'Traci Oden', 'uploader': 'TraciJHines',
'thumbnail': 're:^https?://.*$', 'thumbnail': 're:^https?://.*$',
'upload_date': '20150211', 'upload_date': '20150211',
'description': 'md5:8af6425f50bd46fbf29f3db0fc3a8364', 'description': 'md5:8af6425f50bd46fbf29f3db0fc3a8364',
'uploader_id': '@TraciOden', 'uploader_id': '@TraciHinesMusic',
'categories': ['Entertainment'], 'categories': ['Entertainment'],
'duration': 282, 'duration': 282,
'view_count': int, 'view_count': int,
'tags': 'count:39', 'tags': 'count:39',
'age_limit': 0, 'age_limit': 0,
'channel': 'Traci Oden', 'channel': 'TraciJHines',
'channel_url': 'https://www.youtube.com/channel/UCGLim4T2loE5rwCMdpCIPVg', 'channel_url': 'https://www.youtube.com/channel/UCGLim4T2loE5rwCMdpCIPVg',
'live_status': 'not_live', 'live_status': 'not_live',
'like_count': int, 'like_count': int,
@ -126,7 +125,7 @@ class PatreonIE(PatreonBaseIE):
'availability': 'public', 'availability': 'public',
'channel_follower_count': int, 'channel_follower_count': int,
'playable_in_embed': True, 'playable_in_embed': True,
'uploader_url': 'https://www.youtube.com/@TraciOden', 'uploader_url': 'https://www.youtube.com/@TraciHinesMusic',
'comment_count': int, 'comment_count': int,
'channel_is_verified': True, 'channel_is_verified': True,
'chapters': 'count:4', 'chapters': 'count:4',
@ -158,7 +157,6 @@ class PatreonIE(PatreonBaseIE):
}, },
'skip': 'Patron-only content', 'skip': 'Patron-only content',
}, { }, {
# FIXME: Fails due to no description extracted
# m3u8 video (https://github.com/yt-dlp/yt-dlp/issues/2277) # m3u8 video (https://github.com/yt-dlp/yt-dlp/issues/2277)
'url': 'https://www.patreon.com/posts/video-sketchbook-32452882', 'url': 'https://www.patreon.com/posts/video-sketchbook-32452882',
'info_dict': { 'info_dict': {
@ -222,7 +220,6 @@ class PatreonIE(PatreonBaseIE):
'channel_id': '2147162', 'channel_id': '2147162',
'uploader_url': 'https://www.patreon.com/yaboyroshi', 'uploader_url': 'https://www.patreon.com/yaboyroshi',
}, },
'skip': 'HTTP Error 401 for m3u8 request; site now requires login to play the video',
}, { }, {
# NSFW vimeo embed URL # NSFW vimeo embed URL
'url': 'https://www.patreon.com/posts/4k-spiderman-4k-96414599', 'url': 'https://www.patreon.com/posts/4k-spiderman-4k-96414599',
@ -245,7 +242,6 @@ class PatreonIE(PatreonBaseIE):
}, },
'params': {'skip_download': 'm3u8'}, 'params': {'skip_download': 'm3u8'},
'expected_warnings': ['Failed to parse XML: not well-formed'], 'expected_warnings': ['Failed to parse XML: not well-formed'],
'skip': 'Video removed',
}, { }, {
# multiple attachments/embeds # multiple attachments/embeds
'url': 'https://www.patreon.com/posts/holy-wars-solos-100601977', 'url': 'https://www.patreon.com/posts/holy-wars-solos-100601977',
@ -289,7 +285,6 @@ class PatreonIE(PatreonBaseIE):
}, },
'params': {'getcomments': True}, 'params': {'getcomments': True},
}, { }, {
# FIXME: Error: No supported media found in this post
# Inlined media in post; uses _extract_from_media_api # Inlined media in post; uses _extract_from_media_api
'url': 'https://www.patreon.com/posts/scottfalco-146966245', 'url': 'https://www.patreon.com/posts/scottfalco-146966245',
'info_dict': { 'info_dict': {
@ -309,26 +304,6 @@ class PatreonIE(PatreonBaseIE):
'timestamp': 1767061800, 'timestamp': 1767061800,
'upload_date': '20251230', 'upload_date': '20251230',
}, },
}, {
# FIXME: need to extract description
'url': 'https://www.patreon.com/Insanimate/posts/meatcanyon-in-142663524',
'md5': '132332e3bb345f75d8b471242346dee6',
'info_dict': {
'id': '142663524',
'ext': 'mp4',
'title': 'Meatcanyon in Playground',
'uploader': 'Insanimate',
'uploader_id': '2828146',
'uploader_url': 'https://www.patreon.com/Insanimate',
'channel_id': '6260877',
'channel_url': 'https://www.patreon.com/Insanimate',
'channel_follower_count': int,
'comment_count': int,
'like_count': int,
'thumbnail': 're:^https?://.*$',
'timestamp': 1762101034,
'upload_date': '20251102',
},
}] }]
_RETURN_TYPE = 'video' _RETURN_TYPE = 'video'
_HTTP_HEADERS = { _HTTP_HEADERS = {
@ -382,7 +357,7 @@ class PatreonIE(PatreonBaseIE):
post = self._call_api( post = self._call_api(
f'posts/{video_id}', video_id, query={ f'posts/{video_id}', video_id, query={
'fields[media]': 'download_url,mimetype,size_bytes,file_name', 'fields[media]': 'download_url,mimetype,size_bytes,file_name',
'fields[post]': 'comment_count,content,content_teaser_text,cleaned_teaser_text,embed,image,like_count,post_file,published_at,title,current_user_can_view', 'fields[post]': 'comment_count,content,embed,image,like_count,post_file,published_at,title,current_user_can_view',
'fields[user]': 'full_name,url', 'fields[user]': 'full_name,url',
'fields[post_tag]': 'value', 'fields[post_tag]': 'value',
'fields[campaign]': 'url,name,patron_count', 'fields[campaign]': 'url,name,patron_count',
@ -392,7 +367,7 @@ class PatreonIE(PatreonBaseIE):
attributes = post['data']['attributes'] attributes = post['data']['attributes']
info = traverse_obj(attributes, { info = traverse_obj(attributes, {
'title': ('title', {str.strip}), 'title': ('title', {str.strip}),
'description': (('content', 'content_teaser_text', 'cleaned_teaser_text'), {clean_html}, any), 'description': ('content', {clean_html}),
'thumbnail': ('image', ('large_url', 'url'), {url_or_none}, any), 'thumbnail': ('image', ('large_url', 'url'), {url_or_none}, any),
'timestamp': ('published_at', {parse_iso8601}), 'timestamp': ('published_at', {parse_iso8601}),
'like_count': ('like_count', {int_or_none}), 'like_count': ('like_count', {int_or_none}),

View File

@ -4,7 +4,6 @@ import re
from .common import InfoExtractor from .common import InfoExtractor
from ..utils import ( from ..utils import (
OnDemandPagedList, OnDemandPagedList,
filter_dict,
format_field, format_field,
int_or_none, int_or_none,
parse_resolution, parse_resolution,
@ -1359,7 +1358,7 @@ class PeerTubeIE(InfoExtractor):
'ext': 'mp4', 'ext': 'mp4',
'title': 'E2E tests', 'title': 'E2E tests',
'categories': ['Unknown'], 'categories': ['Unknown'],
'channel': 'Chocobozzz test channel', 'channel': 'Main chocobozzz channel',
'channel_id': '5187', 'channel_id': '5187',
'channel_url': 'https://peertube2.cpy.re/video-channels/chocobozzz_channel', 'channel_url': 'https://peertube2.cpy.re/video-channels/chocobozzz_channel',
'description': 'md5:67daf92c833c41c95db874e18fcb2786', 'description': 'md5:67daf92c833c41c95db874e18fcb2786',
@ -1383,7 +1382,7 @@ class PeerTubeIE(InfoExtractor):
'ext': 'mp4', 'ext': 'mp4',
'title': 'E2E tests', 'title': 'E2E tests',
'categories': ['Unknown'], 'categories': ['Unknown'],
'channel': 'Chocobozzz test channel', 'channel': 'Main chocobozzz channel',
'channel_id': '5187', 'channel_id': '5187',
'channel_url': 'https://peertube2.cpy.re/video-channels/chocobozzz_channel', 'channel_url': 'https://peertube2.cpy.re/video-channels/chocobozzz_channel',
'description': 'md5:67daf92c833c41c95db874e18fcb2786', 'description': 'md5:67daf92c833c41c95db874e18fcb2786',
@ -1407,7 +1406,7 @@ class PeerTubeIE(InfoExtractor):
'ext': 'mp4', 'ext': 'mp4',
'title': 'E2E tests', 'title': 'E2E tests',
'categories': ['Unknown'], 'categories': ['Unknown'],
'channel': 'Chocobozzz test channel', 'channel': 'Main chocobozzz channel',
'channel_id': '5187', 'channel_id': '5187',
'channel_url': 'https://peertube2.cpy.re/video-channels/chocobozzz_channel', 'channel_url': 'https://peertube2.cpy.re/video-channels/chocobozzz_channel',
'description': 'md5:67daf92c833c41c95db874e18fcb2786', 'description': 'md5:67daf92c833c41c95db874e18fcb2786',
@ -1453,36 +1452,6 @@ class PeerTubeIE(InfoExtractor):
}, { }, {
'url': 'peertube:framatube.org:b37a5b9f-e6b5-415c-b700-04a5cd6ec205', 'url': 'peertube:framatube.org:b37a5b9f-e6b5-415c-b700-04a5cd6ec205',
'only_matching': True, 'only_matching': True,
}, {
'url': 'https://videos.john-livingston.fr/w/mna1A6SxZ94cra4hMtjRQm',
'md5': '6a5faad22916e41ba4078ef59c33bc9f',
'info_dict': {
'id': 'mna1A6SxZ94cra4hMtjRQm',
'ext': 'mp4',
'title': 'test yt-dlp',
'description': 'md5:d8556ee790ad9b3fac6f0bb3eb5b67bd',
'thumbnail': r're:https?://videos.john-livingston\.fr/lazy-static/thumbnails/.+\.jpg',
'timestamp': 1780645286,
'upload_date': '20260605',
'uploader': 'John Livingston',
'uploader_id': '5',
'uploader_url': 'https://videos.john-livingston.fr/accounts/john',
'channel': 'john_livingston',
'channel_id': '4',
'channel_url': 'https://videos.john-livingston.fr/video-channels/john_livingston',
'license': 'Unknown',
'duration': 16,
'view_count': int,
'like_count': int,
'dislike_count': int,
'tags': 'count:0',
'categories': ['Unknown'],
},
'params': {
'videopassword': 'thepassword',
'format': '600p',
},
'expected_warnings': ['Ignoring subtitle tracks found in the HLS manifest'],
}] }]
_WEBPAGE_TESTS = [{ _WEBPAGE_TESTS = [{
'url': 'https://video.macver.org/w/6gvhZpUGQVd4SQ6oYDc9pC', 'url': 'https://video.macver.org/w/6gvhZpUGQVd4SQ6oYDc9pC',
@ -1523,9 +1492,6 @@ class PeerTubeIE(InfoExtractor):
'>We are sorry but it seems that PeerTube is not compatible with your web browser.<')): '>We are sorry but it seems that PeerTube is not compatible with your web browser.<')):
return 'peertube:{}:{}'.format(*mobj.group('host', 'id')) return 'peertube:{}:{}'.format(*mobj.group('host', 'id'))
def _get_headers(self):
return filter_dict({'x-peertube-video-password': self.get_param('videopassword')})
@classmethod @classmethod
def _extract_embed_urls(cls, url, webpage): def _extract_embed_urls(cls, url, webpage):
embeds = tuple(super()._extract_embed_urls(url, webpage)) embeds = tuple(super()._extract_embed_urls(url, webpage))
@ -1539,7 +1505,7 @@ class PeerTubeIE(InfoExtractor):
def _call_api(self, host, video_id, path, note=None, errnote=None, fatal=True): def _call_api(self, host, video_id, path, note=None, errnote=None, fatal=True):
return self._download_json( return self._download_json(
self._API_BASE % (host, video_id, path), video_id, self._API_BASE % (host, video_id, path), video_id,
note=note, errnote=errnote, fatal=fatal, headers=self._get_headers()) note=note, errnote=errnote, fatal=fatal)
def _get_subtitles(self, host, video_id): def _get_subtitles(self, host, video_id):
captions = self._call_api( captions = self._call_api(
@ -1579,7 +1545,7 @@ class PeerTubeIE(InfoExtractor):
if playlist_url := url_or_none(playlist.get('playlistUrl')): if playlist_url := url_or_none(playlist.get('playlistUrl')):
is_live = True is_live = True
formats.extend(self._extract_m3u8_formats( formats.extend(self._extract_m3u8_formats(
playlist_url, video_id, fatal=False, live=True, headers=self._get_headers())) playlist_url, video_id, fatal=False, live=True))
playlist_files = playlist.get('files') playlist_files = playlist.get('files')
if not (playlist_files and isinstance(playlist_files, list)): if not (playlist_files and isinstance(playlist_files, list)):
continue continue
@ -1663,8 +1629,6 @@ class PeerTubeIE(InfoExtractor):
'subtitles': subtitles, 'subtitles': subtitles,
'is_live': is_live, 'is_live': is_live,
'webpage_url': webpage_url, 'webpage_url': webpage_url,
# Headers are needed for ALL format requests, but not thumbnails
'http_headers': self._get_headers(),
} }

View File

@ -5218,17 +5218,12 @@ class _UnsafeExtensionError(Exception):
'sbv', 'sbv',
]) ])
_enabled = True
def __init__(self, extension, /): def __init__(self, extension, /):
super().__init__(f'unsafe file extension: {extension!r}') super().__init__(f'unsafe file extension: {extension!r}')
self.extension = extension self.extension = extension
@classmethod @classmethod
def sanitize_extension(cls, extension, /, *, prepend=False, _allowed_exts=()): def sanitize_extension(cls, extension, /, *, prepend=False, _allowed_exts=()):
if not cls._enabled:
return extension
if extension is None: if extension is None:
return None return None

View File

@ -64,7 +64,7 @@ class HTTPHeaderDict(dict):
other = other.sensitive() other = other.sensitive()
if isinstance(other, dict): if isinstance(other, dict):
self.update(other) self.update(other)
return self return
return NotImplemented return NotImplemented
def __or__(self, other, /) -> typing.Self: def __or__(self, other, /) -> typing.Self: