]> jfr.im git - yt-dlp.git/blob - yt_dlp/update.py
Ensure pre-processor errors do not block video download
[yt-dlp.git] / yt_dlp / update.py
1 import hashlib
2 import json
3 import os
4 import platform
5 import subprocess
6 import sys
7 import traceback
8 from zipimport import zipimporter
9
10 from .compat import compat_realpath, functools
11 from .utils import Popen, encode_compat_str, write_string
12 from .version import __version__
13
14
15 @functools.cache
16 def detect_variant():
17 if hasattr(sys, 'frozen'):
18 prefix = 'mac' if sys.platform == 'darwin' else 'win'
19 if getattr(sys, '_MEIPASS', None):
20 if sys._MEIPASS == os.path.dirname(sys.executable):
21 return f'{prefix}_dir'
22 return f'{prefix}_exe'
23 return 'py2exe'
24 elif isinstance(__loader__, zipimporter):
25 return 'zip'
26 elif os.path.basename(sys.argv[0]) == '__main__.py':
27 return 'source'
28 return 'unknown'
29
30
31 _NON_UPDATEABLE_REASONS = {
32 'win_exe': None,
33 'zip': None,
34 'mac_exe': None,
35 'py2exe': None,
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',
38 'source': 'You cannot update when running from source code; Use git to pull the latest changes',
39 'unknown': 'It looks like you installed yt-dlp with a package manager, pip or setup.py; Use that to update',
40 }
41
42
43 def is_non_updateable():
44 return _NON_UPDATEABLE_REASONS.get(detect_variant(), _NON_UPDATEABLE_REASONS['unknown'])
45
46
47 def run_update(ydl):
48 """
49 Update the program file with the latest version from the repository
50 Returns whether the program should terminate
51 """
52
53 JSON_URL = 'https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest'
54
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)
66
67 def calc_sha256sum(path):
68 h = hashlib.sha256()
69 b = bytearray(128 * 1024)
70 mv = memoryview(b)
71 with open(os.path.realpath(path), 'rb', buffering=0) as f:
72 for n in iter(lambda: f.readinto(mv), 0):
73 h.update(mv[:n])
74 return h.hexdigest()
75
76 # Download and check versions info
77 try:
78 version_info = ydl._opener.open(JSON_URL).read().decode()
79 version_info = json.loads(version_info)
80 except Exception:
81 return report_network_error('obtain version info', delim='; Please try again later or')
82
83 def version_tuple(version_str):
84 return tuple(map(int, version_str.split('.')))
85
86 version_id = version_info['tag_name']
87 ydl.to_screen(f'Latest version: {version_id}, Current version: {__version__}')
88 if version_tuple(__version__) >= version_tuple(version_id):
89 ydl.to_screen(f'yt-dlp is up to date ({__version__})')
90 return
91
92 err = is_non_updateable()
93 if err:
94 return report_error(err, True)
95
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])
100 ydl.to_screen(f'Current Build Hash {calc_sha256sum(filename)}')
101 ydl.to_screen(f'Updating to version {version_id} ...')
102
103 version_labels = {
104 'zip_3': '',
105 'win_exe_64': '.exe',
106 'py2exe_64': '_min.exe',
107 'win_exe_32': '_x86.exe',
108 'mac_exe_64': '_macos',
109 }
110
111 def get_bin_info(bin_or_exe, version):
112 label = version_labels[f'{bin_or_exe}_{version}']
113 return next((i for i in version_info['assets'] if i['name'] == 'yt-dlp%s' % label), {})
114
115 def get_sha256sum(bin_or_exe, version):
116 filename = 'yt-dlp%s' % version_labels[f'{bin_or_exe}_{version}']
117 urlh = next(
118 (i for i in version_info['assets'] if i['name'] in ('SHA2-256SUMS')),
119 {}).get('browser_download_url')
120 if not urlh:
121 return None
122 hash_data = ydl._opener.open(urlh).read().decode()
123 return dict(ln.split()[::-1] for ln in hash_data.splitlines()).get(filename)
124
125 if not os.access(filename, os.W_OK):
126 return report_permission_error(filename)
127
128 # PyInstaller
129 variant = detect_variant()
130 if variant in ('win_exe', 'py2exe'):
131 directory = os.path.dirname(filename)
132 if not os.access(directory, os.W_OK):
133 return report_permission_error(directory)
134 try:
135 if os.path.exists(filename + '.old'):
136 os.remove(filename + '.old')
137 except OSError:
138 return report_unable('remove the old version')
139
140 try:
141 arch = platform.architecture()[0][:2]
142 url = get_bin_info(variant, arch).get('browser_download_url')
143 if not url:
144 return report_network_error('fetch updates')
145 urlh = ydl._opener.open(url)
146 newcontent = urlh.read()
147 urlh.close()
148 except OSError:
149 return report_network_error('download latest version')
150
151 try:
152 with open(filename + '.new', 'wb') as outf:
153 outf.write(newcontent)
154 except OSError:
155 return report_permission_error(f'{filename}.new')
156
157 expected_sum = get_sha256sum(variant, arch)
158 if not expected_sum:
159 ydl.report_warning('no hash information found for the release')
160 elif calc_sha256sum(filename + '.new') != expected_sum:
161 report_network_error('verify the new executable')
162 try:
163 os.remove(filename + '.new')
164 except OSError:
165 return report_unable('remove corrupt download')
166
167 try:
168 os.rename(filename, filename + '.old')
169 except OSError:
170 return report_unable('move current version')
171 try:
172 os.rename(filename + '.new', filename)
173 except OSError:
174 report_unable('overwrite current version')
175 os.rename(filename + '.old', filename)
176 return
177 try:
178 # Continues to run in the background
179 Popen(
180 'ping 127.0.0.1 -n 5 -w 1000 & del /F "%s.old"' % filename,
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:
185 report_unable('delete the old version')
186
187 elif variant in ('zip', 'mac_exe'):
188 pack_type = '3' if variant == 'zip' else '64'
189 try:
190 url = get_bin_info(variant, pack_type).get('browser_download_url')
191 if not url:
192 return report_network_error('fetch updates')
193 urlh = ydl._opener.open(url)
194 newcontent = urlh.read()
195 urlh.close()
196 except OSError:
197 return report_network_error('download the latest version')
198
199 expected_sum = get_sha256sum(variant, pack_type)
200 if not expected_sum:
201 ydl.report_warning('no hash information found for the release')
202 elif hashlib.sha256(newcontent).hexdigest() != expected_sum:
203 return report_network_error('verify the new package')
204
205 try:
206 with open(filename, 'wb') as outf:
207 outf.write(newcontent)
208 except OSError:
209 return report_unable('overwrite current version')
210
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}'
215
216
217 # Deprecated
218 def update_self(to_screen, verbose, opener):
219
220 printfn = to_screen
221
222 write_string(
223 'DeprecationWarning: "yt_dlp.update.update_self" is deprecated and may be removed in a future version. '
224 'Use "yt_dlp.update.run_update(ydl)" instead\n')
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())