]> jfr.im git - yt-dlp.git/blame - yt_dlp/update.py
[cleanup] Misc cleanup
[yt-dlp.git] / yt_dlp / update.py
CommitLineData
15938ab6
PH
1from __future__ import unicode_literals
2
c19bc311 3import hashlib
d5ed35b6 4import json
ce02ed60 5import os
e5813e53 6import platform
d2790370 7import subprocess
46353f67 8import sys
c19bc311 9import traceback
d5ed35b6
FV
10from zipimport import zipimporter
11
bfe2b8cf 12from .compat import compat_realpath
ee8dd27a 13from .utils import encode_compat_str, Popen, write_string
d3d3e2e3 14
d5ed35b6
FV
15from .version import __version__
16
5f6a1245 17
3dd264bf 18''' # Not signed
d5ed35b6 19def rsa_verify(message, signature, key):
d5ed35b6 20 from hashlib import sha256
15938ab6 21 assert isinstance(message, bytes)
4d318be1
FV
22 byte_size = (len(bin(key[0])) - 2 + 8 - 1) // 8
23 signature = ('%x' % pow(int(signature, 16), key[1], key[0])).encode()
24 signature = (byte_size * 2 - len(signature)) * b'0' + signature
25 asn1 = b'3031300d060960864801650304020105000420'
26 asn1 += sha256(message).hexdigest().encode()
27 if byte_size < len(asn1) // 2 + 11:
5f6a1245 28 return False
4d318be1
FV
29 expected = b'0001' + (byte_size - len(asn1) // 2 - 3) * b'ff' + b'00' + asn1
30 return expected == signature
3dd264bf 31'''
d5ed35b6 32
d7386f62 33
4c88ff87 34def detect_variant():
5d535b4a 35 if hasattr(sys, 'frozen'):
0e5927ee 36 prefix = 'mac' if sys.platform == 'darwin' else 'win'
5d535b4a 37 if getattr(sys, '_MEIPASS', None):
38 if sys._MEIPASS == os.path.dirname(sys.executable):
0e5927ee
R
39 return f'{prefix}_dir'
40 return f'{prefix}_exe'
5d535b4a 41 return 'py2exe'
4c88ff87 42 elif isinstance(globals().get('__loader__'), zipimporter):
43 return 'zip'
44 elif os.path.basename(sys.argv[0]) == '__main__.py':
45 return 'source'
46 return 'unknown'
47
48
5d535b4a 49_NON_UPDATEABLE_REASONS = {
0e5927ee 50 'win_exe': None,
5d535b4a 51 'zip': None,
0e5927ee 52 'mac_exe': None,
386cdfdb 53 'py2exe': None,
0e5927ee
R
54 'win_dir': 'Auto-update is not supported for unpackaged windows executable; Re-download the latest release',
55 'mac_dir': 'Auto-update is not supported for unpackaged MacOS executable; Re-download the latest release',
e6faf2be 56 'source': 'You cannot update when running from source code; Use git to pull the latest changes',
57 'unknown': 'It looks like you installed yt-dlp with a package manager, pip, setup.py or a tarball; Use that to update',
5d535b4a 58}
59
60
61def is_non_updateable():
62 return _NON_UPDATEABLE_REASONS.get(detect_variant(), _NON_UPDATEABLE_REASONS['unknown'])
63
64
c19bc311 65def run_update(ydl):
e5813e53 66 """
67 Update the program file with the latest version from the repository
68 Returns whether the program should terminate
69 """
d5ed35b6 70
7a5c1cfe 71 JSON_URL = 'https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest'
d5ed35b6 72
e6faf2be 73 def report_error(msg, expected=False):
74 ydl.report_error(msg, tb='' if expected else None)
75
76 def report_unable(action, expected=False):
77 report_error(f'Unable to {action}', expected)
78
79 def report_permission_error(file):
80 report_unable(f'write to {file}; Try running as administrator', True)
81
82 def report_network_error(action, delim=';'):
83 report_unable(f'{action}{delim} Visit https://github.com/yt-dlp/yt-dlp/releases/latest', True)
c19bc311 84
44f705d0 85 def calc_sha256sum(path):
fa57af1e
U
86 h = hashlib.sha256()
87 b = bytearray(128 * 1024)
88 mv = memoryview(b)
44f705d0 89 with open(os.path.realpath(path), 'rb', buffering=0) as f:
fa57af1e
U
90 for n in iter(lambda: f.readinto(mv), 0):
91 h.update(mv[:n])
92 return h.hexdigest()
93
28234287 94 # Download and check versions info
95 try:
96 version_info = ydl._opener.open(JSON_URL).read().decode('utf-8')
97 version_info = json.loads(version_info)
98 except Exception:
e6faf2be 99 return report_network_error('obtain version info', delim='; Please try again later or')
28234287 100
101 def version_tuple(version_str):
102 return tuple(map(int, version_str.split('.')))
103
104 version_id = version_info['tag_name']
75b725a7 105 ydl.to_screen(f'Latest version: {version_id}, Current version: {__version__}')
28234287 106 if version_tuple(__version__) >= version_tuple(version_id):
107 ydl.to_screen(f'yt-dlp is up to date ({__version__})')
108 return
109
5d535b4a 110 err = is_non_updateable()
4040428e 111 if err:
e6faf2be 112 return report_error(err, True)
d5ed35b6 113
7815e555 114 # sys.executable is set to the full pathname of the exe-file for py2exe
115 # though symlinks are not followed so that we need to do this manually
116 # with help of realpath
117 filename = compat_realpath(sys.executable if hasattr(sys, 'frozen') else sys.argv[0])
91f071af 118 ydl.to_screen(f'Current Build Hash {calc_sha256sum(filename)}')
28234287 119 ydl.to_screen(f'Updating to version {version_id} ...')
3bf79c75 120
44f705d0 121 version_labels = {
122 'zip_3': '',
386cdfdb 123 'win_exe_64': '.exe',
124 'py2exe_64': '_min.exe',
125 'win_exe_32': '_x86.exe',
126 'mac_exe_64': '_macos',
44f705d0 127 }
128
e5813e53 129 def get_bin_info(bin_or_exe, version):
44f705d0 130 label = version_labels['%s_%s' % (bin_or_exe, version)]
c19bc311 131 return next((i for i in version_info['assets'] if i['name'] == 'yt-dlp%s' % label), {})
44f705d0 132
133 def get_sha256sum(bin_or_exe, version):
beb982be 134 filename = 'yt-dlp%s' % version_labels['%s_%s' % (bin_or_exe, version)]
44f705d0 135 urlh = next(
c19bc311 136 (i for i in version_info['assets'] if i['name'] in ('SHA2-256SUMS')),
137 {}).get('browser_download_url')
44f705d0 138 if not urlh:
139 return None
c19bc311 140 hash_data = ydl._opener.open(urlh).read().decode('utf-8')
4c88ff87 141 return dict(ln.split()[::-1] for ln in hash_data.splitlines()).get(filename)
d5ed35b6
FV
142
143 if not os.access(filename, os.W_OK):
e6faf2be 144 return report_permission_error(filename)
d5ed35b6 145
3dd264bf 146 # PyInstaller
0e5927ee 147 variant = detect_variant()
386cdfdb 148 if variant in ('win_exe', 'py2exe'):
149 directory = os.path.dirname(filename)
d5ed35b6 150 if not os.access(directory, os.W_OK):
e6faf2be 151 return report_permission_error(directory)
b25522ba 152 try:
153 if os.path.exists(filename + '.old'):
154 os.remove(filename + '.old')
155 except (IOError, OSError):
e6faf2be 156 return report_unable('remove the old version')
d5ed35b6
FV
157
158 try:
e5813e53 159 arch = platform.architecture()[0][:2]
386cdfdb 160 url = get_bin_info(variant, arch).get('browser_download_url')
44f705d0 161 if not url:
e6faf2be 162 return report_network_error('fetch updates')
c19bc311 163 urlh = ydl._opener.open(url)
d5ed35b6
FV
164 newcontent = urlh.read()
165 urlh.close()
e6faf2be 166 except (IOError, OSError):
167 return report_network_error('download latest version')
d5ed35b6
FV
168
169 try:
733d8e8f 170 with open(filename + '.new', 'wb') as outf:
d5ed35b6 171 outf.write(newcontent)
0b63aed8 172 except (IOError, OSError):
733d8e8f 173 return report_permission_error(f'{filename}.new')
d5ed35b6 174
733d8e8f 175 expected_sum = get_sha256sum(variant, arch)
44f705d0 176 if not expected_sum:
c19bc311 177 ydl.report_warning('no hash information found for the release')
733d8e8f 178 elif calc_sha256sum(filename + '.new') != expected_sum:
e6faf2be 179 report_network_error('verify the new executable')
44f705d0 180 try:
733d8e8f 181 os.remove(filename + '.new')
44f705d0 182 except OSError:
e6faf2be 183 return report_unable('remove corrupt download')
44f705d0 184
d5ed35b6 185 try:
733d8e8f 186 os.rename(filename, filename + '.old')
b25522ba 187 except (IOError, OSError):
e6faf2be 188 return report_unable('move current version')
b25522ba 189 try:
733d8e8f 190 os.rename(filename + '.new', filename)
0b63aed8 191 except (IOError, OSError):
e6faf2be 192 report_unable('overwrite current version')
733d8e8f 193 os.rename(filename + '.old', filename)
b25522ba 194 return
195 try:
196 # Continues to run in the background
d3c93ec2 197 Popen(
733d8e8f 198 'ping 127.0.0.1 -n 5 -w 1000 & del /F "%s.old"' % filename,
b25522ba 199 shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
200 ydl.to_screen('Updated yt-dlp to version %s' % version_id)
201 return True # Exit app
202 except OSError:
e6faf2be 203 report_unable('delete the old version')
d5ed35b6 204
0e5927ee 205 elif variant in ('zip', 'mac_exe'):
386cdfdb 206 pack_type = '3' if variant == 'zip' else '64'
d5ed35b6 207 try:
386cdfdb 208 url = get_bin_info(variant, pack_type).get('browser_download_url')
44f705d0 209 if not url:
e6faf2be 210 return report_network_error('fetch updates')
c19bc311 211 urlh = ydl._opener.open(url)
d5ed35b6
FV
212 newcontent = urlh.read()
213 urlh.close()
e6faf2be 214 except (IOError, OSError):
215 return report_network_error('download the latest version')
d5ed35b6 216
386cdfdb 217 expected_sum = get_sha256sum(variant, pack_type)
beb982be
NA
218 if not expected_sum:
219 ydl.report_warning('no hash information found for the release')
220 elif hashlib.sha256(newcontent).hexdigest() != expected_sum:
0e5927ee 221 return report_network_error('verify the new package')
44f705d0 222
223 try:
c4a508ab 224 with open(filename, 'wb') as outf:
225 outf.write(newcontent)
226 except (IOError, OSError):
e6faf2be 227 return report_unable('overwrite current version')
d5ed35b6 228
0e5927ee
R
229 ydl.to_screen('Updated yt-dlp to version %s; Restart yt-dlp to use the new version' % version_id)
230 return
231
232 assert False, f'Unhandled variant: {variant}'
3bf79c75 233
5f6a1245 234
3dd264bf 235''' # UNUSED
46a127ee 236def get_notes(versions, fromVersion):
3bf79c75 237 notes = []
5f6a1245 238 for v, vdata in sorted(versions.items()):
3bf79c75
PH
239 if v > fromVersion:
240 notes.extend(vdata.get('notes', []))
46a127ee
PH
241 return notes
242
5f6a1245 243
46a127ee
PH
244def print_notes(to_screen, versions, fromVersion=__version__):
245 notes = get_notes(versions, fromVersion)
3bf79c75 246 if notes:
15938ab6 247 to_screen('PLEASE NOTE:')
3bf79c75
PH
248 for note in notes:
249 to_screen(note)
3dd264bf 250'''
e6faf2be 251
252
ee8dd27a 253# Deprecated
e6faf2be 254def update_self(to_screen, verbose, opener):
e6faf2be 255
256 printfn = to_screen
257
ee8dd27a 258 write_string(
259 'DeprecationWarning: "yt_dlp.update.update_self" is deprecated and may be removed in a future version. '
b69fd25c 260 'Use "yt_dlp.update.run_update(ydl)" instead\n')
e6faf2be 261
262 class FakeYDL():
263 _opener = opener
264 to_screen = printfn
265
266 @staticmethod
267 def report_warning(msg, *args, **kwargs):
268 return printfn('WARNING: %s' % msg, *args, **kwargs)
269
270 @staticmethod
271 def report_error(msg, tb=None):
272 printfn('ERROR: %s' % msg)
273 if not verbose:
274 return
275 if tb is None:
276 # Copied from YoutubeDl.trouble
277 if sys.exc_info()[0]:
278 tb = ''
279 if hasattr(sys.exc_info()[1], 'exc_info') and sys.exc_info()[1].exc_info[0]:
280 tb += ''.join(traceback.format_exception(*sys.exc_info()[1].exc_info))
281 tb += encode_compat_str(traceback.format_exc())
282 else:
283 tb_data = traceback.format_list(traceback.extract_stack())
284 tb = ''.join(tb_data)
285 if tb:
286 printfn(tb)
287
288 return run_update(FakeYDL())