Restructure step R: the app is Starsync (dev.foxyz.starsync)
ci / core (push) Successful in 31s
ci / server (push) Successful in 1m3s
ci / updater-windows (push) Successful in 6m6s

- Linux app id dev.foxyz.starsync, binary starsync; Windows starsync.exe,
  company foxyz; server starsync-server; Dart package starsync; archives
  starsync-<ver>-*.
- 1.2.x compatibility: watch_together and watch-together-server
  launchers (Linux) / copies (Windows) in the archive; the updater
  accepts both names and relaunches the new binary; update.sh/.bat use
  whichever server exists.
- First start copies (never moves) the old support folder and this
  app's Hive boxes out of ~/Documents into <support>/hive; a marker
  makes it run once; AppDirs is a port; settings schema version with
  ordered migrations.
- Launcher entry dev.foxyz.starsync.desktop; the old one removed.
- Repository name and URLs unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
foxyzandClaude Opus 5.5 committed 2026-10-15 20:21:20 +03:00
1 parent f4ca02c3cf
commit 51c1c5ab78
96 files changed
+1169 -489

No files matched your search

+71 -19
View File
@@ -9,7 +9,7 @@ Plain overview first: [HOW-IT-WORKS.md](HOW-IT-WORKS.md). Wire format:
|---|---|---|
| `packages/core` | protocol messages, sync engine (`SyncEngine`), constants, config, Range parsing, logging, Jellyfin client, updater, watched rule (`WatchSession`); `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, `SyncClient`, `Connection`, `SessionCoordinator`, `PlaybackController` (media_kit/mpv), `ServerIsolate`, update UI (`UpdateController`) | ships with `watch-together-server` |
| `apps/desktop` | Flutter app: UI, `SyncClient`, `Connection`, `SessionCoordinator`, `PlaybackController` (media_kit/mpv), `ServerIsolate`, 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 | |
@@ -58,7 +58,7 @@ size (`ServerIsolate.setFile`): the host's in-session File swap then sends a
cancelled, solo starts again with the same media. Stop leaves without
asking.
- **Dedicated server**: `bin/server.dart` compiled with `dart compile exe`
and shipped next to the app as `watch-together-server`. It parses flags
and shipped next to the app as `starsync-server`. It parses flags
(`--port`, `--secret`, `--name`, `--media`, `--advertise-host`,
`--jellyfin <file>`, `--[no-]yt-dlp`, `--yt-dlp-dir`, `--yt-dlp-cookies`, `--config`; default port 8080) into the same map and calls `serve`
(`--log-dir` only picks where its log files go, see "Logging")
@@ -1174,6 +1174,10 @@ handed on from there (the coordinator gets its `Notifier`, `Connection` and
options, warm-up.
- `SystemNotifications`: an interface only; desktop shows none
(`NoSystemNotifications`).
- `AppDirsPort` (`DesktopAppDirs`): where Hive and the logs live
(`AppDirs`, "Desktop UI"), and the copy of the data from before the
rename; resolved in `main` before the composition root exists, so not a
`Ports` field.
Still direct: the player (`PlaybackController`, media_kit, and its
`AppYtDlp.instance.mpvRawOptions`), the updater's restart
(`Process.start`), the session's own file reads for what it plays or
@@ -1503,11 +1507,38 @@ Settings persist in two Hive boxes: `connect_form` (last form, recent
servers and links, the profile picture and colour; saved even when a join
fails) and
`settings`. Where Hive and the logs live is `AppDirs`
(`platform/app_dirs.dart`, resolved once in `main`): Hive's default folder
(the documents folder) and `<app support>/logs`; with `STARSYNC_PROFILE`
(DEVELOPMENT "Dev loop") `<app support>/profiles/<name>/hive` and `logs`,
and the window title gets ` [<name>]`. yt-dlp, shaders and caches stay
shared. Link media names are a web page's title (see "yt-dlp"), else
(`platform/ports/app_dirs.dart`, resolved once in `main` by the
`AppDirsPort`, `DesktopAppDirs` on the desktop): `<app support>/hive` and
`<app support>/logs`; with `STARSYNC_PROFILE` (DEVELOPMENT "Dev loop")
`<app support>/profiles/<name>/hive` and `logs`, and the window title gets
` [<name>]`. yt-dlp, shaders and caches stay shared. The app support
folder is path_provider's: Linux `~/.local/share/dev.foxyz.starsync` (the
application id), Windows `%APPDATA%\foxyz\Starsync` (the exe's company and
product name).
**Data from before the rename** (`platform/data_migration.dart`, run by
`DesktopAppDirs.resolve` before Hive opens and the log starts; its lines
are logged once the log is open): once per support folder (the marker
`<app support>/migration.json`: `migratedFrom`, `hiveFrom`, `boxes`,
`files`), the first existing old support folder (Linux
`moe.senkochan.watch_together`, then `watch_together`, next to the new one;
Windows `%APPDATA%\com.example\watch_together`) is copied in whole
(profiles, logs, shaders, yt-dlp; modes kept, so `cookies.txt` stays 0600;
symlinks as links), and the app's own Hive boxes (`appHiveBoxes`:
`settings`, `connect_form`, `files`, `jellyfin`, `track_prefs`, `.hive`
and `.lock`, by name only, never other apps' boxes there) from the
documents folder, where `Hive.initFlutter()` kept them up to 1.2.x, into
`<app support>/hive`. Copied, never moved (1.2.x keeps working), never
over a file already there. Lines name paths and counts, never contents.
The caches folder (thumbnails, the AniList schedule) is not copied: it
refills.
**Settings schema** (`state/settings_schema.dart`): the `settings` box
stores `schemaVersion`; `migrateSettings` (run when `AppSettings` loads)
applies the ordered `settingsMigrations` steps past the stored version up
to `settingsSchemaVersion`, stamping the version after each. 1 is the
layout of 1.2.x (a box without a version is at 1, no steps yet); a box
from a newer build is left alone. Link media names are a web page's title (see "yt-dlp"), else
derived from the URL path, never containing a query string.
Jellyfin on the desktop (`lib/src/jellyfin/`, over the core client):
@@ -1697,7 +1728,7 @@ dedicated server (`package:logging` loggers named by area: `relay`,
`app`).
- **Where**: the app writes `<app support>/logs/app-YYYY-MM-DD.log`
(Linux `~/.local/share/moe.senkochan.watch_together/logs`; with a
(Linux `~/.local/share/dev.foxyz.starsync/logs`; with a
profile `<app support>/profiles/<name>/logs`, see `AppDirs` under
"Desktop UI"); its hosted server's records arrive over the isolate's
`events` port and go in the same file as `server.<logger>` (`relogLine`). The dedicated server
@@ -1809,13 +1840,19 @@ status.
`assets/icon/icon.svg` is the source; `icon_<size>.png` and the Windows
`app_icon.ico` are rendered from it (rsvg-convert, magick). The GTK
application id is `moe.senkochan.watch_together`, which Wayland uses as the
window's `app_id`; compositors take the icon from the `.desktop` file of
that id. A tarball install has none, so on start a Linux release build
(`platform/desktop_entry.dart`, skipped in debug) writes
`~/.local/share/applications/moe.senkochan.watch_together.desktop` (Exec =
the running binary) and the icons into the hicolor theme, only when they
changed. The runner also sets the window icon from the bundle (X11).
application id is `dev.foxyz.starsync` (also the X11 `WM_CLASS`, from
`g_set_prgname`), which Wayland uses as the window's `app_id`; compositors
take the icon from the `.desktop` file of that id. A tarball install has
none, so on start a Linux release build (`platform/desktop_entry.dart`,
skipped in debug) writes
`~/.local/share/applications/dev.foxyz.starsync.desktop` (Exec = the
running binary, `StartupWMClass` and `Icon` = the id) and the icons into
the hicolor theme, only when they changed, and deletes the entry and
icons 1.2.x wrote under its id `moe.senkochan.watch_together` (those exact
files only). The runner also sets the window icon from the bundle (X11).
Windows: the exe's version resource (`windows/runner/Runner.rc`) says
company `foxyz`, product `Starsync`; the window class is
`STARSYNC_WINDOW`.
## Updater
@@ -1829,8 +1866,23 @@ folder (same filesystem, so moves are renames; the folder must be
user-writable). Each top-level entry is moved aside and the new one moved in,
with rollback on failure (writing the running executable fails with ETXTBSY;
rename works). A packaged install is detected by `data/` and `lib/` next to
the binary (app or server, same folder); anything else exits with "update
manually". A manifest whose
the binary (app or server, same folder; Windows: `data/` and
`flutter_windows.dll`), whatever the binary is called; anything else exits
with "update manually". The archive must contain the running binary's name;
an old name (`watch_together`, `watch-together-server`, `.exe` on
Windows) is also satisfied by the new one (`starsync`, `starsync-server`).
**Names before the rename**: the archive (`starsync-<ver>-<platform>`,
top folder `starsync-<ver>/`) also carries those old names, because a
1.2.x updater checks that its own name is in the archive and then restarts
it, and VPS units and old `update.sh` copies run `watch-together-server`.
Linux: shell launchers (`lib/src/update/bundle/watch_together`,
`watch-together-server`, `exec` the new binary next to the resolved
script); Windows: copies of `starsync.exe` and `starsync-server.exe` (same
version resource, so the same data folder). After an update the app
restarts `relaunchExecutable(...)`: the running binary, or for an old name
the new one next to it, so a copy started by 1.2.x moves to `starsync.exe`
on its first own update. `update.sh` / `update.bat` run `starsync-server`,
else the old name. A manifest whose
protocol is newer than the local one is a required update. Windows apply
renames in-use files to `*.old` (swept on next start); CI tests it against a
running exe on NTFS. The manifest's `assets` maps `linux-x64` /
@@ -1842,9 +1894,9 @@ About and the tooltip of the strip's Settings tab): a release's version,
a test build's with its commit (`GIT_COMMIT`, short hash, `-dirty` with
uncommitted changes, passed by `tool/package_linux.sh`), and for a dev
run the pubspec's version (`FLUTTER_BUILD_NAME`, which the Flutter tool
defines for every build and run) with "dev run". CLI: `watch-together-server --upgrade` (no display needed;
defines for every build and run) with "dev run". CLI: `starsync-server --upgrade` (no display needed;
the shipped `update.sh` / `update.bat` run it), `--check-update` prints only;
it ends with "restart the server to run <version>". `watch_together --upgrade`
it ends with "restart the server to run <version>". `starsync --upgrade`
does the same from the app binary. In the app, `UpdateController`
(`apps/desktop/lib/src/update/`) runs the same code for the update bar
(`UpdateBar`, `screens/home/update_indicator.dart`): a band across the
+11 -5
View File
@@ -21,7 +21,7 @@ cd apps/desktop && flutter pub get && flutter run -d linux
cd apps/desktop && flutter build linux --release --dart-define=APP_VERSION=<x.y.z>
tool/package_linux.sh [--skip-build] [--noupload] # dist/*.tar.gz
powershell -File tool/package_windows.ps1 [-SkipBuild] # dist/*.zip (on Windows)
cd packages/server && dart compile exe --define=APP_VERSION=<x.y.z> bin/server.dart -o watch-together-server
cd packages/server && dart compile exe --define=APP_VERSION=<x.y.z> bin/server.dart -o starsync-server
```
`APP_VERSION` defaults to `0.0.0-dev`; the packagers stamp it from the
@@ -30,7 +30,13 @@ also passes `GIT_COMMIT` (what the app shows: "Updater" in
[ARCHITECTURE.md](ARCHITECTURE.md)). Setting `DISCORD_WEBHOOK_URL` makes the packager post the
tarball to Discord (never commit it). The packager always compiles the
dedicated server (with the Flutter SDK's `dart`) into the tarball as
`watch-together-server`, also with `--skip-build`.
`starsync-server`, also with `--skip-build`. The app binary is `starsync`
(`BINARY_NAME`; Linux application id `dev.foxyz.starsync`), the archive
`dist/starsync-<version>-linux-x64.tar.gz` / `-windows-x64.zip` with one
top folder `starsync-<version>/`; it also carries the names up to 1.2.x
(`watch_together`, `watch-together-server`: launchers on Linux, copies on
Windows; "Updater" in [ARCHITECTURE.md](ARCHITECTURE.md)). The Dart
package of the app is `starsync` (`package:starsync/...`).
## Dev loop
@@ -224,8 +230,8 @@ unexpectedly` line just before it is expected. Without the app:
After a session, read the log files rather than reproducing
("Logging" in [ARCHITECTURE.md](ARCHITECTURE.md) has what goes in them):
the app's are `~/.local/share/moe.senkochan.watch_together/logs/app-*.log`
on Linux (on Windows under `%APPDATA%`; Settings → About → "Open logs
the app's are `~/.local/share/dev.foxyz.starsync/logs/app-*.log`
on Linux (on Windows `%APPDATA%\foxyz\Starsync\logs`; Settings → About → "Open logs
folder" finds it anywhere), with the hosted server's lines
in the same file; a dedicated server prints its folder at start
(`--log-dir`). Users send theirs with Settings → About → "Copy
@@ -259,7 +265,7 @@ rasterizer: missing or blocked GPU drivers, `LIBGL_ALWAYS_SOFTWARE=1`);
no usable EGL context: check `render env:` for an X11 session or a
`GDK_BACKEND` / `FLUTTER_LINUX_RENDERER` override). Its reason is only on
stderr: have the user start the app from a terminal
(`watch-together 2>&1 | grep VideoOutput`). `hwdec-current=no` on the
(`./starsync 2>&1 | grep VideoOutput`). `hwdec-current=no` on the
first-frame line means software decoding (no hwdec for that codec, or it
is off), not software rendering.
+1 -1
View File
@@ -42,7 +42,7 @@ A five-minute tour. Details: [ARCHITECTURE.md](ARCHITECTURE.md), wire format:
- **Host from the app**: the app you click Host in also runs the server.
Simplest. The server lives as long as that app.
- **Dedicated server**: `watch-together-server`, a separate program in the
- **Dedicated server**: `starsync-server`, a separate program in the
release tarball, for an always-on box. See the README.
Friends join either way with Join a friend in the strip at the top of the
+14 -14
View File
@@ -85,15 +85,6 @@ 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).
R. Rename (before step 1, so dev builds install side by side with 1.2.x):
app id `dev.foxyz.starsync`, binary, Windows company `foxyz`; Hive out
of `~/Documents` into the app's support folder, copied not moved;
a settings schema version with ordered migrations; `AppDirs` (today
Hive and logs, and `STARSYNC_PROFILE`) becomes a port and the default
moves next to the profiles;
the 1.3.0 archive still contains `watch_together(.exe)` so 1.2.x's
self-update restart works; the old `.desktop` entry removed; the
manifest kept at the old URL; VPS `ExecStart`/`update.sh` paths.
1. Connection + Identity + Presence; proto 16 opened (version ranges on
both sides, refusal frame, identity, grace, presence, relay resume,
ids in the feed); later steps add fields, it is frozen at the 1.3.0
@@ -161,9 +152,14 @@ proto 16 together at the 1.3.0 release. Rooms are out (no room id).
No Windows dev builds: restructure testing happens on Linux; Windows
friends stay on 1.2.x until the 1.3.0 release (Windows-only fixes like
P30 reach them then).
Rename to Starsync everywhere (repo, app id, data folder, updater URL
migration) is its own step; app id `dev.foxyz.starsync`, Windows
company name `foxyz` (replacing Flutter's default `com.example`). Auto
Left of the rename (app, binaries, archives and data folder are
Starsync): the repository (`Foxyz/sync_replay_project`) and with it the
manifest and asset URLs (a new manifest URL needs a release that reads
both first); the dedicated server's fallback folder
`~/.cache/watch-together`, the release token file
`~/.config/watch-together-release-token`, Jellyfin's client name "Watch
Together" (devices are listed under it) and `WatchTogetherApp`; dropping
the 1.2.x launchers from the archive once nobody updates from 1.2.x. Auto
sit-out: idle or minimised for a while sets you Away, activity sets you
In; it replaces the 10-minute auto-unready.
@@ -338,8 +334,12 @@ over `wss://`/`https://` (`/health.tls`); apps use `wss://` when offered,
5. **Ops**: Check shaders on Windows; the relay e2e "source drops
uncleanly and resumes" fails about half the time under the full suite.
6. **Windows: first real use.** CI builds and publishes
`watch-together-<ver>-windows-x64.zip` (libmpv bundled) and runs the
6. **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
restarts `starsync.exe`. CI builds and publishes
`starsync-<ver>-windows-x64.zip` (libmpv bundled) and runs the
updater tests on NTFS; a friend has joined from it. Still unchecked:
fullscreen controls, hosting (firewall prompt), in-app update from one
release to the next. The rest of the desktop tests are not in CI.