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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ A-B repeat · crossfade (static / smart-album-aware / dynamic-tempo-aware) · ga

### Library ([`docs/features/library.md`](docs/features/library.md))

Scanner with parallel BLAKE3 extraction + transactional commit + fs-watcher silent rescan + `scan:progress` toast · folder-cover fallback (cover/folder/front/albumart…) · advanced search (FTS5 + structured filters: genre, year, BPM, duration, format, Hi-Res, liked) · tag editor round-trips through lofty + DB · track ratings (POPM round-trip, half-step UI) · duplicate detection (BLAKE3 grouping) · folder removal + drag-and-drop import · listening history (`HistoryView` with month scrubber) · album grouping with sticky compilation flag (see cross-cutting rules above).
Scanner with parallel BLAKE3 extraction + transactional commit + fs-watcher silent rescan + `scan:progress` toast · folder-cover fallback (cover/folder/front/albumart…) · local artist images (`artist.jpg` or `<artist_name>.jpg` resolved up to 3 parent dirs, prioritised over Deezer; `rescan_local_artist_images` backfills existing libraries) · advanced search (FTS5 + structured filters: genre, year, BPM, duration, format, Hi-Res, liked) · tag editor round-trips through lofty + DB · track ratings (POPM round-trip, half-step UI) · duplicate detection (BLAKE3 grouping) · folder removal + drag-and-drop import · listening history (`HistoryView` with month scrubber) · album grouping with sticky compilation flag (see cross-cutting rules above).

### UI ([`docs/features/ui.md`](docs/features/ui.md))

Expand Down
32 changes: 16 additions & 16 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 32 additions & 0 deletions docs/features/library.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,35 @@ UI: [`DuplicatesModal`](../../src/components/common/DuplicatesModal.tsx) launche
## Cover picker

[`commands/deezer.rs::set_album_artwork_from_deezer`](../../src-tauri/src/commands/deezer.rs) and `set_album_artwork_from_file`. The file picker validates magic bytes (JPEG / PNG / WebP) before accepting upload, and `batch_fetch_missing_album_covers` walks all albums without an `artwork_id`, querying Deezer in parallel with a small concurrency cap.

## Local artist images

Scanner sidecar lookup, mirror of the folder-cover fallback but resolved against the track's ancestors instead of the immediate parent.

[`commands/scan.rs::extract_artist_image`](../../src-tauri/src/commands/scan.rs) walks up to **3 parent directories** from each track and accepts the first match where either:

- the filename stem is one of `ARTIST_IMAGE_STEMS = ["artist", "performer", "band"]`, **or**
- the stem's `canonical_name(...)` equals the artist's canonical name (covers `Daft Punk.jpg` at the root of a `Daft Punk/` folder).

Both common layouts from issue #31 work out of the box:

- `Music/<Artist>/<Album>/track.flac` → matches `artist.jpg` two levels up.
- `Music/<Album>/track.flac` → matches `<Artist>.jpg` sitting beside the album folder (strict name-match so an unrelated `cover.jpg` is never mistaken for an artist photo).

Hash-addressed via BLAKE3 into the shared `artwork/<hash>.{jpg,png,webp,…}` cache and linked through the existing `artist.artwork_id → artwork` foreign key (no schema change). The `UPDATE … WHERE artwork_id IS NULL` guard means scanner runs never overwrite a manually uploaded image or a previously cached Deezer picture.

Resolution priority in [`commands/browse.rs::get_artist_detail`](../../src-tauri/src/commands/browse.rs) is now: **local sidecar → Deezer cache → live Deezer fetch** (last skipped when offline). [`ArtistDetailView`](../../src/components/views/ArtistDetailView.tsx) prefers `artwork_path` over `picture_path` and refuses to clobber a local image with a late-arriving Deezer response.

The `"Various Artists"` sentinel is explicitly excluded so a compilation folder never inherits a stray album cover as an artist photo.

For libraries scanned before the feature shipped, [`commands/scan.rs::rescan_local_artist_images`](../../src-tauri/src/commands/scan.rs) (exposed as **Settings → Library → Local artist images**) walks every `artist WHERE artwork_id IS NULL` and probes up to 16 tracks per artist with `extract_artist_image`, stopping at the first hit. Already-linked rows are filtered out at the SQL level, so the rescan is cheap to re-run.

### Manual override

The pencil overlay on the artist photo in [`ArtistDetailView`](../../src/components/views/ArtistDetailView.tsx) opens [`ArtistImagePickerModal`](../../src/components/common/ArtistImagePickerModal.tsx), which exposes three actions backed by [`commands/deezer.rs`](../../src-tauri/src/commands/deezer.rs):

- **Search Deezer** → `search_artists_deezer` + `set_artist_artwork_from_deezer` (downloads the chosen picture into the profile artwork cache, marks source `"deezer"`).
- **Pick a local file** → `set_artist_artwork_from_file` (same magic-byte validation as the album cover picker: jpg / png / webp).
- **Remove image** → `clear_artist_artwork` sets `artist.artwork_id = NULL` so the next render falls back through the resolution chain (Deezer cache → live fetch).

Both `set_artist_artwork_from_*` overwrite `artwork_id` unconditionally — an explicit user pick beats any automatic resolution.
10 changes: 5 additions & 5 deletions docs/features/ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,11 @@ Track tables themselves are **borderless** — no `rounded-2xl border bg-white`

Right side of [`PlayerBar`](../../src/components/player/PlayerBar.tsx) is the highest-pressure real estate in the UI — every new feature wants an icon there. To keep the bar from running out of width on narrow windows, controls cluster by frequency:

| Tier | Controls | Where |
| ------------ | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| **Primary** | Lyrics, Queue, Device picker, "⋯", Volume, **Mini-player**, **Fullscreen** | Always visible. Spotify-style right cluster (mini-player + fullscreen) sits after volume |
| **Overflow** | Playback speed (slider + presets), A-B loop, Sleep timer (panel) | [`MoreActionsMenu`](../../src/components/player/MoreActionsMenu.tsx) — "⋯" popover; the trigger itself is hidden when nothing inside is left |
| **Pinnable** | A-B loop, Sleep timer (promote to primary) | Toggle in Settings → Lecture (see below) |
| Tier | Controls | Where |
| ------------ | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| **Primary** | Lyrics, Queue, Device picker, "⋯", Volume, **Mini-player**, **Fullscreen** | Always visible. Spotify-style right cluster (mini-player + fullscreen) sits after volume |
| **Overflow** | Playback speed (slider + presets), A-B loop, Sleep timer (panel) | [`MoreActionsMenu`](../../src/components/player/MoreActionsMenu.tsx) — "⋯" popover; the trigger itself is hidden when nothing inside is left |
| **Pinnable** | A-B loop, Sleep timer (promote to primary) | Toggle in Settings → Lecture (see below) |

When adding a new player-bar action: default it into the overflow menu first — promote to primary only when usage data or user feedback warrants it. If both placements make sense, expose a pin toggle. The "⋯" trigger auto-hides when its menu would be empty.

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"@commitlint/cli": "^21.0.1",
"@commitlint/config-conventional": "^21.0.1",
"@eslint/js": "^10.0.1",
"@tauri-apps/cli": "^2.11.1",
"@tauri-apps/cli": "^2.11.2",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.2",
Expand Down
3 changes: 2 additions & 1 deletion public/splash.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
align-items: center;
justify-content: center;
gap: 22px;
background: radial-gradient(
background:
radial-gradient(
120% 80% at 50% 0%,
rgba(16, 185, 129, 0.18) 0%,
rgba(16, 185, 129, 0) 60%
Expand Down
32 changes: 16 additions & 16 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 5 additions & 7 deletions src-tauri/src/commands/browse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1013,13 +1013,11 @@ pub async fn get_genre_detail(
let profile_id = state.require_profile_id().await?;
let artwork_dir = state.paths.profile_artwork_dir(profile_id);

let header = sqlx::query_as::<_, GenreHeaderRaw>(
r#"SELECT id, name FROM genre WHERE id = ?"#,
)
.bind(genre_id)
.fetch_optional(&pool)
.await?
.ok_or_else(|| crate::error::AppError::Other("genre not found".into()))?;
let header = sqlx::query_as::<_, GenreHeaderRaw>(r#"SELECT id, name FROM genre WHERE id = ?"#)
.bind(genre_id)
.fetch_optional(&pool)
.await?
.ok_or_else(|| crate::error::AppError::Other("genre not found".into()))?;

let rows = sqlx::query_as::<_, GenreTrackRaw>(
r#"
Expand Down
Loading
Loading