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