Skip to content
Merged
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
18 changes: 9 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,15 +98,15 @@ One line each. **The reasoning, the failure mode and the exceptions are in [`doc

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`](docs/features/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, output test tone, files played without importing them |
| Library | [`library.md`](docs/features/library.md) | scanner + watcher, folder covers, local artist images, search + filters, tag editor, ratings, duplicates, import, history, multi-artist split |
| Playlists | [`playlists.md`](docs/features/playlists.md) · [`smart-playlists.md`](docs/features/smart-playlists.md) | CRUD, sorting, auto-covers, M3U, Daily Mix + On Repeat generators, rule tree |
| Integrations | [`integrations.md`](docs/features/integrations.md) | Deezer, Last.fm, TheAudioDB, lyrics providers + editor, artist overrides, Discord RPC, OS notifications, scrobbling |
| Plugins | [`plugins.md`](docs/features/plugins.md) | WASM host + sandbox, store, options, `source` / `metadata` / `ui` / `canvas` worlds, Web Radio + offline catalogue |
| UI & UX | [`ui.md`](docs/features/ui.md) | layout, 6 skins × 14 themes + custom / cover accent, suggestion of the day on Home, immersive view, Canvas, cover slideshow, artist hero, mini-player, desktop lyrics, Wrapped, profiles, onboarding, settings, updater, backups |
| LAN servers | [`dlna.md`](docs/features/dlna.md) · [`mpd.md`](docs/features/mpd.md) | opt-in MediaServer and MPD control surface |
| Area | Doc | Covers |
| ------------ | ------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Playback | [`playback.md`](docs/features/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, output test tone, files played without importing them or opened from the system |
| Library | [`library.md`](docs/features/library.md) | scanner + watcher, folder covers, local artist images, search + filters, tag editor, ratings, duplicates, import, history, multi-artist split |
| Playlists | [`playlists.md`](docs/features/playlists.md) · [`smart-playlists.md`](docs/features/smart-playlists.md) | CRUD, sorting, auto-covers, M3U, Daily Mix + On Repeat generators, rule tree |
| Integrations | [`integrations.md`](docs/features/integrations.md) | Deezer, Last.fm, TheAudioDB, lyrics providers + editor, artist overrides, Discord RPC, OS notifications, scrobbling |
| Plugins | [`plugins.md`](docs/features/plugins.md) | WASM host + sandbox, store, options, `source` / `metadata` / `ui` / `canvas` worlds, Web Radio + offline catalogue |
| UI & UX | [`ui.md`](docs/features/ui.md) | layout, 6 skins × 14 themes + custom / cover accent, suggestion of the day on Home, immersive view, Canvas, cover slideshow, artist hero, mini-player, desktop lyrics, Wrapped, profiles, onboarding, settings, updater, backups |
| LAN servers | [`dlna.md`](docs/features/dlna.md) · [`mpd.md`](docs/features/mpd.md) | opt-in MediaServer and MPD control surface |

## Conventions

Expand Down
8 changes: 6 additions & 2 deletions docs/features/playback.md
Original file line number Diff line number Diff line change
Expand Up @@ -469,8 +469,12 @@ A file can play without becoming a library track: opened from the system (a doub
- **Otherwise nothing is written.** The files' queue lives in memory on the same `DetachedSession` the remote queue uses ([`remote_playback`](../../src-tauri/crates/app/src/remote_playback.rs)), so it takes over next / previous, media keys and the end of a track while active, under the same ordering rules (#622): nothing is installed for a superseded intent, and an install the user overtook while it started is undone only if still ours. Starting one clears the other; a library track, a radio stream or a profile switch clears both. The sleep timer's "end of this track" stops either queue at the end of the track, as it stops the library one; until now a remote queue played on past it. Each file plays under a fresh negative id, through `AudioCmd::LoadRemoteFileAndPlay` with `DetachedOrigin::File`, so it records no `play_event`, no statistics and no scrobble — the decision for a file played in passing, like radio.
- **Its tags are read when it plays**, not when it is queued (a folder of a thousand files reads one): title (else the file name), artist, length, ReplayGain, and the cover — embedded, else the folder's — written under its hash into the metadata artwork cache, where the asset protocol can serve it. DSD reads its own container; a file whose tags cannot be read still plays, its length from the decoder's probe.
- **On screen** it is told apart by `isFileTrack`: a negative id with a `file_path` (`player:radio-metadata` carries `is_file` and the path; radio and remote tracks leave the path empty). It has no library affordance (`isStreamTrack` covers it: no ♥, no start radio, no slideshow, no file menu), keeps Previous / Next / Repeat like a remote track, no Shuffle, and its queue shows in the remote queue's view (`RemoteQueueView source="file"`, in the panel and the mini-player). Its lyrics are looked up by artist and title and render synced.
- **From the app.** A drop on the window offers two targets, **Play now** and **Add to the library** (see [Drag-and-drop import](library.md#drag-and-drop-import)); the "Play without importing" button in the sidebar's *My music* header opens a menu with **Play files…** and **Play a folder…**, also bound to `Ctrl+O` and `Ctrl+Shift+O`. All of them end in [`playWithoutImporting`](../../src/lib/openToPlay.ts), which hands the paths to `player_play_paths`; the file picker offers the extensions the scanner takes. A failure, from the picker or from the command (a folder with no audio file in it), goes through the playback alert as `files-failed` / `picker-failed`, emitted as a `player:error` from the frontend, since none of these paths has anything else on screen to show it.
- **The command line.** [`launch_args`](../../src-tauri/crates/app/src/launch_args.rs) reads the files a launch was opened with — the first launch from its own arguments once the profile is open, a later one from what the single-instance plugin hands the running instance, relative paths taken from its working directory. Options are skipped; the Flatpak's `%U` arrives as `file://` URIs and is converted.
- **From the app.** A drop on the window offers two targets, **Play now** and **Add to the library** (see [Drag-and-drop import](library.md#drag-and-drop-import)); the "Play without importing" button in the sidebar's _My music_ header opens a menu with **Play files…** and **Play a folder…**, also bound to `Ctrl+O` and `Ctrl+Shift+O`. All of them end in [`playWithoutImporting`](../../src/lib/openToPlay.ts), which hands the paths to `player_play_paths`; the file picker offers the extensions the scanner takes. A failure, from the picker or from the command (a folder with no audio file in it), goes through the playback alert as `files-failed` / `picker-failed`, emitted as a `player:error` from the frontend, since none of these paths has anything else on screen to show it.
- **The command line.** [`launch_args`](../../src-tauri/crates/app/src/launch_args.rs) reads the files a launch was opened with — the first launch from its own arguments once the profile is open, a later one from what the single-instance plugin hands the running instance, relative paths taken from its working directory. Options are skipped; the Flatpak's `%U` arrives as `file://` URIs and is converted. macOS passes no arguments: it sends `RunEvent::Opened` with file URLs, at launch as well as later, read by `paths_from_urls`. **Files that arrive together play together**: Windows starts the program once per file when several are opened at once, so each reaches the running window on its own, a moment after the last, and played as they came each would replace the queue the one before had started. `play_when_ready` gathers them until none has arrived for 400 ms (at most 3 s once the player is up; the wait for the player at a cold start gathers too), each file once, then plays them as one queue: in the order given when one launch brought them all, in path order when several did, since those reach the window in no reliable order (installed and measured: three files opened together arrived third, second, first). The open's load intent (#622) is claimed as soon as the engine exists, not when the files finally play: a track the user picks during the wait is the newer choice, and the files give way to it. On macOS the window is brought forward when files are opened, as the single-instance callback does elsewhere, except while the splash still stands at a launch, whose handoff reveals it once painted.
- **Opened from the system.** The installers offer WaveFlow for the formats it plays (mp3, flac, wav, aiff/aif, ogg/oga, opus, m4a, aac, dsf, dff) and make it the default for none: the default player stays the user's choice. `.mp4` is left out, since it is a video type first.
- **macOS**: `bundle.fileAssociations` in [`tauri.macos.conf.json`](../../src-tauri/crates/app/tauri.macos.conf.json), one per format with its MIME type, role `Viewer` and rank `Alternate` (`LSHandlerRank`): listed under _Open With_, never picked over the owner. No `exportedType`, so WaveFlow declares none of these types as its own. **They are declared for macOS only, on purpose**: on Windows Tauri's associations write each extension's default class (NSIS `APP_ASSOCIATE`, advertised `ProgId`s in the MSI), making the installer the default player for every format. A `tauri.windows.conf.json` setting them to `null` cannot remove them: the CLI merges the base config, then the platform file, then the `--config` overlay — and [`scripts/tauri.mjs`](../../scripts/tauri.mjs) passes the whole `tauri.conf.json` as that overlay, which puts them back.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- **Linux** (.deb, .rpm and the AppImage, so the AUR and COPR packages too): a desktop template of our own ([`linux/waveflow.desktop`](../../src-tauri/crates/app/linux/waveflow.desktop)) lists the same formats as `MimeType`, with the legacy aliases (`audio/x-flac`, ...). Tauri's default template has `Exec=waveflow` with no field code, so "Open with WaveFlow" started an empty window; ours passes `%F`. The Flatpak's own entry lists the same types and launches with `%U`.
- **Windows**: NSIS hooks ([`windows/open-with.nsh`](../../src-tauri/crates/app/windows/open-with.nsh)) and a WiX fragment for the MSI ([`windows/open-with.wxs`](../../src-tauri/crates/app/windows/open-with.wxs)) write instead the `WaveFlow.Audio` ProgID, `.ext\OpenWithProgids` (the _Open with_ entry), `Applications\<exe>\SupportedTypes` (no entry for other types), and `Capabilities` + `RegisteredApplications` (WaveFlow listed in _Settings > Default apps_, where the user can choose it). NSIS writes under the hive its install mode picked and tells Explorer (`SHChangeNotify`); the MSI installs per machine. Uninstalling removes only WaveFlow's own values: the extension keys are shared with the other players, and an update leaves them in place, as it leaves the shortcuts.

## A-B loop

Expand Down
2 changes: 1 addition & 1 deletion packaging/copr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ adds for a Linux-side respin. So instead the spec:
- in `%install`, runs `rpm2cpio | cpio -idmv` to extract the binary
blob directly into the buildroot.

This is the same pattern used by Spotify, Discord, and other binary
This is the usual pattern for binary-only
Linux apps that ship to COPR. The Fedora project itself wouldn't
accept this in `Fedora/`, but COPR is for user-hosted repos and
explicitly allows it.
Expand Down
6 changes: 3 additions & 3 deletions packaging/flatpak/app.waveflow.WaveFlow.desktop
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ Version=1.0
Name=WaveFlow
GenericName=Music Player
GenericName[fr]=Lecteur de musique
Comment=Local-first music player with a Spotify-inspired UI
Comment[fr]=Lecteur de musique local avec une interface inspirée de Spotify
Comment=Local-first music player with Hi-Res audio and synced lyrics
Comment[fr]=Lecteur de musique local, audio haute résolution et paroles synchronisées
Exec=waveflow %U
Icon=app.waveflow.WaveFlow
Terminal=false
Expand All @@ -14,5 +14,5 @@ StartupWMClass=waveflow
Categories=AudioVideo;Audio;Player;Music;
Keywords=music;player;audio;flac;mp3;dsd;library;playlist;lyrics;scrobble;equalizer;
Keywords[fr]=musique;lecteur;audio;flac;mp3;dsd;bibliothèque;playlist;paroles;scrobble;égaliseur;
MimeType=audio/flac;audio/mpeg;audio/mp4;audio/ogg;audio/opus;audio/wav;audio/x-wav;audio/x-wavpack;audio/x-aiff;audio/x-dsd;audio/x-dsf;audio/x-dff;audio/x-musepack;audio/aac;audio/aiff;application/vnd.apple.mpegurl;application/x-mpegurl;audio/x-mpegurl;audio/x-scpls;
MimeType=audio/mpeg;audio/flac;audio/x-flac;audio/wav;audio/x-wav;audio/aiff;audio/x-aiff;audio/ogg;audio/x-vorbis+ogg;audio/opus;audio/x-opus+ogg;audio/mp4;audio/x-m4a;audio/aac;audio/x-dsf;audio/x-dff;
SingleMainWindow=true
15 changes: 8 additions & 7 deletions packaging/flatpak/app.waveflow.WaveFlow.metainfo.xml
Original file line number Diff line number Diff line change
Expand Up @@ -96,16 +96,17 @@

<provides>
<binary>waveflow</binary>
<mediatype>audio/flac</mediatype>
<mediatype>audio/mpeg</mediatype>
<mediatype>audio/mp4</mediatype>
<mediatype>audio/ogg</mediatype>
<mediatype>audio/opus</mediatype>
<mediatype>audio/flac</mediatype>
<mediatype>audio/wav</mediatype>
<mediatype>audio/x-wavpack</mediatype>
<mediatype>audio/aiff</mediatype>
<mediatype>audio/x-aiff</mediatype>
<mediatype>audio/x-dsd</mediatype>
<mediatype>audio/x-musepack</mediatype>
<mediatype>audio/ogg</mediatype>
<mediatype>audio/opus</mediatype>
<mediatype>audio/mp4</mediatype>
<mediatype>audio/aac</mediatype>
<mediatype>audio/x-dsf</mediatype>
<mediatype>audio/x-dff</mediatype>
</provides>

<recommends>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ PackageUrl: https://waveflow.app
License: GPL-3.0-only
LicenseUrl: https://github.com/InstaZDLL/WaveFlow/blob/main/LICENSE
Copyright: Copyright © 2026 InstaZDLL. Licensed under GPL-3.0-only.
ShortDescription: Local-first music player with a Spotify-inspired UI.
ShortDescription: Local-first music player with Hi-Res audio and synced lyrics.
Description: |-
WaveFlow is a local music player desktop app built with Tauri 2 and React 19. It scans your local audio folders, organizes tracks by album/artist/genre, and plays them with a real-time audio engine — no streaming, no cloud, your music stays on your machine.

Expand Down
Loading
Loading