mirror of
https://github.com/yt-dlp/yt-dlp
synced 2024-12-27 21:59:17 +01:00
Merge branch 'master' into GoogleDriveFolderFix
This commit is contained in:
commit
3068d9897d
7 changed files with 59 additions and 30 deletions
2
.github/workflows/core.yml
vendored
2
.github/workflows/core.yml
vendored
|
@ -59,4 +59,4 @@ jobs:
|
||||||
continue-on-error: False
|
continue-on-error: False
|
||||||
run: |
|
run: |
|
||||||
python3 -m yt_dlp -v || true # Print debug head
|
python3 -m yt_dlp -v || true # Print debug head
|
||||||
python3 ./devscripts/run_tests.py core
|
python3 ./devscripts/run_tests.py --pytest-args '--reruns 2 --reruns-delay 3.0' core
|
||||||
|
|
2
.github/workflows/quick-test.yml
vendored
2
.github/workflows/quick-test.yml
vendored
|
@ -20,7 +20,7 @@ jobs:
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
run: |
|
run: |
|
||||||
python3 -m yt_dlp -v || true
|
python3 -m yt_dlp -v || true
|
||||||
python3 ./devscripts/run_tests.py core
|
python3 ./devscripts/run_tests.py --pytest-args '--reruns 2 --reruns-delay 3.0' core
|
||||||
check:
|
check:
|
||||||
name: Code check
|
name: Code check
|
||||||
if: "!contains(github.event.head_commit.message, 'ci skip all')"
|
if: "!contains(github.event.head_commit.message, 'ci skip all')"
|
||||||
|
|
|
@ -80,6 +80,7 @@ static-analysis = [
|
||||||
]
|
]
|
||||||
test = [
|
test = [
|
||||||
"pytest~=8.1",
|
"pytest~=8.1",
|
||||||
|
"pytest-rerunfailures~=14.0",
|
||||||
]
|
]
|
||||||
pyinstaller = [
|
pyinstaller = [
|
||||||
"pyinstaller>=6.10.0", # Windows temp cleanup fixed in 6.10.0
|
"pyinstaller>=6.10.0", # Windows temp cleanup fixed in 6.10.0
|
||||||
|
@ -162,7 +163,6 @@ lint-fix = "ruff check --fix {args:.}"
|
||||||
features = ["test"]
|
features = ["test"]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pytest-randomly~=3.15",
|
"pytest-randomly~=3.15",
|
||||||
"pytest-rerunfailures~=14.0",
|
|
||||||
"pytest-xdist[psutil]~=3.5",
|
"pytest-xdist[psutil]~=3.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ import unicodedata
|
||||||
from .cache import Cache
|
from .cache import Cache
|
||||||
from .compat import urllib # isort: split
|
from .compat import urllib # isort: split
|
||||||
from .compat import compat_os_name, urllib_req_to_req
|
from .compat import compat_os_name, urllib_req_to_req
|
||||||
from .cookies import LenientSimpleCookie, load_cookies
|
from .cookies import CookieLoadError, LenientSimpleCookie, load_cookies
|
||||||
from .downloader import FFmpegFD, get_suitable_downloader, shorten_protocol_name
|
from .downloader import FFmpegFD, get_suitable_downloader, shorten_protocol_name
|
||||||
from .downloader.rtmp import rtmpdump_version
|
from .downloader.rtmp import rtmpdump_version
|
||||||
from .extractor import gen_extractor_classes, get_info_extractor
|
from .extractor import gen_extractor_classes, get_info_extractor
|
||||||
|
@ -1624,7 +1624,7 @@ class YoutubeDL:
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
return func(self, *args, **kwargs)
|
return func(self, *args, **kwargs)
|
||||||
except (DownloadCancelled, LazyList.IndexError, PagedList.IndexError):
|
except (CookieLoadError, DownloadCancelled, LazyList.IndexError, PagedList.IndexError):
|
||||||
raise
|
raise
|
||||||
except ReExtractInfo as e:
|
except ReExtractInfo as e:
|
||||||
if e.expected:
|
if e.expected:
|
||||||
|
@ -3580,6 +3580,8 @@ class YoutubeDL:
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
try:
|
try:
|
||||||
res = func(*args, **kwargs)
|
res = func(*args, **kwargs)
|
||||||
|
except CookieLoadError:
|
||||||
|
raise
|
||||||
except UnavailableVideoError as e:
|
except UnavailableVideoError as e:
|
||||||
self.report_error(e)
|
self.report_error(e)
|
||||||
except DownloadCancelled as e:
|
except DownloadCancelled as e:
|
||||||
|
@ -4113,8 +4115,13 @@ class YoutubeDL:
|
||||||
@functools.cached_property
|
@functools.cached_property
|
||||||
def cookiejar(self):
|
def cookiejar(self):
|
||||||
"""Global cookiejar instance"""
|
"""Global cookiejar instance"""
|
||||||
return load_cookies(
|
try:
|
||||||
self.params.get('cookiefile'), self.params.get('cookiesfrombrowser'), self)
|
return load_cookies(
|
||||||
|
self.params.get('cookiefile'), self.params.get('cookiesfrombrowser'), self)
|
||||||
|
except CookieLoadError as error:
|
||||||
|
cause = error.__context__
|
||||||
|
self.report_error(str(cause), tb=''.join(traceback.format_exception(cause)))
|
||||||
|
raise
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _opener(self):
|
def _opener(self):
|
||||||
|
|
|
@ -15,7 +15,7 @@ import re
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from .compat import compat_os_name
|
from .compat import compat_os_name
|
||||||
from .cookies import SUPPORTED_BROWSERS, SUPPORTED_KEYRINGS
|
from .cookies import SUPPORTED_BROWSERS, SUPPORTED_KEYRINGS, CookieLoadError
|
||||||
from .downloader.external import get_external_downloader
|
from .downloader.external import get_external_downloader
|
||||||
from .extractor import list_extractor_classes
|
from .extractor import list_extractor_classes
|
||||||
from .extractor.adobepass import MSO_INFO
|
from .extractor.adobepass import MSO_INFO
|
||||||
|
@ -1084,7 +1084,7 @@ def main(argv=None):
|
||||||
_IN_CLI = True
|
_IN_CLI = True
|
||||||
try:
|
try:
|
||||||
_exit(*variadic(_real_main(argv)))
|
_exit(*variadic(_real_main(argv)))
|
||||||
except DownloadError:
|
except (CookieLoadError, DownloadError):
|
||||||
_exit(1)
|
_exit(1)
|
||||||
except SameFileError as e:
|
except SameFileError as e:
|
||||||
_exit(f'ERROR: {e}')
|
_exit(f'ERROR: {e}')
|
||||||
|
|
|
@ -34,6 +34,7 @@ from .dependencies import (
|
||||||
from .minicurses import MultilinePrinter, QuietMultilinePrinter
|
from .minicurses import MultilinePrinter, QuietMultilinePrinter
|
||||||
from .utils import (
|
from .utils import (
|
||||||
DownloadError,
|
DownloadError,
|
||||||
|
YoutubeDLError,
|
||||||
Popen,
|
Popen,
|
||||||
error_to_str,
|
error_to_str,
|
||||||
expand_path,
|
expand_path,
|
||||||
|
@ -86,24 +87,31 @@ def _create_progress_bar(logger):
|
||||||
return printer
|
return printer
|
||||||
|
|
||||||
|
|
||||||
|
class CookieLoadError(YoutubeDLError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def load_cookies(cookie_file, browser_specification, ydl):
|
def load_cookies(cookie_file, browser_specification, ydl):
|
||||||
cookie_jars = []
|
try:
|
||||||
if browser_specification is not None:
|
cookie_jars = []
|
||||||
browser_name, profile, keyring, container = _parse_browser_specification(*browser_specification)
|
if browser_specification is not None:
|
||||||
cookie_jars.append(
|
browser_name, profile, keyring, container = _parse_browser_specification(*browser_specification)
|
||||||
extract_cookies_from_browser(browser_name, profile, YDLLogger(ydl), keyring=keyring, container=container))
|
cookie_jars.append(
|
||||||
|
extract_cookies_from_browser(browser_name, profile, YDLLogger(ydl), keyring=keyring, container=container))
|
||||||
|
|
||||||
if cookie_file is not None:
|
if cookie_file is not None:
|
||||||
is_filename = is_path_like(cookie_file)
|
is_filename = is_path_like(cookie_file)
|
||||||
if is_filename:
|
if is_filename:
|
||||||
cookie_file = expand_path(cookie_file)
|
cookie_file = expand_path(cookie_file)
|
||||||
|
|
||||||
jar = YoutubeDLCookieJar(cookie_file)
|
jar = YoutubeDLCookieJar(cookie_file)
|
||||||
if not is_filename or os.access(cookie_file, os.R_OK):
|
if not is_filename or os.access(cookie_file, os.R_OK):
|
||||||
jar.load()
|
jar.load()
|
||||||
cookie_jars.append(jar)
|
cookie_jars.append(jar)
|
||||||
|
|
||||||
return _merge_cookie_jars(cookie_jars)
|
return _merge_cookie_jars(cookie_jars)
|
||||||
|
except Exception:
|
||||||
|
raise CookieLoadError('failed to load cookies')
|
||||||
|
|
||||||
|
|
||||||
def extract_cookies_from_browser(browser_name, profile=None, logger=YDLLogger(), *, keyring=None, container=None):
|
def extract_cookies_from_browser(browser_name, profile=None, logger=YDLLogger(), *, keyring=None, container=None):
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import functools
|
||||||
import itertools
|
import itertools
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
|
@ -22,13 +23,19 @@ from ..utils import (
|
||||||
|
|
||||||
|
|
||||||
class PatreonBaseIE(InfoExtractor):
|
class PatreonBaseIE(InfoExtractor):
|
||||||
USER_AGENT = 'Patreon/7.6.28 (Android; Android 11; Scale/2.10)'
|
@functools.cached_property
|
||||||
|
def patreon_user_agent(self):
|
||||||
|
# Patreon mobile UA is needed to avoid triggering Cloudflare anti-bot protection.
|
||||||
|
# Newer UA yields higher res m3u8 formats for locked posts, but gives 401 if not logged-in
|
||||||
|
if self._get_cookies('https://www.patreon.com/').get('session_id'):
|
||||||
|
return 'Patreon/72.2.28 (Android; Android 14; Scale/2.10)'
|
||||||
|
return 'Patreon/7.6.28 (Android; Android 11; Scale/2.10)'
|
||||||
|
|
||||||
def _call_api(self, ep, item_id, query=None, headers=None, fatal=True, note=None):
|
def _call_api(self, ep, item_id, query=None, headers=None, fatal=True, note=None):
|
||||||
if headers is None:
|
if headers is None:
|
||||||
headers = {}
|
headers = {}
|
||||||
if 'User-Agent' not in headers:
|
if 'User-Agent' not in headers:
|
||||||
headers['User-Agent'] = self.USER_AGENT
|
headers['User-Agent'] = self.patreon_user_agent
|
||||||
if query:
|
if query:
|
||||||
query.update({'json-api-version': 1.0})
|
query.update({'json-api-version': 1.0})
|
||||||
|
|
||||||
|
@ -111,6 +118,7 @@ class PatreonIE(PatreonBaseIE):
|
||||||
'comment_count': int,
|
'comment_count': int,
|
||||||
'channel_is_verified': True,
|
'channel_is_verified': True,
|
||||||
'chapters': 'count:4',
|
'chapters': 'count:4',
|
||||||
|
'timestamp': 1423689666,
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'noplaylist': True,
|
'noplaylist': True,
|
||||||
|
@ -221,6 +229,7 @@ class PatreonIE(PatreonBaseIE):
|
||||||
'thumbnail': r're:^https?://.+',
|
'thumbnail': r're:^https?://.+',
|
||||||
},
|
},
|
||||||
'params': {'skip_download': 'm3u8'},
|
'params': {'skip_download': 'm3u8'},
|
||||||
|
'expected_warnings': ['Failed to parse XML: not well-formed'],
|
||||||
}, {
|
}, {
|
||||||
# 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',
|
||||||
|
@ -326,8 +335,13 @@ class PatreonIE(PatreonBaseIE):
|
||||||
if embed_url and (urlh := self._request_webpage(
|
if embed_url and (urlh := self._request_webpage(
|
||||||
embed_url, video_id, 'Checking embed URL', headers=headers,
|
embed_url, video_id, 'Checking embed URL', headers=headers,
|
||||||
fatal=False, errnote=False, expected_status=403)):
|
fatal=False, errnote=False, expected_status=403)):
|
||||||
|
# Vimeo's Cloudflare anti-bot protection will return HTTP status 200 for 404, so we need
|
||||||
|
# to check for "Sorry, we couldn’t find that page" in the meta description tag
|
||||||
|
meta_description = clean_html(self._html_search_meta(
|
||||||
|
'description', self._webpage_read_content(urlh, embed_url, video_id, fatal=False), default=None))
|
||||||
# Password-protected vids.io embeds return 403 errors w/o --video-password or session cookie
|
# Password-protected vids.io embeds return 403 errors w/o --video-password or session cookie
|
||||||
if urlh.status != 403 or VidsIoIE.suitable(embed_url):
|
if ((urlh.status != 403 and meta_description != 'Sorry, we couldn’t find that page')
|
||||||
|
or VidsIoIE.suitable(embed_url)):
|
||||||
entries.append(self.url_result(smuggle_url(embed_url, headers)))
|
entries.append(self.url_result(smuggle_url(embed_url, headers)))
|
||||||
|
|
||||||
post_file = traverse_obj(attributes, ('post_file', {dict}))
|
post_file = traverse_obj(attributes, ('post_file', {dict}))
|
||||||
|
@ -427,7 +441,7 @@ class PatreonCampaignIE(PatreonBaseIE):
|
||||||
'title': 'Cognitive Dissonance Podcast',
|
'title': 'Cognitive Dissonance Podcast',
|
||||||
'channel_url': 'https://www.patreon.com/dissonancepod',
|
'channel_url': 'https://www.patreon.com/dissonancepod',
|
||||||
'id': '80642',
|
'id': '80642',
|
||||||
'description': 'md5:eb2fa8b83da7ab887adeac34da6b7af7',
|
'description': r're:(?s).*We produce a weekly news podcast focusing on stories that deal with skepticism and religion.*',
|
||||||
'channel_id': '80642',
|
'channel_id': '80642',
|
||||||
'channel': 'Cognitive Dissonance Podcast',
|
'channel': 'Cognitive Dissonance Podcast',
|
||||||
'age_limit': 0,
|
'age_limit': 0,
|
||||||
|
@ -445,7 +459,7 @@ class PatreonCampaignIE(PatreonBaseIE):
|
||||||
'id': '4767637',
|
'id': '4767637',
|
||||||
'channel_id': '4767637',
|
'channel_id': '4767637',
|
||||||
'channel_url': 'https://www.patreon.com/notjustbikes',
|
'channel_url': 'https://www.patreon.com/notjustbikes',
|
||||||
'description': 'md5:9f4b70051216c4d5c58afe580ffc8d0f',
|
'description': r're:(?s).*Not Just Bikes started as a way to explain why we chose to live in the Netherlands.*',
|
||||||
'age_limit': 0,
|
'age_limit': 0,
|
||||||
'channel': 'Not Just Bikes',
|
'channel': 'Not Just Bikes',
|
||||||
'uploader_url': 'https://www.patreon.com/notjustbikes',
|
'uploader_url': 'https://www.patreon.com/notjustbikes',
|
||||||
|
@ -462,7 +476,7 @@ class PatreonCampaignIE(PatreonBaseIE):
|
||||||
'id': '4243769',
|
'id': '4243769',
|
||||||
'channel_id': '4243769',
|
'channel_id': '4243769',
|
||||||
'channel_url': 'https://www.patreon.com/secondthought',
|
'channel_url': 'https://www.patreon.com/secondthought',
|
||||||
'description': 'md5:69c89a3aba43efdb76e85eb023e8de8b',
|
'description': r're:(?s).*Second Thought is an educational YouTube channel.*',
|
||||||
'age_limit': 0,
|
'age_limit': 0,
|
||||||
'channel': 'Second Thought',
|
'channel': 'Second Thought',
|
||||||
'uploader_url': 'https://www.patreon.com/secondthought',
|
'uploader_url': 'https://www.patreon.com/secondthought',
|
||||||
|
@ -512,7 +526,7 @@ class PatreonCampaignIE(PatreonBaseIE):
|
||||||
|
|
||||||
campaign_id, vanity = self._match_valid_url(url).group('campaign_id', 'vanity')
|
campaign_id, vanity = self._match_valid_url(url).group('campaign_id', 'vanity')
|
||||||
if campaign_id is None:
|
if campaign_id is None:
|
||||||
webpage = self._download_webpage(url, vanity, headers={'User-Agent': self.USER_AGENT})
|
webpage = self._download_webpage(url, vanity, headers={'User-Agent': self.patreon_user_agent})
|
||||||
campaign_id = self._search_nextjs_data(
|
campaign_id = self._search_nextjs_data(
|
||||||
webpage, vanity)['props']['pageProps']['bootstrapEnvelope']['pageBootstrap']['campaign']['data']['id']
|
webpage, vanity)['props']['pageProps']['bootstrapEnvelope']['pageBootstrap']['campaign']['data']['id']
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue