'use strict'; import { EmbyClient } from '../emby/EmbyClient'; import { Logger } from '../Logger'; /** * Reports a party member's own playback progress back to Emby, under * *their own* account - the one piece of this app that ever writes * anything to Emby rather than just reading/proxying it (see * docs/architecture.md's "Sync watch-party progress back to Emby" * section for the full design and its deliberate v1 scope). Called from * index.ts's periodic sweep, never from a request/response cycle - see * that file for who actually decides *when* and *with what* to call * this. * * Emby's own "PlaystateService" (the controller name in both Emby's and * Jellyfin's server source) is what exposes this route. * * Endpoint/param shape CONFIRMED against Emby's own real OpenAPI spec * (swagger.emby.media, pointed at a real server) - the original guess * here was wrong in a way that plausibly broke this entire feature: * PositionTicks/IsPaused were sent as a JSON body, but Emby declares * them (and a REQUIRED MediaSourceId this app wasn't sending at all) as * *query string* parameters instead - the request body schema * (Api.OnPlaybackProgress) covers a handful of unrelated fields * (PlaylistIndex/Shuffle/SleepTimerMode/...), none of them these. A * missing required query parameter almost certainly meant every one of * these calls was failing outright. */ class PlaystateServiceClass { /** * Best-effort: any failure (Emby unreachable, a stale/expired token, * a 4xx/5xx) is caught and logged here, never thrown - this runs on a * timer with no request to fail, and one member's bad token or one * transient Emby hiccup must never stop the rest of the sweep, let * alone touch real playback. `mediaSourceId` comes from * PartyStreamService's own cache (see index.ts's sweep) - resolving a * fresh one here would mean an extra PlaybackInfo round trip on every * tick, for a value the party's already-negotiated stream already * produced for free. */ async reportProgress(embyToken: string, userId: string, itemId: string, mediaSourceId: string, positionTicks: number, isPaused: boolean): Promise { try { const params = new URLSearchParams({ MediaSourceId: mediaSourceId, PositionTicks: String(Math.round(positionTicks)), IsPaused: String(isPaused) }); await EmbyClient.fetch(`/Users/${userId}/PlayingItems/${itemId}/Progress?${params}`, embyToken, { method: 'POST' }); } catch (err) { Logger.playstate(`progress report failed userId=${userId} itemId=${itemId}: ${(err as Error).message}`); } } } export const PlaystateService = new PlaystateServiceClass();