Update PVideoDl to support custom download strategies and enhance cookie handling. Added new downloader registration system, updated README for clarity on downloaders, and introduced cookies configuration in settings. Frontend now includes a dedicated tab for download strategies and API endpoints for listing them.

This commit is contained in:
Leonid Pershin
2026-06-20 09:08:31 +03:00
parent 3e47e95fc4
commit eae4def0bf
15 changed files with 639 additions and 74 deletions
+9
View File
@@ -64,3 +64,12 @@ async def test_health(client):
c, _ = client
resp = await c.get("/api/health")
assert resp.status_code == 200 and resp.json()["status"] == "ok"
async def test_list_downloaders(client):
c, _ = client
resp = await c.get("/api/downloaders")
assert resp.status_code == 200
data = resp.json()
kinds = {d["kind"] for d in data}
assert "direct" in kinds and "fallback" in kinds # встроенные httpx + yt-dlp
+121
View File
@@ -0,0 +1,121 @@
import tempfile
from pathlib import Path
import pytest
from app.services.downloader import (
HttpxDownloader,
UnsupportedURLError,
YtDlpDownloader,
_create_unique,
_unique_path,
_ytdlp_cookie_opts,
list_strategies,
pick_downloader,
)
from app.services.extractors import load_extractors
from app.services.extractors.google_drive import GoogleDriveExtractor
def test_unique_path_appends_counter():
d = Path(tempfile.mkdtemp())
assert _unique_path(d, "a.zip") == d / "a.zip"
(d / "a.zip").write_bytes(b"")
assert _unique_path(d, "a.zip") == d / "a (1).zip"
(d / "a (1).zip").write_bytes(b"")
assert _unique_path(d, "a.zip") == d / "a (2).zip"
def test_create_unique_reserves_atomically():
d = Path(tempfile.mkdtemp())
p1, f1 = _create_unique(d, "file.bin")
p2, f2 = _create_unique(d, "file.bin")
try:
assert p1 == d / "file.bin"
assert p2 == d / "file (1).bin" # имя занято — следующий уходит в (1)
assert p1.exists() and p2.exists() # оба зарезервированы сразу, не на старте записи
finally:
f1.close()
f2.close()
def test_create_unique_without_extension():
d = Path(tempfile.mkdtemp())
p1, f1 = _create_unique(d, "noext")
p2, f2 = _create_unique(d, "noext")
try:
assert p1 == d / "noext"
assert p2 == d / "noext (1)"
finally:
f1.close()
f2.close()
# --- Реестр стратегий и расширения ---
def test_pick_downloader_chain():
load_extractors() # подгружаем экстракторы (как делает lifespan)
gd = pick_downloader("https://drive.google.com/file/d/ABC123_xyz/view?usp=sharing")
assert isinstance(gd, GoogleDriveExtractor) # расширение перехватывает раньше всех
assert isinstance(pick_downloader("https://e.com/archive.zip"), HttpxDownloader)
assert isinstance(pick_downloader("https://youtube.com/watch?v=x"), YtDlpDownloader)
def test_pick_downloader_raises_for_unsupported():
# Не прямой файл и не из загрузчиков (example.com — резервный домен, yt-dlp
# его не знает) -> явная ошибка, а не молчаливый фолбэк.
with pytest.raises(UnsupportedURLError):
pick_downloader("https://example.com/just-a-page")
def test_ytdlp_cookie_opts():
assert _ytdlp_cookie_opts(None, None) == {}
assert _ytdlp_cookie_opts("chrome", None) == {
"cookiesfrombrowser": ("chrome", None, None, None)
}
assert _ytdlp_cookie_opts("chrome:Work", None)["cookiesfrombrowser"] == (
"chrome",
"Work",
None,
None,
)
assert _ytdlp_cookie_opts(None, "/tmp/c.txt") == {"cookiefile": "/tmp/c.txt"}
both = _ytdlp_cookie_opts("firefox", "/tmp/c.txt")
assert both["cookiefile"] == "/tmp/c.txt"
assert both["cookiesfrombrowser"][0] == "firefox"
def test_list_strategies_sorted_with_builtins():
strategies = list_strategies()
kinds = {s["kind"] for s in strategies}
assert {"direct", "fallback"} <= kinds
prios = [s["priority"] for s in strategies]
assert prios == sorted(prios, reverse=True) # по убыванию приоритета
def test_load_extractors_idempotent():
from app.services.downloader import _REGISTRY
load_extractors()
before = len(_REGISTRY)
load_extractors() # повторный вызов не должен задваивать реестр
assert len(_REGISTRY) == before
def test_gdrive_matches():
assert GoogleDriveExtractor.matches("https://drive.google.com/file/d/XyZ_1/view")
assert not GoogleDriveExtractor.matches("https://example.com/file.zip")
async def test_gdrive_resolves_to_direct_url():
extractor = GoogleDriveExtractor()
resolved = await extractor.resolve(
"https://drive.google.com/file/d/ABC123_xyz/view?usp=sharing"
)
assert (
resolved.download_url
== "https://drive.google.com/uc?export=download&id=ABC123_xyz"
)