~hackershohag/yt-dlp-ut/master

« back to all changes in this revision

Viewing changes to youtube_dl/update.py

  • Committer: GitHub Actions
  • Author(s): Lesmiscore
  • Date: 2023-03-15 00:54:14 UTC
  • Revision ID: git-v1:96c43d89b4a8ec1e555efff6dd92dc29c7c8e433
Tags: 2023.03.15.40298, 2023.03.16.1919, 2023.03.17.114514, 2023.03.18.334, 2023.03.19.43044, 2023.03.20.43044, 2023.03.21.810, 2023.03.22.40298, 2023.03.23.114514, 2023.03.24.114514, 2023.03.25.810, 2023.03.26.43044, 2023.03.27.19419, 2023.03.28.43044, 2023.03.29.1919, 2023.03.30.19419, 2023.03.31.334, 2023.04.01.1919, 2023.04.02.810
Automated Daily Builds

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
from __future__ import unicode_literals
2
2
 
3
 
import io
 
3
import hashlib
4
4
import json
5
 
import traceback
6
 
import hashlib
7
5
import os
 
6
import platform
8
7
import subprocess
9
8
import sys
 
9
import traceback
10
10
from zipimport import zipimporter
11
11
 
12
12
from .compat import compat_realpath
13
 
from .utils import encode_compat_str
 
13
from .utils import encode_compat_str, write_string
14
14
 
15
15
from .version import __version__
16
16
 
17
17
 
18
 
def rsa_verify(message, signature, key):
19
 
    from hashlib import sha256
20
 
    assert isinstance(message, bytes)
21
 
    byte_size = (len(bin(key[0])) - 2 + 8 - 1) // 8
22
 
    signature = ('%x' % pow(int(signature, 16), key[1], key[0])).encode()
23
 
    signature = (byte_size * 2 - len(signature)) * b'0' + signature
24
 
    asn1 = b'3031300d060960864801650304020105000420'
25
 
    asn1 += sha256(message).hexdigest().encode()
26
 
    if byte_size < len(asn1) // 2 + 11:
27
 
        return False
28
 
    expected = b'0001' + (byte_size - len(asn1) // 2 - 3) * b'ff' + b'00' + asn1
29
 
    return expected == signature
30
 
 
31
 
 
32
 
def update_self(to_screen, verbose, opener):
33
 
    """Update the program file with the latest version from the repository"""
34
 
 
35
 
    UPDATE_URL = 'https://yt-dl.org/update/'
36
 
    VERSION_URL = UPDATE_URL + 'LATEST_VERSION'
37
 
    JSON_URL = UPDATE_URL + 'versions.json'
38
 
    UPDATES_RSA_KEY = (0x9d60ee4d8f805312fdb15a62f87b95bd66177b91df176765d13514a0f1754bcd2057295c5b6f1d35daa6742c3ffc9a82d3e118861c207995a8031e151d863c9927e304576bc80692bc8e094896fcf11b66f3e29e04e3a71e9a11558558acea1840aec37fc396fb6b65dc81a1c4144e03bd1c011de62e3f1357b327d08426fe93, 65537)
39
 
 
40
 
    if not isinstance(globals().get('__loader__'), zipimporter) and not hasattr(sys, 'frozen'):
41
 
        to_screen('It looks like you installed youtube-dl with a package manager, pip, setup.py or a tarball. Please use that to update.')
42
 
        return
43
 
 
44
 
    # Check if there is a new version
45
 
    try:
46
 
        newversion = opener.open(VERSION_URL).read().decode('utf-8').strip()
47
 
    except Exception:
48
 
        if verbose:
49
 
            to_screen(encode_compat_str(traceback.format_exc()))
50
 
        to_screen('ERROR: can\'t find the current version. Please try again later.')
51
 
        return
52
 
    if newversion == __version__:
53
 
        to_screen('youtube-dl is up-to-date (' + __version__ + ')')
54
 
        return
 
18
def detect_variant():
 
19
    if hasattr(sys, 'frozen'):
 
20
        if getattr(sys, '_MEIPASS', None):
 
21
            return 'win_exe'
 
22
        return 'py2exe'
 
23
    elif isinstance(globals().get('__loader__'), zipimporter):
 
24
        return 'zip'
 
25
    elif os.path.basename(sys.argv[0]) == '__main__.py':
 
26
        return 'source'
 
27
    return 'unknown'
 
28
 
 
29
 
 
30
_NON_UPDATEABLE_REASONS = {
 
31
    'win_exe': None,
 
32
    'zip': None,
 
33
    'mac_exe': None,
 
34
    'py2exe': None,
 
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',
 
37
    'source': 'You cannot update when running from source code; Use git to pull the latest changes',
 
38
    'unknown': 'It looks like you installed youtube-dl with a package manager, pip or setup.py; Use that to update',
 
39
}
 
40
 
 
41
 
 
42
def is_non_updateable():
 
43
    return _NON_UPDATEABLE_REASONS.get(detect_variant(), _NON_UPDATEABLE_REASONS['unknown'])
 
44
 
 
45
 
 
46
def run_update(ydl):
 
47
    """
 
48
    Update the program file with the latest version from the repository
 
49
    Returns whether the program should terminate
 
50
    """
 
51
 
 
52
    JSON_URL = 'https://api.github.com/repos/ytdl-patched/youtube-dl/releases/latest'
 
53
 
 
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('Unable to %s' % action, expected)
 
59
 
 
60
    def report_permission_error(file):
 
61
        report_unable('write to %s; Try running as administrator' % file, True)
 
62
 
 
63
    def report_network_error(action, delim=';'):
 
64
        report_unable('%s%s Visit  https://github.com/ytdl-patched/youtube-dl/releases/latest' % (action, delim), True)
 
65
 
 
66
    def calc_sha256sum(path):
 
67
        h = hashlib.sha256()
 
68
        b = bytearray(128 * 1024)
 
69
        mv = memoryview(b)
 
70
        with open(os.path.realpath(path), 'rb', buffering=0) as f:
 
71
            for n in iter(lambda: f.readinto(mv), 0):
 
72
                h.update(mv[:n])
 
73
        return h.hexdigest()
55
74
 
56
75
    # Download and check versions info
57
76
    try:
58
 
        versions_info = opener.open(JSON_URL).read().decode('utf-8')
59
 
        versions_info = json.loads(versions_info)
 
77
        version_info = ydl._opener.open(JSON_URL).read().decode('utf-8')
 
78
        version_info = json.loads(version_info)
60
79
    except Exception:
61
 
        if verbose:
62
 
            to_screen(encode_compat_str(traceback.format_exc()))
63
 
        to_screen('ERROR: can\'t obtain versions info. Please try again later.')
64
 
        return
65
 
    if 'signature' not in versions_info:
66
 
        to_screen('ERROR: the versions file is not signed or corrupted. Aborting.')
67
 
        return
68
 
    signature = versions_info['signature']
69
 
    del versions_info['signature']
70
 
    if not rsa_verify(json.dumps(versions_info, sort_keys=True).encode('utf-8'), signature, UPDATES_RSA_KEY):
71
 
        to_screen('ERROR: the versions file signature is invalid. Aborting.')
72
 
        return
73
 
 
74
 
    version_id = versions_info['latest']
 
80
        return report_network_error('obtain version info', delim='; Please try again later or')
75
81
 
76
82
    def version_tuple(version_str):
77
83
        return tuple(map(int, version_str.split('.')))
 
84
 
 
85
    version_id = version_info['tag_name']
 
86
    ydl.to_screen('Latest version: %s, Current version: %s' % (version_id, __version__))
78
87
    if version_tuple(__version__) >= version_tuple(version_id):
79
 
        to_screen('youtube-dl is up to date (%s)' % __version__)
 
88
        ydl.to_screen('youtube-dl is up to date (%s)' % __version__)
80
89
        return
81
90
 
82
 
    to_screen('Updating to version ' + version_id + ' ...')
83
 
    version = versions_info['versions'][version_id]
84
 
 
85
 
    print_notes(to_screen, versions_info['versions'])
 
91
    err = is_non_updateable()
 
92
    if err:
 
93
        return report_error(err, True)
86
94
 
87
95
    # sys.executable is set to the full pathname of the exe-file for py2exe
88
96
    # though symlinks are not followed so that we need to do this manually
89
97
    # with help of realpath
90
98
    filename = compat_realpath(sys.executable if hasattr(sys, 'frozen') else sys.argv[0])
 
99
    ydl.to_screen('Current Build Hash %s' % calc_sha256sum(filename))
 
100
    ydl.to_screen('Updating to version %s ...' % version_id)
 
101
 
 
102
    version_labels = {
 
103
        'zip_3': '',
 
104
        'py2exe_32': '.exe',
 
105
        'py2exe_64': '.exe',
 
106
    }
 
107
 
 
108
    def get_bin_info(bin_or_exe, version):
 
109
        label = version_labels['%s_%s' % (bin_or_exe, version)]
 
110
        return next((i for i in version_info['assets'] if i['name'] == 'youtube-dl%s' % label), {})
 
111
 
 
112
    def get_sha256sum(bin_or_exe, version):
 
113
        filename = 'youtube-dl%s' % version_labels['%s_%s' % (bin_or_exe, version)]
 
114
        urlh = next(
 
115
            (i for i in version_info['assets'] if i['name'] in ('SHA2-256SUMS')),
 
116
            {}).get('browser_download_url')
 
117
        if not urlh:
 
118
            return None
 
119
        hash_data = ydl._opener.open(urlh).read().decode('utf-8')
 
120
        return dict(ln.split()[::-1] for ln in hash_data.splitlines()).get(filename)
91
121
 
92
122
    if not os.access(filename, os.W_OK):
93
 
        to_screen('ERROR: no write permissions on %s' % filename)
94
 
        return
 
123
        return report_permission_error(filename)
95
124
 
96
 
    # Py2EXE
97
 
    if hasattr(sys, 'frozen'):
98
 
        exe = filename
99
 
        directory = os.path.dirname(exe)
 
125
    # PyInstaller
 
126
    variant = detect_variant()
 
127
    if variant in ('win_exe', 'py2exe'):
 
128
        directory = os.path.dirname(filename)
100
129
        if not os.access(directory, os.W_OK):
101
 
            to_screen('ERROR: no write permissions on %s' % directory)
102
 
            return
 
130
            return report_permission_error(directory)
 
131
        try:
 
132
            if os.path.exists(filename + '.old'):
 
133
                os.remove(filename + '.old')
 
134
        except (IOError, OSError):
 
135
            return report_unable('remove the old version')
103
136
 
104
137
        try:
105
 
            urlh = opener.open(version['exe'][0])
 
138
            arch = platform.architecture()[0][:2]
 
139
            url = get_bin_info(variant, arch).get('browser_download_url')
 
140
            if not url:
 
141
                return report_network_error('fetch updates')
 
142
            urlh = ydl._opener.open(url)
106
143
            newcontent = urlh.read()
107
144
            urlh.close()
108
145
        except (IOError, OSError):
109
 
            if verbose:
110
 
                to_screen(encode_compat_str(traceback.format_exc()))
111
 
            to_screen('ERROR: unable to download latest version')
112
 
            return
113
 
 
114
 
        newcontent_hash = hashlib.sha256(newcontent).hexdigest()
115
 
        if newcontent_hash != version['exe'][1]:
116
 
            to_screen('ERROR: the downloaded file hash does not match. Aborting.')
117
 
            return
 
146
            return report_network_error('download latest version')
118
147
 
119
148
        try:
120
 
            with open(exe + '.new', 'wb') as outf:
 
149
            with open(filename + '.new', 'wb') as outf:
121
150
                outf.write(newcontent)
122
151
        except (IOError, OSError):
123
 
            if verbose:
124
 
                to_screen(encode_compat_str(traceback.format_exc()))
125
 
            to_screen('ERROR: unable to write the new version')
126
 
            return
127
 
 
128
 
        try:
129
 
            bat = os.path.join(directory, 'youtube-dl-updater.bat')
130
 
            with io.open(bat, 'w') as batfile:
131
 
                batfile.write('''
132
 
@echo off
133
 
echo Waiting for file handle to be closed ...
134
 
ping 127.0.0.1 -n 5 -w 1000 > NUL
135
 
move /Y "%s.new" "%s" > NUL
136
 
echo Updated youtube-dl to version %s.
137
 
start /b "" cmd /c del "%%~f0"&exit /b"
138
 
                \n''' % (exe, exe, version_id))
139
 
 
140
 
            subprocess.Popen([bat])  # Continues to run in the background
141
 
            return  # Do not show premature success messages
142
 
        except (IOError, OSError):
143
 
            if verbose:
144
 
                to_screen(encode_compat_str(traceback.format_exc()))
145
 
            to_screen('ERROR: unable to overwrite current version')
146
 
            return
147
 
 
148
 
    # Zip unix package
149
 
    elif isinstance(globals().get('__loader__'), zipimporter):
150
 
        try:
151
 
            urlh = opener.open(version['bin'][0])
 
152
            return report_permission_error('%s.new' % filename)
 
153
 
 
154
        expected_sum = get_sha256sum(variant, arch)
 
155
        if not expected_sum:
 
156
            ydl.report_warning('no hash information found for the release')
 
157
        elif calc_sha256sum(filename + '.new') != expected_sum:
 
158
            report_network_error('verify the new executable')
 
159
            try:
 
160
                os.remove(filename + '.new')
 
161
            except OSError:
 
162
                return report_unable('remove corrupt download')
 
163
 
 
164
        try:
 
165
            os.rename(filename, filename + '.old')
 
166
        except (IOError, OSError):
 
167
            return report_unable('move current version')
 
168
        try:
 
169
            os.rename(filename + '.new', filename)
 
170
        except (IOError, OSError):
 
171
            report_unable('overwrite current version')
 
172
            os.rename(filename + '.old', filename)
 
173
            return
 
174
        try:
 
175
            # Continues to run in the background
 
176
            subprocess.Popen(
 
177
                'ping 127.0.0.1 -n 5 -w 1000 & del /F "%s.old"' % filename,
 
178
                shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
 
179
            ydl.to_screen('Updated youtube-dl to version %s' % version_id)
 
180
            return True  # Exit app
 
181
        except OSError:
 
182
            report_unable('delete the old version')
 
183
 
 
184
    elif variant in ('zip', 'mac_exe'):
 
185
        pack_type = '3' if variant == 'zip' else '64'
 
186
        try:
 
187
            url = get_bin_info(variant, pack_type).get('browser_download_url')
 
188
            if not url:
 
189
                return report_network_error('fetch updates')
 
190
            urlh = ydl._opener.open(url)
152
191
            newcontent = urlh.read()
153
192
            urlh.close()
154
193
        except (IOError, OSError):
155
 
            if verbose:
156
 
                to_screen(encode_compat_str(traceback.format_exc()))
157
 
            to_screen('ERROR: unable to download latest version')
158
 
            return
 
194
            return report_network_error('download the latest version')
159
195
 
160
 
        newcontent_hash = hashlib.sha256(newcontent).hexdigest()
161
 
        if newcontent_hash != version['bin'][1]:
162
 
            to_screen('ERROR: the downloaded file hash does not match. Aborting.')
163
 
            return
 
196
        expected_sum = get_sha256sum(variant, pack_type)
 
197
        if not expected_sum:
 
198
            ydl.report_warning('no hash information found for the release')
 
199
        elif hashlib.sha256(newcontent).hexdigest() != expected_sum:
 
200
            return report_network_error('verify the new package')
164
201
 
165
202
        try:
166
203
            with open(filename, 'wb') as outf:
167
204
                outf.write(newcontent)
168
205
        except (IOError, OSError):
169
 
            if verbose:
170
 
                to_screen(encode_compat_str(traceback.format_exc()))
171
 
            to_screen('ERROR: unable to overwrite current version')
172
 
            return
173
 
 
174
 
    to_screen('Updated youtube-dl. Restart youtube-dl to use the new version.')
175
 
 
176
 
 
177
 
def get_notes(versions, fromVersion):
178
 
    notes = []
179
 
    for v, vdata in sorted(versions.items()):
180
 
        if v > fromVersion:
181
 
            notes.extend(vdata.get('notes', []))
182
 
    return notes
183
 
 
184
 
 
185
 
def print_notes(to_screen, versions, fromVersion=__version__):
186
 
    notes = get_notes(versions, fromVersion)
187
 
    if notes:
188
 
        to_screen('PLEASE NOTE:')
189
 
        for note in notes:
190
 
            to_screen(note)
 
206
            return report_unable('overwrite current version')
 
207
 
 
208
        ydl.to_screen('Updated youtube-dl to version %s; Restart youtube-dl to use the new version' % version_id)
 
209
        return
 
210
 
 
211
    assert False, ('Unhandled variant: %s' % variant)
 
212
 
 
213
 
 
214
# Deprecated
 
215
def update_self(to_screen, verbose, opener):
 
216
 
 
217
    printfn = to_screen
 
218
 
 
219
    class FakeYDL():
 
220
        _opener = opener
 
221
        to_screen = printfn
 
222
 
 
223
        @staticmethod
 
224
        def report_warning(msg, *args, **kwargs):
 
225
            return printfn('WARNING: %s' % msg, *args, **kwargs)
 
226
 
 
227
        @staticmethod
 
228
        def report_error(msg, tb=None):
 
229
            printfn('ERROR: %s' % msg)
 
230
            if not verbose:
 
231
                return
 
232
            if tb is None:
 
233
                # Copied from YoutubeDl.trouble
 
234
                if sys.exc_info()[0]:
 
235
                    tb = ''
 
236
                    if hasattr(sys.exc_info()[1], 'exc_info') and sys.exc_info()[1].exc_info[0]:
 
237
                        tb += ''.join(traceback.format_exception(*sys.exc_info()[1].exc_info))
 
238
                    tb += encode_compat_str(traceback.format_exc())
 
239
                else:
 
240
                    tb_data = traceback.format_list(traceback.extract_stack())
 
241
                    tb = ''.join(tb_data)
 
242
            if tb:
 
243
                printfn(tb)
 
244
 
 
245
    return run_update(FakeYDL())