Commit graph

343 commits

Author SHA1 Message Date
Deluan Quintão
07c756db3c
fix(ui): update Portuguese (BR) translations from POEditor (#6197)
Co-authored-by: navidrome-bot <navidrome-bot@navidrome.org>
2026-09-21 16:18:38 -04:00
Deluan Quintão
cabfd16f9f
fix(ui): update Finnish, Dutch translations from POEditor (#6148)
Co-authored-by: navidrome-bot <navidrome-bot@navidrome.org>
2026-09-20 21:36:35 -04:00
strecke
a5334ee46b
fix(ui): update missing German translations (#6146)
* Update missing German translations

* Fix typo in idHelp message in German translation

* Update German translations to remove formal "Sie" forms

---------

Co-authored-by: Deluan Quintão <deluan@navidrome.org>
2026-09-20 12:17:57 -04:00
Jonatan Nyberg
2913c13cdf
fix(i18n): improve Swedish translation (#6177)
Signed-off-by: Jonatan Nyberg <84130654+NickWick13@users.noreply.github.com>
2026-09-20 12:10:47 -04:00
fxj368
97270d44e1
fix(ui): update Chinese Simplified translations (#6152) 2026-09-16 07:24:50 -04:00
Deluan Quintão
2d09ebc676
fix(ui): update Chinese (traditional) translations from POEditor (#6128)
Co-authored-by: navidrome-bot <navidrome-bot@navidrome.org>
2026-09-12 12:58:00 -04:00
Deluan Quintão
d9d38f842a
fix(ui): update German, Greek, Finnish, Galician, Polish, Portuguese (BR), Thai, Ukrainian, Chinese (traditional) translations from POEditor (#5833)
Co-authored-by: navidrome-bot <navidrome-bot@navidrome.org>
2026-09-12 00:52:54 -04:00
karigane
8a2135f076
fix(i18n): Update Japanese translation (#6080)
* fix(i18n): Update Japanese translation

* fix(i18n): fix Japanese translation

* fix(i18n): fix Japanese translation

Update Japanese translations for `recentlyAdded`, `recentlyPlayed`, and `mostPlayed` in album lists

---------

Co-authored-by: Deluan Quintão <deluan@navidrome.org>
2026-09-06 13:30:25 -04:00
Aditya Raj Singh
3867fab4da
fix(transcoding): report AAC streams as audio/aac instead of audio/mp4 (#5998)
The default AAC transcode emits raw ADTS (`ffmpeg ... -f adts -`), but
the MIME table mapped `.aac` to `audio/mp4`. Clients that dispatch
strictly on Content-Type could reject the stream because the declared
container did not match the payload.

`.m4a` and `.alac` stay on `audio/mp4`, since those really are MP4.

Fixes #5958

Signed-off-by: Aditya Raj Singh <aditya@bncw.in>
Co-authored-by: Deluan Quintão <deluan@navidrome.org>
2026-08-30 11:21:14 -04:00
Deluan Quintão
b134f16fd5
feat(plugins): surface the valid agent names in logs and the Plugins UI (#5910)
The agent name used in the `Agents` config option comes from the .ndp file
name, not from the manifest. The Plugins UI showed the ID but never said what
it was for, so renaming a plugin file silently breaks the config with only a
Debug-level "Unknown agent ignored" line to go on.

Add a caption under the ID in the Plugins UI, and list the accepted names
alongside the rejected one in that log line.

Related to navidrome/apple-music-plugin#14
2026-08-30 11:11:35 -04:00
Deluan Quintão
f08b5297ee
feat(ui): add Refresh Metadata to the album and artist context menus (#6036)
* refactor(artwork): move artworkItemName into core/artwork as ItemName

* feat(external): add RefreshInfo to force an external info refresh

RefreshInfo re-fetches and re-saves external info for one artist or
album, bypassing the TTL check that UpdateArtistInfo/UpdateAlbumInfo
use. It is synchronous; callers that must not block detach it themselves.

Also makes MockArtistRepo/MockAlbumRepo.UpdateExternalInfo persist to
Data (previously a no-op) and adds the new method to the e2e noopProvider,
both required so the interface addition compiles and is observable in tests.

* feat(external): broadcast RefreshResource after external info is saved

populateArtistInfo and populateAlbumInfo now emit the same RefreshResource
event the artwork worker uses, so the UI learns about both foreground and
background metadata refreshes.

* feat(nativeapi): replace artwork refresh endpoint with metadata refresh

* feat(ui): add refreshMetadata to the data provider

* feat(ui): add a Refresh Metadata item to the album and artist context menus

* fix(ui): re-fetch artist info when the record is refreshed

* test: fix mislabeled spec, add kind-gate negative case, guard nil mock maps

- Rename the RefreshInfo spec that claimed to cover the save-failure/broadcast
  path: SetError(true) fails Get too, so it only proves RefreshInfo bails out
  early at getArtist.
- Add a spec proving playlist refreshes skip the external-info step, since
  that asymmetry (al/ar only) was documented but unasserted.
- Add lazy nil-map init to MockAlbumRepo/MockArtistRepo.UpdateExternalInfo so
  a composite-literal-constructed mock doesn't panic on first save.

* test: relocate discArtworkName specs from cmd to core/artwork

artworkItemName moved into core/artwork as ItemName in an earlier commit, but
its disc-name specs stayed behind in cmd/artwork_test.go, reaching across
packages. Move them to core/artwork/item_name_test.go where the code now lives.

* fix(ui): shape refreshMetadata like a react-admin response

react-admin validates custom dataProvider methods and rejects any response
without a `data` key, so the raw httpClient promise made every click surface
an error toast instead of the success message. The unit test mocked
useDataProvider, which skips that validation.

Also folds "which kinds have external info" into external.HasInfo so the
handler stops restating it, drops the nil-broker guard that only existed for
tests, and delegates the mocks' UpdateExternalInfo to Put.

* refactor(external): unexport infoKinds

Only HasInfo is used outside the package, so the slice itself does not need
to be exported.

* refactor(artwork): fold ItemName into housekeeping.go next to Refresh

ItemName exists to guard Refresh from ids that would orphan a queue row, and
both callers invoke them back to back. A separate file hid that pairing; it was
only split out to keep the move out of cmd/ legible in review.

* fix(nativeapi): return 500 when the refresh lookup fails for a non-ErrNotFound reason

A transient repository error told the admin the id did not exist, and the error
was dropped without a log line, so nothing pointed at the real cause.

Also drops the inherited claim that clearing artwork state shows a placeholder.
Reads fall back to local resolution, so that only holds when there is no local art.

* fix(ui): move Refresh Metadata above Get Info in the context menu

Menu order follows key insertion order in the options object, so the new spec
pins the position rather than leaving it to be shuffled by the next addition.
2026-08-25 23:59:40 -04:00
Deluan Quintão
82fde00ecc
feat(scrobbler): add per-user scrobble filter (#5964)
* feat(scrobbler): add scrobble_filter column to user

* feat(scrobbler): validate scrobble filter criteria on user save

* refactor(persistence): make smart playlist join helpers package-level

* feat(scrobbler): add MediaFileRepository.MatchesCriteria

* feat(scrobbler): filter external scrobbles with per-user criteria

* feat(ui): add scrobble filter field to user form

* fix(scrobbler): default scrobble_filter to empty string for existing users

* refactor(scrobbler): also gate playback reports on the scrobble filter

Playback reports carry the same track metadata to plugin scrobblers, so a
filtered track leaked through that third dispatch path. Skip the filter
evaluation entirely when no scrobbler is active.

* refactor(persistence): move criteria join building into criteria_sql.go

The join set a criteria needs was decided in criteria_sql.go but built in
smart_playlist_repository.go, so both callers had to pair the two by hand.

* refactor(persistence): unexport smartPlaylistCriteria methods

The type never leaves the package, so the exported names advertised an API
that callers outside persistence could never reach. Also disambiguates
where/orderBy from squirrel's SelectBuilder methods of the same name.

* fix(ui): cap the scrobble filter field width

fullWidth stretched it across the whole page next to 256px inputs. Bounded
at 40em, with two rows and a resize handle so JSON rules stay readable.

* refactor(ui): move scrobble filter input in UserEdit component

* feat(ui): add pt-BR translations for the scrobble filter

* fix(scrobbler): take the filter verdict before incPlay

incPlay mutates play counts and dates a filter can test on, so evaluating at
dispatch time let one play decide differently on either side of the increment:
a track could be scrobbled despite matching, or lose only its stopped report
and strand presence plugins. Reject limit/offset too, rather than silently
ignoring part of a rule copied from a smart playlist.

* fix(scrobbler): filter the report from an expired session

The expiry callback runs with a stub user carrying no filter, so evaluating
there always returned false and leaked the track to plugin scrobblers. That is
the normal path for clients that never send stopped, such as legacy Subsonic
now-playing. Carry the last verdict on the session instead.

* refactor(scrobbler): skip the now-playing enqueue instead of threading the verdict

Queuing an entry only to drop it at dispatch also cancelled a pending
announcement for the previous, unfiltered track, since the queue is keyed by
player and a new entry replaces the old one.

* fix(scrobbler): evaluate the filter regardless of active scrobblers

The verdict is stored on the session and dispatched at expiry, so skipping
evaluation when no scrobbler was active let a plugin enabled mid-session
receive a filtered track. The empty-filter guard above already gives servers
without scrobbling the same free path, so the shortcut only ever applied to
users who had a filter set.
2026-08-15 16:10:53 -04:00
Deluan Quintão
95615bcb18
fix(artwork): serve images whose format has no registered decoder (#5952)
* fix(artwork): serve images whose format has no registered decoder

The new pipeline derives dimensions, mime and the placeholder hashes at
resolution time, so a decode became a precondition for recording artwork at
all. An image in a format Go has no decoder for therefore failed acquisition,
retried until the 12h budget ran out, and then settled as absent, serving a
placeholder from that point on. The old pipeline decoded only to resize and
fell back to the original bytes when that failed, so these covers used to work.

A local file is picked by matching an image extension, so bytes it cannot
decode are most likely a codec we lack: image.ErrFormat on a folder, upload or
embedded source now yields an Artwork row carrying just the hash and mime, and
the bytes stay servable. An external response carries no such guarantee, so it
still fails and retries rather than pinning a non-image body as a cover. A
corrupt image of a known format and an over-cap declared size still fail, so
the decompression bomb guard is unchanged. Absent rows recorded by earlier
builds are re-resolved by the existing stale-absent recheck within a day, so
no epoch bump is needed to repair them.

Reusing a stored image now re-decodes when it carries no dimensions, so a
row recorded while a decoder was missing can still be upgraded later.

Registers jxl, heic and heif in mime_types.yaml: image detection resolves the
extension through the host MIME table, and the Alpine release image ships no
/etc/mime.types, so those covers were never recorded in folder.ImageFiles
there and never reached the pipeline at all.

* fix(artwork): never record empty bytes as artwork

image.DecodeConfig returns image.ErrFormat for an empty payload just as it
does for a codec with no registered decoder, so a zero-byte cover file was
recorded as found artwork and served as an empty response instead of falling
back to the placeholder. A truncated image of a known format already fails
with unexpected EOF rather than ErrFormat, so only the empty case needed the
guard.
2026-08-14 09:43:13 -04:00
fxj368
600ea5482c fix(ui): update Chinese (Simplified) translation (#5873)
Signed-off-by: Deluan <deluan@navidrome.org>
2026-07-30 12:38:35 -04:00
Marco Ciotola
ecf606e523
fix(ui): update Italian translation (#5848) 2026-07-25 13:23:32 -04:00
fxj368
3cd4f1eb24
fix(ui): update Chinese Simplified translation (#5779)
* Update Chinese (Simplified) translation

* Update Chinese (Simplified) translation

---------

Co-authored-by: Deluan Quintão <deluan@navidrome.org>
2026-07-14 07:40:55 -04:00
Deluan Quintão
b2b4fc1943
fix(ui): update Turkish translations from POEditor (#5685)
Co-authored-by: navidrome-bot <navidrome-bot@navidrome.org>
2026-07-06 22:32:43 -04:00
Yuuta
3a14faa033
feat(subsonic): add structured sidecar lyrics support with OpenSubsonic v2 karaoke cues and agent layers (#5076)
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/213
  https://github.com/opensubsonic/open-subsonic-api/pull/218 (songLyrics v2)
  https://github.com/opensubsonic/open-subsonic-api/pull/228 (cue byte offsets)
2026-06-19 12:00:58 -04:00
Deluan Quintão
08a027dbcc
fix(transcoding): honor player forced format on the WebUI transcode flow (#5613)
* feat(stream): add ClientInfo.ForceFormat for browser-aware forced format

Restricts the client to a forced transcoding format and suppresses direct
play, but only when the client declares it supports that format. Part of #5583.

* fix(transcoding): honor player forced format on getTranscodeDecision

When the WebUI player has a forced transcoding format configured and the
browser declares it can play that format, transcode to it (suppressing
direct play). Fall back to normal negotiation with a warning when the
format is unsupported. The MaxBitRate cap still applies on top. Fixes #5583.

* test(e2e): cover player forced format on getTranscodeDecision

Forced format honored when the client supports it, falls back to negotiation
otherwise, and the MaxBitRate cap still applies on top. Part of #5583.

* feat(ui): remove obsolete 'format ignored' helper text on player form

The web player now honors the forced transcoding format, so the caveat added
in #5611 no longer applies. Reverts the Transcoding field to a plain selector.
Part of #5583.
2026-06-14 20:52:19 -04:00
Deluan Quintão
c4c70519b5
fix(transcoding): enforce server-side player MaxBitRate on /rest/stream (#5611)
* fix(transcoding): enforce player MaxBitRate on getTranscodeDecision

The Web UI streams via getTranscodeDecision, which (since #5473) ignored
the server-side player config. Apply the player's MaxBitRate as a bitrate
ceiling on the client's declared limits before MakeDecision, restoring
per-player bitrate enforcement without reintroducing the forced-format
override. Fixes #5583.

* test(e2e): assert player MaxBitRate is enforced on getTranscodeDecision

Invert the assertions added in #5473 that expected the player cap to be
ignored; getTranscodeDecision now enforces it (issue #5583).

* feat(ui): clarify web player ignores forced transcoding format

Add helper text to the Transcoding field on the player edit form when the
player is the NavidromeUI web client, since it enforces only the Max. Bit
Rate, not the forced format. Part of issue #5583.

* refactor(stream): extract ClientInfo.CapBitrate, share across transcode paths

Move the player MaxBitRate ceiling logic into a canonical ClientInfo.CapBitrate
method in core/stream, used by both getTranscodeDecision and the legacy
ResolveRequest path. Removes handler-layer duplication and corrects a
misleading comment that wrongly implied the legacy single-field cap was buggy.

* fix(transcoding): downsample on legacy /stream when only player MaxBitRate is set

A bare /stream or /download request from a player configured with a
server-side MaxBitRate (but no forced format) was served raw, ignoring the
cap. buildLegacyClientInfo now triggers DefaultDownsamplingFormat when the
player MaxBitRate alone is below the source bitrate, matching the
already-correct forced-format and request-bitrate paths. Part of #5583.

* fix(ui): add Brazilian Portuguese translation for player transcoding helper text

Translates the new resources.player.helperTexts.transcodingId key added for
the web player transcoding-format clarification. Part of #5583.

* fix(ui): restore Transcoding field styling and render helper text

The TranscodingInput wrapper swallowed the variant SimpleForm injects into
its direct children (field lost its outlined box) and put helperText on the
ReferenceInput, which does not forward it to the input. Spread the form props
onto ReferenceInput and move helperText to the SelectInput child so both the
outlined styling and the helper text render. Part of #5583.

* fix(i18n): update Brazilian Portuguese translation for album artist field

Signed-off-by: Deluan <deluan@navidrome.org>

* fix(ui): clean up comments in PlayerEdit component

Signed-off-by: Deluan <deluan@navidrome.org>

* test(ui): mock useTranslate in PlayerEdit test for determinism

Avoid depending on ra-core's out-of-provider translation behavior, which can
vary by version. Part of #5583.

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-06-14 16:52:01 -04:00
Deluan Quintão
1b46b97712
fix(ui): update Indonesian translations from POEditor (#5575)
Co-authored-by: navidrome-bot <navidrome-bot@navidrome.org>
2026-06-08 08:22:59 -04:00
Deluan Quintão
5c387630ff
fix(ui): update Estonian translations from POEditor (#5573)
Co-authored-by: navidrome-bot <navidrome-bot@navidrome.org>
2026-06-07 12:29:17 -04:00
Daniel Barrientos Anariba
1709ce37f6
fix(ui): update Spanish translations and add missing gain keys (#5433)
* fix(ui): update Spanish translations and add missing gain keys

- Add missing 'albumGain' and 'trackGain' keys (matches recent additions in pt-br, ru)
- Translate 'Playlists' and 'Shared Playlists' to 'Listas de reproducción' / '...compartidas'
- Translate 'OFFLINE' (server down indicator) to 'DESCONECTADO'

Spanish translation now covers 553/553 keys (was 551/553).

Signed-off-by: Daniel Banariba <banaribad@gmail.com>

* fix(ui): address review feedback on Spanish translations

- Use 'Ganancia del álbum' (with article 'del') for consistency with the existing pattern in line 624 ('album': 'Ganancia del álbum') and 'Artista del álbum'. Thanks @gemini-code-assist for the catch.
- Revert 'playlists' and 'sharedPlaylists' to keep the loanword 'Playlist(s)' which is the form actually used by Spanish-speaking music app users (Spotify ES, etc.) and matches existing usage elsewhere in this same file (e.g. line 48 'Agregar a la playlist').

Signed-off-by: Daniel Banariba <banaribad@gmail.com>

---------

Signed-off-by: Daniel Banariba <banaribad@gmail.com>
Co-authored-by: Deluan Quintão <deluan@navidrome.org>
2026-06-05 18:32:57 -04:00
Xabi
a6451f75d6
fix(ui): update Basque localisation (#5364)
Added two strings (gain), imrpoved some, fixed a typo

Co-authored-by: Deluan Quintão <deluan@navidrome.org>
2026-06-05 18:32:10 -04:00
Deluan Quintão
03841ffe96
fix(ui): update German, Finnish, Galician, Dutch, Slovak, Thai, Chinese (traditional) translations from POEditor (#5351)
Co-authored-by: navidrome-bot <navidrome-bot@navidrome.org>
2026-06-05 18:16:21 -04:00
nenadjokic
545a9ecc3c
fix(ui): update Serbian translation (#5444)
* fix(i18n): correct grammar errors in Serbian (sr) translation

Eight objective grammatical / lexical errors in `resources/i18n/sr.json`.
No stylistic or strategic re-wording — only corrections where the
current string is grammatically wrong, contains a non-word, or breaks
plural / case agreement.

| Key | Before | After | Why |
| --- | --- | --- | --- |
| `resources.song.fields.bitDepth` | `Битова` | `Битска дубина` | "Битова" is genitive plural of "bit" ("of bits") and drops the "depth" semantics. Adjective+noun shape matches `Битски проток` already used for the adjacent "Bit rate" field |
| `resources.song.fields.channels` | `Канала` | `Канали` | "Канала" is genitive plural ("of channels"); a column header needs nominative plural "Канали" |
| `resources.radio.name` plural | `Радији` | `Радио-станице` | "Радији" is not a valid plural of "Радио" in Serbian. The standard plural for radio stations is "Радио-станице" |
| `ra.input.file.upload_*` and `ra.input.image.upload_*` | `Упустите фајлове / слике …` | `Превуците фајлове / слике …` | "Упустити" means "to engage in / to indulge", not "to drop". For drag-and-drop UIs the standard Serbian verb is "Превуците" ("Drag") |
| `ra.navigation.prev` | `Претход` | `Претх.` | "Претход" is not a word — looks like a truncated "Претходна" missing the period. Restored as a proper abbreviation |
| `about.links.featureRequests` | `Захтеви за функцијама` | `Захтеви за функције` | Wrong case. Serbian "захтев за X" takes accusative ("захтев за помоћ"), not instrumental ("за функцијама") |
| `player.clickToPauseText` / `clickToPlayText` | `Кликни за паузирање / пуштање` | `Кликните за паузирање / пуштање` | The rest of the file uses formal plural imperative ("Кликните…"). Only these two used the singular informal "Кликни", which broke the consistent register |

JSON validated with `python3 -m json.tool`. No keys added, removed, or
re-ordered — diff is purely value substitution. Stylistic and lexical
modernization (e.g. "Уметник" → "Извођач" for music artist, filling
the missing `library` / `plugin` / `nowPlaying` blocks added in upstream
en.json) intentionally left for a follow-up PR after this baseline of
objective fixes lands.

* fix(i18n): fill missing keys in Serbian (sr) translation — 100% coverage

The Serbian translation was at 70% of upstream `en.json` (389 of 553 keys).
This commit fills all 164 missing keys, bringing coverage to 100%.

The file is also re-ordered to match `en.json`'s key sequence so that
future translation drift is easy to detect by diffing the two files
side-by-side. This is the same regeneration pattern used by the previous
maintainer's PR #3941.

## What was missing

| Block | Keys | Notes |
| --- | ---: | --- |
| `resources.plugin` | 58 | Whole Plugin system block — settings, config schema, permissions, notifications |
| `resources.library` | 43 | Whole Library management block — fields, scan actions, validation, notifications |
| `about.config` + `about.tabs` | 12 | Configuration export feature (TOML) |
| `message.*` | 11 | Cover-art upload/remove + Instant Mix + remove-all-missing |
| `resources.song` | 9 | composer, sample rate, gain fields, instant mix, show-in-playlist |
| `resources.playlist` | 6 | search-or-create UX, save-queue-to-playlist |
| `resources.artist` | 5 | top songs / shuffle / radio actions, missing field, maincredit role |
| `resources.user` | 5 | Per-user library access controls |
| `menu.librarySelector` | 4 | Multi-library selector |
| `activity` | 4 | selectiveScan, scanType, status, elapsedTime |
| `nowPlaying` | 3 | Now Playing widget (title / empty / minutesAgo plural) |
| `resources.album` | 2 | libraryName, missing |
| `resources.missing` | 2 | remove_all action, libraryName field |

## Translation conventions followed

- Stuck with the existing terminology already in `sr.json` for consistency
  — e.g. `Уметник` for "Artist", `Плејлиста` for "Playlist", `Жетон`
  for "Token". Whether `Уметник` → `Извођач` (music-context "performer")
  is a worthwhile rename is a separate question that deserves its own PR
  with a focused review surface; not in scope here.
- Cyrillic throughout (matches `languageName: "српски"`).
- Variable interpolation (`%{var}`) preserved exactly.
- Pluralisation separator (` |||| `) preserved on plural-aware keys
  (`nowPlaying.minutesAgo`, `resources.library.name`, `resources.plugin.name`,
  `resources.artist.roles.maincredit`).

## Validation

- JSON validated with `python3 -m json.tool`
- Key-count parity check: 553 keys in `en.json` → 553 keys in `sr.json`,
  zero missing, zero extra.
- Diff is +408 / -200 (line moves due to canonical ordering plus the
  net 164 new translations). All existing translations preserved verbatim.

## Builds on PR #5444

This branch sits on top of `i18n-sr-grammar-fixes` (PR #5444). If that
PR merges first, this one auto-rebases cleanly. If this one merges first,
PR #5444 has trivial conflicts in the same 7 strings (all already
resolved here as part of regeneration).
2026-05-19 14:50:34 -03:00
Deluan
725f6ab34b feat(i18n): add Estonian translation file
Signed-off-by: Deluan <deluan@navidrome.org>
2026-05-18 09:31:57 -03:00
Deluan Quintão
8f0b4930ff
refactor(conf): replace eager dir creation with lazy Dir type (#5495)
* feat(conf): add Dir type with lazy directory creation

Introduces the Dir type that wraps a directory path string and defers
os.MkdirAll until the first call to Path() or MustPath(), using sync.Once
to ensure the creation happens exactly once. Implements fmt.Stringer,
encoding.TextMarshaler, and encoding.TextUnmarshaler for config integration.
Includes Ginkgo/Gomega tests covering all methods and error paths.

* refactor(conf): replace eager dir creation with lazy Dir type

Change DataFolder, CacheFolder, Plugins.Folder, and Backup.Path from
string to Dir. Remove all os.MkdirAll calls from Load() so directories
are created lazily on first Path()/MustPath() call. Artwork folder
creation was already handled at point-of-use in image_upload.go.

Add SnapshotConfig() to conf package for safe test config save/restore
that avoids copying sync.Once inside Dir fields. Fix copy-lock vet
warning in nativeapi/config.go by marshalling pointer instead of value.

* refactor(conf): migrate tests and db init to lazy Dir type

Update all test files to use conf.NewDir() for Dir field assignments.
Ensure DataFolder is created lazily when the database is first opened
in db.Db(). Remove eager directory creation from conf.Load() tests.

* fix(conf): address review findings for Dir type

- Use os.ModePerm for DataFolder/CacheFolder (was 0700, should match
  original behavior). Add NewDirWithPerm for PluginsFolder (0700).
- Use Path() instead of MustPath() in db.Prune() to avoid logFatal
  from background cron job.
- Panic on marshal/unmarshal errors in SnapshotConfig (test helper).
- Clean up redundant String()/MustPath() calls in plugin manager.
- Remove dead code in dir_test.go.

Signed-off-by: Deluan <deluan@navidrome.org>

* fix(conf): add GoString to Dir for clean config dump output

Implement fmt.GoStringer on Dir so pretty.Sprintf shows the path
string instead of internal struct fields (sync.Once, perm, err).
Also add TODO comment to configtest about removing the indirection.

* fix(dir): improve error logging in MustPath method

Signed-off-by: Deluan <deluan@navidrome.org>

* refactor(tests): remove redundant tests for unwritable DataFolder and CacheFolder

Signed-off-by: Deluan <deluan@navidrome.org>

* fix(conf): address PR review feedback

- Ensure Plugins.Folder always uses 0700, even when user-configured
  (previously only the derived default got restrictive permissions).
- Create LogFile parent directory before opening, so LogFile paths
  inside a not-yet-created DataFolder work correctly.

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-05-13 17:44:22 -03:00
IEEE-754
59c1454ffb
fix(ui): update zh-Hant.json (#5470) 2026-05-06 08:49:13 -04:00
Daniele Massa
9824102efb
fix(ui): completed Italian translation (#5407)
Co-authored-by: Daniele Massa <x@danielemassa.org>
2026-04-25 16:18:51 -04:00
Deluan
aa84e645ba fix(ui): add albumGain and trackGain translations in Brazilian Portuguese
Signed-off-by: Deluan <deluan@navidrome.org>
2026-04-12 13:22:56 -04:00
Alexander Makeenkov
9dfd9ac849 fix(ui): update Russian translations and add missing gain keys (#5329)
* feat(i18n): add album and track gain translation strings

* chore(i18n): update Russian translations

---------

Co-authored-by: Alexander Makeenkov <amakeenk@altlinux.org>
Signed-off-by: Deluan <deluan@navidrome.org>
2026-04-12 13:17:46 -04:00
fxj368
1044c173cb
fix(ui): update Chinese (Simplified) translation (#5323) 2026-04-07 11:11:05 -04:00
obskyr
7834674381
fix(scanner): map ORIGYEAR tag for VorbisComment and MP4 formats
* Use ORIGYEAR tag for original date

As it is a default mapping in MP3Tag. https://docs.mp3tag.de/mapping/#origyear

* Test parsing `originaldate` and `ORIGYEAR` tags

`originaldate` is populated by TagLib’s mappings. https://taglib.org/api/p_propertymapping.html
2026-04-06 21:35:22 -04:00
Deluan Quintão
e7c7cba873
fix(ui): update Esperanto, Dutch translations from POEditor (#5301)
Co-authored-by: navidrome-bot <navidrome-bot@navidrome.org>
2026-04-04 15:18:00 -04:00
Xabi
93631cdee9
fix(ui): update Basque localisation (#5278)
Added missing strings
2026-04-04 15:17:40 -04:00
Deluan Quintão
9fe9cf3ff6
fix(ui): update Spanish, French translations from POEditor (#5260)
Co-authored-by: navidrome-bot <navidrome-bot@navidrome.org>
2026-03-29 19:55:29 -04:00
ChekeredList71
a293d12034
fix(ui): update Hungarian translation (#5263)
* [ui] hungarian translation

* Update resources/i18n/hu.json

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* Update resources/i18n/hu.json

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

---------

Co-authored-by: ChekeredList71 <asd@asd.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-03-29 19:50:58 -04:00
Deluan Quintão
79e1af7cd6
fix(ui): update Danish, German, Greek, Finnish, Galician, Portuguese (BR), Swedish, Ukrainian, Chinese (traditional) translations from POEditor (#5218)
Co-authored-by: navidrome-bot <navidrome-bot@navidrome.org>
2026-03-27 18:04:47 -04:00
Tom Boucher
356b0716b6
fix(scanner): exclude Vorbis VERSION from albumversion tag mapping (#5194)
The Vorbis/FLAC VERSION field is for track-level disambiguation (e.g.
remix, live, 30s edit), not album versioning. Including it in the
albumversion aliases caused albums to split incorrectly when tracks
had different VERSION values and no MusicBrainz Album ID was set.

Remove 'version' from the albumversion aliases in mappings.yaml.
Users who want the old behavior can re-add it via Tags config.

Update the PID test to use 'albumversion' directly instead of
'version' as the raw PID attribute, with a realistic value.

Fixes #5082

Co-authored-by: Deluan Quintão <deluan@navidrome.org>
2026-03-23 18:32:05 -04:00
JRoshthen1
a4c289b28c
feat(ui): add Slovak language translation (#5231)
* feat(i18n): Add Slovak language translation

Signed-off-by: jrosh <martin@jrosh.eu>

* fix(i18n): Fix typos and add missing translations

Signed-off-by: jrosh <martin@jrosh.eu>

---------

Signed-off-by: jrosh <martin@jrosh.eu>
Co-authored-by: Deluan Quintão <deluan@navidrome.org>
2026-03-19 13:33:09 -04:00
Deluan Quintão
ab8a58157a
feat: add artist image uploads and image-folder artwork source (#5198)
* feat: add shared ImageUploadService for entity image management

* feat: add UploadedImage field and methods to Artist model

* feat: add uploaded_image column to artist table

* feat: add ArtistImageFolder config option

* refactor: wire ImageUploadService and delegate playlist file ops to it

Wire ImageUploadService into the DI container and refactor the playlist
service to delegate image file operations (SetImage/RemoveImage) to the
shared ImageUploadService, removing duplicated file I/O logic. A local
ImageUploadService interface is defined in core/playlists to avoid an
import cycle between core and core/playlists.

* feat: artist artwork reader checks uploaded image first

* feat: add image-folder priority source for artist artwork

* feat: cache key invalidation for image-folder and uploaded images

* refactor: extract shared image upload HTTP helpers

* feat: add artist image upload/delete API endpoints

* refactor: playlist handlers use shared image upload helpers

* feat: add shared ImageUploadOverlay component

* feat: add i18n keys for artist image upload

* feat: add image upload overlay to artist detail pages

* refactor: playlist details uses shared ImageUploadOverlay component

* fix: add gosec nolint directive for ParseMultipartForm

* refactor: deduplicate image upload code and optimize dir scanning

- Remove dead ImageFilename methods from Artist and Playlist models
  (production code uses core.imageFilename exclusively)
- Extract shared uploadedImagePath helper in model/image.go
- Extract findImageInArtistFolder to deduplicate dir-scanning logic
  between fromArtistImageFolder and getArtistImageFolderModTime
- Fix fileInputRef in useCallback dependency array

* fix: include artist UpdatedAt in artwork cache key

Without this, uploading or deleting an artist image would not
invalidate the cached artwork because the cache key was only based
on album folder timestamps, not the artist's own UpdatedAt field.

* feat: add Portuguese translations for artist image upload

* refactor: use shared i18n keys for cover art upload messages

Move cover art upload/remove translations from per-entity sections
(artist, playlist) to a shared top-level "message" section, avoiding
duplication across entity types and translation files.

* refactor: move cover art i18n keys to shared message section for all languages

* refactor: simplify image upload code and eliminate redundancies

Extracted duplicate image loading/lightbox state logic from
DesktopArtistDetails and MobileArtistDetails into a shared
useArtistImageState hook. Moved entity type constants to the consts
package and replaced raw string literals throughout model, core, and
nativeapi packages. Exported model.UploadedImagePath and reused it in
core/image_upload.go to consolidate path construction. Cached the
ArtistImageFolder lookup result in artistReader to eliminate a redundant
os.ReadDir call on every artwork request.

Signed-off-by: Deluan <deluan@navidrome.org>

* style: fix prettier formatting in ImageUploadOverlay

* fix: address code review feedback on image upload error handling

- RemoveImage now returns errors instead of swallowing them
- Artist handlers distinguish not-found from other DB errors
- Defer multipart temp file cleanup after parsing

* fix: enforce hard request size limit with MaxBytesReader for image uploads

Signed-off-by: Deluan <deluan@navidrome.org>

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-15 22:19:55 -04:00
Deluan Quintão
be06196168
fix(ui): update Bulgarian, Catalan, Danish, German, Greek, Spanish, Finnish, French, Galician, Russian, Slovenian, Swedish, Thai, Chinese (traditional) translations from POEditor (#5044)
Co-authored-by: navidrome-bot <navidrome-bot@navidrome.org>
2026-03-15 20:44:59 -04:00
adrbn
d004f99f8f
feat(playlist): add custom playlist cover art upload (#5110)
* feat(playlist): add custom playlist cover art upload - #406

Allow users to upload, view, and remove custom cover images for playlists.
Custom images take priority over the auto-generated tiled artwork.

Backend:
- Add `image_path` column to playlist table (migration with proper rollback)
- Add `SetImage`/`RemoveImage` methods to playlist service
- Add `POST/DELETE /api/playlist/{id}/image` endpoints
- Prioritize custom image in artwork reader pipeline
- Clean up image files on playlist deletion
- Use glob-based cleanup to prevent orphaned files across format changes
- Reject uploads with undetermined image type (400)

Frontend:
- Hover overlay on playlist cover with upload (camera) and remove (trash) buttons
- Lightbox for full-size cover art viewing
- Cover art thumbnails in the playlist list view
- Loading/error states and i18n strings

Closes #406

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: adrbn <128328324+adrbn@users.noreply.github.com>

* refactor: rename playlist image path migration file

Signed-off-by: Deluan <deluan@navidrome.org>

* fix(playlist): address review feedback for cover art upload - #406

- Use httpClient instead of raw fetch for image upload/remove
- Revert glob cleanup to simple imagePath check
- Add log.Error before all error HTTP responses
- Add backend tests for SetImage and RemoveImage

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: adrbn <128328324+adrbn@users.noreply.github.com>

* refactor(playlist): use Playlist.ArtworkPath() for image storage

Migrate all playlist image path handling to use the new
Playlist.ArtworkPath() method as the single source of truth. The DB now
stores only the filename (e.g. "pls-1.jpg") instead of a relative path,
and images are stored under {DataFolder}/artwork/playlist/ instead of
{DataFolder}/playlist_images/. The artwork root directory is created at
startup alongside DataFolder and CacheFolder. This also removes the
conf dependency from reader_playlist.go since path resolution is now
fully encapsulated in the model.

Signed-off-by: Deluan <deluan@navidrome.org>

* refactor(playlist): streamline artwork image selection logic

Signed-off-by: Deluan <deluan@navidrome.org>

* refactor: move translation keys, add pt-BR translations

Signed-off-by: Deluan <deluan@navidrome.org>

* refactor(playlist): rename image_path to image_file

Rename the playlist cover art column and field from image_path/ImagePath
to image_file/ImageFile across the migration, model, service, tests, and
UI. The new name more accurately describes what the field stores (a
filename, not a path) and aligns with the existing ImageFiles/IsImageFile
naming conventions in the codebase.

---------

Signed-off-by: adrbn <128328324+adrbn@users.noreply.github.com>
Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Deluan Quintão <deluan@navidrome.org>
2026-03-01 14:07:18 -05:00
Deluan Quintão
d9a215e1e3
feat(plugins): allow mounting library directories as read-write (#5122)
* feat(plugins): mount library directories as read-only by default

Add an AllowWriteAccess boolean to the plugin model, defaulting to
false. When off, library directories are mounted with the extism "ro:"
prefix (read-only). Admins can explicitly grant write access via a new
toggle in the Library Permission card.

* test: add tests to buildAllowedPaths

Signed-off-by: Deluan <deluan@navidrome.org>

* chore: improve allowed paths logging for library access

Signed-off-by: Deluan <deluan@navidrome.org>

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-02-28 10:59:13 -05:00
Deluan Quintão
875ffc2b78
fix(ui): update Danish, Portuguese (BR) translations from POEditor (#5039)
Co-authored-by: navidrome-bot <navidrome-bot@navidrome.org>
2026-02-12 16:38:57 -05:00
ChekeredList71
885334c819
fix(ui): update Hungarian translation (#5041)
* new strings added

* "empty" solved

---------

Co-authored-by: ChekeredList71 <asd@asd.com>
2026-02-12 16:36:05 -05:00
Xabi
13d3d510f5
fix(ui): update Basque localisation (#5038)
* Update Basque localisation

Added missing strings and a couple of improvements.

* Update resources/i18n/eu.json

typo

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

---------

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-02-12 15:52:37 -05:00
fxj368
656009e5f8
fix(i18n) update Chinese Simplified translation (#5025)
* Update Chinese Simplified translation

* fix some structural issue and an incorrect translation
2026-02-12 15:49:20 -05:00
Denisa Rissa
eb9ebc3fba
fix(ui): add missing keys in Danish translation (#5011)
update Danish translation with 59 missing keys for the `resources.plugin` section as well as `message.startingInstantMix`, `resources.song.actions.instantMix`, `resources.song.fields.composer`, and `resources.plugin.name`.
2026-02-10 14:05:14 -05:00