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
4 changes: 2 additions & 2 deletions docs/features/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ Triggered from [`emit_track_changed`](../../src-tauri/crates/app/src/commands/pl

1. **Cache** — `app.lyrics` row keyed by `track.file_hash` (BLAKE3). Shared across profiles, kept for good — except a partial miss, which carries a `retry_after` date ([below](#which-providers-are-asked-for-which-tracks)).
2. **Embedded** — `LYRICS` / `USLT` / `©lyr` tag in the file (lofty), incl. synced `LRC` blocks. Lookup tries `ItemKey::UnsyncLyrics` first (the only key that maps to ID3v2's `USLT` in lofty 0.24), then `ItemKey::Lyrics` for Vorbis / MP4. For MP3s tagged with Mp3tag / foobar2000 / lame `--tg`, lyrics often live in a TXXX user-defined frame named `LYRICS` or `UNSYNCEDLYRICS` (common on K-Pop / J-Pop rips); these are invisible to the generic `Tag` interface so [`commands/lyrics.rs::read_id3v2_txxx_lyrics`](../../src-tauri/crates/app/src/commands/lyrics.rs) re-opens the file as `MpegFile`, downcasts to `Id3v2Tag`, and scans the TXXX descriptions explicitly.
3. **Sidecar file** — `{stem}.lrc` / `{stem}.txt` next to the audio file (e.g. `01 Song.mp3` + `01 Song.lrc`), or inside a sibling `Lyrics/` folder (case-insensitive, so `lyrics/` is also matched — common Linux convention). Stem matching is also case-insensitive so `Song.MP3` finds `song.lrc` on case-sensitive filesystems. `.lrc` wins over `.txt` at every probed directory because it carries timing info; same-folder hits beat `Lyrics/` hits. Format is auto-detected via `detect_format` and the row is cached with `source = lrc_file`. Whitespace-only files are treated as misses so the waterfall keeps falling through. Writing one is a choice: the destination below decides whether an edit — or a fetch — lands in the tag, in a sidecar or in the database alone.
3. **Sidecar file** — `{stem}.lrc` / `{stem}.txt` next to the audio file (e.g. `01 Song.mp3` + `01 Song.lrc`), or inside a sibling `Lyrics/` folder (case-insensitive, so `lyrics/` is also matched — common Linux convention). Stem matching is also case-insensitive so `Song.MP3` finds `song.lrc` on case-sensitive filesystems. `.lrc` wins over `.txt` at every probed directory because it carries timing info; same-folder hits beat `Lyrics/` hits. Format is auto-detected via `detect_format` and the row is cached with `source = lrc_file`. The file need not be UTF-8 (#910): [`text_encoding::decode`](../../src-tauri/crates/core/src/text_encoding.rs) honours a byte-order mark (UTF-8, UTF-16), reads valid UTF-8 as it is, and only then guesses the encoding from the bytes (`chardetng`, decoded by `encoding_rs`), so lyrics saved as GB18030, Big5, Shift_JIS, EUC-KR or a Windows code page are read too. A fixed fallback would not do: GB18030 accepts almost any byte sequence, so a Japanese or Korean file would come back as Chinese-looking nonsense. A cached miss is checked against the sidecar before it is served (`read_cached_or_new_sidecar`): the cache is keyed by the audio's hash and a complete miss is kept for good, so a sidecar added after the miss — or one that was unreadable before #910 — would otherwise stay hidden behind it forever. The sidecar is the one local source that can change without that hash changing, and it outranks a miss in every order the waterfall runs; the check costs one directory listing, and only for tracks without lyrics. The sidecar is written over the miss only while the row still holds it, so lyrics a lookup or an edit stored in between are served instead. Whitespace-only files are treated as misses so the waterfall keeps falling through. Writing one is a choice: the destination below decides whether an edit — or a fetch — lands in the tag, in a sidecar or in the database alone.

4. **Generic `description` field** — last of the local tiers, and the
only one that is a guess about a field meant for something else.
Expand Down Expand Up @@ -197,7 +197,7 @@ The bulk `run_prefetch` gap-filler stays local-first regardless of the toggle: i

The panel re-fetches per song — the sentinel track id stays constant for the whole session, so the effect keys on title + artist instead — and renders **statically**: synced LRC is timestamp-stripped, because the live stream position can't be aligned to a song that may have been joined mid-play. Library-row mutation actions (edit / import / refetch / clear) are hidden for radio.

`import_lrc_file` is still available for the explicit "pick this file" flow (e.g. when the sidecar lives in a non-conventional location like `~/Documents/lyrics/`); it overwrites the cached row regardless of which tier filled it.
`import_lrc_file` is still available for the explicit "pick this file" flow (e.g. when the sidecar lives in a non-conventional location like `~/Documents/lyrics/`); it decodes the file the same way as a sidecar, and overwrites the cached row regardless of which tier filled it.

**In-app editor.** `save_lyrics(track_id, { content, format, write_to_file })` upserts the cache row with `source = manual` and, when `write_to_file` is true, also writes the content into the file's `USLT` (ID3v2) / `UNSYNCEDLYRICS` (Vorbis) / `©lyr` (MP4) frame via lofty. Same Windows file-lock dance as the tag editor — pause if the engine has the file open, then re-hash with blake3 and update `track.file_hash` so the cache row stays addressable after the write. Emits a typed `lyrics:updated` event the panel listens to.

Expand Down
2 changes: 1 addition & 1 deletion docs/features/playlists.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Drag-and-drop is implemented with `@dnd-kit` over a virtualised list (`@tanstack

## M3U import / export

`export_playlist_m3u` writes a UTF-8 `.m3u8` with `#EXTINF:<duration>,<artist> - <title>` headers and absolute file paths, through a partial sibling of its own (created exclusively) renamed over the destination once complete ([`atomic_write`](../../src-tauri/crates/core/src/atomic_write.rs)), so overwriting an existing playlist file that fails halfway leaves the old one intact rather than truncated; a partial left by a write that never finished is removed by the next write to the same file once it is a day old. The importer accepts both `.m3u` and `.m3u8` (foobar2000, VLC, Rekordbox, hand-written sets) and matches against the active library:
`export_playlist_m3u` writes a UTF-8 `.m3u8` with `#EXTINF:<duration>,<artist> - <title>` headers and absolute file paths, through a partial sibling of its own (created exclusively) renamed over the destination once complete ([`atomic_write`](../../src-tauri/crates/core/src/atomic_write.rs)), so overwriting an existing playlist file that fails halfway leaves the old one intact rather than truncated; a partial left by a write that never finished is removed by the next write to the same file once it is a day old. The importer accepts both `.m3u` and `.m3u8` (foobar2000, VLC, Rekordbox, hand-written sets), decoded like a lyrics sidecar ([`text_encoding`](../../src-tauri/crates/core/src/text_encoding.rs)) — a legacy `.m3u` is in the code page of the machine that wrote it, Windows-1252 as often as GBK or Shift_JIS — and matches against the active library:

1. **Canonical-path match** — strip Windows `\\?\` prefix, lowercase the drive letter, normalise separators.
2. **Basename fallback** — if path-match fails, try the basename. This recovers playlists exported before a library reorganisation.
Expand Down
13 changes: 13 additions & 0 deletions packaging/flatpak/generated/cargo-sources.json
Original file line number Diff line number Diff line change
Expand Up @@ -948,6 +948,19 @@
"dest": "cargo/vendor/chacha20-0.10.2",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/chardetng/chardetng-1.0.0.crate",
"sha256": "13de944a44b5064ee5d3a5ceccc49a41bfec50f2580e66f82e87703acdb88b53",
"dest": "cargo/vendor/chardetng-1.0.0"
},
{
"type": "inline",
"contents": "{\"package\": \"13de944a44b5064ee5d3a5ceccc49a41bfec50f2580e66f82e87703acdb88b53\", \"files\": {}}",
"dest": "cargo/vendor/chardetng-1.0.0",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
Expand Down
14 changes: 14 additions & 0 deletions src-tauri/Cargo.lock

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

2 changes: 2 additions & 0 deletions src-tauri/crates/app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,8 @@ same-file = "1"
# (artwork extraction, folder cover fallback, etc.). Auto-cleaned on
# Drop so a panicked test doesn't leak fixtures across the temp dir.
tempfile = "3"
# Writes a lyrics sidecar in a legacy encoding for the decoding test.
encoding_rs = "0.8.42"

# Linux-only: direct ALSA enumeration so the output-device picker can
# show human-readable descriptions ("HDA Intel PCH, ALC897 Analog
Expand Down
Loading
Loading