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