Skip to content

Commit

Permalink
YT-DLP updated to Release 2024.03.10. Requires FDM 6.20+
Browse files Browse the repository at this point in the history
  • Loading branch information
meowcateatrat committed Apr 2, 2024
1 parent 277879b commit 2a1592d
Show file tree
Hide file tree
Showing 326 changed files with 8,361 additions and 4,327 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build/elephant.fda
2 changes: 1 addition & 1 deletion plugin/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"uuid": "elephant",
"name": "Elephant",
"description": "Provides support for downloading videos from various sites.",
"version": "1.0.12",
"version": "1.0.13",
"icon": "icon.svg",
"mediaParser": true,
"mediaListParser": true,
Expand Down
39 changes: 30 additions & 9 deletions plugin/yt-dlp/yt_dlp/YoutubeDL.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@
NoSupportingHandlers,
RequestError,
SSLError,
_CompatHTTPError,
network_exceptions,
)
from .plugins import directories as plugin_directories
Expand Down Expand Up @@ -576,11 +575,18 @@ class YoutubeDL:
'url', 'manifest_url', 'manifest_stream_number', 'ext', 'format', 'format_id', 'format_note',
'width', 'height', 'aspect_ratio', 'resolution', 'dynamic_range', 'tbr', 'abr', 'acodec', 'asr', 'audio_channels',
'vbr', 'fps', 'vcodec', 'container', 'filesize', 'filesize_approx', 'rows', 'columns',
'player_url', 'protocol', 'fragment_base_url', 'fragments', 'is_from_start',
'player_url', 'protocol', 'fragment_base_url', 'fragments', 'is_from_start', 'is_dash_periods', 'request_data',
'preference', 'language', 'language_preference', 'quality', 'source_preference', 'cookies',
'http_headers', 'stretched_ratio', 'no_resume', 'has_drm', 'extra_param_to_segment_url', 'hls_aes', 'downloader_options',
'page_url', 'app', 'play_path', 'tc_url', 'flash_version', 'rtmp_live', 'rtmp_conn', 'rtmp_protocol', 'rtmp_real_time'
}
_deprecated_multivalue_fields = {
'album_artist': 'album_artists',
'artist': 'artists',
'composer': 'composers',
'creator': 'creators',
'genre': 'genres',
}
_format_selection_exts = {
'audio': set(MEDIA_EXTENSIONS.common_audio),
'video': set(MEDIA_EXTENSIONS.common_video + ('3gp', )),
Expand Down Expand Up @@ -684,7 +690,6 @@ def process_color_policy(stream):
self.params['http_headers'] = HTTPHeaderDict(std_headers, self.params.get('http_headers'))
self._load_cookies(self.params['http_headers'].get('Cookie')) # compat
self.params['http_headers'].pop('Cookie', None)
self._request_director = self.build_request_director(_REQUEST_HANDLERS.values(), _RH_PREFERENCES)

if auto_init and auto_init != 'no_verbose_header':
self.print_debug_header()
Expand Down Expand Up @@ -957,7 +962,9 @@ def __exit__(self, *args):

def close(self):
self.save_cookies()
self._request_director.close()
if '_request_director' in self.__dict__:
self._request_director.close()
del self._request_director

def trouble(self, message=None, tb=None, is_error=True):
"""Determine action to take when a download problem appears.
Expand Down Expand Up @@ -2220,7 +2227,7 @@ def _parse_format_selection(tokens, inside_merge=False, inside_choice=False, ins
selectors = []
current_selector = None
for type, string_, start, _, _ in tokens:
# ENCODING is only defined in python 3.x
# ENCODING is only defined in Python 3.x
if type == getattr(tokenize, 'ENCODING', None):
continue
elif type in [tokenize.NAME, tokenize.NUMBER]:
Expand Down Expand Up @@ -2452,7 +2459,7 @@ def selector_function(ctx):
# for extractors with incomplete formats (audio only (soundcloud)
# or video only (imgur)) best/worst will fallback to
# best/worst {video,audio}-only format
matches = formats
matches = list(filter(lambda f: f.get('vcodec') != 'none' or f.get('acodec') != 'none', formats))
elif seperate_fallback and not ctx['has_merged_format']:
# for compatibility with youtube-dl when there is no pre-merged format
matches = list(filter(seperate_fallback, formats))
Expand Down Expand Up @@ -2641,6 +2648,15 @@ def _fill_common_fields(self, info_dict, final=True):
if final and info_dict.get('%s_number' % field) is not None and not info_dict.get(field):
info_dict[field] = '%s %d' % (field.capitalize(), info_dict['%s_number' % field])

for old_key, new_key in self._deprecated_multivalue_fields.items():
if new_key in info_dict and old_key in info_dict:
if '_version' not in info_dict: # HACK: Do not warn when using --load-info-json
self.deprecation_warning(f'Do not return {old_key!r} when {new_key!r} is present')
elif old_value := info_dict.get(old_key):
info_dict[new_key] = old_value.split(', ')
elif new_value := info_dict.get(new_key):
info_dict[old_key] = ', '.join(v.replace(',', '\N{FULLWIDTH COMMA}') for v in new_value)

def _raise_pending_errors(self, info):
err = info.pop('__pending_error', None)
if err:
Expand Down Expand Up @@ -3484,7 +3500,8 @@ def ffmpeg_fixup(cndn, msg, cls):
or info_dict.get('is_live') and self.params.get('hls_use_mpegts') is None,
'Possible MPEG-TS in MP4 container or malformed AAC timestamps',
FFmpegFixupM3u8PP)
ffmpeg_fixup(info_dict.get('is_live') and downloader == 'dashsegments',
ffmpeg_fixup(downloader == 'dashsegments'
and (info_dict.get('is_live') or info_dict.get('is_dash_periods')),
'Possible duplicate MOOV atoms', FFmpegFixupDuplicateMoovPP)

ffmpeg_fixup(downloader == 'web_socket_fragment', 'Malformed timestamps detected', FFmpegFixupTimestampPP)
Expand Down Expand Up @@ -3561,6 +3578,8 @@ def download_with_info_file(self, info_filename):
raise
self.report_warning(f'The info failed to download: {e}; trying with URL {webpage_url}')
self.download([webpage_url])
except ExtractorError as e:
self.report_error(e)
return self._download_retcode

@staticmethod
Expand Down Expand Up @@ -4110,8 +4129,6 @@ def urlopen(self, req):
'SSLV3_ALERT_HANDSHAKE_FAILURE: The server may not support the current cipher list. '
'Try using --legacy-server-connect', cause=e) from e
raise
except HTTPError as e: # TODO: Remove in a future release
raise _CompatHTTPError(e) from e

def build_request_director(self, handlers, preferences=None):
logger = _YDLLogger(self)
Expand Down Expand Up @@ -4147,6 +4164,10 @@ def build_request_director(self, handlers, preferences=None):
director.preferences.add(lambda rh, _: 500 if rh.RH_KEY == 'Urllib' else 0)
return director

@functools.cached_property
def _request_director(self):
return self.build_request_director(_REQUEST_HANDLERS.values(), _RH_PREFERENCES)

def encode(self, s):
if isinstance(s, bytes):
return s # Already encoded
Expand Down
27 changes: 24 additions & 3 deletions plugin/yt-dlp/yt_dlp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
raise ImportError(
f'You are using an unsupported version of Python. Only Python versions 3.8 and above are supported by yt-dlp') # noqa: F541

__license__ = 'Public Domain'
__license__ = 'The Unlicense'

import collections
import getpass
Expand All @@ -14,7 +14,7 @@
import re
import traceback

from .compat import compat_shlex_quote
from .compat import compat_os_name, compat_shlex_quote
from .cookies import SUPPORTED_BROWSERS, SUPPORTED_KEYRINGS
from .downloader.external import get_external_downloader
from .extractor import list_extractor_classes
Expand Down Expand Up @@ -984,7 +984,28 @@ def _real_main(argv=None):
if pre_process:
return ydl._download_retcode

ydl.warn_if_short_id(sys.argv[1:] if argv is None else argv)
args = sys.argv[1:] if argv is None else argv
ydl.warn_if_short_id(args)

# Show a useful error message and wait for keypress if not launched from shell on Windows
if not args and compat_os_name == 'nt' and getattr(sys, 'frozen', False):
import ctypes.wintypes
import msvcrt

kernel32 = ctypes.WinDLL('Kernel32')

buffer = (1 * ctypes.wintypes.DWORD)()
attached_processes = kernel32.GetConsoleProcessList(buffer, 1)
# If we only have a single process attached, then the executable was double clicked
# When using `pyinstaller` with `--onefile`, two processes get attached
is_onefile = hasattr(sys, '_MEIPASS') and os.path.basename(sys._MEIPASS).startswith('_MEI')
if attached_processes == 1 or is_onefile and attached_processes == 2:
print(parser._generate_error_message(
'Do not double-click the executable, instead call it from a command line.\n'
'Please read the README for further information on how to use yt-dlp: '
'https://github.com/yt-dlp/yt-dlp#readme'))
msvcrt.getch()
_exit(2)
parser.error(
'You must provide at least one URL.\n'
'Type yt-dlp --help to see a list of all options.')
Expand Down
2 changes: 1 addition & 1 deletion plugin/yt-dlp/yt_dlp/__main__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env python3

# Execute with
# $ python -m yt_dlp
# $ python3 -m yt_dlp

import sys

Expand Down
4 changes: 2 additions & 2 deletions plugin/yt-dlp/yt_dlp/__pyinstaller/hook-yt_dlp.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def pycryptodome_module():
try:
import Crypto # noqa: F401
print('WARNING: Using Crypto since Cryptodome is not available. '
'Install with: pip install pycryptodomex', file=sys.stderr)
'Install with: python3 -m pip install pycryptodomex', file=sys.stderr)
return 'Crypto'
except ImportError:
pass
Expand All @@ -31,4 +31,4 @@ def get_hidden_imports():
hiddenimports = list(get_hidden_imports())
print(f'Adding imports: {hiddenimports}')

excludedimports = ['youtube_dl', 'youtube_dlc', 'test', 'ytdlp_plugins', 'devscripts']
excludedimports = ['youtube_dl', 'youtube_dlc', 'test', 'ytdlp_plugins', 'devscripts', 'bundle']
5 changes: 0 additions & 5 deletions plugin/yt-dlp/yt_dlp/casefold.py

This file was deleted.

4 changes: 2 additions & 2 deletions plugin/yt-dlp/yt_dlp/compat/_legacy.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from ..dependencies import brotli as compat_brotli # noqa: F401
from ..dependencies import websockets as compat_websockets # noqa: F401
from ..dependencies.Cryptodome import AES as compat_pycrypto_AES # noqa: F401
from ..networking.exceptions import HTTPError as compat_HTTPError # noqa: F401

passthrough_module(__name__, '...utils', ('WINDOWS_VT_MODE', 'windows_enable_vt_mode'))

Expand Down Expand Up @@ -70,7 +71,6 @@ def compat_setenv(key, value, env=os.environ):
compat_HTMLParser = compat_html_parser_HTMLParser = html.parser.HTMLParser
compat_http_client = http.client
compat_http_server = http.server
compat_HTTPError = urllib.error.HTTPError
compat_input = input
compat_integer_types = (int, )
compat_itertools_count = itertools.count
Expand All @@ -88,7 +88,7 @@ def compat_setenv(key, value, env=os.environ):
compat_subprocess_get_DEVNULL = lambda: subprocess.DEVNULL
compat_tokenize_tokenize = tokenize.tokenize
compat_urllib_error = urllib.error
compat_urllib_HTTPError = urllib.error.HTTPError
compat_urllib_HTTPError = compat_HTTPError
compat_urllib_parse = urllib.parse
compat_urllib_parse_parse_qs = urllib.parse.parse_qs
compat_urllib_parse_quote = urllib.parse.quote
Expand Down
4 changes: 2 additions & 2 deletions plugin/yt-dlp/yt_dlp/compat/urllib/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
from .. import compat_os_name

if compat_os_name == 'nt':
# On older python versions, proxies are extracted from Windows registry erroneously. [1]
# On older Python versions, proxies are extracted from Windows registry erroneously. [1]
# If the https proxy in the registry does not have a scheme, urllib will incorrectly add https:// to it. [2]
# It is unlikely that the user has actually set it to be https, so we should be fine to safely downgrade
# it to http on these older python versions to avoid issues
# it to http on these older Python versions to avoid issues
# This also applies for ftp proxy type, as ftp:// proxy scheme is not supported.
# 1: https://github.com/python/cpython/issues/86793
# 2: https://github.com/python/cpython/blob/51f1ae5ceb0673316c4e4b0175384e892e33cc6e/Lib/urllib/request.py#L2683-L2698
Expand Down
60 changes: 41 additions & 19 deletions plugin/yt-dlp/yt_dlp/cookies.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import base64
import collections
import contextlib
import glob
import http.cookiejar
import http.cookies
import io
Expand All @@ -23,14 +24,16 @@
aes_gcm_decrypt_and_verify_bytes,
unpad_pkcs7,
)
from .compat import functools
from .compat import functools # isort: split
from .compat import compat_os_name
from .dependencies import (
_SECRETSTORAGE_UNAVAILABLE_REASON,
secretstorage,
sqlite3,
)
from .minicurses import MultilinePrinter, QuietMultilinePrinter
from .utils import (
DownloadError,
Popen,
error_to_str,
expand_path,
Expand Down Expand Up @@ -118,17 +121,18 @@ def _extract_firefox_cookies(profile, container, logger):
logger.info('Extracting cookies from firefox')
if not sqlite3:
logger.warning('Cannot extract cookies from firefox without sqlite3 support. '
'Please use a python interpreter compiled with sqlite3 support')
'Please use a Python interpreter compiled with sqlite3 support')
return YoutubeDLCookieJar()

if profile is None:
search_root = _firefox_browser_dir()
search_roots = list(_firefox_browser_dirs())
elif _is_path(profile):
search_root = profile
search_roots = [profile]
else:
search_root = os.path.join(_firefox_browser_dir(), profile)
search_roots = [os.path.join(path, profile) for path in _firefox_browser_dirs()]
search_root = ', '.join(map(repr, search_roots))

cookie_database_path = _find_most_recently_used_file(search_root, 'cookies.sqlite', logger)
cookie_database_path = _newest(_firefox_cookie_dbs(search_roots))
if cookie_database_path is None:
raise FileNotFoundError(f'could not find firefox cookies database in {search_root}')
logger.debug(f'Extracting cookies from: "{cookie_database_path}"')
Expand Down Expand Up @@ -182,12 +186,21 @@ def _extract_firefox_cookies(profile, container, logger):
cursor.connection.close()


def _firefox_browser_dir():
def _firefox_browser_dirs():
if sys.platform in ('cygwin', 'win32'):
return os.path.expandvars(R'%APPDATA%\Mozilla\Firefox\Profiles')
yield os.path.expandvars(R'%APPDATA%\Mozilla\Firefox\Profiles')

elif sys.platform == 'darwin':
return os.path.expanduser('~/Library/Application Support/Firefox')
return os.path.expanduser('~/.mozilla/firefox')
yield os.path.expanduser('~/Library/Application Support/Firefox/Profiles')

else:
yield from map(os.path.expanduser, ('~/.mozilla/firefox', '~/snap/firefox/common/.mozilla/firefox'))


def _firefox_cookie_dbs(roots):
for root in map(os.path.abspath, roots):
for pattern in ('', '*/', 'Profiles/*/'):
yield from glob.iglob(os.path.join(root, pattern, 'cookies.sqlite'))


def _get_chromium_based_browser_settings(browser_name):
Expand Down Expand Up @@ -251,7 +264,7 @@ def _extract_chrome_cookies(browser_name, profile, keyring, logger):

if not sqlite3:
logger.warning(f'Cannot extract cookies from {browser_name} without sqlite3 support. '
'Please use a python interpreter compiled with sqlite3 support')
'Please use a Python interpreter compiled with sqlite3 support')
return YoutubeDLCookieJar()

config = _get_chromium_based_browser_settings(browser_name)
Expand All @@ -268,7 +281,7 @@ def _extract_chrome_cookies(browser_name, profile, keyring, logger):
logger.error(f'{browser_name} does not support profiles')
search_root = config['browser_dir']

cookie_database_path = _find_most_recently_used_file(search_root, 'Cookies', logger)
cookie_database_path = _newest(_find_files(search_root, 'Cookies', logger))
if cookie_database_path is None:
raise FileNotFoundError(f'could not find {browser_name} cookies database in "{search_root}"')
logger.debug(f'Extracting cookies from: "{cookie_database_path}"')
Expand Down Expand Up @@ -307,6 +320,12 @@ def _extract_chrome_cookies(browser_name, profile, keyring, logger):
counts['unencrypted'] = unencrypted_cookies
logger.debug(f'cookie version breakdown: {counts}')
return jar
except PermissionError as error:
if compat_os_name == 'nt' and error.errno == 13:
message = 'Could not copy Chrome cookie database. See https://github.com/yt-dlp/yt-dlp/issues/7271 for more info'
logger.error(message)
raise DownloadError(message) # force exit
raise
finally:
if cursor is not None:
cursor.connection.close()
Expand Down Expand Up @@ -947,7 +966,7 @@ def _get_windows_v10_key(browser_root, logger):
References:
- [1] https://chromium.googlesource.com/chromium/src/+/refs/heads/main/components/os_crypt/sync/os_crypt_win.cc
"""
path = _find_most_recently_used_file(browser_root, 'Local State', logger)
path = _newest(_find_files(browser_root, 'Local State', logger))
if path is None:
logger.error('could not find local state file')
return None
Expand Down Expand Up @@ -1049,17 +1068,20 @@ def _get_column_names(cursor, table_name):
return [row[1].decode() for row in table_info]


def _find_most_recently_used_file(root, filename, logger):
def _newest(files):
return max(files, key=lambda path: os.lstat(path).st_mtime, default=None)


def _find_files(root, filename, logger):
# if there are multiple browser profiles, take the most recently used one
i, paths = 0, []
i = 0
with _create_progress_bar(logger) as progress_bar:
for curr_root, dirs, files in os.walk(root):
for curr_root, _, files in os.walk(root):
for file in files:
i += 1
progress_bar.print(f'Searching for "{filename}": {i: 6d} files searched')
if file == filename:
paths.append(os.path.join(curr_root, file))
return None if not paths else max(paths, key=lambda path: os.lstat(path).st_mtime)
yield os.path.join(curr_root, file)


def _merge_cookie_jars(jars):
Expand All @@ -1073,7 +1095,7 @@ def _merge_cookie_jars(jars):


def _is_path(value):
return os.path.sep in value
return any(sep in value for sep in (os.path.sep, os.path.altsep) if sep)


def _parse_browser_specification(browser_name, profile=None, keyring=None, container=None):
Expand Down
Loading

0 comments on commit 2a1592d

Please sign in to comment.