Skip to content

Latest commit

Β 

History

History
122 lines (91 loc) Β· 19.1 KB

File metadata and controls

122 lines (91 loc) Β· 19.1 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

WaveFlow is a local music player desktop app built with Tauri 2 + React 19 + TypeScript + Vite and a bun toolchain. Spotify / Apple Music-inspired UI on top of a Rust audio engine.

This file is an index, not a manual. It carries the map plus the one-line form of each invariant. The reasoning, algorithms, schema and flow diagrams live under docs/ β€” that's the source of truth, and new detail belongs there, not here.

Need Read
Why a rule exists / how not to break it docs/architecture/invariants.md
How a feature actually works docs/features/
Crate split, audio topology, DB layout docs/architecture/
Design decisions not yet built docs/rfcs/
Release / packaging procedure docs/RELEASING.md

Development Commands

bun install                  # dependencies

bun run tauri dev            # full desktop app (Vite + Rust)
bun run tauri build          # production bundle

bun run dev                  # Vite dev server only (no Tauri shell), port 1420
bun run typecheck            # tsc --noEmit
bun run lint                 # eslint
bun run build                # tsc + Vite prod build

cargo fmt   --manifest-path src-tauri/Cargo.toml --all -- --check
cargo check --manifest-path src-tauri/Cargo.toml --workspace --all-targets
cargo test  --manifest-path src-tauri/Cargo.toml --workspace

The PR checklist is typecheck / lint / cargo fmt --check / cargo check. cargo fmt is not optional β€” CI runs it as the first step of the Rust job, so an unformatted file fails the whole job before a single test runs, and neither cargo check nor clippy will have warned you.

Architecture

Frontend (src/)

React 19 + TypeScript. Entry: src/main.tsx β†’ src/App.tsx.

  • Contexts (provider tree in App.tsx): ThemeContext, PlayerContext, LibraryContext, PlaylistContext, ProfileContext. PageScrollContext mounts lower (in AppLayout) and exposes the main scrollable area to virtualized tables β€” one page-driven scrollbar.
  • Hooks wrap each context: useTheme, usePlayer, useLibrary, usePlaylist, useProfile, usePageScroll.
  • Tauri wrappers (src/lib/tauri/): one typed invoke() per backend command.
  • Views: HomeView, LibraryView, PlaylistView, AlbumDetailView, ArtistDetailView, LikedView, HistoryView, StatisticsView, WrappedView, SettingsView, …
  • Layout: Apple-Music-style sidebar, TopBar with search, PlayerBar at the bottom, right-edge panels (NowPlayingPanel / QueuePanel / LyricsPanel) mutex'd via PlayerContext. A second WebviewWindow (label mini, ?mini=1) ships the always-on-top mini-player β€” docs/features/ui.md β€” and a third (label lyrics, ?lyrics=1, created by the backend) the transparent desktop lyrics overlay β€” #desktop-lyrics.

Backend (src-tauri/) β€” Cargo workspace, two members

  • crates/core/ (waveflow-core) β€” portable business logic, reusable from waveflow-server. Domain DTOs, repository traits + SQLite and Postgres impls (Cargo features: desktop = sqlite, server = postgres), scanner helpers + upserts, smart-playlist engine, audio analysis, DSDβ†’PCM, HTTP clients (Deezer / Last.fm / LRCLIB / TheAudioDB), artwork pipeline, the wasmtime plugin host. Zero Tauri / cpal. Split rules + feature matrix: docs/architecture/crates.md.
  • crates/app/ (waveflow) β€” the Tauri 2 app. Entry crates/app/src/main.rs β†’ lib.rs. #[tauri::command] handlers (thin wrappers over core's repositories), the real-time cpal + rtrb audio engine, DLNA / MPD / OS media controls / Discord RPC, the fs watcher, the tray, the profile pool wiring.

Inside crates/app/src/:

  • commands/ β€” one module per domain (library, playlist, smart_playlists, track, browse, player, scan, edit, profile, analysis, deezer, similar, lyrics, stats, wrapped, maintenance, radio, duplicates, preferences, plugins, canvas, …), all registered in lib.rs::generate_handler![]. CRUD delegates to waveflow_core::repository::sqlite::*; IPC + state + filesystem + emit glue stays in the command.
  • audio/ β€” 3-thread lock-free engine: decoder.rs (symphonia + rubato), output.rs (cpal callback on its own thread, SPSC rtrb ring), state.rs (SharedPlayback atomics), analytics.rs, crossfade.rs, eq.rs, spectrum.rs, the three exclusive backends (wasapi_exclusive.rs, alsa_exclusive.rs, coreaudio_exclusive.rs). Topology: docs/architecture/audio.md.
  • dlna/ (axum + SSDP, opt-in) Β· mpd/ (TCP MPD protocol, opt-in) Β· media_controls.rs (souvlaki β†’ SMTC / MPRIS / MediaRemote) Β· discord_presence.rs Β· queue.rs Β· player_actions.rs (shared control sequence) Β· remote/ (remote source + sync v2, feature sync_v2, now in the default feature set; remote/mirror.rs walks the server's catalogue into the projection so both sources can be browsed from one library; remote/download.rs keeps a track's bytes in a managed folder the scanner never sees while remote/import.rs copies them into a scanned one, where they become a local track linked to the server's; remote/upload.rs is the fourth direction β€” offering the server what it lacks, deduplicated offline against the mirror and hashed once via remote/hashing.rs; mod sync is now a permanent no-op stub β€” v1 was removed in the RFC-005 cutover) Β· backup.rs Β· db/ (pool wiring + migration_heal + the pre-migration snapshot).
  • Scanner β€” the orchestrator scan_folder_inner stays app-side (it emits scan:progress); every pure helper lives in waveflow_core::scanner::{extract, upserts}.
  • Database β€” per-profile SQLite via sqlx + a global app.db for the profile list and app-wide settings. Migrations at src-tauri/migrations/{app,profile}/, compiled in via sqlx::migrate!. Layout: docs/architecture/storage.md.

Cross-cutting rules (always apply)

One line each. The reasoning, the failure mode and the exceptions are in docs/architecture/invariants.md β€” read the matching section before touching one of these.

  • Tauri commands β€” commands/*.rs + generate_handler![]; frontend camelCase, backend snake_case.
  • Profile-scoped pool β€” state.require_profile_pool().await? for anything touching user data; it's a leased ProfilePool, so query with &*pool, keep the handle bound, and never re-resolve it mid-batch. β†’
  • Settings persistence β€” profile_setting / app_setting via INSERT … ON CONFLICT DO UPDATE; a React preference hook builds on useProfileSetting, never hand-rolled. β†’
  • Events β€” backend emits player:*, track:updated, library:rescanned, scan:progress, lyrics:updated, …; frontend uses listen(). β†’
  • Audio callback is hot β€” no allocation, no locks, no logging; all DSP happens on the decoder thread, whose last stage is a [-1.0, 1.0] clamp. β†’
  • Load commands carry a LoadIntent β€” claimed when the intent starts (before the awaits), not before the send; the decoder drops any load older than one already delivered, and a producer claims the dispatch before its first side effect, publishing under lock_publish. An output rebuild is the one exception: it re-dispatches the decoder's own recorded load, under that load's intent. β†’
  • Anything written into a library folder goes under .waveflow/ β€” mp4 is an audio extension taken on sight, so a clip anywhere else becomes a track; the scanner prunes that branch and the watcher ignores events inside it. β†’
  • Never DROP TABLE a parent table in a migration β€” foreign_keys = ON turns it into a cascading delete. Use ALTER TABLE … ADD COLUMN. β†’
  • Migrations are immutable once merged β€” sqlx checksums them; always add a new dated YYYYMMDDhhmmss_<slug>.sql. β†’
  • Single writer to SQLite β€” batch in transactions, pass &mut SqliteConnection to upsert helpers, and any background writer parks behind the scan + retries on SQLITE_BUSY. β†’
  • Multi-artist split is "; " only β€” never ", "; queries rebuild credits by joining track_artist with GROUP_CONCAT ordered by position. β†’
  • Album grouping = (canonical_title, album_artist_id) β€” is_compilation is sticky and renames reuse the old album's flags. β†’
  • File-write safety on Windows β€” pause playback before rewriting the current track's file, then re-hash blake3 into track.file_hash. β†’
  • Tag writes go through the concrete tag β€” edit::patch_file, never read_from_path + save_to_path; the generic Tag drops every non-standard Vorbis comment. β†’
  • One codec registry β€” playback, analysis and the scanner all open streams through waveflow_core::audio_format::opus::codecs(), never symphonia::default::get_codecs(); a disagreement between them puts an unplayable track in the library. β†’
  • Virtual scroll everywhere β€” @tanstack/react-virtual + usePageScroll(), never a nested overflow-y-auto. β†’
  • Modal accessibility β€” every modal calls useModalA11y; no bespoke Escape handlers. β†’
  • Overlays are portalled, not z-indexed β€” any backdrop-filter / transform ancestor caps the stacking context; z-100+ goes through createPortal(…, document.body). Layer scale is documented at the top of src/app.css. β†’
  • Right panels are flex siblings, not overlays β€” the center column carries min-w-0. β†’
  • Process-wide offline mode β€” every outbound HTTP path checks offline::is_offline() first. β†’
  • Non-frontend control surfaces go through player_actions β€” tray, media keys, taskbar thumbnail buttons and MPD must not re-derive the advance + emit sequence. β†’
  • Long-running work announces itself β€” a scan, a sweep, a prefetch, a mirror walk, a backup takes a TaskHandle from tasks.rs; the registry routes cancellation to the task's own stopping point rather than inventing one. β†’
  • New player-bar action β€” lands in the "β‹―" overflow menu first, promoted only when usage warrants it. β†’
  • Plugins β€” loaded at runtime, distributed from separate repos, blake3-pinned by the registry; published manifest strings use *_i18n siblings; ui plugins return a JSON descriptor and get only redacted library reads; canvas fan-out is fail-soft; a plugin that cannot load is shown as broken, never silently skipped. β†’ Β· full surface

Feature catalogue

Names in commands/, audio/ and src/components/ are predictable β€” read the file. For anything that isn't obvious from the name, these are the deep dives:

Area Doc Covers
Playback playback.md decoder (incl. Opus via libopus) + DSD pipeline, native DSD via DoP, crossfade (static / smart / dynamic), gapless, ReplayGain (track / album / auto), EQ, speed, network pre-load, exclusive output (WASAPI / ALSA / CoreAudio), spectrum visualizer, A-B loop, queue + album shuffle
Library library.md scanner + watcher, folder covers, local artist images, search + filters, tag editor, ratings, duplicates, import, history, multi-artist split
Playlists playlists.md Β· smart-playlists.md CRUD, sorting, auto-covers, M3U, Daily Mix + On Repeat generators, rule tree
Integrations integrations.md Deezer, Last.fm, TheAudioDB, lyrics providers + editor, artist overrides, Discord RPC, OS notifications, scrobbling
Plugins plugins.md WASM host + sandbox, store, options, source / metadata / ui / canvas worlds, Web Radio + offline catalogue
UI & UX ui.md layout, 5 skins Γ— 14 themes, immersive view, Canvas, cover slideshow, artist hero, mini-player, desktop lyrics, Wrapped, profiles, onboarding, settings, updater, backups
LAN servers dlna.md Β· mpd.md opt-in MediaServer and MPD control surface

Conventions

  • Conventional commits, enforced locally by husky commit-msg β†’ bunx commitlint --edit. Config in .commitlintrc.cjs (header ≀ 100, kebab-case scopes). Subject stays lowercase β€” not sentence/start/pascal/upper case.
  • PR labels are automatic (.github/workflows/label-pr.yml): scope:* by path, type:* from the title prefix, size:* from the diff.
  • Never hand-tag a release. release-please owns version bumps across package.json (canonical), src-tauri/crates/app/tauri.conf.json, src-tauri/Cargo.toml + Cargo.lock. Tag push drives bundles, the signed updater manifest and the downstream AUR / winget / copr / apt dispatches. Beta channel + full procedure: docs/RELEASING.md.
  • The macOS bundle must stay codesigned β€” a linker-only ad-hoc binary has no sealed bundle and no stable identity, so macOS re-asks for folder access on every launch. Signing runs inside tauri build (never as a post-build pass) and release.yml verifies it. β†’
  • Flatpak sources are generated and go stale silently β€” a lockfile bump without regenerating packaging/flatpak/generated/ fails inside Flathub's offline sandbox, not in CI. check-sources.py guards coverage per PR; note the bun.lock vs npm-lockfile split when pinning a dependency. β†’
  • Issue + PR templates live under .github/.

Language

UI copy ships in 17 locales via i18next β€” fr (source of truth), en, es, de, it, nl, pt, pt-BR, ru, tr, id, ja, ko, zh-CN, zh-TW, ar, hi. Strings in src/i18n/locales/<code>.json; index.ts sets document.documentElement.dir so Arabic renders RTL. The legacy kr code stays accepted as an alias for ko (a startup migration rewrites stored preferences). The README is in English.

fallbackLng: "en" is set, but the convention is that every locale carries every key β€” no language-mixing in the UI. When you add a key, propagate it to all 17 files (a small Python script with json.load/dump + ensure_ascii=False, indent=2 preserves the formatting).

Keep verbatim across locales: brand tokens (WaveFlow, Last.fm, Deezer, ReplayGain, LRCLIB, BPM), smart-playlist family names (Daily Mix, On Repeat β€” Spotify and Apple Music keep theirs untranslated in every market, and translating ours would split the user's mental model), and i18next {{placeholder}} tokens.