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.
246 lines
11 KiB
Markdown
246 lines
11 KiB
Markdown
# 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/`:
|
|
|
|
```bash
|
|
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 check` and `bun run test` must pass.
|
|
- Rust: `cd src-tauri && cargo fmt` then `cargo clippy`, plus `bun 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 run `scripts/sync-android-sources.sh` to sync into the `gen/` tree.
|
|
Never edit the generated `gen/` 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.:
|
|
|
|
```rust
|
|
/// TRACES: UR-005 | DR-001
|
|
pub enum PlayerState { … }
|
|
```
|
|
```typescript
|
|
// 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](docs/requirements.md); the generated matrix is
|
|
[docs/traceability.md](docs/traceability.md).
|
|
|
|
Tooling:
|
|
|
|
```bash
|
|
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](docs/traceability-ci.md) and
|
|
[docs/traces-quick-ref.md](docs/traces-quick-ref.md).
|
|
|
|
### Traces drive release notes
|
|
|
|
Prefer traceability over raw commit subjects when writing release notes for
|
|
[docs/release-checklist.md](docs/release-checklist.md). Raw `git log` subjects are
|
|
noisy; the TRACES graph gives a semantic summary of *what capabilities* the
|
|
release touched.
|
|
|
|
```bash
|
|
bun run release:notes # <latest tag>..HEAD
|
|
bun run release:notes v0.0.15..HEAD # explicit range
|
|
```
|
|
|
|
[scripts/release-notes.ts](scripts/release-notes.ts) resolves a commit range's
|
|
changed files → their `TRACES:` IDs → descriptions in
|
|
[docs/requirements.md](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 in `src-tauri/src/commands/` (`auth.rs`, `catalog.rs`, `player/`,
|
|
`download/`, `offline.rs`, `sessions.rs`, …).
|
|
- **Svelte frontend** (`src/`) — presentation only. Stores in
|
|
`src/lib/stores/`, API wrappers in `src/lib/api/`, components in
|
|
`src/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](docs/architecture/README.md) and:
|
|
|
|
| Doc | Contents |
|
|
|-----|----------|
|
|
| [01-rust-backend.md](docs/architecture/01-rust-backend.md) | Player/session state machines, playback mode, queue, commands |
|
|
| [02-svelte-frontend.md](docs/architecture/02-svelte-frontend.md) | Stores, repository architecture, MiniPlayer, autoplay, nav guard |
|
|
| [03-data-flow.md](docs/architecture/03-data-flow.md) | Cache-first query flow, playback initiation, mode transfer |
|
|
| [04-type-sync-and-threading.md](docs/architecture/04-type-sync-and-threading.md) | **Rust↔TS type sync, the IPC camelCase convention + param table, locking** |
|
|
| [05-platform-backends.md](docs/architecture/05-platform-backends.md) | MpvBackend (Linux), ExoPlayerBackend (Android), MediaSession, HTML5 adapter |
|
|
| [06-downloads-and-offline.md](docs/architecture/06-downloads-and-offline.md) | Download manager/worker, smart cache, offline commands |
|
|
| [07-connectivity.md](docs/architecture/07-connectivity.md) | HTTP retry, ConnectivityMonitor, reachability model |
|
|
| [08-database-design.md](docs/architecture/08-database-design.md) | Tables, relationships, key queries |
|
|
| [09-security.md](docs/architecture/09-security.md) | Token storage, secure storage, network security |
|
|
|
|
Release process lives in [docs/release-checklist.md](docs/release-checklist.md)
|
|
and [docs/build-release.md](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 calling
|
|
`commands.player*` directly. Webview HTML5 `<video>` reports its state back
|
|
into Rust via `src/lib/player/html5Adapter.ts` and the `player_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/Public` probe runs *only while
|
|
offline*, as a recovery detector.
|
|
- **Poison-tolerant locking.** Access shared `std::sync` state via the
|
|
`MutexSafe`/`RwLockSafe` helpers in `utils/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-failed` rather than
|
|
crashing.
|
|
|
|
## Conventions
|
|
|
|
### Rust Backend
|
|
|
|
- Use `#[tauri::command]` for all IPC handlers.
|
|
- Prefer `async` commands 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/path` for paths (never hardcode).
|
|
- Use `@tauri-apps/api/event` for 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:
|
|
|
|
```rust
|
|
#[tauri::command]
|
|
pub async fn cmd(repository_handle: String) { … }
|
|
```
|
|
```typescript
|
|
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/event` or 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
|
|
`AutoplayDecision` to a `let` *before* matching; a tokio `MutexGuard` held in
|
|
the `match` scrutinee deadlocks the `AdvanceToNext` arm.
|
|
- **VideoPlayer native mode**: no lifecycle calls after an `await` in `onMount`
|
|
(it flips to HTML5 mode and breaks Android seek).
|
|
- **Transcoded resume/seek**: `get_video_stream_url` must return the HLS
|
|
`master.m3u8`, not `stream.mp4`, or transcoded playback never starts.
|
|
- **Downloads** cap at 3 concurrent; the backend pump auto-starts pending rows.
|
|
Don't loop `startDownload` from the frontend.
|
|
- **Parallel Claude sessions**: the user may run concurrent sessions. Unexpected
|
|
file changes may be another session — check `git diff` before "repairing".
|
|
|
|
## Testing
|
|
|
|
```bash
|
|
# 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
|
|
```
|