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