Restructure step 6: what's next and watch tracking
- What's next is the server's: state.advance counts down (10 s) when the media finishes with an Up next item first, holds while its performer or warm source is gone, and sets queuePlay at its end; cancel is shared, a seek or swap ends it; the server skips items nobody can put up. Shared rules in core whats_next.dart; the app's janitor, per-screen countdown and end-of-episode card are gone. - The continuation (next Jellyfin episode, next file of a local show with its gaps) is an auto item under "Continues with", prepared like Up next's first item. - Watch tracking by time In (D3): only time played while this viewer is in counts; away, paused, held, jumps and stalls never do (P7, P9). WatchTracker + device WatchHistory (files, links); Jellyfin progress reads the same watch; records are the AniList seam. - A server restart no longer loses what played: apps hold their media and one offers it back (swap.restore, first offer wins), Up next is re-added, the source re-feeds, and the watch carries on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a87ee98faa
commit
741b258ac1
55 files changed
+2724
-964
No files matched your search
@@ -50,7 +50,17 @@ class ServerHub {
|
||||
engine = engine ?? SyncEngine() {
|
||||
this.engine
|
||||
..onGone = _memberGone
|
||||
..onLeft = _memberLeft;
|
||||
..onLeft = _memberLeft
|
||||
..itemSourceGone = _warmSourceGone;
|
||||
}
|
||||
|
||||
/// The warm relay of Up next item [queueId] waits for a member source
|
||||
/// that is `gone`: the countdown holds (see `SyncEngine.whatsNext`).
|
||||
bool _warmSourceGone(String queueId) {
|
||||
final w = warm;
|
||||
if (w == null || w.queueId != queueId || w.fetch || w.sourceToken == null) return false;
|
||||
final src = engine.members[w.sourceToken];
|
||||
return src == null || src.presence == Presence.gone;
|
||||
}
|
||||
|
||||
static String _randomId() {
|
||||
@@ -698,8 +708,16 @@ void mountSyncRoute(
|
||||
final swap = SwapIntent.fromJson(decoded);
|
||||
var parsed = swap.media;
|
||||
final queueId = swap.queueId;
|
||||
final restore = swap.restore;
|
||||
// What played before a restart, offered back: the first
|
||||
// offer wins (see `swap.restore`).
|
||||
if (restore != null && hub.engine.media != null) {
|
||||
_hubLog.info('restore by ${hub.who(token)} ignored: media is up already');
|
||||
return;
|
||||
}
|
||||
_hubLog.info('swap by ${hub.who(token)}: "${parsed.name}" ${_swapKind(parsed)}'
|
||||
'${queueId == null ? '' : ' (Up next $queueId)'}');
|
||||
'${queueId == null ? '' : ' (Up next $queueId)'}'
|
||||
'${restore == null ? '' : ' (restored at ${restore.position.toStringAsFixed(1)} s, ${restore.transport})'}');
|
||||
if (queueId != null) {
|
||||
// Already put up by someone else: nothing to do.
|
||||
if (!hub.engine.queue.has(queueId)) return;
|
||||
@@ -737,7 +755,7 @@ void mountSyncRoute(
|
||||
: promoted != null
|
||||
? hub.promotedRelay(promoted, queueId!)
|
||||
: hub.relayFor(token, const MediaRelay()));
|
||||
if (media == null || !hub.engine.applySwap(token, media, queueId: queueId)) {
|
||||
if (media == null || !hub.engine.applySwap(token, media, queueId: queueId, restore: restore)) {
|
||||
promoted?.dispose();
|
||||
wsErr(ws, ErrCode.bad, 'bad swap media', forType: 'swap');
|
||||
return;
|
||||
@@ -785,7 +803,7 @@ void mountSyncRoute(
|
||||
duration: parsed.duration,
|
||||
ref: parsed.ref,
|
||||
relay: promoted != null ? hub.promotedRelay(promoted, queueId!) : hub.relayFor(token, relayReq));
|
||||
if (!hub.engine.applySwap(token, media, queueId: queueId)) {
|
||||
if (!hub.engine.applySwap(token, media, queueId: queueId, restore: restore)) {
|
||||
promoted?.dispose();
|
||||
wsErr(ws, ErrCode.bad, 'bad swap media', forType: 'swap');
|
||||
return;
|
||||
@@ -827,7 +845,7 @@ void mountSyncRoute(
|
||||
wsErr(ws, ErrCode.bad, 'swap needs a url or the hosted file', forType: 'swap');
|
||||
return;
|
||||
}
|
||||
if (!hub.engine.applySwap(token, intent.media, queueId: queueId)) {
|
||||
if (!hub.engine.applySwap(token, intent.media, queueId: queueId, restore: restore)) {
|
||||
wsErr(ws, ErrCode.bad, 'bad swap media', forType: 'swap');
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user