diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ca17a1e59..0c8831927 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -449,6 +449,19 @@ jobs: asset_name: SHA2-512SUMS asset_content_type: text/plain + - name: Make Update spec + run: | + echo "# This file is used for regulating self-update" >> _update_spec + - name: Upload update spec + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.create_release.outputs.upload_url }} + asset_path: ./_update_spec + asset_name: _update_spec + asset_content_type: text/plain + - name: Finalize release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/yt_dlp/YoutubeDL.py b/yt_dlp/YoutubeDL.py index c6882d0d7..9ebb0b82a 100644 --- a/yt_dlp/YoutubeDL.py +++ b/yt_dlp/YoutubeDL.py @@ -10,7 +10,6 @@ import locale import operator import os -import platform import random import re import shutil @@ -110,7 +109,6 @@ number_of_digits, orderedSet, parse_filesize, - platform_name, preferredencoding, prepend_extension, register_socks_protocols, @@ -126,6 +124,7 @@ strftime_or_none, subtitles_filename, supports_terminal_sequences, + system_identifier, timetuple_from_msec, to_high_limit_path, traverse_obj, @@ -3656,17 +3655,7 @@ def get_encoding(stream): with contextlib.suppress(Exception): sys.exc_clear() - def python_implementation(): - impl_name = platform.python_implementation() - if impl_name == 'PyPy' and hasattr(sys, 'pypy_version_info'): - return impl_name + ' version %d.%d.%d' % sys.pypy_version_info[:3] - return impl_name - - write_debug('Python version %s (%s %s) - %s' % ( - platform.python_version(), - python_implementation(), - platform.architecture()[0], - platform_name())) + write_debug(system_identifier()) exe_versions, ffmpeg_features = FFmpegPostProcessor.get_versions_and_features(self) ffmpeg_features = {key for key, val in ffmpeg_features.items() if val} diff --git a/yt_dlp/update.py b/yt_dlp/update.py index c42144337..9589443a7 100644 --- a/yt_dlp/update.py +++ b/yt_dlp/update.py @@ -3,17 +3,25 @@ import json import os import platform +import re import subprocess import sys from zipimport import zipimporter from .compat import functools # isort: split from .compat import compat_realpath -from .utils import Popen, shell_quote, traverse_obj, version_tuple +from .utils import ( + Popen, + cached_method, + shell_quote, + system_identifier, + traverse_obj, + version_tuple, +) from .version import __version__ REPOSITORY = 'yt-dlp/yt-dlp' -API_URL = f'https://api.github.com/repos/{REPOSITORY}/releases/latest' +API_URL = f'https://api.github.com/repos/{REPOSITORY}/releases' @functools.cache @@ -79,9 +87,20 @@ def __init__(self, ydl): self.ydl = ydl @functools.cached_property - def _new_version_info(self): - self.ydl.write_debug(f'Fetching release info: {API_URL}') - return json.loads(self.ydl.urlopen(API_URL).read().decode()) + def _tag(self): + identifier = f'{detect_variant()} {system_identifier()}' + for line in self._download('_update_spec', 'latest').decode().splitlines(): + if not line.startswith('lock '): + continue + _, tag, pattern = line.split(' ', 2) + if re.match(pattern, identifier): + return f'tags/{tag}' + return 'latest' + + @cached_method + def _get_version_info(self, tag): + self.ydl.write_debug(f'Fetching release info: {API_URL}/{tag}') + return json.loads(self.ydl.urlopen(f'{API_URL}/{tag}').read().decode()) @property def current_version(self): @@ -91,7 +110,7 @@ def current_version(self): @property def new_version(self): """Version of the latest release""" - return self._new_version_info['tag_name'] + return self._get_version_info(self._tag)['tag_name'] @property def has_update(self): @@ -103,9 +122,8 @@ def filename(self): """Filename of the executable""" return compat_realpath(_get_variant_and_executable_path()[1]) - def _download(self, name=None): - name = name or self.release_name - url = traverse_obj(self._new_version_info, ( + def _download(self, name, tag): + url = traverse_obj(self._get_version_info(tag), ( 'assets', lambda _, v: v['name'] == name, 'browser_download_url'), get_all=False) if not url: raise Exception('Unable to find download URL') @@ -123,7 +141,7 @@ def release_name(self): @functools.cached_property def release_hash(self): """Hash of the latest release""" - hash_data = dict(ln.split()[::-1] for ln in self._download('SHA2-256SUMS').decode().splitlines()) + 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): @@ -176,7 +194,7 @@ def update(self): return self._report_error('Unable to remove the old version') try: - newcontent = self._download() + newcontent = self._download(self.release_name, self._tag) except OSError: return self._report_network_error('download latest version') except Exception: diff --git a/yt_dlp/utils.py b/yt_dlp/utils.py index 40cefd62e..9c9be5fe5 100644 --- a/yt_dlp/utils.py +++ b/yt_dlp/utils.py @@ -18,6 +18,7 @@ import http.client import http.cookiejar import importlib.util +import inspect import io import itertools import json @@ -1909,12 +1910,23 @@ def __str__(self): def platform_name(): """ Returns the platform name as a str """ - res = platform.platform() - if isinstance(res, bytes): - res = res.decode(preferredencoding()) + write_string('DeprecationWarning: yt_dlp.utils.platform_name is deprecated, use platform.platform instead') + return platform.platform() - assert isinstance(res, str) - return res + +@functools.cache +def system_identifier(): + python_implementation = platform.python_implementation() + if python_implementation == 'PyPy' and hasattr(sys, 'pypy_version_info'): + python_implementation += ' version %d.%d.%d' % sys.pypy_version_info[:3] + + return 'Python %s (%s %s) - %s %s' % ( + platform.python_version(), + python_implementation, + platform.architecture()[0], + platform.platform(), + format_field(join_nonempty(*platform.libc_ver(), delim=' '), None, '(%s)'), + ) @functools.cache @@ -5544,8 +5556,27 @@ def merge_headers(*dicts): return {k.title(): v for k, v in itertools.chain.from_iterable(map(dict.items, dicts))} +def cached_method(f): + """Cache a method""" + signature = inspect.signature(f) + + @functools.wraps(f) + def wrapper(self, *args, **kwargs): + bound_args = signature.bind(self, *args, **kwargs) + bound_args.apply_defaults() + key = tuple(bound_args.arguments.values()) + + if not hasattr(self, '__cached_method__cache'): + self.__cached_method__cache = {} + cache = self.__cached_method__cache.setdefault(f.__name__, {}) + if key not in cache: + cache[key] = f(self, *args, **kwargs) + return cache[key] + return wrapper + + class classproperty: - """classmethod(property(func)) that works in py < 3.9""" + """property access for class methods""" def __init__(self, func): functools.update_wrapper(self, func)