feat(simkl): report playback progress while it happens
Simkl only heard about an item once playback crossed the media server's watched threshold, so stopping partway recorded nothing at all: no resumable position, no watch. Drive Simkl's /scrobble/start, /pause and /stop from the player lifecycle instead, carrying the measured progress. Seeks report nothing, as Simkl asks. The terminal stop owns watched state for in-player playback, so real-time trackers are excluded from the threshold markWatched fan-out and one watch never produces two writes. Progress is reported as measured — it doubles as the user's resume position — so when a server threshold configured below Simkl's own 80% rule would leave the watch unrecorded, the tracker records it through /sync/history rather than inflating progress. Manual, container, offline-replay and external-player marks keep using /sync/history. Only /scrobble/stop accepts a 409, which is the sole action documented to return one. Reports go out one at a time because Simkl serialises scrobble writes per user and fails queued ones with a 400; overflow sheds the oldest non-terminal report so an episode swap cannot drop the previous item's stop. A playback session is pinned to the account bound when it began and every send re-checks that binding, so a profile switch or a disconnect/reconnect can neither redirect a queued report nor misfile the watched fallback. Also close the paths that lost the terminal report entirely: app exit flushes it instead of dropping it, the desktop window button goes through the app shutdown rather than exit(0), a detached VOD player reports a stop, and a finished item reports completion at EOF instead of waiting for teardown. A session that opened at 0% is still closed on stop, or Simkl keeps showing the item as playing until its runtime elapses. close #1719
This commit is contained in:
@@ -21,4 +21,32 @@ void main() {
|
||||
expect(requestedType, ui.AppExitType.required);
|
||||
expect(requestedCode, 0);
|
||||
});
|
||||
|
||||
// The window's close button must run the registered onExitRequested handlers
|
||||
// (app-level teardown, including the terminal playback report), which only a
|
||||
// cancelable request does — `required` skips them.
|
||||
test('graceful desktop exit uses a cancelable application exit', () async {
|
||||
ui.AppExitType? requestedType;
|
||||
int? requestedCode;
|
||||
|
||||
expect(
|
||||
await AppExitService.requestGracefulExit(
|
||||
exitApplicationForTesting: (exitType, exitCode) async {
|
||||
requestedType = exitType;
|
||||
requestedCode = exitCode;
|
||||
return ui.AppExitResponse.exit;
|
||||
},
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
expect(requestedType, ui.AppExitType.cancelable);
|
||||
expect(requestedCode, 0);
|
||||
});
|
||||
|
||||
test('a declined graceful exit reports failure so the caller can hard-exit', () async {
|
||||
expect(
|
||||
await AppExitService.requestGracefulExit(exitApplicationForTesting: (_, _) async => ui.AppExitResponse.cancel),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user