Files
DepressedCat 5f2aad00f4
Check extension and helper / check (push) Canceled after 0s
Publish YouTubeDL 1.4.5 and website on Gitea
2026-10-10 23:53:17 +03:00

117 lines
6.5 KiB
Python

"""Measured dependency status and isolated installers for the helper."""
from __future__ import annotations
import importlib.metadata
import importlib.util
import json
import re
import subprocess
import sys
import tempfile
import urllib.request
import zipfile
from pathlib import Path
from host.media import ROOT, NO_WINDOW, config, run_small, tool
from host.bundle import manifest
LABELS = {"python": "Python", "yt_dlp": "yt-dlp", "ejs": "YouTube JS (EJS)", "node": "Node.js", "ffmpeg": "FFmpeg + FFprobe"}
def status():
python_ok = sys.version_info >= (3, 10)
result = [{"id": "python", "label": LABELS["python"], "status": "installed" if python_ok else "missing", "version": sys.version.split()[0]}]
for key, package in (("yt_dlp", "yt-dlp"), ("ejs", "yt-dlp-ejs")):
try:
version = importlib.metadata.version(package)
result.append({"id": key, "label": LABELS[key], "status": "installed", "version": version})
except importlib.metadata.PackageNotFoundError:
result.append({"id": key, "label": LABELS[key], "status": "missing", "version": ""})
for key in ("node", "ffmpeg"):
try:
measured = run_small([tool(key), "--version" if key == "node" else "-version"], timeout=8)
first = measured.stdout.splitlines()[0] if measured.stdout else ""
if measured.returncode != 0 or not first:
raise ValueError(f"Version check failed ({measured.returncode}): {measured.stderr[-200:]}")
version = first if key == "node" else first.split("version ", 1)[-1].split(" ", 1)[0]
installed = int(version.lstrip("v").split(".", 1)[0]) >= 22 if key == "node" else run_small([tool("ffprobe"), "-version"], timeout=8).returncode == 0
result.append({"id": key, "label": LABELS[key], "status": "installed" if installed else "outdated", "version": version})
except (OSError, ValueError, subprocess.TimeoutExpired) as error:
result.append({"id": key, "label": LABELS[key], "status": "missing", "version": "", "message": str(error)[:300]})
return result
def fetch(url, destination, progress):
request = urllib.request.Request(url, headers={"User-Agent": "YouTubeDL-Helper/1.1"})
with urllib.request.urlopen(request, timeout=30) as response, destination.open("wb") as output:
total = int(response.headers.get("Content-Length", 0)); received = 0
while chunk := response.read(1024 * 512):
output.write(chunk); received += len(chunk)
progress(min(75, received / total * 75) if total else None, "Скачиваю компонент")
def extract(archive, folder):
root = folder.resolve()
with zipfile.ZipFile(archive) as bundle:
for entry in bundle.infolist():
target = (root / entry.filename).resolve()
if target != root and root not in target.parents:
raise ValueError("Некорректный путь в архиве компонента.")
bundle.extract(entry, root)
def install(key, progress, workspace=None):
if manifest():
raise ValueError("Нажмите «Обновить компоненты» для установки новых версий из интернета или «Восстановить» для переустановки встроенного комплекта.")
if key not in {"node", "ffmpeg", "yt_dlp", "ejs"}:
raise ValueError("Неизвестный компонент.")
runtime = ROOT / ".runtime"
runtime.mkdir(exist_ok=True)
cfg = config()
with tempfile.TemporaryDirectory(prefix=".ytdl-install-", dir=runtime) as temporary:
scratch = Path(temporary)
if workspace:
workspace(str(scratch))
if key in {"yt_dlp", "ejs"}:
progress(None, "Устанавливаю Python-пакет")
package = "yt-dlp[default]>=2026.8.19" if key == "yt_dlp" else "yt-dlp-ejs"
measured = subprocess.run([sys.executable, "-m", "pip", "install", "--disable-pip-version-check", "--upgrade", package],
stdin=subprocess.DEVNULL, capture_output=True, text=True, encoding="utf-8", errors="replace", creationflags=NO_WINDOW, timeout=600)
if measured.returncode:
raise ValueError("Не удалось установить компонент. " + measured.stderr[-1200:])
else:
archive = scratch / "component.zip"
if key == "node":
with urllib.request.urlopen("https://nodejs.org/dist/index.json", timeout=30) as response:
versions = json.load(response)
release = next(item["version"] for item in versions if item.get("lts") and "win-x64-zip" in item.get("files", []) and re.fullmatch(r"v\d+\.\d+\.\d+", item["version"]) and int(item["version"].split(".")[0][1:]) >= 22)
fetch(f"https://nodejs.org/dist/{release}/node-{release}-win-x64.zip", archive, progress)
destination = runtime / "tools" / "node"
else:
fetch("https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip", archive, progress)
destination = runtime / "tools" / "ffmpeg"
progress(80, "Распаковываю компонент")
extract(archive, destination)
# Newer versions may coexist; choose the executable from this archive.
with zipfile.ZipFile(archive) as bundle:
relative = next(name for name in bundle.namelist() if name.endswith("/node.exe" if key == "node" else "/bin/ffmpeg.exe"))
executable = destination / relative
cfg[key] = str(executable.resolve())
if key == "ffmpeg":
cfg["ffprobe"] = str(executable.with_name("ffprobe.exe").resolve())
progress(95, "Проверяю компонент")
runtime.joinpath("config.json").write_text(json.dumps(cfg, ensure_ascii=False, indent=2), encoding="utf-8")
measured = next(item for item in status() if item["id"] == key)
if measured["status"] != "installed":
raise ValueError("Компонент установлен, но проверка запуска не прошла.")
progress(100, "Установка завершена")
return {"title": LABELS[key], "dependency": key, "version": measured["version"]}
def main():
key = sys.argv[1]
result = install(key, lambda percent, message: print(f"{message}: {percent if percent is not None else '…'}%", flush=True))
print(json.dumps(result, ensure_ascii=False))
if __name__ == "__main__": main()