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