Files
YouTubeDL/host/common.py
T
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

158 lines
8.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import math
import re
from pathlib import Path
from urllib.parse import parse_qs, urlsplit
HOST_NAME = "com.youtubedl.native"
VERSION = "1.3.2"
EXTENSION_ID = "ndnoeiaecgmjnjneekaeapeecmmjheli"
VIDEO_CONTAINERS = {"mp4", "mkv", "webm"}
AUDIO_CONTAINERS = {"m4a", "mp3", "opus", "flac", "wav"}
VIDEO_CODECS = {"auto", "copy", "h264", "hevc", "av1", "vp9"}
ENGINES = {"auto", "nvenc", "amf", "cpu"}
MODES = {"video", "video_only", "audio"}
FORMAT_ID = re.compile(r"^[A-Za-z0-9_.-]{1,80}$")
VIDEO_ID = re.compile(r"^[A-Za-z0-9_-]{11}$")
def youtube_url(value: object) -> str:
if not isinstance(value, str) or len(value) > 2048:
raise ValueError("Нужна ссылка на один ролик YouTube.")
try:
parsed = urlsplit(value.strip())
if parsed.scheme != "https" or parsed.username or parsed.password or parsed.port not in (None, 443):
raise ValueError()
host = (parsed.hostname or "").lower()
parts = parsed.path.strip("/").split("/")
if host == "youtu.be" and len(parts) == 1:
video_id = parts[0]
elif host in {"youtube.com", "www.youtube.com", "m.youtube.com", "music.youtube.com"}:
if parsed.path == "/watch":
video_id = parse_qs(parsed.query).get("v", [""])[0]
elif len(parts) == 2 and parts[0] in {"shorts", "live", "embed"}:
video_id = parts[1]
else:
raise ValueError()
else:
raise ValueError()
if not VIDEO_ID.fullmatch(video_id):
raise ValueError()
except (ValueError, TypeError):
raise ValueError("Поддерживаются HTTPS-ссылки на видео и Shorts с YouTube.") from None
return f"https://www.youtube.com/watch?v={video_id}"
def format_id(value: object) -> str:
if not isinstance(value, str) or not FORMAT_ID.fullmatch(value):
raise ValueError("Некорректный идентификатор дорожки.")
return value
def number(value, default=0):
return value if isinstance(value, (int, float)) and math.isfinite(value) else default
def video_formats(info: dict) -> list[dict]:
return [f for f in info.get("formats", []) if f.get("vcodec") not in (None, "none")
and number(f.get("height")) > 0 and not f.get("has_drm") and FORMAT_ID.fullmatch(str(f.get("format_id", "")))]
def audio_formats(info: dict) -> list[dict]:
return [f for f in info.get("formats", []) if f.get("vcodec") == "none"
and f.get("acodec") not in (None, "none") and not f.get("has_drm")
and FORMAT_ID.fullmatch(str(f.get("format_id", "")))]
def summarize(info: dict) -> dict:
if info.get("is_live"):
raise ValueError("Дождитесь окончания прямой трансляции. Сейчас поддерживаются готовые ролики.")
videos = []
for f in reversed(video_formats(info)):
videos.append({"id": str(f["format_id"]), "height": number(f.get("height")),
"width": number(f.get("width")), "fps": number(f.get("fps")),
"codec": f.get("vcodec", ""), "extension": f.get("ext", ""),
"hasAudio": f.get("acodec") not in (None, "none"),
"size": number(f.get("filesize") or f.get("filesize_approx")),
"hdr": f.get("dynamic_range", "SDR")})
audios = [{"id": str(f["format_id"]), "codec": f.get("acodec", ""),
"extension": f.get("ext", ""), "bitrate": number(f.get("abr") or f.get("tbr")),
"language": f.get("language") or "", "note": f.get("format_note") or "",
"size": number(f.get("filesize") or f.get("filesize_approx"))}
for f in reversed(audio_formats(info))]
if not videos and not audios:
raise ValueError("YouTube не вернул доступных дорожек для этого ролика.")
return {"id": str(info.get("id", "")), "url": youtube_url(info.get("webpage_url", "")),
"title": str(info.get("title", "Видео"))[:300],
"channel": str(info.get("uploader") or info.get("channel") or "")[:200],
"duration": number(info.get("duration")), "videos": videos[:180], "audios": audios[:100]}
def validate_download(payload: dict) -> dict:
if not isinstance(payload, dict):
raise ValueError("Некорректная заявка.")
mode = payload.get("mode", "video")
container = payload.get("container", "mp4")
codec = payload.get("codec", "auto")
engine = payload.get("engine", "auto")
if mode not in MODES or container not in (AUDIO_CONTAINERS if mode == "audio" else VIDEO_CONTAINERS):
raise ValueError("Выберите корректный тип загрузки и формат файла.")
if codec not in VIDEO_CODECS or engine not in ENGINES or payload.get("preset", "balanced") not in {"fast", "balanced", "quality"}:
raise ValueError("Некорректные настройки кодирования.")
bitrate = payload.get("bitrate", 256)
if isinstance(bitrate, bool) or bitrate not in {128, 192, 256, 320}:
raise ValueError("Некорректный битрейт аудио.")
folder = payload.get("folder")
if not isinstance(folder, str) or not folder or len(folder) > 4096:
raise ValueError("Выберите папку сохранения.")
path = Path(folder).expanduser()
if not path.is_absolute() or not path.is_dir():
raise ValueError("Папка сохранения должна существовать и иметь абсолютный путь.")
work_folder = payload.get("workFolder", "")
if not isinstance(work_folder, str) or len(work_folder) > 4096:
raise ValueError("Выберите корректную рабочую папку.")
work_folder = work_folder or folder
work_path = Path(work_folder).expanduser()
if not work_path.is_absolute() or not work_path.is_dir():
raise ValueError("Рабочая папка должна существовать и иметь абсолютный путь.")
name = payload.get("filename", "")
if not isinstance(name, str) or len(name) > 180:
raise ValueError("Имя файла слишком длинное.")
return {"url": youtube_url(payload.get("url")), "mode": mode, "container": container,
"codec": codec, "engine": engine, "preset": payload.get("preset", "balanced"),
"bitrate": bitrate, "folder": str(path.resolve()), "workFolder": str(work_path.resolve()), "filename": name,
"videoId": format_id(payload.get("videoId", "best")),
"audioId": format_id(payload.get("audioId", "best"))}
def select_streams(info: dict, options: dict) -> str:
videos, audios = video_formats(info), audio_formats(info)
def choose(items, wanted, label):
if not items:
raise ValueError(f"Недоступна дорожка: {label}.")
if wanted == "best":
return items[-1]
for item in items:
if str(item["format_id"]) == wanted:
return item
raise ValueError("Выбранная дорожка исчезла. Обновите список качества.")
if options["mode"] == "audio":
# A progressive video is a valid fallback when separate audio is absent.
return str(choose(audios or [f for f in videos if f.get("acodec") not in (None, "none")],
options["audioId"], "аудио")["format_id"])
video = choose(videos, options["videoId"], "видео")
if options["mode"] == "video_only" or video.get("acodec") not in (None, "none"):
return str(video["format_id"])
audio = choose(audios, options["audioId"], "аудио")
return f"{video['format_id']}+{audio['format_id']}"
def safe_filename(value: str, fallback: str) -> str:
value = re.sub(r'[<>:"/\\|?*\x00-\x1f]', "_", value).strip(" .")[:150].rstrip(" .")
if not value:
value = fallback
if value.split(".", 1)[0].upper() in {"CON", "PRN", "AUX", "NUL", *(f"COM{i}" for i in range(1, 10)), *(f"LPT{i}" for i in range(1, 10))}:
value = "_" + value
return value