Files
kodi-api-server/src/WatchState/Dto/playback-report.dto.ts
T
Jurgis SakalauskasandClaude Opus 5.5 be7d0bc535 add shared watch state, continue watching and new main menu
- watch_state table: resume point, watched, play count, last played per file,
  shared by all clients (Kodi plugin, Android TV app)
- POST/GET /api/state/:fileId for players' progress reports (watched at 90% or
  on end; late resends carry `at` and lose to newer state)
- listings carry watch state (files: playcount, resume, lastplayed; shows and
  seasons: watched/total episodes), episodes sorted by season/episode
- all/continue: unfinished files and next episodes, latest activity first
- context menus: mark (un)watched for a file, season or show; hide from
  continue watching
- main menu: Continue watching, Movies, Shows, scans (Torr/LRT unlisted)
- DATA_ROOT env overrides /srv/data for local runs outside docker

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 10:01:39 +03:00

38 lines
1.3 KiB
TypeScript

import { BadRequestException } from '@nestjs/common';
export type PlaybackEvent = 'start' | 'tick' | 'pause' | 'stop' | 'end';
const EVENTS: PlaybackEvent[] = ['start', 'tick', 'pause', 'stop', 'end'];
/** a player's progress report, positions in seconds */
export class PlaybackReport {
constructor(
readonly position: number,
readonly duration: number | null,
readonly event: PlaybackEvent,
readonly client: string | null,
/**
* when it happened (ms since epoch), only for reports sent late (offline,
* imported history); live reports leave it out and get the server's time
*/
readonly at: number | null,
) {}
static fromBody(body: Record<string, unknown>): PlaybackReport {
const position = Number(body?.position);
if (!Number.isFinite(position) || position < 0) {
throw new BadRequestException('position (seconds) is required');
}
const duration = Number(body.duration);
const at = Number(body.at);
return new PlaybackReport(
position,
Number.isFinite(duration) && duration > 0 ? duration : null,
EVENTS.find((event) => event === body.event) ?? 'tick',
typeof body.client === 'string' ? body.client.slice(0, 32) : null,
Number.isFinite(at) && at > 0 ? at : null,
);
}
}