Plex, library and /watch tell the operating system what plays (title, line, picture) and take its
play / pause / skip / seek through one hook (lib/mediaSession); the Plex player drives it with its own
seek and clock, so the lock screen shows the film's position, not the stream's. The YouTube player is
left alone: its iframe already publishes all of it, and the OS routes to the frame that plays.
The library and /watch players get the basic keys (lib/mediaKeys, the keymap's mediaPlayer rows) and
open where the viewer stopped (lib/resume): the library keeps the point on the server, per user and
library item (library_states, POST /api/downloads/{id}/progress, listed back on both listings), the
shared link in this browser. A started library row continues, its menu can start it over, and its
picture carries the cards' progress bar. The resume rule (too early / near the end = from the start)
is one function shared with the YouTube route.
The library dialog opens focused on its panel (Modal initialFocus "player"): on the ✕, Space closed
the film. While the media element itself has the focus, Space and the arrows stay its own controls'
— the browser acts on them whatever a listener does, and one Space played and paused.
70 lines
2.9 KiB
Python
70 lines
2.9 KiB
Python
"""A progress checkpoint takes a FINITE position.
|
|
|
|
The JSON parser under FastAPI accepts `Infinity` and `NaN`, and both progress handlers truncate the
|
|
position with `int()` — so one hand-written checkpoint came back as a 500 with a traceback. Found in
|
|
the 2026-09 bug hunt's second review round, as the sibling of the subtitle routes' non-finite
|
|
`at`/`offset`. Pure: the body is refused before the handler touches the database.
|
|
"""
|
|
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
import app.state as app_state
|
|
from app.auth import current_user
|
|
from app.main import app
|
|
|
|
|
|
@pytest.fixture
|
|
def client(monkeypatch, override_dep) -> TestClient:
|
|
override_dep(current_user, lambda: SimpleNamespace(id=1))
|
|
monkeypatch.setattr(app_state, "_configured_cache", True) # pass the instance-setup gate, no DB
|
|
return TestClient(app)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"path",
|
|
["/api/videos/v1/progress", "/api/plex/item/98614/progress", "/api/downloads/1/progress"],
|
|
)
|
|
@pytest.mark.parametrize("raw", ["Infinity", "-Infinity", "NaN"])
|
|
def test_a_non_finite_position_is_refused(client, path, raw):
|
|
r = client.post(
|
|
path,
|
|
content=f'{{"position_seconds": {raw}, "duration_seconds": 3000}}',
|
|
headers={"content-type": "application/json"},
|
|
)
|
|
assert r.status_code == 422, r.text
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"path",
|
|
["/api/videos/v1/progress", "/api/plex/item/98614/progress", "/api/downloads/1/progress"],
|
|
)
|
|
@pytest.mark.parametrize("field", ["position_seconds", "duration_seconds"])
|
|
def test_a_position_past_the_int4_column_is_refused_not_a_500(client, path, field):
|
|
"""Finite but past the int4 column: the flush was a DataError, i.e. a 500 with a traceback."""
|
|
body = {"position_seconds": 10, "duration_seconds": 3000, field: 2**31}
|
|
assert client.post(path, json=body).status_code == 422
|
|
|
|
|
|
def test_the_refusal_keeps_the_shape_the_ui_reads(client):
|
|
"""The handler replaces FastAPI's own for every validation error in the app, and the UI turns
|
|
exactly this shape — a LIST of errors — into its localized "check your input" sentence. The
|
|
refused value is echoed by its name, because JSON cannot carry NaN."""
|
|
r = client.post(
|
|
"/api/videos/v1/progress",
|
|
content='{"position_seconds": NaN, "duration_seconds": 3000}',
|
|
headers={"content-type": "application/json"},
|
|
)
|
|
detail = r.json()["detail"]
|
|
assert isinstance(detail, list) and detail[0]["loc"] == ["body", "position_seconds"]
|
|
assert detail[0]["input"] == "nan"
|
|
|
|
|
|
@pytest.mark.parametrize("q", ["start=inf", "start=nan", "aoff=inf", "aoff=-inf", "aoff=nan"])
|
|
def test_a_session_at_an_impossible_offset_is_refused(client, q):
|
|
"""`start=inf` reached `int()` in the session builder (a 500); `aoff=inf` spawned an ffmpeg
|
|
with a non-finite offset."""
|
|
assert client.post(f"/api/plex/stream/98614/session?{q}").status_code == 422
|