refactor(plex): the item progress body extends the shared checkpoint
ItemProgressIn repeated ProgressIn's config, fields and bounds; it now adds only `final`. The threshold comment points where the constants live now, and plexProgressBeacon's comment is back on it.
This commit is contained in:
@@ -15,7 +15,7 @@ from fastapi import (
|
||||
Response,
|
||||
)
|
||||
from fastapi.responses import FileResponse
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import select, update
|
||||
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -23,7 +23,6 @@ from sqlalchemy.orm import Session
|
||||
from app.auth import current_user, resolved_user_id
|
||||
from app.db import SessionLocal, get_db
|
||||
from app.models import (
|
||||
POSITION_MAX_S,
|
||||
PlexItem,
|
||||
PlexSeason,
|
||||
PlexShow,
|
||||
@@ -39,7 +38,7 @@ from app.plex import watch_sync as plex_watch
|
||||
from app.plex.client import PlexClient, PlexError, PlexNotConfigured
|
||||
from app.routes.plex._shared import item_or_404
|
||||
from app.refusal import refused
|
||||
from app.routes._common import not_found
|
||||
from app.routes._common import ProgressIn, not_found
|
||||
|
||||
router = APIRouter(prefix="/api/plex", tags=["plex"])
|
||||
|
||||
@@ -160,11 +159,11 @@ def subtitle(
|
||||
return Response(subtitles.shift_vtt(hit, offset), media_type="text/vtt")
|
||||
|
||||
|
||||
# Resume/watched thresholds. _PROGRESS_MIN_S matches the feed's PROGRESS_MIN_SECONDS (5), but
|
||||
# _FINISH_MARGIN_S is DELIBERATELY larger than the feed's FINISH_MARGIN_SECONDS (30 vs 10): this is
|
||||
# long-form content (movies/episodes with credits) and crossing it MARKS THE ITEM WATCHED, whereas
|
||||
# the feed's margin only clears a short video's resume bar — different domains, intentional divergence,
|
||||
# not drift. Keep them separate on purpose (see routes/feed.py).
|
||||
# Resume/watched thresholds. _PROGRESS_MIN_S matches PROGRESS_MIN_SECONDS (5), but _FINISH_MARGIN_S is
|
||||
# DELIBERATELY larger than FINISH_MARGIN_SECONDS (30 vs 10): this is long-form content
|
||||
# (movies/episodes with credits) and crossing it MARKS THE ITEM WATCHED, whereas the feed's and the
|
||||
# library's margin only clears a resume bar — different domains, intentional divergence, not drift.
|
||||
# Keep them separate on purpose (see routes/_common.py).
|
||||
_PROGRESS_MIN_S = 5
|
||||
|
||||
_FINISH_MARGIN_S = 30
|
||||
@@ -337,16 +336,9 @@ def _mark_finished(db: Session, user_id: int, item_id: int, now: datetime) -> bo
|
||||
return False
|
||||
|
||||
|
||||
class ItemProgressIn(BaseModel):
|
||||
# Finite only: JSON parsing here accepts `Infinity`/`NaN`, and the handler's `int()` turned
|
||||
# one into a 500.
|
||||
model_config = ConfigDict(allow_inf_nan=False)
|
||||
class ItemProgressIn(ProgressIn):
|
||||
"""The shared checkpoint (its bounds included), plus whether the player is leaving the item."""
|
||||
|
||||
# float, truncated in the handler — the player may post a raw currentTime (a float); a plain
|
||||
# `int` field would 422 it on the periodic progress checkpoint (the old `int(...)` truncated).
|
||||
# Bounded above by the int4 column (see POSITION_MAX_S): past it the flush was a 500.
|
||||
position_seconds: float = Field(0, le=POSITION_MAX_S)
|
||||
duration_seconds: float = Field(0, le=POSITION_MAX_S)
|
||||
final: bool = False
|
||||
|
||||
|
||||
|
||||
@@ -720,10 +720,6 @@ export const plexApi = {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ position_seconds, duration_seconds, final }),
|
||||
}),
|
||||
// Fire-and-forget progress save that SURVIVES page unload (F5/close/navigate): a normal fetch is
|
||||
// cancelled on unload and React effect cleanup doesn't run on a full reload, so the last position
|
||||
// (esp. right after a seek) would be lost. `keepalive` lets the POST complete during unload; we
|
||||
// replicate req()'s credentials + account header (no retry — there's no page left to retry on).
|
||||
// The player no longer wants this HLS session: its ffmpeg stops now instead of at the idle reaper.
|
||||
// A keepalive POST, so it also goes out from a page being unloaded. The server only stops it while
|
||||
// it is still the viewer's registered session for the item.
|
||||
@@ -732,6 +728,10 @@ export const plexApi = {
|
||||
`/api/plex/stream/${encodeURIComponent(id)}/session/${encodeURIComponent(sessionId)}/stop`,
|
||||
{},
|
||||
),
|
||||
// Fire-and-forget progress save that SURVIVES page unload (F5/close/navigate): a normal fetch is
|
||||
// cancelled on unload and React effect cleanup doesn't run on a full reload, so the last position
|
||||
// (esp. right after a seek) would be lost. `keepalive` lets the POST complete during unload; we
|
||||
// replicate req()'s credentials + account header (no retry — there's no page left to retry on).
|
||||
plexProgressBeacon: (
|
||||
id: string,
|
||||
position_seconds: number,
|
||||
|
||||
Reference in New Issue
Block a user