117 lines
6.5 KiB
Python
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()
|