Files
plezy/lib/services/app_exit_service.dart
T
edde746 5a25c1f9cc 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
2026-07-30 11:55:13 +02:00

61 lines
2.3 KiB
Dart

import 'dart:io' show Platform;
import 'dart:ui' as ui;
import 'package:flutter/services.dart';
import '../utils/platform_detector.dart';
typedef AppExitApplication = Future<ui.AppExitResponse> Function(ui.AppExitType exitType, int exitCode);
class AppExitService {
static const bool _tvosBuild = bool.fromEnvironment('TVOS_BUILD');
static const MethodChannel _channel = MethodChannel('com.plezy/app_exit');
/// Requests that the host platform closes or backgrounds the app.
///
/// tvOS has no public API for force-quitting or going Home, so callers that
/// handle a physical back/Menu key should let the event continue instead.
static Future<bool> requestExit({AppExitApplication? exitApplicationForTesting}) async {
if (_tvosBuild || PlatformDetector.isAppleTV()) return false;
if (Platform.isAndroid) {
try {
return await _channel.invokeMethod<bool>('requestExit') ?? true;
} on MissingPluginException {
await SystemNavigator.pop();
return true;
} on PlatformException {
await SystemNavigator.pop();
return true;
}
}
if (PlatformDetector.isDesktopOS()) {
final exitApplication =
exitApplicationForTesting ??
(exitType, exitCode) => ServicesBinding.instance.exitApplication(exitType, exitCode);
final response = await exitApplication(ui.AppExitType.required, 0);
return response == ui.AppExitResponse.exit;
}
await SystemNavigator.pop();
return true;
}
/// Requests a *cancelable* exit so registered `onExitRequested` handlers run
/// before the process goes away — app-level teardown depends on it, including
/// the terminal playback report for trackers that own their own watched
/// semantics.
///
/// Desktop only; returns false elsewhere, and when the platform declined, so
/// the caller can fall back to a hard exit.
static Future<bool> requestGracefulExit({AppExitApplication? exitApplicationForTesting}) async {
if (!PlatformDetector.isDesktopOS()) return false;
final exitApplication =
exitApplicationForTesting ??
(exitType, exitCode) => ServicesBinding.instance.exitApplication(exitType, exitCode);
final response = await exitApplication(ui.AppExitType.cancelable, 0);
return response == ui.AppExitResponse.exit;
}
}