* refactor(lyrics): read sidecar files via library storage FS
Routes fromExternalFile reads through storage.For(mf.LibraryPath) instead
of os.Open on AbsolutePath, fixing sidecar reads for non-local backends.
UTF-16 LE/BE and BOM handling preserved via ioutils.UTF8Reader.
* refactor(lyrics): address review feedback on sidecar FS read
- Move blank local-storage import from sources.go into lyrics_suite_test.go
(the test suite already imports the local package for RegisterExtractor,
so local's init() runs; production binaries get the scheme via normal wiring)
- Fix misleading comment: model.ParseLyrics → model.ParseLyricsFile
- Replace what-comment with why-comment in BeforeSuite explaining the
log.Fatal guard that requires the no-op extractor registration
* test(lyrics): add subsonic e2e baseline for getLyrics endpoints
Establishes a behavioral baseline for getLyricsBySongId (v2 structured)
and getLyrics (legacy) before the lyrics parser refactor. Covers embedded
formats (LRC synced, plain text, TTML) and sidecar formats (LRC, SRT,
YAML), all isolated under a Lyrics/ fixture folder so the new fixtures
do not perturb existing test behavior beyond fixture counts.
Sidecar files are injected as raw &fstest.MapFile{Data: []byte(...)}
entries; the scanner skips non-audio extensions (.lrc, .srt, .yaml) so
they are invisible to scanning but reachable via the fake FS at request
time through fromExternalFile/storage.For.
Update album/artist/song counts in the album-list, multi-library, and
search3 empty-query tests to reflect the six new tracks (1 new artist,
1 new album, 6 new songs).
* test(lyrics): strengthen e2e lyrics baseline (lang assertions, rename helper)
Rename the local helper `main` to `firstLyric` to avoid collision with the
reserved-feeling built-in name. Add `Lang` assertions to both embedded and
sidecar DescribeTable entries, locking the current observed values: "xxx"
(ISO 639-2 "no language specified") for all embedded and LRC/SRT sidecars,
and "eng" for the YAML sidecar (which explicitly sets `language: eng`).
* feat(lyrics): detect Lyricsfile YAML in content-sniffing
* feat(plugins): content-sniff plugin lyrics for all formats
Replace model.ToLyrics (LRC/plain only) with model.ParseEmbedded so plugin
responses are content-sniffed for TTML, SRT, YAML, LRC, and plain text.
ParseEmbedded returns a LyricList, so the loop now flattens multiple tracks
per response entry.
The test-lyrics WASM plugin gains a "ttml" format mode (configured via
pdk.GetConfig) that returns a minimal TTML document; rebuilt with the
standard Go wasip1 toolchain (GOOS=wasip1 GOARCH=wasm). A new Ginkgo test
asserts Synced==true and the exact cue value, which the old plain-text path
could not produce.
GetLyrics doc comment updated to reflect content-sniffing; a later task will
retarget it to ParseLyrics once that function is introduced.
* test(plugins): validate plugin lyrics auto-detect across all formats
The test-lyrics WASM plugin now supports per-format modes via the
"format" config key: ttml, srt, yaml, lrc, and plain, in addition to
the existing default plain-text response. The plugin is rebuilt with the
standard Go wasip1 compiler.
lyrics_adapter_test.go gains a DescribeTable covering all five formats,
asserting both Synced (the discriminator that proves correct format
detection) and the exact line value. This validates the full
auto-detect chain (TTML → SRT → YAML/Lyricsfile → LRC → plain) end-to-end
through the real plugin → adapter → parser flow.
* refactor(lyrics): consolidate parsers into model.ParseLyrics
* refactor(lyrics): retarget legacy callers to model.ParseLyrics
Pin suffix to ".lrc" to preserve byte-identical output for stored
plain/LRC text that was previously handled by the now-removed ToLyrics.
* test(lyrics): fix lyrics tests after parser consolidation
- Rewrite the YAML-fallback test to assert the correct design: a
non-Lyricsfile .yaml sidecar returns as plain text and shadows
lower-priority sources (rather than falling through to .lrc).
- Add LibraryPath + relative Path split to the three subsonic tests
that read sidecar files via storage.For(), so they resolve against
the correct fixtures directory.
- Register a no-op extractor in api_suite_test.go BeforeSuite so
newLocalStorage does not fatal when storage.For is called during
sidecar-lyrics tests.
* test(lyrics): add per-format ParseLyrics benchmarks
Baseline measurements (count=2 runs) on M2:
BenchmarkParseLyrics_LRC-8 5725 178796 ns/op 49.78 MB/s 427877 B/op 523 allocs/op
BenchmarkParseLyrics_Plain-8 5425 230854 ns/op 32.44 MB/s 102508 B/op 16 allocs/op
BenchmarkParseLyrics_EnhancedLRC-8 1942 605893 ns/op 17.66 MB/s 860678 B/op 4256 allocs/op
BenchmarkParseLyrics_SRT-8 3249 373991 ns/op 25.91 MB/s 1113575 B/op 4407 allocs/op
BenchmarkParseLyrics_TTML-8 1483 813027 ns/op 13.86 MB/s 2198052 B/op 8665 allocs/op
BenchmarkParseLyrics_YAML-8 1700 678250 ns/op 13.01 MB/s 1235096 B/op 8288 allocs/op
BenchmarkParseLyrics_SniffTTML-8 1525 776482 ns/op 14.51 MB/s 2225448 B/op 8681 allocs/op
BenchmarkParseLyrics_SniffSRT-8 2528 451210 ns/op 21.48 MB/s 1157000 B/op 4422 allocs/op
BenchmarkParseLyrics_SniffYAML-8 1333 827152 ns/op 10.67 MB/s 1337195 B/op 8718 allocs/op
BenchmarkParseLyrics_SniffLRC-8 2820 413038 ns/op 21.55 MB/s 588934 B/op 1812 allocs/op
BenchmarkParseLyrics_SniffPlain-8 2968 409091 ns/op 18.31 MB/s 254470 B/op 1491 allocs/op
Content-sniff path overhead: 1.5–15% depending on format.
* test(lyrics): use real public-domain fixtures for parser benchmarks
Replace synthetic benchmark payloads with 'Auld Lang Syne' (Robert Burns,
1788, public domain) rendered into every supported format (LRC, plain,
enhanced LRC, SRT, TTML, Lyricsfile YAML) so the numbers reflect realistic
content. Same song across formats makes per-format cost comparable.
Baseline (Apple M-series, -benchmem, real fixtures):
LRC ~28 us/op 42 KB 147 allocs
Plain ~23 us/op 18 KB 22 allocs
EnhancedLRC ~37 us/op 51 KB 374 allocs
SRT ~52 us/op 139 KB 581 allocs
TTML ~119 us/op 276 KB 1227 allocs
YAML ~142 us/op 193 KB 1732 allocs
Sniff(LRC) ~47 us/op 57 KB 237 allocs
Sniff(TTML) ~122 us/op 282 KB 1250 allocs
Sniff(YAML) ~186 us/op 218 KB 1847 allocs
Fixtures in tests/fixtures/lyrics/.
* fix(lyrics): preserve [] (not null) for empty lyrics in backfill migration
ParseLyrics returns nil for zero-line input (whitespace-only stored
lyrics). json.Marshal(nil LyricList) produces null, violating the DB
invariant that media_file.lyrics uses [] for empty lyrics, never null.
Initialize to model.LyricList{} when ParseLyrics returns nil so the
marshalled result is always [].
* refactor(lyrics): unify parser dispatch and centralize empty-list invariant
Apply thermo-nuclear review findings (behavior-preserving):
- Replace the suffix switch + three single-use closure adapters
(parseTTMLKnown/parseSRTKnown + inline YAML closure) with a
bySuffix map of a single lyricParser(lang, contents) signature.
Normalize parseTTMLWithDefaultLang/parseSRTWithLanguage to that
(lang, contents) order so no adapter glue is needed.
- Collapse the parallel sniffLyrics engine into one parseFirstMatch
primitive shared by both the suffix and content-sniff paths
(sniffOrder candidate list). TTML stays gated via parseTTMLIfDocument
in sniff mode to avoid running the XML decoder on plain/LRC text.
- Add LyricList.MarshalJSON so empty/nil always serializes to [] (the
lyrics column invariant), in one canonical place. Delete the
migration's nil-guard, which the marshaler now subsumes.
Behavior verified unchanged: full suite + race + e2e green.
* refactor(lyrics): single registry drives both suffix dispatch and sniff order
Collapse the bySuffix map and sniffOrder slice into one ordered registry:
slice order is the content-sniff probe order, each row's suffixes drive
sidecar dispatch, and per-row bySuffix/byContent parsers preserve the
gated-TTML-when-sniffing distinction. One source of truth, no duplicated
parser references.
* refactor(lyrics): self-skipping parsers collapse the format table to one column
Move the TTML <tt>-document gate into parseTTMLWithDefaultLang itself (after
the encoding fixup, so UTF-16-declared docs are still recognized): non-TTML
content returns (nil, nil) to skip; a malformed <tt> document still errors.
SRT and Lyricsfile YAML already self-skip. With every structured parser
self-skipping, the format table drops to one {suffixes, parse} column named
lyricFormats — no bySuffix/byContent split, no separate sniff-only TTML gate.
Both the suffix and content-sniff paths share the same parser per format.
* refactor(lyrics): strip BOM once at ParseLyrics entry for all paths
Previously only the content-sniff path stripped the BOM; the suffix path
relied on its callers (fromExternalFile via UTF8Reader) having already
stripped it. That implicit contract was fragile — a caller passing raw
BOM-prefixed bytes with a suffix would reach the parsers with the BOM intact
(SanitizeText does not strip it). Strip once at entry so every path and
parser sees clean bytes regardless of caller. No-op for already-stripped
input.
* refactor(lyrics): trim verbose comments to essential why
* refactor(lyrics): move LRC parser to its own lyrics_lrc.go
Extract parseLRC, the enhanced-LRC helpers (parseEnhancedLine, adjustGroup,
stripEnhancedMarkers, shiftELRCCues), parseTime, and the LRC regexes from
lyrics.go into lyrics_lrc.go, with the parseLRC tests in lyrics_lrc_test.go.
This makes the layout symmetric — one file per format (lrc/srt/ttml/yaml) —
and leaves lyrics.go holding only shared types and cue normalization. All
moved symbols were already LRC-private; no behavior change.
* refactor(lyrics): collapse ParseLyrics suffix/sniff branches into one loop
Both modes differ only in which formats to try, so select candidates in a
single loop (all formats when sniffing, the suffix's own otherwise) and run
them through parseFirstMatch once. Drops the projected-slice make+index and
the ContainsFunc closure; unmatched suffixes yield no candidates and fall to
the plain-text floor, as before.
* refactor(lyrics): apply simplify-review cleanups
- stripBOM: bytes.TrimPrefix instead of []byte<->string round-trip (no alloc)
- ParseLyrics: pre-size the candidates slice
- move isTTMLDocument to lyrics_ttml.go beside its only caller (the dispatch
layer should hold no per-format knowledge)
* refactor(lyrics): simplify test descriptions for structured lyrics
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(lyrics): fold parseLyricsfile into lyricParser signature and rename file
- parseLyricsfile now matches the lyricParser signature directly (reads via
bytes.NewReader), removing the parseLyricsfileBytes adapter and the
string(contents) copy; the lyricFormats table references it directly.
- StructuredLyrics drops the vestigial LyricList{} init (json.Unmarshal
overwrites; MarshalJSON owns the empty->[] invariant).
- Rename lyricsfile.go -> lyrics_lyricsfile.go (and its test) to match the
lyrics_<format>.go convention used by lrc/srt/ttml.
* refactor(lyrics): move test-only parseTTML/parseSRT wrappers to test files
These zero-arg wrappers (defaulting lang to "xxx") had no production callers
after the consolidation — only the format tests used them. Move each beside
its tests so the production files carry no test-only code.
* build: exclude generated *_gen.go files from linting
The plugin host *_gen.go files (ndpgen output) were tripping the whitespace
linter despite carrying a generated marker. Exclude them by path so make lint
and the pre-push hook pass on untouched generated code.
* perf(lyrics): drop []byte/string round-trips in parsers
Apply code-review feedback to remove avoidable allocations in the lyrics
parsers. isTTMLDocument now takes []byte directly, so parseTTMLWithDefaultLang
no longer copies its buffer into a string before the TTML probe. parseSRTBlock
splits its block with strings.Split instead of converting to []byte and back
per line. ParseLyrics hoists strings.ToLower(suffix) out of the format loop.
No behavior change; the dropped len(scanner)==0 SRT guard was dead (strings.Split
never returns an empty slice, and the existing len(lines)==0 check still covers
empty input).
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(lyrics): colocate and unexport cue-normalization helpers
Move the cue-normalization machinery out of lyrics.go into a dedicated
lyrics_normalize.go (with lyrics_normalize_test.go), leaving lyrics.go to hold
just the shared lyric types and their methods. lyrics.go was mixing the domain
type/contract definitions with format-agnostic post-processing.
Unexport normalizeLyrics, normalizeCueLines, and normalizeLineTiming: they have
no callers outside the model package, so they should not be part of its public
API. NormalizeCueEnds stays exported because the Subsonic enhanced-lyrics
serializer (server/subsonic/lyrics.go) resolves cue ends per agent group while
building the response; that is the only legitimate cross-package caller.
Also includes a small no-op robustness tweak in parseLRC: len(times) == 0
instead of times == nil (equivalent here, more idiomatic).
No behavior change.
* test(lyrics): add direct coverage for NormalizeCueEnds
NormalizeCueEnds is exported and carries the most intricate logic in the
normalization cluster (fill-from-next, fill-from-fallback, both clamps, and the
all-or-none clear), but was only exercised transitively. Add a focused spec
covering each branch plus the empty-input and no-mutation guarantees, bringing
the function to 100% coverage.
* test(lyrics): cover legacy getLyrics across formats and sources
Expand the legacy getLyrics e2e coverage from a single embedded-plain case to a
table over all six fixtures: embedded LRC/plain/TTML and sidecar LRC/SRT/YAML.
Each case asserts the v1 plain-text fallback contract — the structured lyric is
flattened to LRC-style plain text with no timing markup leaking through (no LRC
brackets, SRT arrows, or XML tags), regardless of the source format or whether
it is embedded or a sidecar file. This pins the behavior that synced TTML/SRT/
YAML formats degrade gracefully to plain text on the legacy endpoint.
* test(lyrics): cover songLyrics v1 vs v2 with word-level fixtures
Correct and expand the e2e lyrics coverage to match the OpenSubsonic songLyrics
extension contract:
- v1 (getLyricsBySongId, no enhanced): line-level lyrics with no cueLine, kind,
or agents — even for word-level formats (ELRC, Lyricsfile YAML).
- v2 (getLyricsBySongId?enhanced=true): word-level cueLine surfaces for ELRC and
YAML sources; kind="main" is set; a line-level source (SRT) still yields no
cueLine even when enhanced.
- legacy getLyrics (artist/title): the original Subsonic endpoint, flattening any
format to plain text. A prior commit mislabeled this as the "v1 contract";
getLyrics predates OpenSubsonic and is unrelated to the extension versions.
Drive these with the public-domain tests/fixtures/lyrics files (the same set the
parser benchmarks use) so the e2e content stays in sync and actually carries the
word-level timing needed to distinguish v1 from v2. The embedded "synced LRC"
fixture is upgraded to ELRC (word-level); track counts are unchanged, so the
rest of the suite is unaffected.
* test(lyrics): parameterize v2 enhanced coverage across all formats
Convert the v2 (enhanced) e2e block from three ad-hoc cases into a DescribeTable
covering all six formats, matching the v1 and legacy tables. Each entry declares
whether the source carries word-level timing: ELRC, TTML, and Lyricsfile YAML
surface a cueLine; LRC, SRT, and plain text do not. All six get kind="main".
Add word-level <span> timing to the first line of the auld-lang-syne.ttml
fixture so TTML exercises the word-level cueLine path (the parser already
supports <span begin/end>, but the fixture was line-level only). The first line
now yields the same five word cues as the ELRC and YAML fixtures, keeping the
table assertions uniform across formats.
* fix(lyrics): honor caller language when Lyricsfile YAML omits it
parseLyricsfile discarded the caller's language argument, so a Lyricsfile YAML
parsed from an embedded tag or plugin response with no metadata.language was
labeled "xxx" even when ParseLyrics was given a language. The SRT and TTML
parsers already use the caller language as their default; fall back to it here
too, preferring the document's own metadata.language when present.
Also reword a misleading TTML comment: isTTMLDocument still runs an XML decode
(it stops at the first element), so the skip avoids the full TTML parse, not the
XML decoder entirely.
* refactor(lyrics): consolidate lyrics parsing functions names
Signed-off-by: Deluan <deluan@navidrome.org>
* test(lyrics): drop test-only parse wrappers after parser rename
Commit 48c0173e8 renamed the production parsers to parseTTML/parseSRT, which
collided with the same-named test-only wrappers and broke the model test build
(parseTTML/parseSRT redeclared). Remove the wrappers and call the production
parsers directly with the placeholder language at each test site.
* test(lyrics): complete the truncated enhanced-LRC fixture
The auld-lang-syne.elrc fixture stopped after the first two stanzas (8 lyric
lines) while every other format fixture carries the full 24-line song. Extend it
to all 24 lines with per-word timing so it is a faithful enhanced-LRC sample and
the EnhancedLRC parser benchmark runs on a workload comparable to the others.
The first line's word timings are unchanged, so the e2e cueLine assertions still
hold.
---------
Signed-off-by: Deluan <deluan@navidrome.org>
Expand backend lyrics support with richer sidecar formats and upgrade the
OpenSubsonic songLyrics implementation to the version 2 structured karaoke
contract, while preserving version 1 behavior by default.
Sidecar formats and parsing:
- Add a TTML parser (core/lyrics/ttml.go): clock time, offset time, bare
decimal seconds, nested timing contexts, and token-level <span> timing for
word/syllable karaoke. Parses Apple Music-style metadata tracks (translation
and pronunciation/transliteration) and agent metadata into per-track agents[]
plus per-cue-line agentId. Hydrates missing line timing from cue timing.
- Add an SRT parser (core/lyrics/srt.go).
- Add a LRCLIB Lyricsfile (.yaml/.yml) parser (model/lyricsfile.go): maps
per-word lines[].words[] to cues with inclusive UTF-8 byte offsets and
attributes overlapping lines to synthetic voice agents so parallel vocals
split correctly in the enhanced response.
- Extend LRC parsing for Enhanced LRC inline <mm:ss.xx> word-timing markers.
- Add UTF-8 BOM and UTF-16 LE support for TTML/LRC sidecars.
- Parse the above formats from embedded tags as well as sidecar files.
Source resolution:
- Default lyricspriority is now
".ttml,.yaml,.yml,.elrc,.lrc,.srt,.txt,embedded" so the new formats are
discoverable without manual configuration.
- Preserve configured source priority across duplicate media-file candidates
instead of only checking the first DB match, so higher-priority sidecar
lyrics on older duplicates can still win.
- Raise the embedded-lyrics tag maxLength to 1 MB to fit word-timed
TTML/Enhanced-LRC karaoke for a full song.
OpenSubsonic songLyrics v2:
- Advertise songLyrics versions [1, 2].
- With enhanced=true, getLyricsBySongId may return structuredLyrics.kind
(main/translation/pronunciation), cueLine[] line-level karaoke groupings,
cueLine.cue[] timed words/syllables with required UTF-8 byteStart/byteEnd,
reusable structuredLyrics.agents[], and cueLine.agentId references.
- Without enhanced=true, the response stays v1-compatible: no kind, no cueLine,
no agents, no non-main tracks; the existing line[] payload is always
populated so legacy clients keep working.
Contract details:
- cueLine is emitted only for synced lyrics with cue data.
- Within a cueLine, cue.end is normalized all-or-none and overlaps are removed;
overlaps across separate cueLines remain valid for parallel vocal layers.
- Missing cue end-times are filled from the next cue or the parent line.
- When cueLines share an index, the one whose agent has role "main" is first.
- LyricCue.Value is serialized as XML chardata; cues with nil start are skipped
rather than serialized as 0.
Refactoring:
- Move pure format parsers into model/ (lyrics.go, lyrics_ttml.go,
lyrics_srt.go, lyrics_embedded.go, lyricsfile.go) and extract Subsonic
response building into server/subsonic/lyrics.go.
- Centralize lyric-kind constants and add Lyrics.EffectiveKind/IsMainKind.
- Add gg.Clone helper.
Spec references:
https://github.com/opensubsonic/open-subsonic-api/discussions/213https://github.com/opensubsonic/open-subsonic-api/pull/218 (songLyrics v2)
https://github.com/opensubsonic/open-subsonic-api/pull/228 (cue byte offsets)
The scanner classified a file by the name of the directory entry, so a
symlink was treated as audio/image/playlist based on the link name rather
than what it actually points to. Now symlinks are fully resolved (following
the whole chain) and classified by the resolved target's extension, so a
symlink to a non-audio file is no longer imported as a track.
This also makes Scanner.FollowSymlinks apply to file symlinks, not just
directory symlinks as before. The default stays true, so following symlinks
to real audio files (second drives, shared folders, etc.) keeps working.
Adds trace logging for symlink resolution decisions and real-fs regression
tests covering multi-level symlink chains.
* feat(playlist): add migration for playlist image field rename and external URL
* refactor(playlist): rename ImageFile to UploadedImage and ArtworkPath to UploadedImagePath
Rename playlist model fields and methods for clarity in preparation for
adding external image URL and sidecar image support. Add the new
ExternalImageURL field to the Playlist model.
* feat(playlist): parse #EXTALBUMARTURL directive in M3U imports
* feat(playlist): always sync ExternalImageURL on re-scan, preserve UploadedImage
* feat(artwork): add sidecar image discovery and cache invalidation for playlists
Add playlist sidecar image support to the artwork reader fallback chain.
A sidecar image (e.g., MyPlaylist.jpg next to MyPlaylist.m3u) is discovered
via case-insensitive base name matching using model.IsImageFile(). Cache
invalidation uses max(playlist.UpdatedAt, imageFile.ModTime()) to bust
stale artwork when sidecar or ExternalImageURL local files change.
* feat(artwork): add external image URL source to playlist artwork reader
Add fromPlaylistExternalImage source function that resolves playlist
cover art from ExternalImageURL, supporting both HTTP(S) URLs (via
the existing fromURL helper) and local file paths (via os.Open).
Insert it in the Reader() fallback chain between sidecar and tiled cover.
* refactor(artwork): simplify playlist artwork source functions
Extract shared fromLocalFile helper, use url.Parse for scheme check,
and collapse sidecar directory scan conditions.
* test(artwork): remove redundant fromPlaylistSidecar tests
These tests duplicated scenarios already covered by findPlaylistSidecarPath
tests combined with fromLocalFile (tested via fromPlaylistExternalImage).
After refactoring fromPlaylistSidecar to a one-liner composing those two
functions, the wrapper tests add no value.
* fix(playlist): address security review comments from PR #5131:
- Use url.PathUnescape instead of url.QueryUnescape for file:// URLs so
that '+' in filenames is preserved (not decoded as space).
- Validate all local image paths (file://, absolute, relative) against
known library boundaries via libraryMatcher, rejecting paths outside
any configured library.
- Harden #EXTALBUMARTURL against path traversal and SSRF by adding EnableM3UExternalAlbumArt config flag (default false, also
disabled by EnableExternalServices=false) to gate HTTP(S) URL storage
at parse time and fetching at read time (defense in depth).
- Log a warning when os.ReadDir fails in findPlaylistSidecarPath for
diagnosability.
- Extract resolveLocalPath helper to simplify resolveImageURL.
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(playlist): implement human-friendly filename generation for uploaded playlist cover images
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: split reflex -R flags to preserve directory exclusion optimization
Combining the _test.go exclusion pattern (which uses $) into the same -R
regex as the directory prefixes (^ui, ^data, ^db/migrations) disabled
reflex's ExcludePrefix optimization. Reflex disables prefix-based
directory skipping when the regex AST contains $, \z, or \b operators,
causing it to traverse into ui/node_modules and hit "too many open files".
Splitting into two separate -R flags fixes this: the directory prefix
regex remains $-free so ExcludePrefix works, while the _test.go pattern
gets its own flag where the $ anchor doesn't affect directory skipping.
* fix(gotaglib): pass filename hint to OpenStream for format detection
OpenStream relies on content-sniffing when no filename is provided,
which fails for some files (e.g. OPUS). Pass the filename via the new
WithFilename option so TagLib can use the file extension as a hint.
Also adds an OPUS test fixture and test entry.
Relates to https://github.com/navidrome/navidrome/issues/4604#issuecomment-3868569113, #4998, #5010
* feat(agents): Add artist url and top songs to ListenBrainz agent
* add newline at end of file
* respond to some feedback
* add more tests, include more metadata in top songs
* add duration to album info
* add similar artists from labs
* add similar artists and track radio
* fix(client): replace sort with slices.SortFunc for deterministic ordering of recordings with same score
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: typos
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: use struct literal initialization consistently
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: configurable artist and track algorithms
Signed-off-by: Deluan <deluan@navidrome.org>
* test configuration changes
---------
Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: Deluan Quintão <deluan@navidrome.org>
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(lastfm): support multiple languages for album and artist info retrieval
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(lastfm): improve content validation for album and artist descriptions
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(lastfm): remove single language test and clarify languages field in configuration
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(deezer): support multiple languages for artist bio retrieval
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(lastfm): rename ignoredBiographies to ignoredContent for clarity
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: rename ArtistRadio to SimilarSongs for clarity and consistency
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: implement GetSimilarSongsByTrack and related functionality for song similarity retrieval
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: enhance GetSimilarSongsByTrack to include artist and album details and update tests
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: enhance song matching by implementing title and artist filtering in loadTracksByTitleAndArtist
Signed-off-by: Deluan <deluan@navidrome.org>
* test: add unit tests for song matching functionality in provider
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: extract song matching functionality into its own file
Signed-off-by: Deluan <deluan@navidrome.org>
* docs: clarify similarSongsFallback function description in provider.go
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: initialize result slice for songs with capacity based on response length
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: simplify agent method calls for retrieving images and similar songs
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: simplify agent method calls for retrieving images and similar songs
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: remove outdated comments in GetSimilarSongs methods
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: use composite key for song matches to handle duplicates by title and artist
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: consolidate expectations setup for similar songs tests
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: add instant mix action to song context menu and update translations
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(provider): handle unknown entity types in GetSimilarSongs
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: move playSimilar action to playbackActions and streamline song processing
Signed-off-by: Deluan <deluan@navidrome.org>
* format
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: enhance instant mix functionality with loading notification and shuffle option
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: implement fuzzy matching for similar songs based on configurable threshold
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: implement track matching with multiple specificity levels
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: enhance track matching by implementing unified scoring with specificity levels
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: enhance deezer top tracks result with album
Signed-off-by: Deluan <deluan@navidrome.org>
* feat: enhance track matching with fuzzy album similarity for improved scoring
Signed-off-by: Deluan <deluan@navidrome.org>
* docs: document multi-phase song matching algorithm with detailed scoring and prioritization
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(deezer): add functions to fetch related artists, biographies, and top tracks for an artist
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(deezer): add language support for Deezer API client
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(deezer): Use GraphQL API for translated biographies
The previous implementation scraped the __DZR_APP_STATE__ from HTML,
which only contained English content. The actual biography displayed
on Deezer's website comes from their GraphQL API at pipe.deezer.com,
which properly respects the Accept-Language header and returns
translated content.
This change:
- Switches from HTML scraping to the GraphQL API
- Uses Accept-Language header instead of URL path for language
- Updates tests to match the new implementation
- Removes unused HTML fixture file
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(deezer): move JWT token handling to a separate file for better organization
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(deezer): enhance JWT token handling with expiration validation
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(deezer): change log level for unknown agent warnings from Warn to Debug
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(deezer): reduce JWT token expiration buffer from 10 minutes to 1 minute
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: handle UTF-8 BOM in lyrics and playlist files
Added UTF-8 BOM (Byte Order Mark) detection and stripping for external lyrics files and playlist files. This ensures that files with BOM markers are correctly parsed and recognized as synced lyrics or valid playlists.
The fix introduces a new ioutils package with UTF8Reader and UTF8ReadFile functions that automatically detect and remove UTF-8, UTF-16 LE, and UTF-16 BE BOMs. These utilities are now used when reading external lyrics and playlist files to ensure consistent parsing regardless of BOM presence.
Added comprehensive tests for BOM handling in both lyrics and playlists, including test fixtures with actual BOM markers to verify correct behavior.
* test: add test for UTF-16 LE encoded LRC files
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(agents): Add Deezer API artist image provider agent
* fix(agents): Use proper naming convention of consts
* fix(agents): Check if json test data can be read
* fix(agents): Use underscores for unused function arguments
* fix(agents): Move int literal to deezerArtistSearchLimit const
* feat: add Deezer configuration option to disable it.
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: Deluan Quintão <deluan@navidrome.org>
* fix(ui,scanner,subsonic): Allow nullable replaygain and support 0.0
Resolves#4236.
Makes the replaygain columns (track/album gain/peak) nullable.
Converts the type to a pointer, allowing for 0.0 (a valid value) to be returned from Subsonic.
Updates tests for this behavior.
* small refactor
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: Deluan <deluan@navidrome.org>
* draft commit
* time to fight pipeline
* round 2 changes
* remove unnecessary line
* fight taglib. again
* make taglib work again???
* add id3 tags
* taglib 1.12 vs 1.13
* use int instead for windows
* store as json now
* add migration, more tests
* support repeated line, multiline
* fix ms and support .m, .mm, .mmm
* address some concerns, make cpp a bit safer
* separate responses from model
* remove [:]
* Add trace log
* Try to unblock pipeline
* Fix merge errors
* Fix SIGSEGV error (proper handling of empty frames)
* Add fallback artist/title to structured lyrics
* Rename conflicting named vars
* Fix tests
* Do we still need ffmpeg in the pipeline?
* Revert "Do we still need ffmpeg in the pipeline?"
Yes we do.
This reverts commit 87df7f6df79bccee83f48c4b7a8118a7636a5e66.
* Does this passes now, with a newer ffmpeg version?
* Revert "Does this passes now, with a newer ffmpeg version?"
No, it does not :(
This reverts commit 372eb4b0ae05d9ffe98078e9bc4e56a9b2921f32.
* My OCD made me do it :P
---------
Co-authored-by: Deluan Quintão <deluan@navidrome.org>
* wip: API endpoint for creating playlists from m3u files
* wip: get user id from context
* temporarily disable failing test
* custom logic for playlist route to accomodate m3u content type
* incorporate playlist parsing into existing logic in core
* re-enable test
* fix locally failing test
* Address requested changes.
* Improve ImportFile tests.
* Remove ownerID as a parameter of ImportM3U.
* Write tests for ImportM3U.
* Separate ImportM3U test into two.
* Test OwnerID and playlist Name.
---------
Co-authored-by: Sam Watson <SwatsonCodes@users.noreply.github.com>
Co-authored-by: caiocotts <caio@cotts.com.br>
* Fix MB recording ID parameter name for ListenBrainz submission
This follows the ListenBrainz API documentation.
Fixes#1657
* Rename MediaFile.MbzTrackID to MbzRecordingID
This better reflects the actual data. That the MusicBrainz
recording ID is stored in file metadata as musicbrainz_trackid
is a historical artifact.
* Rename database column mbz_track_id to mbz_recording_id
* lastfm album.getInfo, getAlbuminfo(2) endpoints
* ... for description and reduce not found log level
* address first comments
* return all images
* Update migration timestamp
* Handle a few edge cases
* Add CoverArtPriority option to retrieve albumart from external sources
* Make agents methods more descriptive
* Use Last.fm name consistently
Co-authored-by: Deluan <deluan@navidrome.org>
* ReplayGain support
- extract ReplayGain tags from files, expose via native api
- use metadata to normalize audio in web player
* make pre-push happy
* remove unnecessary prints
* remove another unnecessary print
* add tooltips, see metadata
* address comments, use settings instead
* remove console.log
* use better language for gain modes
* fix(taglib): Fix misleading error message on unreadable media - #1576
Signed-off-by: reo <reo_999@proton.me>
* fix(taglib): Add unit test and exclude scan for only unreadable file - #1576
Signed-off-by: reo <reo_999@proton.me>
* fix(taglib): Add unit test and exclude scan for only unreadable file - #1576
Signed-off-by: reo <reo_999@proton.me>
* fix(taglib): Add unit test and exclude scan for only unreadable file - #1576
Signed-off-by: reo <reo_999@proton.me>
* fix(taglib): Add unit test and exclude scan for only unreadable file - #1576
Signed-off-by: reo <reo_999@proton.me>
* fix(taglib): Add unit test and exclude scan for only unreadable file - #1576
Signed-off-by: reo <reo_999@proton.me>
* Fix test and simplify code a bit
We don't need to expose the type of error: `taglib.Parse()` always return nil
* Fix comment
Signed-off-by: reo <reo_999@proton.me>
Co-authored-by: Deluan <deluan@navidrome.org>
* Refactor session_keys to its own package
* Adjust play_tracker
- Don't send external NowPlaying/Scrobble for tracks with unknown artist
- Continue to the next agent on error
* Implement ListenBrainz Agent and Auth Router
* Implement frontend for ListenBrainz linking
* Update listenBrainzRequest
- Don't marshal Player to json
- Rename Track to Title
* Return ErrRetryLater on ListenBrainz server errors
* Add tests for listenBrainzAgent
* Add tests for ListenBrainz Client
* Adjust ListenBrainzTokenDialog to handle errors better
* Refactor listenbrainz.formatListen and listenBrainzRequest structs
* Refactor agent auth_routers
* Refactor session_keys to agents package
* Add test for listenBrainzResponse
* Add tests for ListenBrainz auth_router
* Update ListenBrainzTokenDialog and auth_router
* Adjust player scrobble toggle
* Rename manifest.json to manifest.webmanifest
browser consoles keep complaining that the manifest doesn't have the `.webmanifest` extension.
* FIx manifest.webmanifest references
Co-authored-by: Deluan <deluan@navidrome.org>
Syntax is Ant-style Globs, with support for '**' (any subfolder). Default: '.:**' (or '.;**' in Windows`, meaning all folders and subfolders under `MusicFolder`
* Call Last.FM's getInfo again without mbid when artist is not found
* Call Last.FM's getSimilar again without mbid when artist is not found
* Call Last.FM's getTopTracks again without mbid when artist is not found