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:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
Reference in New Issue
Block a user