atto-downloaders 3.0.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/atto-ytdlp.js +19 -3
- package/package.json +72 -69
- package/start-atto.js +130 -2
package/README.md
CHANGED
|
@@ -86,6 +86,8 @@ Everything is initiated by pure JS — no shell script, no manual install, no ad
|
|
|
86
86
|
5. Resolves ffmpeg: path → winget/static zip (Windows) → `pkg` (Termux) → `brew`/`apt`/`dnf`/`pacman`/`apk` → static `tar.xz` build (Linux, no root).
|
|
87
87
|
6. Writes `state.json` — later calls take ~30 ms.
|
|
88
88
|
|
|
89
|
+
**Alpine/musl note:** musl containers get python from official Alpine apks (extracted user-space, no root) — yt-dlp runs fully. The PO-token server's `canvas` has no musl prebuilt, so PO-token stays optional there: the ladder leans on client rotation and closes at the ATTO providers. A glibc image (`ghcr.io/parkervcp/yolks:nodejs_22`) enables the complete path.
|
|
90
|
+
|
|
89
91
|
In an npm **global** install the package dir is root-owned, so the whole bootstrap lands in `~/.atto/` instead (override with `ATTO_HOME`).
|
|
90
92
|
|
|
91
93
|
## API reference
|
package/atto-ytdlp.js
CHANGED
|
@@ -335,6 +335,16 @@ function findBinary(o) {
|
|
|
335
335
|
return which('yt-dlp') || which('yt-dlp.exe') || which('yt-dlp.cmd') || which('yt-dlp.bat');
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
+
// musl (Alpine) containers cannot run glibc portable builds — detect before wasting a download
|
|
339
|
+
function isMusl() {
|
|
340
|
+
if (process.platform !== 'linux') return false;
|
|
341
|
+
try { return !process.report.getReport().header.glibcVersionRuntime; } catch (_) { /* fall through */ }
|
|
342
|
+
try {
|
|
343
|
+
const r = spawnSync('ldd', ['--version'], { encoding: 'utf8', windowsHide: true });
|
|
344
|
+
return /musl/i.test(`${r.stderr || ''}${r.stdout || ''}`);
|
|
345
|
+
} catch (_) { return true; }
|
|
346
|
+
}
|
|
347
|
+
|
|
338
348
|
let pyCache = null;
|
|
339
349
|
function resolvePython() {
|
|
340
350
|
if (pyCache !== null) return pyCache;
|
|
@@ -1031,7 +1041,7 @@ async function setupPot(o) {
|
|
|
1031
1041
|
if (!fs.existsSync(path.join(dir, 'server', 'package.json'))) {
|
|
1032
1042
|
if (fs.existsSync(dir)) { say.err(`${dir} exists but has no server/ — remove it or pass --pot-dir`); return; }
|
|
1033
1043
|
say.info(`cloning bgutil provider @ ${POT_BRANCH}`);
|
|
1034
|
-
const clone = runTool('git', ['clone', '--depth', '1', '--branch', POT_BRANCH, POT_REPO, dir],
|
|
1044
|
+
const clone = runTool('git', ['-c', 'advice.detachedHead=false', 'clone', '--depth', '1', '--branch', POT_BRANCH, POT_REPO, dir],
|
|
1035
1045
|
{ stdio: 'inherit' });
|
|
1036
1046
|
if (clone.status !== 0) { say.err('git clone failed'); return; }
|
|
1037
1047
|
}
|
|
@@ -1052,7 +1062,13 @@ async function setupPot(o) {
|
|
|
1052
1062
|
const serverDir = path.join(dir, 'server');
|
|
1053
1063
|
say.info('npm ci (this pulls botguard + canvas)');
|
|
1054
1064
|
if (runTool('npm', ['ci'], { cwd: serverDir, stdio: 'inherit' }).status !== 0) {
|
|
1055
|
-
|
|
1065
|
+
if (isMusl()) {
|
|
1066
|
+
say.err('npm ci failed: canvas ships no musl prebuilt — the PO-token server needs a glibc image.');
|
|
1067
|
+
say.err('optional: switch the egg image to ghcr.io/parkervcp/yolks:nodejs_22. Without PO-token the ladder runs pot-less (client rotation) and still closes at ATTO providers.');
|
|
1068
|
+
} else {
|
|
1069
|
+
say.err('npm ci failed');
|
|
1070
|
+
}
|
|
1071
|
+
return;
|
|
1056
1072
|
}
|
|
1057
1073
|
say.info('npx tsc');
|
|
1058
1074
|
if (runTool('npx', ['tsc'], { cwd: serverDir, stdio: 'inherit' }).status !== 0) {
|
|
@@ -1103,7 +1119,7 @@ async function main() {
|
|
|
1103
1119
|
module.exports = {
|
|
1104
1120
|
parseArgs, runUrl, doctor, setupPot, ensurePot, probePot,
|
|
1105
1121
|
classify, findBinary, buildStrategies, applyTypePreset,
|
|
1106
|
-
spawnSpec, resolvePython, have, which, run, say,
|
|
1122
|
+
spawnSpec, resolvePython, have, which, run, say, isMusl,
|
|
1107
1123
|
AUDIO_TYPES, RENDER_TYPES, SOURCE_TYPES, POT_PORT,
|
|
1108
1124
|
};
|
|
1109
1125
|
|
package/package.json
CHANGED
|
@@ -1,69 +1,72 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "atto-downloaders",
|
|
3
|
-
"version": "3.0.
|
|
4
|
-
"description": "Typed media downloader for sites and bots - yt-dlp ladder with PO-token provider, stream-copy exports (zero re-encode), provider fallback, zero-setup bootstrap on Windows/Linux/macOS/Termux",
|
|
5
|
-
"main": "index.js",
|
|
6
|
-
"types": "index.d.ts",
|
|
7
|
-
"exports": {
|
|
8
|
-
".": {
|
|
9
|
-
"types": "./index.d.ts",
|
|
10
|
-
"require": "./index.js",
|
|
11
|
-
"default": "./index.js"
|
|
12
|
-
},
|
|
13
|
-
"./providers": "./atto-downloaders.js",
|
|
14
|
-
"./ytdlp": "./atto-ytdlp.js",
|
|
15
|
-
"./package.json": "./package.json"
|
|
16
|
-
},
|
|
17
|
-
"bin": {
|
|
18
|
-
"atto": "start-atto.js"
|
|
19
|
-
},
|
|
20
|
-
"files": [
|
|
21
|
-
"index.js",
|
|
22
|
-
"index.d.ts",
|
|
23
|
-
"start-atto.js",
|
|
24
|
-
"atto-ytdlp.js",
|
|
25
|
-
"atto-downloaders.js",
|
|
26
|
-
"build-ytdlp.sh",
|
|
27
|
-
"README.md"
|
|
28
|
-
],
|
|
29
|
-
"scripts": {
|
|
30
|
-
"start": "node start-atto.js",
|
|
31
|
-
"setup": "node start-atto.js --setup",
|
|
32
|
-
"atto": "node start-atto.js",
|
|
33
|
-
"test": "node --check index.js && node --check start-atto.js && node --check atto-ytdlp.js && node --check atto-downloaders.js && node -e \"require('./index.js')\"",
|
|
34
|
-
"help": "node start-atto.js --help",
|
|
35
|
-
"providers": "node atto-downloaders.js --list-providers",
|
|
36
|
-
"build": "bash build-ytdlp.sh",
|
|
37
|
-
"doctor": "node start-atto.js --doctor",
|
|
38
|
-
"smoke": "node atto-ytdlp.js --doctor --smoke https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
|
39
|
-
"pot": "node atto-ytdlp.js --setup-pot",
|
|
40
|
-
"ytdl": "node atto-ytdlp.js",
|
|
41
|
-
"prepublishOnly": "npm test"
|
|
42
|
-
},
|
|
43
|
-
"keywords": [
|
|
44
|
-
"yt-dlp",
|
|
45
|
-
"youtube-dl",
|
|
46
|
-
"downloader",
|
|
47
|
-
"media",
|
|
48
|
-
"video",
|
|
49
|
-
"audio",
|
|
50
|
-
"mp3",
|
|
51
|
-
"mp4",
|
|
52
|
-
"bot",
|
|
53
|
-
"scraper",
|
|
54
|
-
"savetube",
|
|
55
|
-
"ytultra",
|
|
56
|
-
"saveclip",
|
|
57
|
-
"po-token",
|
|
58
|
-
"cli",
|
|
59
|
-
"library"
|
|
60
|
-
],
|
|
61
|
-
"author": "Zyon",
|
|
62
|
-
"license": "MIT",
|
|
63
|
-
"optionalDependencies": {
|
|
64
|
-
"playwright-core": "^1.64.1"
|
|
65
|
-
},
|
|
66
|
-
"engines": {
|
|
67
|
-
"node": ">=18.0.0"
|
|
68
|
-
}
|
|
69
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "atto-downloaders",
|
|
3
|
+
"version": "3.0.1",
|
|
4
|
+
"description": "Typed media downloader for sites and bots - yt-dlp ladder with PO-token provider, stream-copy exports (zero re-encode), provider fallback, zero-setup bootstrap on Windows/Linux/macOS/Termux",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./index.d.ts",
|
|
10
|
+
"require": "./index.js",
|
|
11
|
+
"default": "./index.js"
|
|
12
|
+
},
|
|
13
|
+
"./providers": "./atto-downloaders.js",
|
|
14
|
+
"./ytdlp": "./atto-ytdlp.js",
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"bin": {
|
|
18
|
+
"atto": "start-atto.js"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"index.js",
|
|
22
|
+
"index.d.ts",
|
|
23
|
+
"start-atto.js",
|
|
24
|
+
"atto-ytdlp.js",
|
|
25
|
+
"atto-downloaders.js",
|
|
26
|
+
"build-ytdlp.sh",
|
|
27
|
+
"README.md"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"start": "node start-atto.js",
|
|
31
|
+
"setup": "node start-atto.js --setup",
|
|
32
|
+
"atto": "node start-atto.js",
|
|
33
|
+
"test": "node --check index.js && node --check start-atto.js && node --check atto-ytdlp.js && node --check atto-downloaders.js && node -e \"require('./index.js')\"",
|
|
34
|
+
"help": "node start-atto.js --help",
|
|
35
|
+
"providers": "node atto-downloaders.js --list-providers",
|
|
36
|
+
"build": "bash build-ytdlp.sh",
|
|
37
|
+
"doctor": "node start-atto.js --doctor",
|
|
38
|
+
"smoke": "node atto-ytdlp.js --doctor --smoke https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
|
39
|
+
"pot": "node atto-ytdlp.js --setup-pot",
|
|
40
|
+
"ytdl": "node atto-ytdlp.js",
|
|
41
|
+
"prepublishOnly": "npm test"
|
|
42
|
+
},
|
|
43
|
+
"keywords": [
|
|
44
|
+
"yt-dlp",
|
|
45
|
+
"youtube-dl",
|
|
46
|
+
"downloader",
|
|
47
|
+
"media",
|
|
48
|
+
"video",
|
|
49
|
+
"audio",
|
|
50
|
+
"mp3",
|
|
51
|
+
"mp4",
|
|
52
|
+
"bot",
|
|
53
|
+
"scraper",
|
|
54
|
+
"savetube",
|
|
55
|
+
"ytultra",
|
|
56
|
+
"saveclip",
|
|
57
|
+
"po-token",
|
|
58
|
+
"cli",
|
|
59
|
+
"library"
|
|
60
|
+
],
|
|
61
|
+
"author": "Zyon",
|
|
62
|
+
"license": "MIT",
|
|
63
|
+
"optionalDependencies": {
|
|
64
|
+
"playwright-core": "^1.64.1"
|
|
65
|
+
},
|
|
66
|
+
"engines": {
|
|
67
|
+
"node": ">=18.0.0"
|
|
68
|
+
},
|
|
69
|
+
"dependencies": {
|
|
70
|
+
"atto-downloaders": "^3.0.0"
|
|
71
|
+
}
|
|
72
|
+
}
|
package/start-atto.js
CHANGED
|
@@ -42,6 +42,11 @@ const PYTHON_DIR = path.join(DIST, 'python');
|
|
|
42
42
|
const PBS_VER = process.env.ATTO_PY_VER || '3.13.16';
|
|
43
43
|
const PBS_TAG = process.env.ATTO_PY_TAG || '20261013';
|
|
44
44
|
const PBS_URL = triple => `https://github.com/astral-sh/python-build-standalone/releases/download/${PBS_TAG}/cpython-${PBS_VER}+${PBS_TAG}-${triple}-install_only.tar.gz`;
|
|
45
|
+
// musl containers: python from official Alpine apks (extracted user-space, no root);
|
|
46
|
+
// repo paths are per-architecture: /alpine/v<ver>/main/<arch>/
|
|
47
|
+
const ALPINE_VER = process.env.ATTO_ALPINE_VER || '3.22';
|
|
48
|
+
const ALPINE_ARCH = { x64: 'x86_64', arm64: 'aarch64', arm: 'armv7' }[process.arch] || process.arch;
|
|
49
|
+
const ALPINE_REPO = `https://dl-cdn.alpinelinux.org/alpine/v${ALPINE_VER}/main/${ALPINE_ARCH}`;
|
|
45
50
|
const YTDLP_PIN = process.env.ATTO_YTDLP_VERSION || '2026.08.19';
|
|
46
51
|
const POT_BRANCH = process.env.ATTO_POT_BRANCH || '2.0.2';
|
|
47
52
|
const POT_TARBALL = `https://codeload.github.com/Brainicism/bgutil-ytdlp-pot-provider/tar.gz/refs/heads/${POT_BRANCH}`;
|
|
@@ -74,6 +79,8 @@ function findSystemPython() {
|
|
|
74
79
|
|
|
75
80
|
// provisioned portable python wins over PATH — pinned, deterministic, zero admin
|
|
76
81
|
function portablePythonCmd() {
|
|
82
|
+
const alpineWrap = path.join(PYTHON_DIR, 'bin', 'atto-python');
|
|
83
|
+
if (fs.existsSync(alpineWrap)) return { cmd: alpineWrap, prefix: [], name: 'alpine', portable: true };
|
|
77
84
|
const exe = process.platform === 'win32'
|
|
78
85
|
? path.join(PYTHON_DIR, 'python.exe')
|
|
79
86
|
: path.join(PYTHON_DIR, 'bin', 'python3');
|
|
@@ -200,6 +207,13 @@ function findPyRoot(dir, depth = 0) {
|
|
|
200
207
|
async function ensurePortablePython() {
|
|
201
208
|
const have = portablePythonCmd();
|
|
202
209
|
if (have) return have;
|
|
210
|
+
if (ytdlp.isMusl()) {
|
|
211
|
+
// glibc builds never run here — pull python from official Alpine apks instead
|
|
212
|
+
const alpine = await ensureAlpinePython();
|
|
213
|
+
if (alpine) return alpine;
|
|
214
|
+
say.warn('alpine python bootstrap failed — a glibc image (ghcr.io/parkervcp/yolks:nodejs_22) removes this problem');
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
203
217
|
const triple = pbsTriple();
|
|
204
218
|
if (!triple) { say.warn(`no portable python build for ${process.arch}/${process.platform}`); return null; }
|
|
205
219
|
const tar = ytdlp.which('tar');
|
|
@@ -235,6 +249,108 @@ async function ensurePortablePython() {
|
|
|
235
249
|
return port;
|
|
236
250
|
}
|
|
237
251
|
|
|
252
|
+
// musl containers: official Alpine python3 + dependency closure, extracted from .apk
|
|
253
|
+
// tarballs into dist/python (apks ARE gzipped tars) — no root, no apk tooling. The
|
|
254
|
+
// container's musl loader serves the binaries; a wrapper exports LD_LIBRARY_PATH and
|
|
255
|
+
// PYTHONHOME so the relocated prefix resolves its own stdlib.
|
|
256
|
+
function parseApkIndex(text) {
|
|
257
|
+
const pkgs = new Map();
|
|
258
|
+
let cur = null;
|
|
259
|
+
for (const line of text.split(/\r?\n/)) {
|
|
260
|
+
if (!line) { if (cur && cur.name) pkgs.set(cur.name, cur); cur = null; continue; }
|
|
261
|
+
const k = line.slice(0, 2), v = line.slice(2);
|
|
262
|
+
if (k === 'P:') cur = { name: v, ver: '', deps: [], provides: [] };
|
|
263
|
+
else if (!cur) continue;
|
|
264
|
+
else if (k === 'V:') cur.ver = v;
|
|
265
|
+
else if (k === 'D:') cur.deps = v.split(/\s+/).filter(Boolean);
|
|
266
|
+
else if (k === 'p:') cur.provides = v.split(/\s+/).filter(Boolean);
|
|
267
|
+
}
|
|
268
|
+
if (cur && cur.name) pkgs.set(cur.name, cur);
|
|
269
|
+
return pkgs;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// resolve the dependency closure of a root package; so: providers map to their package,
|
|
273
|
+
// cmd:/pc:/path: tokens live in busybox/toolchain the container already has — skipped
|
|
274
|
+
function apkClosure(pkgs, root) {
|
|
275
|
+
const provider = new Map();
|
|
276
|
+
for (const p of pkgs.values()) for (const pr of p.provides) {
|
|
277
|
+
const tok = pr.split('=')[0];
|
|
278
|
+
if (!provider.has(tok)) provider.set(tok, p.name);
|
|
279
|
+
}
|
|
280
|
+
const out = new Map();
|
|
281
|
+
const queue = [root];
|
|
282
|
+
while (queue.length) {
|
|
283
|
+
const name = queue.shift();
|
|
284
|
+
if (out.has(name)) continue;
|
|
285
|
+
const p = pkgs.get(name);
|
|
286
|
+
if (!p) continue;
|
|
287
|
+
out.set(name, p);
|
|
288
|
+
for (let d of p.deps) {
|
|
289
|
+
d = d.split('=')[0];
|
|
290
|
+
if (!d || d.startsWith('!') || d === 'musl' || d.startsWith('cmd:') || d.startsWith('pc:') || d.startsWith('path:')) continue;
|
|
291
|
+
if (d.startsWith('so:')) {
|
|
292
|
+
const hit = provider.get(d);
|
|
293
|
+
if (hit && hit !== 'musl') queue.push(hit);
|
|
294
|
+
} else if (pkgs.has(d)) queue.push(d);
|
|
295
|
+
else if (provider.has(d)) queue.push(provider.get(d));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return [...out.values()];
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function ensureAlpinePython() {
|
|
302
|
+
const wrapper = path.join(PYTHON_DIR, 'bin', 'atto-python');
|
|
303
|
+
if (fs.existsSync(wrapper)) return { cmd: wrapper, prefix: [], name: 'alpine', portable: true };
|
|
304
|
+
const tar = ytdlp.which('tar');
|
|
305
|
+
if (!tar) { say.warn('tar missing — cannot unpack Alpine python'); return null; }
|
|
306
|
+
const idx = path.join(CACHE, `APKINDEX-${ALPINE_VER}.tar.gz`);
|
|
307
|
+
if (!fs.existsSync(idx)) {
|
|
308
|
+
say.info(`fetching Alpine ${ALPINE_VER} package index`);
|
|
309
|
+
try { await downloadFile(`${ALPINE_REPO}/APKINDEX.tar.gz`, idx, 'APKINDEX'); }
|
|
310
|
+
catch (e) { say.warn(`alpine index download failed: ${e.message}`); return null; }
|
|
311
|
+
}
|
|
312
|
+
const tmp = path.join(CACHE, 'apkindex-x');
|
|
313
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
314
|
+
fs.mkdirSync(tmp, { recursive: true });
|
|
315
|
+
if (runTool(tar, ['-xzf', idx, '-C', tmp], { stdio: 'ignore' }).status !== 0) {
|
|
316
|
+
say.warn('APKINDEX unpack failed'); return null;
|
|
317
|
+
}
|
|
318
|
+
let text = '';
|
|
319
|
+
try { text = fs.readFileSync(path.join(tmp, 'APKINDEX'), 'utf8'); } catch (_) { say.warn('APKINDEX missing'); return null; }
|
|
320
|
+
const pkgs = parseApkIndex(text);
|
|
321
|
+
if (!pkgs.has('python3')) { say.warn('python3 not in alpine main repo'); return null; }
|
|
322
|
+
|
|
323
|
+
const closure = apkClosure(pkgs, 'python3');
|
|
324
|
+
say.info(`installing alpine python ${pkgs.get('python3').ver} + ${closure.length - 1} deps (user dir, no root)`);
|
|
325
|
+
fs.mkdirSync(PYTHON_DIR, { recursive: true });
|
|
326
|
+
for (const p of closure) {
|
|
327
|
+
const apk = path.join(CACHE, `${p.name}-${p.ver}.apk`);
|
|
328
|
+
if (!fs.existsSync(apk)) {
|
|
329
|
+
try { await downloadFile(`${ALPINE_REPO}/${p.name}-${p.ver}.apk`, apk, p.name); }
|
|
330
|
+
catch (e) { say.warn(`${p.name}: download failed (${e.message}) — continuing`); continue; }
|
|
331
|
+
}
|
|
332
|
+
if (runTool(tar, ['-xzf', apk, '-C', PYTHON_DIR], { stdio: 'ignore' }).status !== 0) {
|
|
333
|
+
say.warn(`${p.name}: unpack failed — continuing`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
const pyBin = path.join(PYTHON_DIR, 'usr', 'bin', 'python3');
|
|
337
|
+
if (!fs.existsSync(pyBin)) { say.warn('python3 missing after apk extraction'); return null; }
|
|
338
|
+
fs.mkdirSync(path.dirname(wrapper), { recursive: true });
|
|
339
|
+
fs.writeFileSync(wrapper, '#!/bin/sh\n' +
|
|
340
|
+
'HERE=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)\n' +
|
|
341
|
+
'export LD_LIBRARY_PATH="$HERE/usr/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"\n' +
|
|
342
|
+
'export PYTHONHOME="$HERE/usr"\n' +
|
|
343
|
+
'exec "$HERE/usr/bin/python3" "$@"\n');
|
|
344
|
+
fs.chmodSync(wrapper, 0o755);
|
|
345
|
+
const probe = spawnSync(wrapper, ['-V'], { encoding: 'utf8', windowsHide: true });
|
|
346
|
+
if (probe.status !== 0) {
|
|
347
|
+
say.warn(`alpine python failed to start: ${(probe.stderr || probe.stdout || '').trim().slice(0, 200)}`);
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
say.ok(`alpine python ready: ${(probe.stdout || '').trim()}`);
|
|
351
|
+
return { cmd: wrapper, prefix: [], name: 'alpine', portable: true };
|
|
352
|
+
}
|
|
353
|
+
|
|
238
354
|
// ---------------- setup steps ----------------
|
|
239
355
|
|
|
240
356
|
async function stepPython(opts) {
|
|
@@ -251,6 +367,9 @@ async function stepPython(opts) {
|
|
|
251
367
|
if (port) return { ok: true, how: 'portable', at: port.cmd };
|
|
252
368
|
const sys = findSystemPython();
|
|
253
369
|
if (sys) return { ok: true, how: 'found', at: sys.cmd };
|
|
370
|
+
if (ytdlp.isMusl()) {
|
|
371
|
+
return { ok: false, msg: 'musl: alpine python bootstrap failed — a glibc image (ghcr.io/parkervcp/yolks:nodejs_22) fixes this' };
|
|
372
|
+
}
|
|
254
373
|
const plat = platInfo();
|
|
255
374
|
say.warn('portable python unavailable — trying the package manager');
|
|
256
375
|
if (plat.name === 'android') {
|
|
@@ -341,8 +460,17 @@ async function stepYtDlp(prevMinor) {
|
|
|
341
460
|
// phase 1 — core (must succeed)
|
|
342
461
|
let pr = pipRun([`yt-dlp==${want}`, 'yt-dlp-ejs']);
|
|
343
462
|
if (pr.status !== 0) {
|
|
344
|
-
say.warn('pip missing or failed —
|
|
463
|
+
say.warn('pip missing or failed — bootstrapping pip, then retrying');
|
|
345
464
|
runTool(py.cmd, [...py.prefix, '-m', 'ensurepip', '--upgrade'], { encoding: 'utf8', windowsHide: true });
|
|
465
|
+
if (runTool(py.cmd, [...py.prefix, '-m', 'pip', '--version'], { encoding: 'utf8', windowsHide: true }).status !== 0) {
|
|
466
|
+
// Alpine pythons ship without pip/ensurepip — get-pip.py is pure python
|
|
467
|
+
try {
|
|
468
|
+
const gp = path.join(CACHE, 'get-pip.py');
|
|
469
|
+
if (!fs.existsSync(gp)) await downloadFile('https://bootstrap.pypa.io/get-pip.py', gp, 'get-pip');
|
|
470
|
+
runTool(py.cmd, [...py.prefix, gp, '--disable-pip-version-check', '--no-warn-script-location'],
|
|
471
|
+
{ encoding: 'utf8', windowsHide: true });
|
|
472
|
+
} catch (e) { say.warn(`get-pip failed: ${e.message}`); }
|
|
473
|
+
}
|
|
346
474
|
pr = pipRun([`yt-dlp==${want}`, 'yt-dlp-ejs']);
|
|
347
475
|
if (pr.status !== 0) {
|
|
348
476
|
const tail = pr.out.trim().split(/\r?\n/).slice(-6).join('\n');
|
|
@@ -815,7 +943,7 @@ async function cli() {
|
|
|
815
943
|
process.exit(failed ? 1 : 0);
|
|
816
944
|
}
|
|
817
945
|
|
|
818
|
-
module.exports = { setup, download, links, formats, doctor, platInfo, TYPES, ROOT };
|
|
946
|
+
module.exports = { setup, download, links, formats, doctor, platInfo, TYPES, ROOT, parseApkIndex, apkClosure };
|
|
819
947
|
|
|
820
948
|
if (require.main === module) {
|
|
821
949
|
cli().catch(e => { say.err(e.stack || e.message); process.exit(1); });
|