Add a docs-site (mdBook) with a Gitea publish-docs workflow, a release-notes generator script (release:notes) that turns a commit range's TRACES into grouped notes, the background-audio feature spec, and CLAUDE.md. Ignore docs-site build artifacts.
11 KiB
JellyTau
A cross-platform Jellyfin client. Business logic lives in a Rust backend
(src-tauri/); a SvelteKit + TypeScript frontend (src/) handles presentation
and talks to it over Tauri v2 IPC. Targets Linux (libmpv, WebKitGTK HTML5
<video> for transcoded playback) and Android (ExoPlayer).
Package manager is bun.
Build / Run / Test
All routine tasks go through package.json scripts and helper scripts in
scripts/:
bun install # install deps
bun run dev # vite dev server (frontend)
bun run tauri dev # run the desktop app
bun run check # svelte-check (types)
bun run test # vitest (frontend unit/integration)
bun run test:rust # cargo test (scripts/test-rust.sh)
bun run test:all # full suite (scripts/test-all.sh)
bun run test:e2e # webdriverio e2e
# Android — canonical entry points (see scripts/):
bun run android:build # debug APK
bun run android:build:release # release APK
bun run android:deploy # install to connected device
bun run android:dev # build + deploy
bun run android:logs # logcat
CI runs on Gitea Actions (.gitea/workflows/), not GitHub. Use the gh CLI
only against the mirror if one exists; the canonical remote is
gitea.tourolle.paris.
Before Committing
- Frontend:
bun run checkandbun run testmust pass. - Rust:
cd src-tauri && cargo fmtthencargo clippy, plusbun run test:rust. - Traceability: new requirement-implementing code must carry a
// TRACES:comment (see below). - Android source edits: edit
src-tauri/android/src(the canonical tree), then runscripts/sync-android-sources.shto sync into thegen/tree. Never edit the generatedgen/sources directly.
Traceability (TRACES)
This project practices requirement-driven development: code that implements a
requirement is tagged with a TRACES: comment linking it to requirement IDs, and
an extraction tool builds the traceability matrix. When you add or change code
that implements a requirement, add/update its TRACES comment. Internal helpers
and requirement-less code stay untraced.
Format — // TRACES: <URs> | <DRs> | <tests>, e.g.:
/// TRACES: UR-005 | DR-001
pub enum PlayerState { … }
// TRACES: UR-005, UR-026 | DR-029
export function autoplayNextEpisode() { }
ID types: UR user requirement, IR integration, DR development, JA Jellyfin API, UT unit test, IT integration test. Requirements are defined in docs/requirements.md; the generated matrix is docs/traceability.md.
Tooling:
bun run traces # extract traces (default format)
bun run traces:json # JSON — e.g. | jq '.byType' or '.requirements."UR-005"'
bun run traces:markdown # regenerate docs/traceability.md
git diff --name-only | xargs grep -L "TRACES:" # find untraced changed files
CI is Gitea Actions (.gitea/workflows/, remote gitea.tourolle.paris), not
GitHub. traceability-check.yml fails the build if coverage drops below
50% (MIN_THRESHOLD); build-and-test.yml runs frontend + Rust tests and an
Android cargo check. See docs/traceability-ci.md and
docs/traces-quick-ref.md.
Traces drive release notes
Prefer traceability over raw commit subjects when writing release notes for
docs/release-checklist.md. Raw git log subjects are
noisy; the TRACES graph gives a semantic summary of what capabilities the
release touched.
bun run release:notes # <latest tag>..HEAD
bun run release:notes v0.0.15..HEAD # explicit range
scripts/release-notes.ts resolves a commit range's
changed files → their TRACES: IDs → descriptions in
docs/requirements.md, then groups UR into Features
and DR/IR into Improvements (deduped, so many commits touching one
requirement collapse to one line). It also lists changed files that carry no
TRACES so nothing is silently dropped — those still need a manual line. Treat the
output as a reviewed draft, not a final changelog.
Architecture
- Rust backend (
src-tauri/src/) — all business logic: auth, catalog, sessions, downloads, offline cache, playback control. Commands grouped by domain insrc-tauri/src/commands/(auth.rs,catalog.rs,player/,download/,offline.rs,sessions.rs, …). - Svelte frontend (
src/) — presentation only. Stores insrc/lib/stores/, API wrappers insrc/lib/api/, components insrc/lib/components/. - Playback layers — Linux uses libmpv for direct playback and a WebKitGTK
HTML5
<video>element for HLS-transcoded (h264) streams; Android uses ExoPlayer with a foreground media service +MediaSessionCompat. - tauri-specta generates TypeScript bindings and typed events from the Rust
command/event definitions (registered via the Builder in
src-tauri/src/lib.rs).
Read the architecture docs before making structural changes — they are the canonical, maintained source; this file only summarizes. See docs/architecture/README.md and:
| Doc | Contents |
|---|---|
| 01-rust-backend.md | Player/session state machines, playback mode, queue, commands |
| 02-svelte-frontend.md | Stores, repository architecture, MiniPlayer, autoplay, nav guard |
| 03-data-flow.md | Cache-first query flow, playback initiation, mode transfer |
| 04-type-sync-and-threading.md | Rust↔TS type sync, the IPC camelCase convention + param table, locking |
| 05-platform-backends.md | MpvBackend (Linux), ExoPlayerBackend (Android), MediaSession, HTML5 adapter |
| 06-downloads-and-offline.md | Download manager/worker, smart cache, offline commands |
| 07-connectivity.md | HTTP retry, ConnectivityMonitor, reachability model |
| 08-database-design.md | Tables, relationships, key queries |
| 09-security.md | Token storage, secure storage, network security |
Release process lives in docs/release-checklist.md and docs/build-release.md.
Core principles (from the architecture docs)
- Playback state is one-directional. The player (ExoPlayer on Android, MPV on
Linux, session poller in remote mode) is the authoritative source of state
— position, pause, seeking, rate, track changes. The Svelte UI, OS
MediaSession/lockscreen, and MPRIS are consumers; they reflect what the player reports and never determine it. - Unified player boundary. UI controls playback only through the frontend
facade
src/lib/player/index.ts(playerController) — never by callingcommands.player*directly. Webview HTML5<video>reports its state back into Rust viasrc/lib/player/html5Adapter.tsand theplayer_report_*commands, so the controller stays the single source of truth in both native and HTML5 modes. - Reachability from real traffic. Server online/offline is derived from the
outcome of actual repository requests (reported to
ConnectivityMonitor), not a side-channel poller. The/System/Info/Publicprobe runs only while offline, as a recovery detector. - Poison-tolerant locking. Access shared
std::syncstate via theMutexSafe/RwLockSafehelpers inutils/lock.rs, which recover a poisoned lock instead of cascading a panic across the player. - Graceful backend init. If a native player backend fails to initialize, the
app falls back to a no-op backend and emits
backend-init-failedrather than crashing.
Conventions
Rust Backend
- Use
#[tauri::command]for all IPC handlers. - Prefer
asynccommands for I/O-bound work. - Return
Result<T, String>from commands (the established convention here). - Use
tauri::State<>for shared state. - Group related commands in domain modules under
commands/. - Use official Tauri plugins before writing custom native code.
Frontend
- Use
invoke<T>()from@tauri-apps/api/core, or the tauri-specta bindings. - Define TS types matching the Rust structs; prefer the generated bindings.
- Handle IPC errors with try/catch.
- Use
@tauri-apps/api/pathfor paths (never hardcode). - Use
@tauri-apps/api/eventfor backend→frontend events.
🔴 IPC parameter naming (Tauri v2)
The command name must match the Rust function name exactly
(invoke("player_play_queue", …)). But parameter names do NOT — Tauri v2's
#[tauri::command] macro auto-converts snake_case Rust params to camelCase
on the frontend:
#[tauri::command]
pub async fn cmd(repository_handle: String) { … }
await invoke("cmd", { repositoryHandle: "…" }); // camelCase, auto-converted
Nested struct fields need #[serde(rename_all = "camelCase")]; tagged unions use
#[serde(tag = "type")] and both sides must match the tag. Note: tauri-specta
tagged responses keep the Rust field names as-is (e.g. new_url, not newUrl).
Events
- Backend events use kebab-case names (
download-event,search-event). - Emit from Rust via
emit(...); consume on the frontend via@tauri-apps/api/eventor the tauri-specta typed event bindings.
Security
- Declare minimum permissions in
src-tauri/capabilities/. - Keep the CSP restrictive in
tauri.conf.json. - Validate all inputs in Rust command handlers.
- Never read credentials (tokens/keys from keyring, env, or stores) without asking the user first.
Gotchas (hard-won)
- Never call sync/blocking APIs from event callbacks that can re-enter the
player or hold a lock — it deadlocks. On Android, bind a locked
AutoplayDecisionto aletbefore matching; a tokioMutexGuardheld in thematchscrutinee deadlocks theAdvanceToNextarm. - VideoPlayer native mode: no lifecycle calls after an
awaitinonMount(it flips to HTML5 mode and breaks Android seek). - Transcoded resume/seek:
get_video_stream_urlmust return the HLSmaster.m3u8, notstream.mp4, or transcoded playback never starts. - Downloads cap at 3 concurrent; the backend pump auto-starts pending rows.
Don't loop
startDownloadfrom the frontend. - Parallel Claude sessions: the user may run concurrent sessions. Unexpected
file changes may be another session — check
git diffbefore "repairing".
Testing
# Rust
cd src-tauri && cargo test
cd src-tauri && cargo test test_name # single test
# Frontend
bun run test
bun run test:coverage
# Tauri IPC param-naming integration tests (guard the camelCase rule):
bun run test -- tauriIntegration.test.ts