[update] Overhaul self-updater

Authored by: bashonly, Grub4K

Co-authored-by: Simon Sawicki <contact@grub4k.xyz>
pull/3498/head^2
bashonly 2 years ago
parent 5438593a35
commit 0b6ad22e6a
No known key found for this signature in database
GPG Key ID: 783F096F253D15B0

@ -60,7 +60,7 @@ from .postprocessor import (
get_postprocessor, get_postprocessor,
) )
from .postprocessor.ffmpeg import resolve_mapping as resolve_recode_mapping from .postprocessor.ffmpeg import resolve_mapping as resolve_recode_mapping
from .update import REPOSITORY, _get_system_deprecation, current_git_head, detect_variant from .update import REPOSITORY, _get_system_deprecation, _make_label, current_git_head, detect_variant
from .utils import ( from .utils import (
DEFAULT_OUTTMPL, DEFAULT_OUTTMPL,
IDENTITY, IDENTITY,
@ -3928,8 +3928,7 @@ class YoutubeDL:
klass = type(self) klass = type(self)
write_debug(join_nonempty( write_debug(join_nonempty(
f'{REPOSITORY.rpartition("/")[2]} version', f'{REPOSITORY.rpartition("/")[2]} version',
f'{CHANNEL.rpartition("@")[2]}@{__version__}', _make_label(ORIGIN, CHANNEL.partition('@')[2] or __version__, __version__),
not ORIGIN.startswith('yt-dlp/') and f'from {ORIGIN}',
f'[{RELEASE_GIT_HEAD[:9]}]' if RELEASE_GIT_HEAD else '', f'[{RELEASE_GIT_HEAD[:9]}]' if RELEASE_GIT_HEAD else '',
'' if source == 'unknown' else f'({source})', '' if source == 'unknown' else f'({source})',
'' if _IN_CLI else 'API' if klass == YoutubeDL else f'API:{self.__module__}.{klass.__qualname__}', '' if _IN_CLI else 'API' if klass == YoutubeDL else f'API:{self.__module__}.{klass.__qualname__}',

@ -1,3 +1,5 @@
from __future__ import annotations
import atexit import atexit
import contextlib import contextlib
import hashlib import hashlib
@ -7,6 +9,7 @@ import platform
import re import re
import subprocess import subprocess
import sys import sys
from dataclasses import dataclass
from zipimport import zipimporter from zipimport import zipimporter
from .compat import functools # isort: split from .compat import functools # isort: split
@ -14,16 +17,23 @@ from .compat import compat_realpath, compat_shlex_quote
from .networking import Request from .networking import Request
from .networking.exceptions import HTTPError, network_exceptions from .networking.exceptions import HTTPError, network_exceptions
from .utils import ( from .utils import (
NO_DEFAULT,
Popen, Popen,
cached_method,
deprecation_warning, deprecation_warning,
format_field,
remove_end, remove_end,
remove_start,
shell_quote, shell_quote,
system_identifier, system_identifier,
version_tuple, version_tuple,
) )
from .version import CHANNEL, UPDATE_HINT, VARIANT, __version__ from .version import (
CHANNEL,
ORIGIN,
RELEASE_GIT_HEAD,
UPDATE_HINT,
VARIANT,
__version__,
)
UPDATE_SOURCES = { UPDATE_SOURCES = {
'stable': 'yt-dlp/yt-dlp', 'stable': 'yt-dlp/yt-dlp',
@ -31,8 +41,11 @@ UPDATE_SOURCES = {
'master': 'yt-dlp/yt-dlp-master-builds', 'master': 'yt-dlp/yt-dlp-master-builds',
} }
REPOSITORY = UPDATE_SOURCES['stable'] REPOSITORY = UPDATE_SOURCES['stable']
_INVERSE_UPDATE_SOURCES = {value: key for key, value in UPDATE_SOURCES.items()}
_VERSION_RE = re.compile(r'(\d+\.)*\d+') _VERSION_RE = re.compile(r'(\d+\.)*\d+')
_HASH_PATTERN = r'[\da-f]{40}'
_COMMIT_RE = re.compile(rf'Generated from: https://(?:[^/?#]+/){{3}}commit/(?P<hash>{_HASH_PATTERN})')
API_BASE_URL = 'https://api.github.com/repos' API_BASE_URL = 'https://api.github.com/repos'
@ -113,6 +126,10 @@ def is_non_updateable():
detect_variant(), _NON_UPDATEABLE_REASONS['unknown' if VARIANT else 'other']) detect_variant(), _NON_UPDATEABLE_REASONS['unknown' if VARIANT else 'other'])
def _get_binary_name():
return format_field(_FILE_SUFFIXES, detect_variant(), template='yt-dlp%s', ignore=None, default=None)
def _get_system_deprecation(): def _get_system_deprecation():
MIN_SUPPORTED, MIN_RECOMMENDED = (3, 7), (3, 8) MIN_SUPPORTED, MIN_RECOMMENDED = (3, 7), (3, 8)
@ -147,73 +164,117 @@ def _sha256_file(path):
return h.hexdigest() return h.hexdigest()
class Updater: def _make_label(origin, tag, version=None):
_exact = True if '/' in origin:
channel = _INVERSE_UPDATE_SOURCES.get(origin, origin)
else:
channel = origin
label = f'{channel}@{tag}'
if version and version != tag:
label += f' build {version}'
if channel != origin:
label += f' from {origin}'
return label
def __init__(self, ydl, target=None):
self.ydl = ydl
self.target_channel, sep, self.target_tag = (target or CHANNEL).rpartition('@') @dataclass
# stable => stable@latest class UpdateInfo:
if not sep and ('/' in self.target_tag or self.target_tag in UPDATE_SOURCES): """
self.target_channel = self.target_tag Update target information
self.target_tag = None
elif not self.target_channel: Can be created by `query_update()` or manually.
self.target_channel = CHANNEL.partition('@')[0]
Attributes:
tag The release tag that will be updated to. If from query_update,
the value is after API resolution and update spec processing.
The only property that is required.
version The actual numeric version (if available) of the binary to be updated to,
after API resolution and update spec processing. (default: None)
requested_version Numeric version of the binary being requested (if available),
after API resolution only. (default: None)
commit Commit hash (if available) of the binary to be updated to,
after API resolution and update spec processing. (default: None)
This value will only match the RELEASE_GIT_HEAD of prerelease builds.
binary_name Filename of the binary to be updated to. (default: current binary name)
checksum Expected checksum (if available) of the binary to be
updated to. (default: None)
"""
tag: str
version: str | None = None
requested_version: str | None = None
commit: str | None = None
binary_name: str | None = _get_binary_name()
checksum: str | None = None
_has_update = True
if not self.target_tag: class Updater:
self.target_tag = 'latest' # XXX: use class variables to simplify testing
_channel = CHANNEL
_origin = ORIGIN
def __init__(self, ydl, target: str | None = None):
self.ydl = ydl
# For backwards compat, target needs to be treated as if it could be None
self.requested_channel, sep, self.requested_tag = (target or self._channel).rpartition('@')
# Check if requested_tag is actually the requested repo/channel
if not sep and ('/' in self.requested_tag or self.requested_tag in UPDATE_SOURCES):
self.requested_channel = self.requested_tag
self.requested_tag: str = None # type: ignore (we set it later)
elif not self.requested_channel:
# User did not specify a channel, so we are requesting the default channel
self.requested_channel = self._channel.partition('@')[0]
# --update should not be treated as an exact tag request even if CHANNEL has a @tag
self._exact = bool(target) and target != self._channel
if not self.requested_tag:
# User did not specify a tag, so we request 'latest' and track that no exact tag was passed
self.requested_tag = 'latest'
self._exact = False self._exact = False
elif self.target_tag != 'latest':
self.target_tag = f'tags/{self.target_tag}'
if '/' in self.target_channel: if '/' in self.requested_channel:
self._target_repo = self.target_channel # requested_channel is actually a repository
if self.target_channel not in (CHANNEL, *UPDATE_SOURCES.values()): self.requested_repo = self.requested_channel
if not self.requested_repo.startswith('yt-dlp/') and self.requested_repo != self._origin:
self.ydl.report_warning( self.ydl.report_warning(
f'You are switching to an {self.ydl._format_err("unofficial", "red")} executable ' f'You are switching to an {self.ydl._format_err("unofficial", "red")} executable '
f'from {self.ydl._format_err(self._target_repo, self.ydl.Styles.EMPHASIS)}. ' f'from {self.ydl._format_err(self.requested_repo, self.ydl.Styles.EMPHASIS)}. '
f'Run {self.ydl._format_err("at your own risk", "light red")}') f'Run {self.ydl._format_err("at your own risk", "light red")}')
self._block_restart('Automatically restarting into custom builds is disabled for security reasons') self._block_restart('Automatically restarting into custom builds is disabled for security reasons')
else: else:
self._target_repo = UPDATE_SOURCES.get(self.target_channel) # Check if requested_channel resolves to a known repository or else raise
if not self._target_repo: self.requested_repo = UPDATE_SOURCES.get(self.requested_channel)
if not self.requested_repo:
self._report_error( self._report_error(
f'Invalid update channel {self.target_channel!r} requested. ' f'Invalid update channel {self.requested_channel!r} requested. '
f'Valid channels are {", ".join(UPDATE_SOURCES)}', True) f'Valid channels are {", ".join(UPDATE_SOURCES)}', True)
def _version_compare(self, a, b, channel=CHANNEL): self._identifier = f'{detect_variant()} {system_identifier()}'
if self._exact and channel != self.target_channel:
return False
if _VERSION_RE.fullmatch(f'{a}.{b}'): @property
a, b = version_tuple(a), version_tuple(b) def current_version(self):
return a == b if self._exact else a >= b """Current version"""
return a == b return __version__
@functools.cached_property @property
def _tag(self): def current_commit(self):
if self._version_compare(self.current_version, self.latest_version): """Current commit hash"""
return self.target_tag return RELEASE_GIT_HEAD
identifier = f'{detect_variant()} {self.target_channel} {system_identifier()}' def _download_asset(self, name, tag=None):
for line in self._download('_update_spec', 'latest').decode().splitlines(): if not tag:
if not line.startswith('lock '): tag = self.requested_tag
continue
_, tag, pattern = line.split(' ', 2) path = 'latest/download' if tag == 'latest' else f'download/{tag}'
if re.match(pattern, identifier): url = f'https://github.com/{self.requested_repo}/releases/{path}/{name}'
if not self._exact: self.ydl.write_debug(f'Downloading {name} from {url}')
return f'tags/{tag}' return self.ydl.urlopen(url).read()
elif self.target_tag == 'latest' or not self._version_compare(
tag, self.target_tag[5:], channel=self.target_channel):
self._report_error(
f'yt-dlp cannot be updated above {tag} since you are on an older Python version', True)
return f'tags/{self.current_version}'
return self.target_tag
@cached_method def _call_api(self, tag):
def _get_version_info(self, tag): tag = f'tags/{tag}' if tag != 'latest' else tag
url = f'{API_BASE_URL}/{self._target_repo}/releases/{tag}' url = f'{API_BASE_URL}/{self.requested_repo}/releases/{tag}'
self.ydl.write_debug(f'Fetching release info: {url}') self.ydl.write_debug(f'Fetching release info: {url}')
return json.loads(self.ydl.urlopen(Request(url, headers={ return json.loads(self.ydl.urlopen(Request(url, headers={
'Accept': 'application/vnd.github+json', 'Accept': 'application/vnd.github+json',
@ -221,105 +282,175 @@ class Updater:
'X-GitHub-Api-Version': '2022-11-28', 'X-GitHub-Api-Version': '2022-11-28',
})).read().decode()) })).read().decode())
@property def _get_version_info(self, tag: str) -> tuple[str | None, str | None]:
def current_version(self): if _VERSION_RE.fullmatch(tag):
"""Current version""" return tag, None
return __version__
@staticmethod api_info = self._call_api(tag)
def _label(channel, tag):
"""Label for a given channel and tag"""
return f'{channel}@{remove_start(tag, "tags/")}'
def _get_actual_tag(self, tag): if tag == 'latest':
if tag.startswith('tags/'): requested_version = api_info['tag_name']
return tag[5:] else:
return self._get_version_info(tag)['tag_name'] match = re.search(rf'\s+(?P<version>{_VERSION_RE.pattern})$', api_info.get('name', ''))
requested_version = match.group('version') if match else None
@property
def new_version(self):
"""Version of the latest release we can update to"""
return self._get_actual_tag(self._tag)
@property if re.fullmatch(_HASH_PATTERN, api_info.get('target_commitish', '')):
def latest_version(self): target_commitish = api_info['target_commitish']
"""Version of the target release""" else:
return self._get_actual_tag(self.target_tag) match = _COMMIT_RE.match(api_info.get('body', ''))
target_commitish = match.group('hash') if match else None
@property if not (requested_version or target_commitish):
def has_update(self): self._report_error('One of either version or commit hash must be available on the release', expected=True)
"""Whether there is an update available"""
return not self._version_compare(self.current_version, self.new_version)
@functools.cached_property return requested_version, target_commitish
def filename(self):
"""Filename of the executable"""
return compat_realpath(_get_variant_and_executable_path()[1])
def _download(self, name, tag): def _download_update_spec(self, source_tags):
slug = 'latest/download' if tag == 'latest' else f'download/{tag[5:]}' for tag in source_tags:
url = f'https://github.com/{self._target_repo}/releases/{slug}/{name}' try:
self.ydl.write_debug(f'Downloading {name} from {url}') return self._download_asset('_update_spec', tag=tag).decode()
return self.ydl.urlopen(url).read() except network_exceptions as error:
if isinstance(error, HTTPError) and error.status == 404:
continue
self._report_network_error(f'fetch update spec: {error}')
@functools.cached_property self._report_error(
def release_name(self): f'The requested tag {self.requested_tag} does not exist for {self.requested_repo}', True)
"""The release filename""" return None
return f'yt-dlp{_FILE_SUFFIXES[detect_variant()]}'
@functools.cached_property def _process_update_spec(self, lockfile: str, resolved_tag: str):
def release_hash(self): lines = lockfile.splitlines()
"""Hash of the latest release""" is_version2 = any(line.startswith('lockV2 ') for line in lines)
hash_data = dict(ln.split()[::-1] for ln in self._download('SHA2-256SUMS', self._tag).decode().splitlines())
return hash_data[self.release_name]
def _report_error(self, msg, expected=False): for line in lines:
self.ydl.report_error(msg, tb=False if expected else None) if is_version2:
self.ydl._download_retcode = 100 if not line.startswith(f'lockV2 {self.requested_repo} '):
continue
_, _, tag, pattern = line.split(' ', 3)
else:
if not line.startswith('lock '):
continue
_, tag, pattern = line.split(' ', 2)
def _report_permission_error(self, file): if re.match(pattern, self._identifier):
self._report_error(f'Unable to write to {file}; Try running as administrator', True) if _VERSION_RE.fullmatch(tag):
if not self._exact:
return tag
elif self._version_compare(tag, resolved_tag):
return resolved_tag
elif tag != resolved_tag:
continue
def _report_network_error(self, action, delim=';'):
self._report_error( self._report_error(
f'Unable to {action}{delim} visit ' f'yt-dlp cannot be updated to {resolved_tag} since you are on an older Python version', True)
f'https://github.com/{self._target_repo}/releases/{self.target_tag.replace("tags/", "tag/")}', True) return None
return resolved_tag
def _version_compare(self, a: str, b: str):
"""
Compare two version strings
This function SHOULD NOT be called if self._exact == True
"""
if _VERSION_RE.fullmatch(f'{a}.{b}'):
return version_tuple(a) >= version_tuple(b)
return a == b
def query_update(self, *, _output=False) -> UpdateInfo | None:
"""Fetches and returns info about the available update"""
if not self.requested_repo:
self._report_error('No target repository could be determined from input')
return None
def check_update(self):
"""Report whether there is an update available"""
if not self._target_repo:
return False
try: try:
self.ydl.to_screen(( requested_version, target_commitish = self._get_version_info(self.requested_tag)
f'Available version: {self._label(self.target_channel, self.latest_version)}, ' if self.target_tag == 'latest' else ''
) + f'Current version: {self._label(CHANNEL, self.current_version)}')
except network_exceptions as e: except network_exceptions as e:
return self._report_network_error(f'obtain version info ({e})', delim='; Please try again later or') self._report_network_error(f'obtain version info ({e})', delim='; Please try again later or')
return None
if not is_non_updateable():
self.ydl.to_screen(f'Current Build Hash: {_sha256_file(self.filename)}')
if self.has_update: if self._exact and self._origin != self.requested_repo:
return True has_update = True
elif requested_version:
if self._exact:
has_update = self.current_version != requested_version
else:
has_update = not self._version_compare(self.current_version, requested_version)
elif target_commitish:
has_update = target_commitish != self.current_commit
else:
has_update = False
resolved_tag = requested_version if self.requested_tag == 'latest' else self.requested_tag
current_label = _make_label(self._origin, self._channel.partition("@")[2] or self.current_version, self.current_version)
requested_label = _make_label(self.requested_repo, resolved_tag, requested_version)
latest_or_requested = f'{"Latest" if self.requested_tag == "latest" else "Requested"} version: {requested_label}'
if not has_update:
if _output:
self.ydl.to_screen(f'{latest_or_requested}\nyt-dlp is up to date ({current_label})')
return None
if self.target_tag == self._tag: update_spec = self._download_update_spec(('latest', None) if requested_version else (None,))
self.ydl.to_screen(f'yt-dlp is up to date ({self._label(CHANNEL, self.current_version)})') if not update_spec:
elif not self._exact: return None
self.ydl.report_warning('yt-dlp cannot be updated any further since you are on an older Python version') # `result_` prefixed vars == post-_process_update_spec() values
result_tag = self._process_update_spec(update_spec, resolved_tag)
if not result_tag or result_tag == self.current_version:
return None
elif result_tag == resolved_tag:
result_version = requested_version
elif _VERSION_RE.fullmatch(result_tag):
result_version = result_tag
else: # actual version being updated to is unknown
result_version = None
checksum = None
# Non-updateable variants can get update_info but need to skip checksum
if not is_non_updateable():
try:
hashes = self._download_asset('SHA2-256SUMS', result_tag)
except network_exceptions as error:
if not isinstance(error, HTTPError) or error.status != 404:
self._report_network_error(f'fetch checksums: {error}')
return None
self.ydl.report_warning('No hash information found for the release, skipping verification')
else:
for ln in hashes.decode().splitlines():
if ln.endswith(_get_binary_name()):
checksum = ln.split()[0]
break
if not checksum:
self.ydl.report_warning('The hash could not be found in the checksum file, skipping verification')
if _output:
update_label = _make_label(self.requested_repo, result_tag, result_version)
self.ydl.to_screen(
f'Current version: {current_label}\n{latest_or_requested}'
+ (f'\nUpgradable to: {update_label}' if update_label != requested_label else ''))
return UpdateInfo(
tag=result_tag,
version=result_version,
requested_version=requested_version,
commit=target_commitish if result_tag == resolved_tag else None,
checksum=checksum)
def update(self, update_info=NO_DEFAULT):
"""Update yt-dlp executable to the latest version"""
if update_info is NO_DEFAULT:
update_info = self.query_update(_output=True)
if not update_info:
return False return False
def update(self):
"""Update yt-dlp executable to the latest version"""
if not self.check_update():
return
err = is_non_updateable() err = is_non_updateable()
if err: if err:
return self._report_error(err, True) self._report_error(err, True)
self.ydl.to_screen(f'Updating to {self._label(self.target_channel, self.new_version)} ...') return False
if (_VERSION_RE.fullmatch(self.target_tag[5:])
and version_tuple(self.target_tag[5:]) < (2023, 3, 2)): self.ydl.to_screen(f'Current Build Hash: {_sha256_file(self.filename)}')
self.ydl.report_warning('You are downgrading to a version without --update-to')
self._block_restart('Cannot automatically restart to a version without --update-to') update_label = _make_label(self.requested_repo, update_info.tag, update_info.version)
self.ydl.to_screen(f'Updating to {update_label} ...')
directory = os.path.dirname(self.filename) directory = os.path.dirname(self.filename)
if not os.access(self.filename, os.W_OK): if not os.access(self.filename, os.W_OK):
@ -338,20 +469,17 @@ class Updater:
return self._report_error('Unable to remove the old version') return self._report_error('Unable to remove the old version')
try: try:
newcontent = self._download(self.release_name, self._tag) newcontent = self._download_asset(update_info.binary_name, update_info.tag)
except network_exceptions as e: except network_exceptions as e:
if isinstance(e, HTTPError) and e.status == 404: if isinstance(e, HTTPError) and e.status == 404:
return self._report_error( return self._report_error(
f'The requested tag {self._label(self.target_channel, self.target_tag)} does not exist', True) f'The requested tag {self.requested_repo}@{update_info.tag} does not exist', True)
return self._report_network_error(f'fetch updates: {e}') return self._report_network_error(f'fetch updates: {e}', tag=update_info.tag)
try: if not update_info.checksum:
expected_hash = self.release_hash self._block_restart('Automatically restarting into unverified builds is disabled for security reasons')
except Exception: elif hashlib.sha256(newcontent).hexdigest() != update_info.checksum:
self.ydl.report_warning('no hash information found for the release') return self._report_network_error('verify the new executable', tag=update_info.tag)
else:
if hashlib.sha256(newcontent).hexdigest() != expected_hash:
return self._report_network_error('verify the new executable')
try: try:
with open(new_filename, 'wb') as outf: with open(new_filename, 'wb') as outf:
@ -388,9 +516,14 @@ class Updater:
return self._report_error( return self._report_error(
f'Unable to set permissions. Run: sudo chmod a+rx {compat_shlex_quote(self.filename)}') f'Unable to set permissions. Run: sudo chmod a+rx {compat_shlex_quote(self.filename)}')
self.ydl.to_screen(f'Updated yt-dlp to {self._label(self.target_channel, self.new_version)}') self.ydl.to_screen(f'Updated yt-dlp to {update_label}')
return True return True
@functools.cached_property
def filename(self):
"""Filename of the executable"""
return compat_realpath(_get_variant_and_executable_path()[1])
@functools.cached_property @functools.cached_property
def cmd(self): def cmd(self):
"""The command-line to run the executable, if known""" """The command-line to run the executable, if known"""
@ -413,53 +546,77 @@ class Updater:
return self.ydl._download_retcode return self.ydl._download_retcode
self.restart = wrapper self.restart = wrapper
def _report_error(self, msg, expected=False):
self.ydl.report_error(msg, tb=False if expected else None)
self.ydl._download_retcode = 100
def run_update(ydl): def _report_permission_error(self, file):
"""Update the program file with the latest version from the repository self._report_error(f'Unable to write to {file}; try running as administrator', True)
@returns Whether there was a successful update (No update = False)
"""
return Updater(ydl).update()
# Deprecated def _report_network_error(self, action, delim=';', tag=None):
def update_self(to_screen, verbose, opener): if not tag:
import traceback tag = self.requested_tag
self._report_error(
f'Unable to {action}{delim} visit https://github.com/{self.requested_repo}/releases/'
+ tag if tag == "latest" else f"tag/{tag}", True)
deprecation_warning(f'"{__name__}.update_self" is deprecated and may be removed ' # XXX: Everything below this line in this class is deprecated / for compat only
f'in a future version. Use "{__name__}.run_update(ydl)" instead') @property
def _target_tag(self):
"""Deprecated; requested tag with 'tags/' prepended when necessary for API calls"""
return f'tags/{self.requested_tag}' if self.requested_tag != 'latest' else self.requested_tag
def _check_update(self):
"""Deprecated; report whether there is an update available"""
return bool(self.query_update(_output=True))
def __getattr__(self, attribute: str):
"""Compat getter function for deprecated attributes"""
deprecated_props_map = {
'check_update': '_check_update',
'target_tag': '_target_tag',
'target_channel': 'requested_channel',
}
update_info_props_map = {
'has_update': '_has_update',
'new_version': 'version',
'latest_version': 'requested_version',
'release_name': 'binary_name',
'release_hash': 'checksum',
}
printfn = to_screen if attribute not in deprecated_props_map and attribute not in update_info_props_map:
raise AttributeError(f'{type(self).__name__!r} object has no attribute {attribute!r}')
class FakeYDL(): msg = f'{type(self).__name__}.{attribute} is deprecated and will be removed in a future version'
to_screen = printfn if attribute in deprecated_props_map:
source_name = deprecated_props_map[attribute]
if not source_name.startswith('_'):
msg += f'. Please use {source_name!r} instead'
source = self
mapping = deprecated_props_map
def report_warning(self, msg, *args, **kwargs): else: # attribute in update_info_props_map
return printfn(f'WARNING: {msg}', *args, **kwargs) msg += '. Please call query_update() instead'
source = self.query_update()
if source is None:
source = UpdateInfo('', None, None, None)
source._has_update = False
mapping = update_info_props_map
def report_error(self, msg, tb=None): deprecation_warning(msg)
printfn(f'ERROR: {msg}') for target_name, source_name in mapping.items():
if not verbose: value = getattr(source, source_name)
return setattr(self, target_name, value)
if tb is None:
# Copied from YoutubeDL.trouble
if sys.exc_info()[0]:
tb = ''
if hasattr(sys.exc_info()[1], 'exc_info') and sys.exc_info()[1].exc_info[0]:
tb += ''.join(traceback.format_exception(*sys.exc_info()[1].exc_info))
tb += traceback.format_exc()
else:
tb_data = traceback.format_list(traceback.extract_stack())
tb = ''.join(tb_data)
if tb:
printfn(tb)
def write_debug(self, msg, *args, **kwargs): return getattr(self, attribute)
printfn(f'[debug] {msg}', *args, **kwargs)
def urlopen(self, url):
return opener.open(url)
return run_update(FakeYDL()) def run_update(ydl):
"""Update the program file with the latest version from the repository
@returns Whether there was a successful update (No update = False)
"""
return Updater(ydl).update()
__all__ = ['Updater'] __all__ = ['Updater']

Loading…
Cancel
Save