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