mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-12 12:05:07 +00:00
Renames 314 defs and 41 module/class-level constants. Keeps 11 names underscored where the stripped form collides in scope, plus 5.5.0 shims for the three symbols external service repos call.
142 lines
5.9 KiB
Python
142 lines
5.9 KiB
Python
"""E2E: track quality probing per service.
|
|
|
|
Sends a session_create with the full track-selection knob set (range_,
|
|
vcodec, best_available) so the server returns every available track.
|
|
Picks S01E01 for series, first title for movies, then asserts that the
|
|
discovered video tracks meet the expected_quality limits declared in
|
|
fixtures.yaml. If no expected block is present, the test prints what was
|
|
discovered and skips so you can copy values back into the fixture.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
from typing import Any, Optional
|
|
|
|
import pytest
|
|
|
|
pytestmark = [pytest.mark.live, pytest.mark.slow]
|
|
|
|
|
|
def create_session(http_session, server_url: str, service: str, title_id: str) -> str:
|
|
payload = {
|
|
"service": service,
|
|
"title_id": title_id,
|
|
# Quality knobs — send enum names the server's session_create parser accepts.
|
|
"range_": ["SDR", "HDR10", "DV"],
|
|
"vcodec": ["AVC", "HEVC"],
|
|
"best_available": True,
|
|
}
|
|
r = http_session.post(f"{server_url}/api/session/create", json=payload, timeout=120)
|
|
if r.status_code >= 400:
|
|
pytest.skip(f"{service}: session create failed {r.status_code}: {r.text[:200]}")
|
|
return r.json()["session_id"]
|
|
|
|
|
|
def wait_for_titles(http_session, server_url: str, sid: str, timeout: float = 120.0):
|
|
deadline = time.monotonic() + timeout
|
|
while time.monotonic() < deadline:
|
|
r = http_session.get(f"{server_url}/api/session/{sid}/titles", timeout=30)
|
|
if r.status_code == 200:
|
|
return r.status_code, r.json()
|
|
if r.status_code == 400:
|
|
try:
|
|
body = r.json()
|
|
except Exception:
|
|
body = {}
|
|
if (body.get("details") or {}).get("auth_status") in ("authenticating", "pending_input"):
|
|
time.sleep(2.0)
|
|
continue
|
|
return r.status_code, r.json() if r.text else {}
|
|
return 408, {"message": "auth timeout"}
|
|
|
|
|
|
def pick_target_title(titles: list[dict[str, Any]], season: int = 1, episode: int = 1) -> Optional[dict[str, Any]]:
|
|
"""Pick the configured target episode; fall back to the first title."""
|
|
for t in titles:
|
|
if t.get("type") == "episode" and t.get("season") == season and t.get("number") == episode:
|
|
return t
|
|
return titles[0] if titles else None
|
|
|
|
|
|
def summarize(video_tracks: list[dict[str, Any]]) -> dict[str, Any]:
|
|
max_height = max((v.get("height") or 0 for v in video_tracks), default=0)
|
|
max_width = max((v.get("width") or 0 for v in video_tracks), default=0)
|
|
codecs = sorted({v.get("codec") for v in video_tracks if v.get("codec")})
|
|
ranges = sorted({v.get("range") for v in video_tracks if v.get("range")})
|
|
bitrates = sorted({v.get("bitrate") for v in video_tracks if v.get("bitrate")})
|
|
return {
|
|
"track_count": len(video_tracks),
|
|
"max_width": max_width,
|
|
"max_height": max_height,
|
|
"codecs": codecs,
|
|
"ranges": ranges,
|
|
"bitrate_min_kbps": min(bitrates) if bitrates else None,
|
|
"bitrate_max_kbps": max(bitrates) if bitrates else None,
|
|
}
|
|
|
|
|
|
def resolve_title_id(conf: dict) -> Optional[str]:
|
|
"""Prefer series_url so S01E01 logic kicks in; fall back to movie title_url."""
|
|
return conf.get("series_url") or conf.get("title_url")
|
|
|
|
|
|
def test_track_quality_meets_expected(http_session, server_url: str, service_case, capsys) -> None:
|
|
service, conf = service_case
|
|
title_input = resolve_title_id(conf)
|
|
if not title_input:
|
|
pytest.skip(f"{service}: no title_url/series_url in fixture")
|
|
|
|
sid = create_session(http_session, server_url, service, title_input)
|
|
try:
|
|
code, body = wait_for_titles(http_session, server_url, sid)
|
|
if code != 200:
|
|
pytest.skip(f"{service}: titles fetch failed {code}: {str(body)[:200]}")
|
|
|
|
titles = body.get("titles") or []
|
|
target = pick_target_title(titles, season=conf.get("target_season", 1), episode=conf.get("target_episode", 1))
|
|
if not target:
|
|
pytest.skip(f"{service}: no titles returned")
|
|
|
|
# Movies appear with type=='movie'; episodes with type=='episode'.
|
|
kind = target.get("type")
|
|
title_id = target.get("id")
|
|
if not title_id:
|
|
pytest.skip(f"{service}: target title has no id")
|
|
|
|
r = http_session.post(
|
|
f"{server_url}/api/session/{sid}/tracks",
|
|
json={"title_id": title_id},
|
|
timeout=300,
|
|
)
|
|
if r.status_code != 200:
|
|
pytest.skip(f"{service}: tracks fetch failed {r.status_code}: {r.text[:200]}")
|
|
|
|
tracks = r.json()
|
|
video = tracks.get("video") or []
|
|
summary = summarize(video)
|
|
# Print summary even when test passes — discovery aid.
|
|
with capsys.disabled():
|
|
print(f"\n[{service}] target={kind} '{target.get('name')}' -> {summary}")
|
|
|
|
expected = conf.get("expected_quality") or {}
|
|
if not expected:
|
|
pytest.skip(f"{service}: no expected_quality in fixtures.yaml — discovered={summary}")
|
|
|
|
if "min_height" in expected:
|
|
assert summary["max_height"] >= expected["min_height"], (
|
|
f"{service}: max height {summary['max_height']} < expected min {expected['min_height']}"
|
|
)
|
|
if "min_codecs" in expected:
|
|
missing = set(expected["min_codecs"]) - set(summary["codecs"])
|
|
assert not missing, f"{service}: missing codecs {missing}, got {summary['codecs']}"
|
|
if "min_ranges" in expected:
|
|
missing = set(expected["min_ranges"]) - set(summary["ranges"])
|
|
assert not missing, f"{service}: missing ranges {missing}, got {summary['ranges']}"
|
|
if "min_track_count" in expected:
|
|
assert summary["track_count"] >= expected["min_track_count"], (
|
|
f"{service}: only {summary['track_count']} video tracks, expected >= {expected['min_track_count']}"
|
|
)
|
|
finally:
|
|
http_session.delete(f"{server_url}/api/session/{sid}", timeout=30)
|