forked from Foxyz/sync_replay_project
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
+161
-70
@@ -7,9 +7,9 @@ Plain overview first: [HOW-IT-WORKS.md](HOW-IT-WORKS.md). Wire format:
|
||||
|
||||
| Path | What | Rule |
|
||||
|---|---|---|
|
||||
| `packages/core` | protocol messages, sync engine (`SyncEngine`), constants, config, Range parsing, logging, Jellyfin client, updater, watched rule (`WatchSession`), the library's L1 (`library/`: release and folder names, `groupLocal`); `testing.dart` (test support for all three packages, not in `core.dart`: `FlakyProxy`) | pure Dart, no Flutter |
|
||||
| `packages/core` | protocol messages, sync engine (`SyncEngine`, with what's next: `whatsNext`), constants, config, Range parsing, logging, Jellyfin client, updater, the rules every side reads alike (`whats_next.dart`: who puts an Up next item up, who adds the continuation), watched rule (`WatchSession`), the library's L1 (`library/`: release and folder names, `groupLocal`); `testing.dart` (test support for all three packages, not in `core.dart`: `FlakyProxy`) | pure Dart, no Flutter |
|
||||
| `packages/server` | Relic HTTP + WebSocket server (`SyncServer`, `ServerHub`, `Session`, media endpoint, relay buffer, Jellyfin relay), the `serve()` entrypoint, `bin/server.dart` (dedicated server) | pure Dart, no Flutter |
|
||||
| `apps/desktop` | Flutter app: UI, `AppGraph` (composition root), `SyncClient`, `Connection`, `SessionCoordinator`, `Navigation`, `YourServer` (`ServerIsolate`), `MediaPipeline` (opening media), `PlaybackController` (the session player's marks and commands), the player host (`PlayerHost`, `MediaKitHost`: the only media_kit user besides the `Video` widget glue), update UI (`UpdateController`) | ships with `starsync-server` |
|
||||
| `apps/desktop` | Flutter app: UI, `AppGraph` (composition root), `SyncClient`, `Connection`, `SessionCoordinator`, `Navigation`, `YourServer` (`ServerIsolate`), `MediaPipeline` (opening media), `QueueController` (what's next on this app), `WatchTracker` (watch tracking), `PlaybackController` (the session player's marks and commands), the player host (`PlayerHost`, `MediaKitHost`: the only media_kit user besides the `Video` widget glue), update UI (`UpdateController`) | ships with `starsync-server` |
|
||||
| `tool` | harnesses: `range_logger`, `shaper`, `fake_viewer`, `e2e_check` | dev only |
|
||||
| `deploy` | systemd unit for the dedicated server | |
|
||||
| `third_party/media_kit_video` | upstream media_kit_video 2.0.1 with Starsync's patches (`starsync/README.md`: Flutter ≥ 3.38 EGL display on the platform thread, the X11 GLX step-aside) | vendored; re-apply patches on update |
|
||||
@@ -134,10 +134,13 @@ only stopping sharing sends guests out.
|
||||
`AppGraph` (`app/app_graph.dart`) is built once in `main.dart`
|
||||
(`AppGraph.build`) and constructs and disposes the app's systems: the
|
||||
`Ports` (the player host among them, handed to the coordinator), the sounds (`SynthSfx`), the `Notifier`, the `Connection`, the
|
||||
`SessionCoordinator`, the connect form and recents (`ConnectStore`,
|
||||
`state/connect_prefs.dart`), `YourServer`, `Navigation` and the library's
|
||||
matches (`ShowMatcher`, "Library", whose `refForPath` it hands the
|
||||
coordinator as `fileRef`); it wires them
|
||||
`SessionCoordinator` (handed the settings and the watch tracker), the
|
||||
connect form and recents (`ConnectStore`, `state/connect_prefs.dart`),
|
||||
`YourServer`, `Navigation`, the library's matches (`ShowMatcher`,
|
||||
"Library", whose `refForPath` it hands the coordinator as `fileRef`, and
|
||||
a `LocalNextFinder` over it as `nextFile`) and watch tracking
|
||||
(`WatchTracker` with the device's `WatchHistory`, "Watch tracking"); it
|
||||
wires them
|
||||
through their constructors (and gives `Profile` the form and the push) and
|
||||
has no logic or signals of its own. Widgets reach it through one
|
||||
`AppScope` InheritedWidget (`AppScope.of(context)`, readable in
|
||||
@@ -301,11 +304,35 @@ Loosely modeled on Syncplay, with a different readiness design.
|
||||
unstable", 9 s is lost (well under the server's 15 s). A welcome that
|
||||
resumed needs nothing re-sent; a new member (grace over, server
|
||||
restarted) gets the app's ready and sitting out again, and the media
|
||||
opens again only when it changed (or its URL named the old token). The
|
||||
opens again only when it changed (or its URL named the old token). A
|
||||
restarted server that has no media while this app had some gets it
|
||||
back ("Server restart" below). The
|
||||
app's own `ping` every second keeps it under the server's silence limit
|
||||
even with nothing playing. Cancel during setup aborts the join. Leave
|
||||
and closing the window send `bye` (see "App close" under Desktop UI);
|
||||
Update and restart does not.
|
||||
- **Server restart** (the queue stays in memory, the server stores
|
||||
nothing): a reconnect that finds another `welcome.instance` with no
|
||||
media, while this app had media up, holds it: the player plays on and
|
||||
states without media are not applied (the watch goes on with it, see
|
||||
"Watch tracking"). One app offers it back (`swap.restore`: the media,
|
||||
this player's position, whether it played); who goes first is read
|
||||
from the old state alone, so the apps agree: who put it up or passes
|
||||
it on after 0.5 s, the others by their old member id from 3 s on,
|
||||
1.5 s apart, each only while the server is still empty and only what
|
||||
it can put up again (`_offerBack`: a Jellyfin item with its own
|
||||
account, its file, the link it put up, a link everyone opens). The
|
||||
server keeps the first offer. When it is back as the same media under
|
||||
a new `mediaId`, the player keeps it unless its URL named the old
|
||||
token (a relay viewer's `/r/`, the host's `/m/`: opened again), and
|
||||
what this member passes on is served again (`MediaPipeline.resumeAs`:
|
||||
its file, its link, its Jellyfin item for viewers without an account);
|
||||
the watch, the Jellyfin reports and a cleared continuation carry over.
|
||||
Up next comes back too (`QueueController.restoreAfterRestart`): the
|
||||
app whose offer won adds the links everyone opens and the Jellyfin
|
||||
items, every app its own links passed on and files; a server-fetched
|
||||
link is lost with the old server. Nobody offering within 30 s: the
|
||||
app lets go of it.
|
||||
- **Auto sit-out** (`AutoSitOut`, `session/auto_sit_out.dart`; Settings
|
||||
"Auto sit-out", `AppSettings.autoSitOut`, default on): not playing and
|
||||
no input for 5 min sets this member `away` (reason `idle`); minimised
|
||||
@@ -1154,29 +1181,19 @@ Client side (`lib/src/jellyfin/session_media.dart`, `progress.dart`,
|
||||
should be) or a track change; Stopped on swap, leave, sitting out, window close
|
||||
(bounded to 1.5 s inside the graceful leave) and at `finished`; seeking
|
||||
back from the end sends Playing again. Reports carry `PositionTicks`
|
||||
(see "Watched rule"), `IsPaused` and mpv's `aid`/`sid` as Jellyfin
|
||||
(see "Watched on Jellyfin"), `IsPaused` and mpv's `aid`/`sid` as Jellyfin
|
||||
stream indices (`jellyfinIndexForMpvTrack`, `SubtitleStreamIndex -1` =
|
||||
off). They go out in order on one chain; failures are logged, one quiet
|
||||
toast per session, playback is never touched; a 401 marks the account
|
||||
expired and ends reporting.
|
||||
- **Watched rule** (`WatchSession` in `packages/core/lib/src/watch_session.dart`,
|
||||
pure, meant for any tracker): each opening samples the position every
|
||||
tick; only normal playback counts (forward, at most 2x speed + 1.5 s,
|
||||
samples at most 10 s apart; seeks, skips and pauses add nothing). The
|
||||
union of the watched stretches is the coverage (rewatching counts once),
|
||||
alongside the plain watched time, first / last / furthest position and
|
||||
start / end time. At a stop: played before this opening -> keep played;
|
||||
coverage >= 70 % of the runtime, or the ending reached (furthest position
|
||||
past the Outro / ED start or into the last 15 %, at most 4 min, whichever
|
||||
is earlier) with coverage >= 40 % -> played (someone who watched that
|
||||
far and stops is done with it); coverage under max(2 min, 5 %) (at most a quarter of a short
|
||||
item) -> a peek; else part way, resuming at the last position. An
|
||||
unknown runtime never makes it played. Jellyfin decides played from
|
||||
- **Watched on Jellyfin**: the watch of the item is the session's (see
|
||||
"Watch tracking"); `JellyfinProgressReporter` reads its verdict and
|
||||
never samples itself. Jellyfin decides played from
|
||||
positions alone (10.11): Playing sets a resumable item unplayed, a
|
||||
Progress or Stopped position past "max resume" (90 %) marks it played,
|
||||
one in between keeps a resume point. So `JellyfinProgressReporter`
|
||||
one in between keeps a resume point. So the reporter
|
||||
(played before = `UserData.Played` read at open with
|
||||
`/UserItems/{id}/UserData`):
|
||||
`/UserItems/{id}/UserData`, handed to the watch with the resume point):
|
||||
- played (before, or by the rule at an earlier stop of this opening):
|
||||
`UserPlayedItems` right after Playing, Progress without
|
||||
`PositionTicks` (leaves the saved position and played state), Stopped
|
||||
@@ -1199,22 +1216,10 @@ Client side (`lib/src/jellyfin/session_media.dart`, `progress.dart`,
|
||||
there is one); from `nextPromptStart` (the ending's
|
||||
start when one is known, see "Skip rules", else the last 2 min of the
|
||||
runtime) a floating "Next episode" pill shows while the controls are up
|
||||
(see "Player controls"). Auto-advance still waits for `finished`. Any
|
||||
of them swaps to that episode's ref with the clicker's account. Guests
|
||||
(relay, blocked) see none of it.
|
||||
- **Auto-advance** (Settings, on by default): at `finished` with a known
|
||||
next episode, every client with the setting on shows a 10 s countdown
|
||||
card (Cancel / Play now; without the setting, a "Next" card with Play
|
||||
next). Exactly one client swaps: `autoAdvanceSwapper`, the lowest member
|
||||
id among present members (`in` or `joining`) whose `jf` for that
|
||||
server is true. Only what
|
||||
every client sees in `state` decides it (who put the item up is not in
|
||||
`state`; a late joiner never saw that swap), so clients agree without a
|
||||
wire change. Cancel is local: it stops the countdown on that screen, and
|
||||
the swapper's Cancel stops the swap; a seek by anyone ends `finished`
|
||||
and with it every countdown. Only while Up next is empty and its
|
||||
continuation was not cleared (see "Up next"); a queued item replaces
|
||||
a countdown already running.
|
||||
(see "Player controls"). Any of them puts that episode up with the
|
||||
clicker's account (through Up next when it is the continuation there).
|
||||
At the end, the next episode continues the item by itself (see "What's
|
||||
next"). Guests (relay, blocked) see none of it.
|
||||
- **Tracks**: the controls' Audio and Subtitles buttons (or ⚙ when
|
||||
hidden from the bar) list `PlaybackController.tracks`. A pick is applied, reported, and saved as a `TrackChoice`
|
||||
(language, title, codec, channels, forced/HI: from the Jellyfin streams
|
||||
@@ -1227,6 +1232,43 @@ Client side (`lib/src/jellyfin/session_media.dart`, `progress.dart`,
|
||||
Link goes through `mediaForLink`, a Jellyfin pick as a ref, a file
|
||||
through `ServerIsolate.setFile` (host) or the relay (`swapFile`).
|
||||
|
||||
## What's next
|
||||
|
||||
One system decides which item plays next and when, and has it prepared
|
||||
ahead. The server holds the decision (`SyncEngine.whatsNext`, on every
|
||||
tick and queue change; wire in [PROTOCOL.md](PROTOCOL.md), "Queue"); each
|
||||
app's `QueueController` (`lib/src/queue/`) adds to the one list, puts an
|
||||
item up when the server asks it, shows the countdown and keeps the next
|
||||
item ready ("Getting ready" under "Up next").
|
||||
|
||||
- **One list**: the queue, Up next's items, then at most one `auto`
|
||||
item, the continuation of the media playing (`for` its `mediaId`).
|
||||
Everyone sees it; any swap that does not put it up drops it.
|
||||
- **The continuation**: added by the app that put the media up (or, when
|
||||
that member is not `in`, the first member who is: core's
|
||||
`mayAddContinuation`), when it knows what comes next and the setting
|
||||
"Play the next episode automatically" is on there: a Jellyfin episode
|
||||
played with its own account continues with the series' next one
|
||||
(`adjacentEpisodes`), a file this app put up with the next episode of
|
||||
its show (`LocalNextFinder`: the library the Files page saw this run,
|
||||
else the file's own folder scanned and grouped; `LocalShow.nextAfter`,
|
||||
so the numbers skipped on the way are said in its second line, "skips
|
||||
66–107"). It goes up like any item of its kind (a Jellyfin ref, the
|
||||
adder's file), with a picture, and adds no feed line. A `cancel`
|
||||
(Clear, or Cancel on the end card) drops it for everyone, and no app
|
||||
adds it again for that media (the feed's `advance-cancel`).
|
||||
- **When**: with nothing up, the first item is asked for at once. When
|
||||
the media finishes with an item first, the server's countdown starts
|
||||
(`state.advance`, 10 s, the same on every screen); a new first item
|
||||
takes it over with the same end; a seek, `cancel` (shared) or a swap
|
||||
ends it. At its end the server sets `queuePlay` and the app that puts
|
||||
the item up (`queuePerformer`) swaps it in. It holds while that member
|
||||
is `gone` (in its grace) or nobody is around, or while the item's warm
|
||||
relay waits for a `gone` source, and asks once they are back. An item
|
||||
asked for that nobody here can put up (its adder left, nobody has its
|
||||
Jellyfin server) is skipped by the server (feed `queue-skip`, actor
|
||||
`server`), and the countdown goes on with the next one.
|
||||
|
||||
## Up next
|
||||
|
||||
The queue: what plays after the current media. The server holds the list
|
||||
@@ -1249,27 +1291,28 @@ every `state` and feed), the pure rules in `queue_logic.dart`, the UI in
|
||||
and a file go up as a `key` only: the app keeps the URL or path in
|
||||
memory under it and nothing else leaves it. The adder gets a toast
|
||||
"Added … to Up next"; everyone else an activity-log line.
|
||||
- **Who puts an item up** (`queuePerformer`, from `state` alone, so all
|
||||
clients agree): a link passed on or a file, only its adder (`gone`:
|
||||
- **Who puts an item up** (core's `queuePerformer`, from `state` alone,
|
||||
so the server and every app agree): a link passed on or a file, only its adder (`gone`:
|
||||
the item waits for it through its grace; left for good: skipped, "Bob
|
||||
left"); a Jellyfin item, its adder when it plays that server with its
|
||||
own account, else the lowest present member id that does (`jf`, which
|
||||
lists every working account; nobody: skipped); a link everyone opens
|
||||
or the server fetches, its adder when present, else the lowest present
|
||||
member id (present: `in` or `joining`, `isPresent` in
|
||||
`session/party.dart`). A skip is a `remove` with the reason from the
|
||||
lowest member id that is not `gone` (`queueJanitor`); the feed's
|
||||
`queue-skip` toasts on every screen.
|
||||
- **Advancing**: at `finished` with Up next waiting, every screen shows
|
||||
`UpNextCard` (picture, title, who added it and how; with auto-advance on
|
||||
a 10 s countdown with Cancel, else Play now only); at zero the app that
|
||||
puts the first item up does, with a swap naming it (`swap.queueId`), so
|
||||
member id (present: `in` or `joining`, `isPresentMember`). An item asked
|
||||
for that nobody can put up is skipped by the server (see "What's
|
||||
next"); an item whose swap fails is taken off by the app that tried,
|
||||
with the reason; the feed's `queue-skip` toasts on every screen.
|
||||
- **Advancing**: at `finished` with something next, every screen shows
|
||||
`UpNextCard` (picture, title, who added it and how; the server's
|
||||
countdown with Cancel while `state.advance` runs, "Waiting for Bob"
|
||||
when it ran out without the one who puts it up, else Play now only;
|
||||
see "What's next"). The app that puts the item up does when the server
|
||||
asks (`state.queuePlay`), with a swap naming it (`swap.queueId`), so
|
||||
the server takes it off in the same step and a second swap of it is
|
||||
ignored. Cancel is per screen like the next-episode countdown. "Play
|
||||
now" anywhere (a row, the card, the Up next button's ⏭, `>` / Shift+N) swaps
|
||||
directly from the app that can, else sends `play` and that app reacts
|
||||
to `state.queuePlay`. With nothing up at all, the first item goes up as
|
||||
soon as it is added. The swap goes through the usual paths
|
||||
ignored. "Play now" anywhere (a row, the card, the Up next button's ⏭,
|
||||
`>` / Shift+N) swaps directly from the app that can, else sends `play`
|
||||
and that app reacts to `state.queuePlay`. With nothing up at all, the
|
||||
server asks for the first item as soon as it is added. The swap goes through the usual paths
|
||||
(`swapPicked`, `swapJellyfin`; a server-fetched item through
|
||||
`swapQueued`, without the URL, its picture as the art), so relays, art
|
||||
and refusals work as for any swap; when it fails the item is skipped
|
||||
@@ -1278,9 +1321,9 @@ every `state` and feed), the pure rules in `queue_logic.dart`, the UI in
|
||||
out comes back first; any other queued swap leaves the swapper out.
|
||||
- **Getting ready** (`QueueController._warm`): every app keeps the one
|
||||
item that plays next ready all the time, its own copy, so a skip, Play
|
||||
now or the end of the media starts it at once. That is the first Up
|
||||
next item, else, with Up next empty, the first "Continues with"
|
||||
episode (on the apps that have it); while sitting out only the probe
|
||||
now or the end of the media starts it at once. That is the queue's
|
||||
first item (Up next's, else the continuation, which is in the queue
|
||||
for everyone); while sitting out only the probe
|
||||
of a link this app puts up (its swap reads it). Per
|
||||
kind: a Jellyfin item is resolved with this app's account
|
||||
(`resolveJellyfinRef`: PlaybackInfo and the item) into
|
||||
@@ -1346,19 +1389,19 @@ every `state` and feed), the pure rules in `queue_logic.dart`, the UI in
|
||||
says which): hollow waiting, pulsing getting ready (resolving, then
|
||||
preloading its first 4 s), filled ready, red failed (tried again every
|
||||
2 minutes).
|
||||
- **Continues with**: not queued, computed here like the next episode:
|
||||
with an own-account Jellyfin episode playing, the next three episodes
|
||||
of the series (`/Shows/{id}/Episodes`, cached), greyed under Up next,
|
||||
"+" adds one. "Clear" hides them for the media playing and keeps its
|
||||
next-episode countdown from starting.
|
||||
- **Continues with**: the continuation (the `auto` item, see "What's
|
||||
next"), greyed under Up next for everyone, ▶ plays it now; "Clear"
|
||||
drops it for everyone (`cancel`). Under it, on an app playing a
|
||||
Jellyfin episode with its own account, the series' further episodes
|
||||
(`/Shows/{id}/Episodes`, cached), "+" adds one to Up next.
|
||||
- **UI**: `UpNextButton` is one of the player's top-bar controls, before
|
||||
the window controls (see "Player overlay" under Desktop UI), in the look
|
||||
of the skin's buttons (`UpNextLook`); it shows and hides with the controls,
|
||||
which an open popover holds up: the first item's picture (`QueueThumb`,
|
||||
the rows' faint box and icon until it arrives), "UP NEXT" over its
|
||||
title, a +n count, ⏭ plays it now; without Up next the continuation
|
||||
(the episode's image, "NEXT · S1 E8" over its name, ⏭ = next episode);
|
||||
nothing when both are empty. Resting on it 600 ms (popover closed)
|
||||
(its picture, "NEXT · S1E8" (its second line) over its name, ⏭ plays
|
||||
it); nothing when both are empty. Resting on it 600 ms (popover closed)
|
||||
shows `UpNextPreview` under it, right edges aligned, in `AppGlass`:
|
||||
the picture large, title, second line, "length · added by (avatar)
|
||||
name · how" (an episode: series, length, overview), how ready it is
|
||||
@@ -1464,7 +1507,54 @@ show or movie, `ProviderIds` in the item fields; from a pick's lines,
|
||||
title). Swaps (`swapFile`, `swapJellyfin`, `swapLink`) and Up next adds
|
||||
carry it; Up next names a file item by it. Consumers: receivers' now
|
||||
playing title and episode line (`refTitle`), the swapper's art (by the
|
||||
AniList id); later the watch tracking keys on it.
|
||||
AniList id), the watch tracking's records (see "Watch tracking").
|
||||
|
||||
## Watch tracking
|
||||
|
||||
What this device watched, by D3: watched means at least as good as
|
||||
Jellyfin's own, and a peek, skipping around or a screenshot visit never
|
||||
counts. The truth is split: the device holds its watch history, Jellyfin
|
||||
its played state and resume point (reported now), AniList its list
|
||||
(later).
|
||||
|
||||
- **The rule** (`WatchSession` in `packages/core/lib/src/watch_session.dart`,
|
||||
pure): one per opening, sampled every second with the position,
|
||||
whether the media plays here (the server's transport is playing and
|
||||
this player plays it) and whether this viewer is In (its presence in
|
||||
`state`). The basis is time In: wall time it played while this viewer
|
||||
was `in`. Time sat out (Away), joining, gone, paused or held never
|
||||
counts, nor does a jump (skipping around adds only what was played);
|
||||
samples more than 10 s apart add nothing. A resume carries the earlier
|
||||
watch: when what played starts within 60 s of the resume point, the
|
||||
time up to it counts too. At a stop: played before -> keep played;
|
||||
time In >= 70 % of the runtime, or the ending reached (furthest
|
||||
position while In past the Outro / ED start or into the last 15 %, at
|
||||
most 4 min, whichever is earlier) with time In >= 40 % -> played;
|
||||
under max(2 min, 5 %) (at most a quarter of a short item) -> a peek;
|
||||
else part way, resuming at the last position seen while In. An unknown
|
||||
runtime never makes it played.
|
||||
- **The tracker** (`WatchTracker`, `lib/src/watch/`, made by the
|
||||
composition root): one watch per media of the server (`mediaId`), fed
|
||||
by the coordinator's 1 s tick, also through drops (the player goes
|
||||
on); a reconnect or a server restart never ends it while the media
|
||||
plays on (it follows the media to its new `mediaId`, and goes on while
|
||||
a restarted server waits for it, see "Server restart"); another media
|
||||
or leaving closes it, the end of the media
|
||||
(`finished`) records it as it stands (a seek back goes on with it).
|
||||
Each record (`WatchRecord`: key, title, `media.ref`, verdict, time In,
|
||||
runtime) goes out on `records`, where AniList progress will listen,
|
||||
and to the device's history. Keys (`WatchKey`): a file this app put up
|
||||
by its path, a Jellyfin item by its ids, a link this app put up or
|
||||
everyone opens by its URL, else name and size (someone else's file).
|
||||
- **The history** (`WatchHistory`, Hive box `watch_history`): files and
|
||||
links only (Jellyfin keeps its own): played, or a resume point (a part
|
||||
way watch of an item played before keeps it played); a peek changes
|
||||
nothing. Played before (and the resume point) for the next opening
|
||||
come from it; the Files page's "Next" follows the newest watched
|
||||
episode of a show, else the last one opened (Recent).
|
||||
- **Jellyfin**: the reporter reads the same watch (see "Jellyfin in a
|
||||
session", "Watched on Jellyfin"); Jellyfin's played state and resume
|
||||
point read at open become the watch's "before".
|
||||
|
||||
## App events and platform ports
|
||||
|
||||
@@ -1811,7 +1901,8 @@ panel) and chat messages (Copy message, and a link in it).
|
||||
servers), player (the shared rows below, the controls' choices, custom
|
||||
shader files),
|
||||
Jellyfin & integrations (linked servers, AnimeFlow's `/me` via
|
||||
`AnimeFlowClient.fromAccounts`, auto-advance), notifications & sounds
|
||||
`AnimeFlowClient.fromAccounts`, "Play the next episode automatically":
|
||||
whether this app adds the continuation, see "What's next"), notifications & sounds
|
||||
(what pops up, chat pop-ups, joins and leaves while sitting out or in
|
||||
the background), updates, shortcuts, about.
|
||||
- **Drop and paste** (`handoff.dart`, `fox_utils`): the shell is one
|
||||
@@ -1928,8 +2019,8 @@ panel) and chat messages (Copy message, and a link in it).
|
||||
16 px from the bottom while they are hidden (`AnimatedPositioned`,
|
||||
the shell's `ease` / `easeTime`); in it the status pill centred
|
||||
(moved left only as far as it must to clear the right end, never
|
||||
overlapping it) and at the right end the end card (`UpNextCard`, else
|
||||
`EndOfEpisodeCard`, which stay up) or the action pills (Skip OP / ED /
|
||||
overlapping it) and at the right end the end card (`UpNextCard`, which
|
||||
stays up) or the action pills (Skip OP / ED /
|
||||
1:30 over Next episode, `PillGate`). The Jellyfin blocked card sits in
|
||||
the middle. Two surfaces: cards and popovers use `AppGlass`
|
||||
(`widgets/app_menu.dart`, the menus' recipe: blur, dark tint, hairline
|
||||
@@ -2064,8 +2155,8 @@ Player controls (`lib/src/controls/`): one controls model, two skins.
|
||||
volume, skip offer, render mode; the shell's fullscreen, chat open and
|
||||
unread count) and what they do (`togglePlay` = ready, as Space;
|
||||
`seek` through `player.seek`, the user path; `nudge`, `skipForward`,
|
||||
`skip`, `stepChapter`, `pickChapter`; `next` = Up next's first item,
|
||||
else the next episode, `previous`; volume set / step (flashes) / mute;
|
||||
`skip`, `stepChapter`, `pickChapter`; `next` = the queue's first item
|
||||
(Up next's, else the continuation), else the next episode, `previous`; volume set / step (flashes) / mute;
|
||||
`pickTrack`, `pickShader`; time display; fullscreen, chat, change
|
||||
video, collapse, leave, reload, mark everyone ready, link). The
|
||||
player keys call the same actions. `ControlsSnapshot` (computed) is
|
||||
|
||||
+20
-9
@@ -84,15 +84,18 @@ Three suites, run from each package's folder:
|
||||
what a player opens for a page (`YtDlpPlay`) and the cookie notes of a
|
||||
failed resolve against a stand-in yt-dlp (`cookies_test`, D11),
|
||||
the sync engine (clock, gate, presence and grace, correction, end of
|
||||
file), the `PresencePolicy` table, queue model, the library's L1
|
||||
file), the `PresencePolicy` table, queue model and what's next (who
|
||||
puts an item up, the countdown on a fake clock), the library's L1
|
||||
(release and folder names, `groupLocal` over a real anime folder's
|
||||
names: shows across folders, holes, extras, a movie),
|
||||
relay pieces, updater, logging redaction, watch rule, yt-dlp and
|
||||
relay pieces, updater, logging redaction, the watched rule (time In),
|
||||
yt-dlp and
|
||||
Jellyfin client parsing against captured fixtures.
|
||||
- `cd packages/server && dart test` (well under a minute): live servers
|
||||
on loopback: auth and version refusal, identity, takeover and grace
|
||||
(`presence_test` on a fake clock), media and relay byte serving with
|
||||
source drops, Jellyfin relay against a fake, queue, session log,
|
||||
source drops, Jellyfin relay against a fake, queue (and the shared
|
||||
countdown on a fake clock), session log,
|
||||
isolate `serve`, sharing (`share_test`: a listener attached and detached
|
||||
under the same members and Up next, the host key standing in for the
|
||||
password).
|
||||
@@ -101,7 +104,9 @@ Three suites, run from each package's folder:
|
||||
fake AniList (`show_matcher_test`), `media.ref` producers,
|
||||
shader list, sound engine with a fake backend, invite addresses, the
|
||||
version gate, the media plan's table with the Jellyfin relay rule and
|
||||
the "Waiting for" status (`media_plan_test`)), `singletons_test` (the `.instance` count, see
|
||||
the "Waiting for" status (`media_plan_test`), watch tracking and the
|
||||
device history (`watch_tracker_test`), what continues a file of a
|
||||
local show (`queue_logic_test`)), `singletons_test` (the `.instance` count, see
|
||||
ARCHITECTURE "Composition root"; lower its baseline when a use goes) and the real-mpv `*_e2e`
|
||||
tests plus `coord_presence`. The whole suite takes a couple of minutes;
|
||||
run it only before a restructure step lands, otherwise only the files
|
||||
@@ -114,7 +119,9 @@ else is offline: Jellyfin and link hosts are local fakes.
|
||||
|
||||
Real-mpv tests: `sync_playback_e2e`, `coord_presence` (drops through a
|
||||
`FlakyProxy`: a half-open link and return, 10 flaps, `replaced`, `bye`,
|
||||
a dead or restarted server), `video_reload_e2e` (a fresh player from the
|
||||
a dead or restarted server; a restarted server that came back empty gets
|
||||
what played back at its position, a link everyone opens and a file
|
||||
passed on, nothing reopened, the watch and Up next carrying on), `video_reload_e2e` (a fresh player from the
|
||||
host, the old one retired, leave releases them all),
|
||||
`seek_drag_e2e`, `end_of_file_e2e`, `readahead_e2e`, `chapters_mpv_e2e`,
|
||||
`ytdl_headers_e2e` (a page through the app's own resolve against a
|
||||
@@ -126,7 +133,11 @@ own: a guest's file, links passed on or fetched by the server, a late
|
||||
source, a source that drops and resumes; a seek past what a slow source
|
||||
has sent, timed; a source that drops while a viewer seeks past
|
||||
the buffer and another joins: no seek loop, "Waiting for", all resume
|
||||
(D6)), `jellyfin_e2e`, `queue_switch_e2e` (times Up next switches
|
||||
(D6)), `jellyfin_e2e` (account holders report what they watched, one
|
||||
who sat out is not marked watched, the next episode continues for
|
||||
everyone), `whats_next_e2e` (a file of a show continued by its next
|
||||
episode with the gap said, the server's countdown on both screens, a
|
||||
shared cancel, the watch history), `queue_switch_e2e` (times Up next switches
|
||||
through the media pipeline against a delayed, rate-capped fake Jellyfin
|
||||
and link host: an episode, a direct link, a link passed on and a
|
||||
server-fetched one, each prepared and promoted; no mpv "error running
|
||||
@@ -179,11 +190,11 @@ in [ROADMAP.md](ROADMAP.md)):
|
||||
|
||||
| Scenario | Covered today | Missing, added in |
|
||||
|---|---|---|
|
||||
| Drop and reconnect while sharing a relay | server `flaky_link_test` (blackhole: `gone` with ready, queue item and relay kept, the same member back; reset), `sync_wave_test` (takeover by client id, `replaced`, `stale`), `presence_test` (grace on a fake clock, restart), `relay_test` "the source drops (D6)" (`sourceGone`, buffer served, same `info` continues, another 4002, a second channel 4003, leave ends it); desktop `relay_e2e` "the source drops uncleanly and comes back" (gone past the server's silence, the relay kept, the source re-dials and continues), `coord_presence` (a half-open drop while playing: same member, no reopen, the state kept; 10 flaps: at most one lost/back sound; `replaced` is terminal; a restarted server gets the ready again), `connection_test` (the hello's id, `seq`, caps, `feedAfter`) | |
|
||||
| Drop and reconnect while sharing a relay | server `flaky_link_test` (blackhole: `gone` with ready, queue item and relay kept, the same member back; reset), `sync_wave_test` (takeover by client id, `replaced`, `stale`), `presence_test` (grace on a fake clock, restart), `relay_test` "the source drops (D6)" (`sourceGone`, buffer served, same `info` continues, another 4002, a second channel 4003, leave ends it); desktop `relay_e2e` "the source drops uncleanly and comes back" (gone past the server's silence, the relay kept, the source re-dials and continues), `coord_presence` (a half-open drop while playing: same member, no reopen, the state kept; 10 flaps: at most one lost/back sound; `replaced` is terminal; a restarted server gets the ready again, and what played if it came back empty), server `queue_test` (the first restore offer wins), `connection_test` (the hello's id, `seq`, caps, `feedAfter`) | |
|
||||
| Join mid-playback | core `sync_engine_test` "D2: joiners" (never changes the transport while playing, at most 15 s while held, joining ends on error or timeout), `presence_test` (the table); server `relay_test` "a late joiner gets its own /r/ URL"; desktop `coord_presence` (the returning member plays on in sync) | |
|
||||
| Sit out / Away and back | core `sync_engine_test` "away" (only oneself comes back, through joining), "return" (away survives a drop), `presence_test` (`isAlone`); server `hardening_test` (anyone sets anyone away, `forbidden`); desktop `server_isolate_test` (the host sat out through `presence` on your shared server), `notice_table_test` (the feed actions one-to-one, matched by id; sitting out gets only chat, nudges, "starting now", Up next and the chosen joins and leaves; alone no social sounds), `session_logic_test` (auto sit-out on a fake clock: never alone, a manual away stays), `player_status_test` / `shell_test` (the ready counter leaves away and gone out) | step 6: not tracked as watched (P7) |
|
||||
| Sit out / Away and back | core `sync_engine_test` "away" (only oneself comes back, through joining), "return" (away survives a drop), `presence_test` (`isAlone`); server `hardening_test` (anyone sets anyone away, `forbidden`); desktop `server_isolate_test` (the host sat out through `presence` on your shared server), `notice_table_test` (the feed actions one-to-one, matched by id; sitting out gets only chat, nudges, "starting now", Up next and the chosen joins and leaves; alone no social sounds), `session_logic_test` (auto sit-out on a fake clock: never alone, a manual away stays), `player_status_test` / `shell_test` (the ready counter leaves away and gone out); core `watch_session_test`, desktop `watch_tracker_test`, `jellyfin_session_test` "sat out", `jellyfin_e2e` (sat out: not tracked as watched, P7) | |
|
||||
| Swap, also while someone is Away or gone | core `sync_engine_test` "swap" (a gone member stays out of the gate, comes back joining), "away" (a swap leaves it away), `queue_test` (a swap takes its item, drops the `auto` one); server `queue_test` (a gone adder's item waits), `relay_test` (a swap drops the buffer); desktop `sync_playback_e2e`, `media_switch_e2e` (no command reaches a player mid-switch) | |
|
||||
| Up next advance | core `queue_test` (skip, play request timeout, `auto` and `cancel`); server `queue_test`, `relay_test` "warm relay"; desktop `jellyfin_session_test` (auto-advance swapper, end card countdown), `queue_switch_e2e` (prepared and promoted through the media pipeline) | step 6: the countdown as server state (`state.advance`) |
|
||||
| Up next advance | core `queue_test` (skip, play request timeout, `auto` and `cancel`; the countdown: start, follow the first item, seek, cancel, hold for a gone performer or source, skip what nobody can put up), `whats_next_test`; server `queue_test` (the countdown and cancel shared, a skip by the server); `relay_test` "warm relay"; desktop `jellyfin_session_test` (end card), `jellyfin_e2e` and `whats_next_e2e` (the continuation and the countdown end to end), `queue_switch_e2e` (prepared and promoted through the media pipeline) | |
|
||||
| Close the app cleanly, incl. `bye` | server `lifecycle_test` (a close keeps the member `gone`, `bye` leaves at once, server stop closes sockets); desktop `server_isolate_test` (stop frees the port; Stop sharing tells guests with 4016 and keeps you and Up next; a friend's server and back keeps yours), `coord_presence` (Stop sharing and your server's stop release their ports; leave sends `bye` and others see "left" at once, leaving for a restart keeps the member `gone`), `video_reload_e2e` (leave releases every player); the close smoke run on the built app (`tool/smoke_windows.ps1` in CI, `tool/smoke_linux.sh` by hand) | |
|
||||
|
||||
A test file whose first line is `// restructure: rewrite in step N` pins
|
||||
|
||||
+12
-1
@@ -78,7 +78,13 @@ brings you back.
|
||||
6. **Swap.** Anyone can change the video by pasting another link. Everybody
|
||||
stops at 0 and the gate runs again for everyone there; ready flags stay
|
||||
as they were (a setting unreadies you instead).
|
||||
7. **Leave.** Closing the app or Leave says goodbye and drops you from the
|
||||
7. **What's next.** Up next is one list on the server for everyone, and
|
||||
an episode you put up is followed by its next one. When a video ends
|
||||
with something next, the server counts down 10 s on every screen;
|
||||
anyone can cancel it for everyone (a seek stops it too). At zero the
|
||||
app that has what plays next puts it up; if that person is away for a
|
||||
moment, the countdown waits for them.
|
||||
8. **Leave.** Closing the app or Leave says goodbye and drops you from the
|
||||
server; the others keep going. If you vanish without a goodbye, after 15 s
|
||||
of silence you are "gone": you stop blocking but keep your place
|
||||
(ready flag, sitting out, Up next items, the video you pass on) for
|
||||
@@ -90,6 +96,11 @@ anyone, and sit anyone out (the host too). Someone sitting out stays
|
||||
connected and in the chat, but their app loads nothing and nobody waits
|
||||
for them until they join back.
|
||||
|
||||
Watched or not is counted by the time you were there while it played:
|
||||
sitting out, skipping around or a quick look never counts as watching.
|
||||
Jellyfin hears it for your account; files and links are remembered on
|
||||
your computer.
|
||||
|
||||
## Where video comes from
|
||||
|
||||
- **Link**: you paste an http(s) URL. By default your computer opens it and
|
||||
|
||||
+32
-12
@@ -98,6 +98,7 @@ yet; the restructure step named next to them does.
|
||||
{"t":"swap","media":{"name":"","size":0,"duration":0,"jellyfin":{<ref>}}}
|
||||
{"t":"swap","media":{"name":"","size":123,"duration":0,"url":"...","relay":{}}}
|
||||
{"t":"swap","media":{...},"queueId":"q3"} puts up an Up next item
|
||||
{"t":"swap","media":{...},"restore":{"position":512.3,"transport":"playing"}} puts back what played (see below)
|
||||
{"t":"report","position":123.1,"buffered":6.2,"eventId":41,
|
||||
"duration":1440.5,"mediaId":2,"jf":{"<serverId>":true},"error":"Not allowed (403)"}
|
||||
{"t":"ping","c":1767225600000}
|
||||
@@ -205,6 +206,13 @@ Field notes:
|
||||
every member's `ready` and puts every member in the set (`inSet`; who
|
||||
counts is up to presence); clients that want to be unready for the new
|
||||
media send `ready` themselves (gate: ARCHITECTURE.md, "Ready gate").
|
||||
- `swap.restore`: what played before the server restarted, offered back
|
||||
by a member that still holds it (`position` seconds, `transport`
|
||||
`playing`, `held` or `finished` as it was). The server takes it only
|
||||
while it has no media; any later one is ignored, so the first offer
|
||||
wins. It is a swap otherwise (relays, art, the gate), held at
|
||||
`position` (`finished`: at the end), its feed `swap` line with
|
||||
`reason` `restore`.
|
||||
- Frames before `welcome` (other than `hello`) are ignored; unparsable or
|
||||
unknown frames are ignored silently. Frames
|
||||
from a socket another hello took over are ignored.
|
||||
@@ -282,8 +290,8 @@ Field notes:
|
||||
`source` (the same while the relay's source is `gone`), `start` (nobody
|
||||
counts for the gate: someone has to ready up). `members`: the member
|
||||
ids the gate waits for (empty for `start`).
|
||||
- `state.advance` (**reserved**, step 6): the shared countdown to Up next
|
||||
(see "Queue").
|
||||
- `state.advance` (omitted when none runs): the shared countdown to Up
|
||||
next's first item at the end of the media (see "Queue").
|
||||
- `pong.c` echoes the ping's `c`; `pong.s` is the server's clock in epoch ms
|
||||
(the clock `serverTime` uses). With `t3` the local receive time, the offset
|
||||
is `s - (c + t3) / 2`, accurate to half the round trip.
|
||||
@@ -299,9 +307,10 @@ Field notes:
|
||||
the actor set itself; `reason` `idle`/`minimised`/`bye`, empty for
|
||||
manual), `in` (back from away), `swap`, `seek`, `pause`, `resume`,
|
||||
`finished`, `chat`, `rename`, `avatar`, `queue-add`, `queue-remove`,
|
||||
`queue-skip`, `advance-cancel` (`target` the dropped continuation's
|
||||
title, if any), `fetch-fail` (the server could not fetch the current
|
||||
media, `target` its name). `reason` carries `auto`/`manual` for ready.
|
||||
`queue-skip` (actor `server` when the server skipped it), `advance-cancel`
|
||||
(`target` the dropped continuation's title, if any), `fetch-fail` (the server could not fetch the current
|
||||
media, `target` its name). `reason` carries `auto`/`manual` for ready,
|
||||
`restore` for a swap that put back what played (`swap.restore`).
|
||||
`rename`: `actor` is the new name, `target` the old one. `avatar`: the
|
||||
actor changed only the picture. `queue-*`: `target` is the item's
|
||||
title, `reason` (skip) why it could not play (see "Queue").
|
||||
@@ -623,7 +632,7 @@ state "queue":[<item>,...], "queuePlay":"q3"
|
||||
<item> = {"id":"q3","media":{<swap media>},"how":"direct"|"server"|"through"|"file"|"jellyfin",
|
||||
"title":"...","sub":"...","duration":213,"thumb":"<hash>",
|
||||
"by":"m2","byName":"Bob","key":"<adder's handle>","auto":true?,"for":2?}
|
||||
state "advance":{"id":"q3","at":"<server time>","secs":10} reserved
|
||||
state "advance":{"id":"q3","at":"<server time>","secs":10}
|
||||
```
|
||||
|
||||
- `add`: `item` without `id`, `by`, `byName`, `thumb` (the server sets
|
||||
@@ -634,7 +643,8 @@ state "advance":{"id":"q3","at":"<server time>","secs":10} reserved
|
||||
item is queued. `title` and `sub` are cleaned like media names and cut
|
||||
to 200 characters (an empty title becomes the media's name); `duration`
|
||||
(seconds, omitted when unknown) is informational. At most 100 items;
|
||||
over that the sender gets `err` `Up next is full (100 items)`.
|
||||
over that the sender gets `err` `Up next is full (100 items)`. With
|
||||
nothing up, the first item is asked for at once (`queuePlay`).
|
||||
- `how` and what `media` must carry:
|
||||
- `direct`: an http(s) `url`; everyone opens the link.
|
||||
- `server`: an http(s) `url` and `relay` `{"fetch":true}`, on a server
|
||||
@@ -654,7 +664,10 @@ state "advance":{"id":"q3","at":"<server time>","secs":10} reserved
|
||||
- `play`: moves `id` first and sets `state.queuePlay` to it: a request for
|
||||
the member who can put it up to do so now. `skip` is `play` of the
|
||||
first item. `queuePlay` ends with any swap, when the item is removed,
|
||||
or after 30 s.
|
||||
or after 30 s. An item asked for that nobody here can put up (its
|
||||
adder left for good, nobody has its Jellyfin server; the rule is
|
||||
core's `queuePerformer`, ARCHITECTURE "What's next") is taken off by the
|
||||
server: feed `queue-skip`, actor `server`, `reason` why.
|
||||
- `auto` (with `for`, the `mediaId` it continues): the continuation (the
|
||||
next episode). Only for the current media (else `err` `bad`), and only
|
||||
from the member who put it up (`media.swapBy`) or, when that member is
|
||||
@@ -662,12 +675,19 @@ state "advance":{"id":"q3","at":"<server time>","secs":10} reserved
|
||||
one: a newer one replaces it; it stays after the other items (an `add`
|
||||
goes before it, `next` puts an item first, `move` never passes it, it
|
||||
does not move itself); any swap that does not put it up drops it.
|
||||
Adding it gives no feed line.
|
||||
- `cancel`: drops the `auto` item and the countdown for everyone (feed
|
||||
`advance-cancel`); nothing to drop, nothing happens.
|
||||
- `state.advance` (**reserved**, step 6): the shared countdown when the
|
||||
media finishes and an item is first; at `at` + `secs` the server would
|
||||
set `queuePlay` to `id`, holding while its performer or source is
|
||||
`gone`. The server never starts one yet; `cancel` and any swap clear it.
|
||||
- `state.advance`: the shared countdown, started when the media finishes
|
||||
with an item first (or an item comes first while it is finished);
|
||||
`secs` is 10, `at` its start (server clock). A new first item takes it
|
||||
over with the same end. At `at` + `secs` the server sets `queuePlay` to
|
||||
`id` and the countdown is over; while the member who puts the item up
|
||||
is not around (its adder `gone`, nobody present) or the item's warm
|
||||
relay waits for a `gone` source, it holds, and asks once they are
|
||||
back. A seek (the end of `finished`), an empty queue, `cancel` and any
|
||||
swap clear it; after `cancel` it does not start again for that media,
|
||||
nor again for the item it already asked for.
|
||||
- `swap.queueId`: the swap puts that item up and takes it off the queue
|
||||
in the same step. A swap naming an item that is no longer queued is
|
||||
ignored (someone put it up first). For a `server` item the swap may
|
||||
|
||||
+42
-32
@@ -35,8 +35,6 @@ Problems are numbered; delete a line when its fix ships.
|
||||
- D11 Login-only sites: error texts say which cookies were used.
|
||||
|
||||
**Problems:**
|
||||
- P7 Sat-out people get tracked as watched on Jellyfin.
|
||||
- P9 The watched rule (position coverage) misses real watches (D3).
|
||||
- P25 ssh to Gitea blocked through the proxy (pushes go over https).
|
||||
- P30 Shaders don't work on Windows. Fix in 1.2.1-dev.3 (list read-back
|
||||
check; Windows hwdec `auto-copy`): a Windows user must confirm shaders
|
||||
@@ -61,16 +59,12 @@ the automatic continuation and countdown, relay state), the device
|
||||
(client id, library and folders, match corrections, watch history,
|
||||
settings, notification prefs, what this app is sourcing) and the
|
||||
integrations (Jellyfin played/resume, AniList list).
|
||||
6. What's next (decides which/when, asks the pipeline to prepare;
|
||||
continuation and countdown as server state, shared cancel) + Watch
|
||||
tracking (time In from the presence history, device history, Jellyfin
|
||||
now, AniList later). Fixes P7 P9; D3.
|
||||
7. Release 1.3.0: publish the manifest (protocol 16 makes it a required
|
||||
update for 1.2.x) and update the VPS dedicated server in the same
|
||||
window; check a 1.2.x app gets a clear "update the app" message.
|
||||
Composition root (in place, ARCHITECTURE "Composition root"): the
|
||||
remaining `.instance` singletons move into `AppGraph` as the steps touch
|
||||
them; `singletons_test` holds the count (today: AppSettings 55,
|
||||
them; `singletons_test` holds the count (today: AppSettings 49,
|
||||
JellyfinAccounts 10, UpdateController 8, Profile 8, FolderStore 6,
|
||||
AppYtDlp 2, RecentLinks 3).
|
||||
Not in the restructure: P25 (ops).
|
||||
@@ -113,6 +107,30 @@ your server leaves them there without you, unwarned; a join refused
|
||||
after dialling (wrong password) costs your own member there (`bye`, back
|
||||
as a new member). Your server writes its session log even when you are
|
||||
alone.
|
||||
Left of step 6 (what's next, watch tracking): a human check, on Linux
|
||||
now, of the server's countdown and the end card in real use (Cancel
|
||||
from another screen, a seek stopping it, "Waiting for" while the one who
|
||||
puts it up is gone), of a file continuation on the real folder (Bleach
|
||||
"skips 66–107" across its folders, SAO S3 into its OP/ED folder, the
|
||||
last episode of a show) and of Jellyfin's played state and resume point
|
||||
under time In against a real Jellyfin (a full watch, one sat out, a
|
||||
resume), and of a dev server restarted mid-episode (what played comes
|
||||
back where it was for everyone, a Jellyfin item passed on to a guest
|
||||
re-fed: only files and links are covered by tests). Not built: AniList
|
||||
progress (the tracker's `records` are its seam); nothing resumes a file
|
||||
or link at the history's resume point (it only keeps it); no watched
|
||||
marks on Files' episode rows (only "Next" follows the history) and no
|
||||
way to see or clear the history. The continuation of a file comes only
|
||||
from the app that put it up (only it has the file). "Play the next episode
|
||||
automatically" is the setting of whoever puts an episode up; Up next's
|
||||
own items count down whatever it says. A play request the performer
|
||||
does not act on ends after 30 s without a new countdown for that item
|
||||
(Play now remains). A warm relay waiting for a `gone` source holds the
|
||||
countdown even when the one who puts the item up is there. After a
|
||||
server restart, a relayed file or link whose member is not back, or a
|
||||
Jellyfin item nobody back has an account for, cannot be put back (the
|
||||
apps let go after 30 s); a server-fetched Up next item is lost (its
|
||||
address stayed with the old server).
|
||||
Left of step 5 (media pipeline): a human check, on Linux now and on
|
||||
Windows at 1.3.0, of a page link in "Everyone fetches it" through the
|
||||
app's own resolve (YouTube's 1080p video + audio EDL, Bilibili with its
|
||||
@@ -131,6 +149,14 @@ CDN wants per-track headers that differ gets the first track's (as
|
||||
mpv's hook did); Bilibili's danmaku is dropped, not shown. The Jellyfin
|
||||
item this member put up re-feeds its relay after an app restart (the
|
||||
state still names it the source).
|
||||
Relay start (P29's rest): a seek into what the source has not sent
|
||||
plays after about 2.8 s in the e2e; a human check of a real session
|
||||
on dev.8+. The source's in-flight window still starts at 256 KiB and
|
||||
grows with the measured rate, so the speed climbs from low after a
|
||||
seek or a new link; the start of a freshly put-up link was never
|
||||
measured on its own. Levers if it still shows: start the window from
|
||||
the last rate, prefetch from the source as soon as a link goes up,
|
||||
a smaller gate buffer when only the relay is behind.
|
||||
Left of step 4 (library): a human check of the Files library on the
|
||||
real folder (`/mnt/torrents/Torrents/Binux/`: Bleach over three folders
|
||||
with 66–107 missing, SAO S3 Alicization with OP/ED, Re.Zero S04E17, the
|
||||
@@ -142,9 +168,7 @@ swapper's bytes (now chosen by the ref's AniList id). Relay logins are
|
||||
sent at Invite: an account linked or signed in again later relays only
|
||||
after Stop sharing and Invite. "Season N" without a name walks AniList's
|
||||
sequels, which can land on the wrong cour where AniList splits seasons;
|
||||
Wrong show? fixes it. The next episode of a local show lives on its page
|
||||
(from Recent); no countdown or "Continues with" for files yet (backlog
|
||||
2). The `library` box keeps every AniList entry seen (no pruning);
|
||||
Wrong show? fixes it. The `library` box keeps every AniList entry seen (no pruning);
|
||||
AniList's episode titles exist only where it lists streaming episodes.
|
||||
Left of step 2: system notifications need a human check (Linux: the
|
||||
notification shows and a click brings the window up, also on Wayland,
|
||||
@@ -170,13 +194,8 @@ Opened in step 1: [PROTOCOL.md](PROTOCOL.md) is proto 16 (version
|
||||
ranges and the refusal frame, error codes and close codes, lasting members by client id with takeover and `seq`, grace and
|
||||
`bye`, presence and `PresencePolicy`, `state.hold`, feed ids, `seq` and
|
||||
replay, relay `sourceGone` and source resume, lasting `queue[].by`,
|
||||
`auto` items and `cancel`, `media.ref`, `members[].app`/`caps`), server,
|
||||
`auto` items, `cancel` and `state.advance`, `media.ref`, `members[].app`/`caps`), server,
|
||||
core and app. Still open on the wire, frozen at the 1.3.0 release:
|
||||
- `state.advance` (step 6): the server starts the countdown when the
|
||||
media finishes and an item is first, sets `queuePlay` at its end and
|
||||
holds it while the item's performer or source is `gone`. Today it is
|
||||
parsed and broadcast but never started; `auto` items and `cancel` are
|
||||
live.
|
||||
- `hello.client` optionally derived per server (HMAC of an install
|
||||
secret and the dialled host:port).
|
||||
- TLS: `/health.tls` is always false; `wss://` only with a CA-valid
|
||||
@@ -207,12 +226,7 @@ core and app. Still open on the wire, frozen at the 1.3.0 release:
|
||||
seeks are shared, and only real ranges (segments, OP/ED chapters) should
|
||||
ever be skipped.
|
||||
|
||||
2. **Files**: the next episode of a local show (the library's
|
||||
`nextAfter`, gaps included) at the end of one, with the countdown, and
|
||||
under Up next's "Continues with". Tracking for file series comes with
|
||||
step 6.
|
||||
|
||||
3. **yt-dlp follow-ups** (see ARCHITECTURE "yt-dlp"): check that the
|
||||
2. **yt-dlp follow-ups** (see ARCHITECTURE "yt-dlp"): check that the
|
||||
bundled Windows libmpv plays the app's `edl://` URLs (video + audio
|
||||
with headers) through media_kit. YouTube: yt-dlp wants a JS runtime
|
||||
(deno) for full extraction.
|
||||
@@ -229,15 +243,15 @@ core and app. Still open on the wire, frozen at the 1.3.0 release:
|
||||
- Thumbnails and scrubbing previews of a relayed media with tracks
|
||||
should open the video track's URL alone, not the EDL.
|
||||
|
||||
4. **AniList account**: sign in (OAuth via a localhost callback), "My list"
|
||||
3. **AniList account**: sign in (OAuth via a localhost callback), "My list"
|
||||
filter on the Schedule from the user's AniList lists. This is the
|
||||
Schedule's personal tracking; AnimeFlow has no per-user follow.
|
||||
|
||||
5. **Ops**: Check shaders on Windows; run the relay e2e "the source drops
|
||||
4. **Ops**: Check shaders on Windows; run the relay e2e "the source drops
|
||||
uncleanly and comes back" a few times under the full suite (its
|
||||
predecessor failed about half the time there).
|
||||
|
||||
6. **Windows: first real use.** Check the rename at 1.3.0: a 1.2.x
|
||||
5. **Windows: first real use.** Check the rename at 1.3.0: a 1.2.x
|
||||
install updates and restarts (`watch_together.exe`, a copy of
|
||||
`starsync.exe`), the data in `%APPDATA%\foxyz\Starsync` came over from
|
||||
`%APPDATA%\com.example\watch_together` and Documents, the next update
|
||||
@@ -247,7 +261,7 @@ core and app. Still open on the wire, frozen at the 1.3.0 release:
|
||||
fullscreen controls, hosting (firewall prompt), in-app update from one
|
||||
release to the next. The rest of the desktop tests are not in CI.
|
||||
|
||||
7. **Relay follow-ups** (see ARCHITECTURE "Relay"). First real use: owner's
|
||||
6. **Relay follow-ups** (see ARCHITECTURE "Relay"). First real use: owner's
|
||||
PC -> VPS -> friend, Jellyfin and a file. Then:
|
||||
- Source takeover: when the source leaves, another member with an
|
||||
account on that Jellyfin (or the same file: name, size, hash) takes
|
||||
@@ -268,9 +282,9 @@ core and app. Still open on the wire, frozen at the 1.3.0 release:
|
||||
fetch of member-resolved links (the member need not upload or stay)
|
||||
is worth building.
|
||||
|
||||
8. **Jellyfin** (epic, below).
|
||||
7. **Jellyfin** (epic, below).
|
||||
|
||||
9. **Smaller items**
|
||||
8. **Smaller items**
|
||||
- Video output robustness: if media_kit's hardware path regresses to
|
||||
software (see ARCHITECTURE, Media: render mode), vendor a
|
||||
media_kit_video fix that builds the EGL display from GDK's native
|
||||
@@ -298,10 +312,6 @@ core and app. Still open on the wire, frozen at the 1.3.0 release:
|
||||
|
||||
What is left (the rest is in [ARCHITECTURE.md](ARCHITECTURE.md), "Jellyfin
|
||||
in a session"):
|
||||
- Auto-advance cancel for everyone (the next episode and Up next): today
|
||||
Cancel is per screen (the designated swapper's Cancel stops the swap; a
|
||||
seek stops all). The wire has it (`queue` `cancel`, `state.advance`);
|
||||
the app uses it in step 6.
|
||||
- Sidecar (external) subtitle streams of Jellyfin items: only embedded
|
||||
tracks are mapped to mpv today.
|
||||
|
||||
|
||||
Reference in new issue
Block a user