Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 65 additions & 8 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,74 @@ reviews:
d'arguments correspondent exactement aux commandes Rust, que les types
frontend/backend restent alignés, et que les erreurs importantes ne
sont pas masquées.
- path: "src-tauri/src/audio/**"
- path: "src-tauri/crates/app/src/audio/**"
instructions: >-
Moteur audio temps réel. Dans les callbacks audio, signale toute
allocation, lock, I/O, log ou opération bloquante. Vérifie aussi les
channels/rates, la gestion des threads, des atomics, des buffers SPSC,
et les cas de fallback de périphérique audio.
- path: "src-tauri/src/commands/**"
Moteur audio temps réel. Dans les callbacks audio (cpal + WASAPI
exclusive), signale toute allocation, lock, I/O, log ou opération
bloquante. Vérifie aussi les channels/rates, la gestion des threads,
des atomics, des buffers SPSC, et les cas de fallback de périphérique
audio.
- path: "src-tauri/crates/app/src/commands/**"
instructions: >-
Commandes Tauri exposées au frontend. Vérifie les contrôles de profil
actif, les accès SQLx, les erreurs retournées à l'UI, les chemins
fichiers, et les changements qui peuvent bloquer le runtime async.
actif (require_profile_pool/require_profile_id), les accès SQLx, les
erreurs retournées à l'UI, les chemins fichiers, et les changements qui
peuvent bloquer le runtime async. La logique métier portable doit vivre
dans `crates/core/` — signale toute duplication entre une commande et
un repository/helper core qui pourrait être déléguée.
- path: "src-tauri/crates/app/**"
instructions: >-
Code Tauri-spécifique (binaire `waveflow`). Aucune restriction sur
`tauri::*`, `cpal`, `souvlaki`, `discord-rich-presence`. Vérifie que la
logique réutilisable côté serveur reste poussée vers `crates/core/`
plutôt que dupliquée ici.
- path: "src-tauri/crates/core/**"
instructions: >-
Crate portable `waveflow-core` (logique métier réutilisable par le
futur `waveflow-server`). Refuse toute dépendance directe à `tauri`,
`cpal`, `souvlaki`, `tauri-plugin-*`. Le code SQLite doit rester
derrière la feature `sqlite`. Privilégie les traits + impl SQLite pour
toute nouvelle CRUD plutôt que des fonctions concrètes liées à
`sqlx::SqlitePool`.
- path: "src-tauri/crates/core/src/repository/**"
instructions: >-
Traits de repository (`ProfileRepository`, `LibraryRepository`,
`PlaylistRepository`, `TrackRepository`) et leurs implémentations
SQLite. Le trait doit rester storage-agnostique (pas de `sqlx::*` dans
la signature). Les impls dans `sqlite/` doivent prendre `&mut
SqliteConnection` pour les helpers d'upsert afin de participer aux
transactions ouvertes (single-writer SQLite). Toute lecture suivie
d'une écriture sur la même clé doit vivre dans une transaction unique
pour éviter les races.
- path: "src-tauri/crates/core/src/scanner/**"
instructions: >-
Helpers purs du scanner (`extract.rs` + `upserts.rs`). Pas de Tauri,
pas d'`AppHandle`, pas d'émission d'events. L'orchestrateur
(`scan_folder_inner`) reste dans `crates/app/src/commands/scan.rs`
parce qu'il émet `scan:progress`. Toute fonction qui aurait besoin
d'émettre un event doit passer par un trait `ScannerEventSink` à
définir, pas un appel direct à `app.emit(...)`.
- path: "src-tauri/crates/core/src/smart_playlists/**"
instructions: >-
Moteur de smart playlists (Daily Mix, On Repeat, custom rules, cover
composer). Doit consommer `PathsContext` (pas `AppPaths`) et rester
portable côté serveur. Vérifie que les changements de schéma sur
`playlist.smart_rules` restent rétro-compatibles (la migration v1 →
v2 est en place).
- path: "src-tauri/crates/core/src/metadata/**"
instructions: >-
Clients HTTP tiers (Deezer / Last.fm / LRCLIB). Pure `reqwest` over
rustls, aucune dépendance Tauri. Le respect du mode offline est géré
par les appelants côté `crates/app/` via `crate::offline::is_offline()`
— signale toute regression où un nouveau chemin réseau ne court-circuite
pas en mode offline.
- path: "src-tauri/crates/core/src/audio_format/**"
instructions: >-
Parseurs DSD (DSF / DFF) + convertisseur 1-bit → PCM 24-bit. Code
portable (utilisé par le scanner et par `crates/app/src/audio/crossfade.rs`
au runtime). Pas de Tauri, pas de cpal. Les ratios de décimation et
les coefficients FIR doivent rester documentés pour qu'un futur
backend serveur puisse les reprendre tels quels.
- path: "src-tauri/migrations/**"
instructions: >-
Migrations SQLite app/profile. Les migrations existantes ne doivent
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,10 @@ jobs:
workspaces: src-tauri

- name: cargo check
run: cargo check --manifest-path src-tauri/Cargo.toml --all-targets
run: cargo check --manifest-path src-tauri/Cargo.toml --workspace --all-targets

- name: cargo test
run: cargo test --manifest-path src-tauri/Cargo.toml
run: cargo test --manifest-path src-tauri/Cargo.toml --workspace

frontend:
name: Frontend
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release-please-lockfile-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ jobs:
pkg-config

- name: Refresh Cargo.lock against bumped Cargo.toml
run: cargo check --manifest-path src-tauri/Cargo.toml --all-targets
run: cargo check --manifest-path src-tauri/Cargo.toml --workspace --all-targets

- name: Stage artifact (Cargo.lock + PR metadata)
# Bundle the refreshed Cargo.lock alongside the PR number,
Expand Down
10 changes: 5 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ target
**/*.rs.bk
*.pdb

# Generated by src-tauri/build.rs based on the `updater` Cargo feature.
# Lives in capabilities/ because tauri-build auto-discovers from there;
# the file is rewritten or removed on every build to match the feature
# flag, so it must never be committed.
src-tauri/capabilities/updater.json
# Generated by build.rs based on the `updater` Cargo feature.
# Lives in `crates/app/capabilities/` because tauri-build auto-discovers
# from there; the file is rewritten or removed on every build to match
# the feature flag, so it must never be committed.
src-tauri/crates/app/capabilities/updater.json

# TypeScript / bundler caches
*.tsbuildinfo
Expand Down
36 changes: 20 additions & 16 deletions CLAUDE.md

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions docs/architecture/audio.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

## Shared state

[`SharedPlayback`](../../src-tauri/src/audio/state.rs) — an `Arc<...>` of atomics plus the rtrb consumer half. Read on the hot path; mutated by the decoder and the command layer. No locks anywhere in the pipeline.
[`SharedPlayback`](../../src-tauri/crates/app/src/audio/state.rs) — an `Arc<...>` of atomics plus the rtrb consumer half. Read on the hot path; mutated by the decoder and the command layer. No locks anywhere in the pipeline.

| Atomic | Owner writes | Hot-path reads |
| --------------------------------------------- | -------------------------------------------- | -------------------------- |
Expand All @@ -37,11 +37,11 @@
| `playback_speed_bits`, `speed_dirty` | command layer / decoder | decoder + UI position math |
| `current_track_id`, `seek_generation` | decoder | UI |

`playback_speed_bits` is read on every position computation (UI 4 Hz + analytics) — see [`current_position_ms`](../../src-tauri/src/audio/state.rs) and [playback / Playback speed](../features/playback.md#playback-speed-05--2). `speed_dirty` is a one-shot flag the decoder consumes once per `'pkt` loop iteration to trigger a resampler rebuild.
`playback_speed_bits` is read on every position computation (UI 4 Hz + analytics) — see [`current_position_ms`](../../src-tauri/crates/app/src/audio/state.rs) and [playback / Playback speed](../features/playback.md#playback-speed-05--2). `speed_dirty` is a one-shot flag the decoder consumes once per `'pkt` loop iteration to trigger a resampler rebuild.

## WASAPI Exclusive Mode (Windows opt-in)

[`audio/wasapi_exclusive.rs`](../../src-tauri/src/audio/wasapi_exclusive.rs) is a parallel output backend to the cpal shared-mode default. Engaged via the `audio.wasapi_exclusive` profile setting (toggle in Settings → Audio). When on:
[`audio/wasapi_exclusive.rs`](../../src-tauri/crates/app/src/audio/wasapi_exclusive.rs) is a parallel output backend to the cpal shared-mode default. Engaged via the `audio.wasapi_exclusive` profile setting (toggle in Settings → Audio). When on:

1. `output::spawn_output_with_mode` tries the exclusive backend first via the [`wasapi` crate](https://crates.io/crates/wasapi).
2. The backend opens the device in **event-driven exclusive mode** at the device's mix-format sample rate (whatever the user picked in the Windows Sound control panel). 32-bit float stereo, anchored on `KSDATAFORMAT_SUBTYPE_IEEE_FLOAT`.
Expand Down
111 changes: 111 additions & 0 deletions docs/architecture/crates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Crate layout

`src-tauri/` is a Cargo workspace with two members:

```text
src-tauri/
├── Cargo.toml (virtual workspace root)
├── Cargo.lock
├── migrations/ (sqlx schema, per-profile + app-level)
├── vendor/ (glib 0.18.5 RUSTSEC backport)
└── crates/
├── core/ (waveflow-core — portable business logic)
│ └── src/
│ ├── analysis.rs (peak / loudness / ReplayGain / BPM)
│ ├── artwork/ (thumbnail pipeline + shared metadata cache)
│ ├── audio_format/dsd/ (DSF + DFF parsing, 1-bit → 24-bit PCM)
│ ├── domain/ (Track, Album, Artist, Playlist, Profile, Library DTOs)
│ ├── error.rs (CoreError + CoreResult)
│ ├── metadata/ (Deezer / Last.fm / LRCLIB HTTP clients)
│ ├── repository/ (storage traits + sqlite implementations)
│ │ ├── library.rs / playlist.rs / profile.rs / track.rs
│ │ └── sqlite/ (Sqlite* impls of each trait)
│ ├── scanner/ (file extract + upsert helpers, no orchestration)
│ └── smart_playlists/ (Daily Mix, On Repeat, custom rule eval, cover composer)
└── app/ (waveflow — Tauri 2 application)
├── Cargo.toml (produces the `waveflow` binary)
├── tauri.conf.json
├── capabilities/ icons/ build.rs
└── src/
├── audio/ (real-time cpal + rtrb pipeline, EQ, WASAPI exclusive)
├── commands/ (#[tauri::command] handlers, thin over core)
├── db/ (per-profile pool wiring + migration_heal)
├── dlna/ (MediaServer worker thread)
├── discord_presence.rs (Rich Presence named-pipe client)
├── media_controls.rs (souvlaki bridge → SMTC / MPRIS)
├── notifications.rs (Tauri notification plugin bridge)
├── paths.rs (AppPaths via tauri::AppHandle + dirs)
├── scrobbler.rs (Last.fm scrobble worker)
├── state.rs (AppState — held by Tauri)
└── watcher.rs (notify-driven fs watch)
```

The full workspace builds with `cargo check --workspace --all-targets --manifest-path src-tauri/Cargo.toml`. CI runs the same command (`.github/workflows/ci.yml`).

## What goes in `waveflow-core`

Anything that could run inside an axum handler in the future `waveflow-server` (RFC-001 §6.2) without dragging Tauri or `cpal` along. Specifically:

- **Domain types** — the DTOs the UI sees. Plain `serde::Serialize`/`Deserialize` plus an opt-in `sqlx::FromRow` derive (via the `sqlite` feature) so the same struct backs `query_as` calls without a parallel row type.
- **Repository traits** — `ProfileRepository`, `LibraryRepository`, `PlaylistRepository`, `TrackRepository`. Each lives in `repository/<entity>.rs` with a SQLite implementation under `repository/sqlite/`. A future `repository/postgres/` will be the server's counterpart.
- **HTTP clients** for the third-party metadata sources we enrich the local library with (`metadata/{deezer,lastfm,lrclib}.rs`). Pure `reqwest` over rustls; no Tauri.
- **Scanner helpers** — every pure function the file-walker calls per track (`scanner/extract.rs` for the lofty / blake3 / cover-extraction side, `scanner/upserts.rs` for the SQL writes and the album-grouping policy). The Tauri-aware orchestrator (`scan_folder_inner` + the `scan:progress` emit) stays in `app/commands/scan.rs` until a future `ScannerEventSink` decoupling.
- **Smart-playlist engine** — Daily Mix generator, On Repeat regen, custom rule evaluator, cover composer. The `PathsContext` struct decouples the engine from `AppPaths`; the app constructs one from its `state.paths`.
- **Audio analysis** — per-track peak / loudness / ReplayGain / BPM (`analysis.rs`). Symphonia-based, no `cpal`.
- **Audio format conversion** — DSD parser + decimating FIR (`audio_format/dsd/*`). The real-time playback pipeline still uses this from `app/src/audio/crossfade.rs`.
- **Artwork pipeline** — the shared blake3-keyed metadata cache + the SIMD thumbnail variants (`artwork/{metadata,thumbnails}.rs`).
- **Error type** — `CoreError`. The desktop's `AppError` wraps it via `#[from] CoreError` so `?` flattens automatically across the boundary.

## What stays in `waveflow` (`crates/app/`)

Anything tied to the Tauri runtime, the real-time audio engine, or the desktop OS:

- **Every `#[tauri::command]`** — even when the body is a thin call into a core function. The IPC bridge contract is desktop-specific.
- **Real-time audio engine** — `audio/{decoder,output,engine,crossfade,eq,resampler,spectrum,state,wasapi_exclusive,analytics}.rs`. The `cpal` callback and the WASAPI exclusive thread must not allocate / log / lock; the surrounding decoder + state machinery only makes sense alongside them.
- **OS media controls** — souvlaki (`media_controls.rs`), Discord Rich Presence named-pipe client (`discord_presence.rs`), system notification plugin bridge (`notifications.rs`).
- **DLNA / UPnP MediaServer** — `dlna/` is integrated as a worker thread driven by the Tauri runtime.
- **Filesystem watcher** — `watcher.rs` wires `notify` events into `library:rescanned` Tauri events.
- **DB pool wiring** — `db/{app_db,profile_db,migration_heal}.rs`. Migrations themselves live at `src-tauri/migrations/` and are compiled in by `sqlx::migrate!(...)` from app; moving migrations into core is a later cleanup once nothing app-side needs to point at them with a relative path.
- **Paths** — `paths.rs::AppPaths` derives the on-disk layout from `tauri::AppHandle` + `dirs::data_dir()`. The server will have its own path resolver.
- **Tray, single-instance, updater, mini-player WebviewWindow** — all platform-specific Tauri wiring.

## Re-export shims

Several files in `crates/app/src/` are one-line re-exports of code that moved to core, kept in place so existing `crate::*` imports across the app keep resolving without churn:

- `app/src/thumbnails.rs` → `waveflow_core::artwork::thumbnails`
- `app/src/metadata_artwork.rs` → `waveflow_core::artwork::metadata`
- `app/src/analysis.rs` → `waveflow_core::analysis`
- `app/src/smart_playlists.rs` → `waveflow_core::smart_playlists::*` (submodules re-exported individually so the `cover` / `custom` / `generator` / `on_repeat` paths still work)

Likewise the domain types in `commands/{track,playlist,library,profile}.rs` and `commands/scan.rs::canonical_name` stay reachable through `pub use waveflow_core::...` lines at the top of their original files.

These shims are cosmetic — they keep the diff small while the migration lands. Future cleanup PRs can collapse them by walking the call sites.

## When to split `waveflow-core` into its own repo

Not now. RFC-001 §5 plans for `waveflow-core` to live in this repo until its public API stabilises (estimated 2-3 months of `waveflow-server` consumption). At that point it moves out in a single `git filter-repo` pass and starts publishing to crates.io; both `waveflow` and `waveflow-server` switch from `path = "../core"` to a git or crates.io dependency.

Until then: anything `waveflow-server` would need lives at `waveflow_core::*` already.

## Feature flags on `waveflow-core`

| Flag | What it enables |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `sqlite` | `sqlx`'s `sqlite` + `runtime-tokio` features (so `repository::sqlite::*` compiles) and the `sqlx::FromRow` derives on the domain types (via `cfg_attr`). |

The desktop crate opts in with `features = ["sqlite"]`. The future server will add a `postgres` feature with the same shape (`sqlx/postgres`, FromRow derives gated on it). Trait definitions in `repository/` itself stay storage-agnostic — only their implementations live behind a feature flag.

## Tauri config + bundle paths

The `tauri-build` crate reads `tauri.conf.json` from the directory containing the building `Cargo.toml`, so after the workspace split it had to move next to `crates/app/Cargo.toml`:

```text
src-tauri/crates/app/
├── tauri.conf.json (frontendDist + licenseFile up two extra levels)
├── build.rs (changelog generator + capabilities writer)
├── capabilities/ (default.json + generated updater.json)
└── icons/
```

The Tauri CLI looks for `tauri.conf.json` via Cargo discovery, which can't see the file from the project root in this layout. A small wrapper at [`scripts/tauri.mjs`](../../scripts/tauri.mjs) injects `--config src-tauri/crates/app/tauri.conf.json` for the three subcommands that load it (`dev` / `build` / `bundle`) and passes everything else through unchanged; `package.json::scripts.tauri` points at it. Contributors keep typing `bun run tauri build`.
2 changes: 1 addition & 1 deletion docs/architecture/storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
- macOS: `~/Library/Application Support/app.waveflow/waveflow/`
- Linux: `~/.local/share/app.waveflow/waveflow/`

The inner `waveflow/` segment is a hardcoded subdirectory in [`paths.rs`](../../src-tauri/src/paths.rs). Don't rename it — existing user libraries point at it. The product display name is `WaveFlow` ([`tauri.conf.json`](../../src-tauri/tauri.conf.json)) but the path stays lowercase for backwards compatibility.
The inner `waveflow/` segment is a hardcoded subdirectory in [`paths.rs`](../../src-tauri/crates/app/src/paths.rs). Don't rename it — existing user libraries point at it. The product display name is `WaveFlow` ([`tauri.conf.json`](../../src-tauri/tauri.conf.json)) but the path stays lowercase for backwards compatibility.

## Two databases

Expand Down
Loading
Loading