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
61 lines
2.3 KiB
Dart
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;
|
|
}
|
|
}
|