File: update_ejs.py

package info (click to toggle)
yt-dlp 2026.02.21-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 14,724 kB
  • sloc: python: 221,100; javascript: 865; makefile: 220; sh: 89
file content (166 lines) | stat: -rwxr-xr-x 5,325 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
#!/usr/bin/env python3
from __future__ import annotations

import contextlib
import io
import json
import hashlib
import pathlib
import urllib.request
import zipfile


TEMPLATE = '''\
# This file is generated by devscripts/update_ejs.py. DO NOT MODIFY!

VERSION = {version!r}
HASHES = {{
{hash_mapping}
}}
'''
PREFIX = '    "yt-dlp-ejs=='
BASE_PATH = pathlib.Path(__file__).parent.parent
PYPROJECT_PATH = BASE_PATH / 'pyproject.toml'
PACKAGE_PATH = BASE_PATH / 'yt_dlp/extractor/youtube/jsc/_builtin/vendor'
RELEASE_URL = 'https://api.github.com/repos/yt-dlp/ejs/releases/latest'
ASSETS = {
    'yt.solver.lib.js': False,
    'yt.solver.lib.min.js': False,
    'yt.solver.deno.lib.js': True,
    'yt.solver.bun.lib.js': True,
    'yt.solver.core.min.js': False,
    'yt.solver.core.js': True,
}
MAKEFILE_PATH = BASE_PATH / 'Makefile'


def request(url: str):
    return contextlib.closing(urllib.request.urlopen(url))


def makefile_variables(
        version: str | None = None,
        name: str | None = None,
        digest: str | None = None,
        data: bytes | None = None,
        keys_only: bool = False,
) -> dict[str, str | None]:
    assert keys_only or all(arg is not None for arg in (version, name, digest, data))

    return {
        'EJS_VERSION': None if keys_only else version,
        'EJS_WHEEL_NAME': None if keys_only else name,
        'EJS_WHEEL_HASH': None if keys_only else digest,
        'EJS_PY_FOLDERS': None if keys_only else list_wheel_contents(data, 'py', files=False),
        'EJS_PY_FILES': None if keys_only else list_wheel_contents(data, 'py', folders=False),
        'EJS_JS_FOLDERS': None if keys_only else list_wheel_contents(data, 'js', files=False),
        'EJS_JS_FILES': None if keys_only else list_wheel_contents(data, 'js', folders=False),
    }


def list_wheel_contents(
        wheel_data: bytes,
        suffix: str | None = None,
        folders: bool = True,
        files: bool = True,
) -> str:
    assert folders or files, 'at least one of "folders" or "files" must be True'

    with zipfile.ZipFile(io.BytesIO(wheel_data)) as zipf:
        path_gen = (zinfo.filename for zinfo in zipf.infolist())

    filtered = filter(lambda path: path.startswith('yt_dlp_ejs/'), path_gen)
    if suffix:
        filtered = filter(lambda path: path.endswith(f'.{suffix}'), filtered)

    files_list = list(filtered)
    if not folders:
        return ' '.join(files_list)

    folders_list = list(dict.fromkeys(path.rpartition('/')[0] for path in files_list))
    if not files:
        return ' '.join(folders_list)

    return ' '.join(folders_list + files_list)


def main():
    current_version = None
    with PYPROJECT_PATH.open() as file:
        for line in file:
            if not line.startswith(PREFIX):
                continue
            current_version, _, _ = line.removeprefix(PREFIX).partition('"')

    if not current_version:
        print('yt-dlp-ejs dependency line could not be found')
        return

    makefile_info = makefile_variables(keys_only=True)
    prefixes = tuple(f'{key} = ' for key in makefile_info)
    with MAKEFILE_PATH.open() as file:
        for line in file:
            if not line.startswith(prefixes):
                continue
            key, _, val = line.partition(' = ')
            makefile_info[key] = val.rstrip()

    with request(RELEASE_URL) as resp:
        info = json.load(resp)

    version = info['tag_name']
    if version == current_version:
        print(f'yt-dlp-ejs is up to date! ({version})')
        return

    print(f'Updating yt-dlp-ejs from {current_version} to {version}')
    hashes = []
    wheel_info = {}
    for asset in info['assets']:
        name = asset['name']
        is_wheel = name.startswith('yt_dlp_ejs-') and name.endswith('.whl')
        if not is_wheel and name not in ASSETS:
            continue
        with request(asset['browser_download_url']) as resp:
            data = resp.read()

        # verify digest from github
        digest = asset['digest']
        algo, _, expected = digest.partition(':')
        hexdigest = hashlib.new(algo, data).hexdigest()
        assert hexdigest == expected, f'downloaded attest mismatch ({hexdigest!r} != {expected!r})'

        if is_wheel:
            wheel_info = makefile_variables(version, name, digest, data)
            continue

        # calculate sha3-512 digest
        asset_hash = hashlib.sha3_512(data).hexdigest()
        hashes.append(f'    {name!r}: {asset_hash!r},')

        if ASSETS[name]:
            (PACKAGE_PATH / name).write_bytes(data)

    hash_mapping = '\n'.join(hashes)
    for asset_name in ASSETS:
        assert asset_name in hash_mapping, f'{asset_name} not found in release'

    assert all(wheel_info.get(key) for key in makefile_info), 'wheel info not found in release'

    (PACKAGE_PATH / '_info.py').write_text(TEMPLATE.format(
        version=version,
        hash_mapping=hash_mapping,
    ))

    content = PYPROJECT_PATH.read_text()
    updated = content.replace(PREFIX + current_version, PREFIX + version)
    PYPROJECT_PATH.write_text(updated)

    makefile = MAKEFILE_PATH.read_text()
    for key in wheel_info:
        makefile = makefile.replace(f'{key} = {makefile_info[key]}', f'{key} = {wheel_info[key]}')
    MAKEFILE_PATH.write_text(makefile)


if __name__ == '__main__':
    main()