From ee2c2b19e95917dc20c1b086d2a90baccafdb232 Mon Sep 17 00:00:00 2001 From: Deluan Date: Wed, 19 Mar 2025 20:18:56 -0400 Subject: [PATCH 1/8] fix(dockerfile): remove the healthcheck, it gives more headaches than benefits. Signed-off-by: Deluan --- Dockerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 9dda15cc6..0ec64f769 100644 --- a/Dockerfile +++ b/Dockerfile @@ -138,7 +138,6 @@ ENV GODEBUG="asyncpreemptoff=1" RUN touch /.nddockerenv EXPOSE ${ND_PORT} -HEALTHCHECK CMD wget -O- http://localhost:${ND_PORT}/ping || exit 1 WORKDIR /app ENTRYPOINT ["/app/navidrome"] From cd552a55efc812a2bc9b3302125146f9a7171e66 Mon Sep 17 00:00:00 2001 From: Deluan Date: Wed, 19 Mar 2025 22:15:20 -0400 Subject: [PATCH 2/8] fix(scanner): pass datafolder and cachefolder to scanner subprocess Fix #3831 Signed-off-by: Deluan --- scanner/external.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scanner/external.go b/scanner/external.go index b00c67cb9..c4a29efa3 100644 --- a/scanner/external.go +++ b/scanner/external.go @@ -33,6 +33,8 @@ func (s *scannerExternal) scanAll(ctx context.Context, fullScan bool, progress c cmd := exec.CommandContext(ctx, exe, "scan", "--nobanner", "--subprocess", "--configfile", conf.Server.ConfigFile, + "--datafolder", conf.Server.DataFolder, + "--cachefolder", conf.Server.CacheFolder, If(fullScan, "--full", "")) in, out := io.Pipe() From 491210ac1207239292181c31fdaf1b5b00fb48c2 Mon Sep 17 00:00:00 2001 From: Deluan Date: Thu, 20 Mar 2025 12:39:40 -0400 Subject: [PATCH 3/8] fix(scanner): ignore NaN ReplayGain values Fix: https://github.com/navidrome/navidrome/issues/3858 Signed-off-by: Deluan --- model/metadata/metadata.go | 2 +- model/metadata/metadata_test.go | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/model/metadata/metadata.go b/model/metadata/metadata.go index 3d5d64dd1..471c2434c 100644 --- a/model/metadata/metadata.go +++ b/model/metadata/metadata.go @@ -120,7 +120,7 @@ func (md Metadata) first(key model.TagName) string { func float(value string, def ...float64) float64 { v, err := strconv.ParseFloat(value, 64) - if err != nil || v == math.Inf(-1) || v == math.Inf(1) { + if err != nil || v == math.Inf(-1) || math.IsInf(v, 1) || math.IsNaN(v) { if len(def) > 0 { return def[0] } diff --git a/model/metadata/metadata_test.go b/model/metadata/metadata_test.go index f3478ccba..5d9c4a3ed 100644 --- a/model/metadata/metadata_test.go +++ b/model/metadata/metadata_test.go @@ -264,6 +264,7 @@ var _ = Describe("Metadata", func() { Entry("1.2dB", "1.2dB", 1.2), Entry("Infinity", "Infinity", 0.0), Entry("Invalid value", "INVALID VALUE", 0.0), + Entry("NaN", "NaN", 0.0), ) DescribeTable("Peak", func(tagValue string, expected float64) { @@ -275,6 +276,7 @@ var _ = Describe("Metadata", func() { Entry("Invalid dB suffix", "0.7dB", 1.0), Entry("Infinity", "Infinity", 1.0), Entry("Invalid value", "INVALID VALUE", 1.0), + Entry("NaN", "NaN", 1.0), ) DescribeTable("getR128GainValue", func(tagValue string, expected float64) { From 59ece403931373a085378537da904a5d162b488f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Thu, 20 Mar 2025 19:26:40 -0400 Subject: [PATCH 4/8] fix(server): better embedded artwork extraction with ffmpeg (#3860) - `-map 0:v` selects all video streams from the input - `-map -0:V` excludes all "main" video streams (capital V) This combination effectively selects only the attached pictures Signed-off-by: Deluan --- core/ffmpeg/ffmpeg.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/ffmpeg/ffmpeg.go b/core/ffmpeg/ffmpeg.go index bb57e5101..2e0d5a4b7 100644 --- a/core/ffmpeg/ffmpeg.go +++ b/core/ffmpeg/ffmpeg.go @@ -29,7 +29,7 @@ func New() FFmpeg { } const ( - extractImageCmd = "ffmpeg -i %s -an -vcodec copy -f image2pipe -" + extractImageCmd = "ffmpeg -i %s -map 0:v -map -0:V -vcodec copy -f image2pipe -" probeCmd = "ffmpeg %s -f ffmetadata" ) From d78c6f6a04df4f8c2b6758d0991c894e859852cc Mon Sep 17 00:00:00 2001 From: Deluan Date: Thu, 20 Mar 2025 22:10:42 -0400 Subject: [PATCH 5/8] fix(subsonic): ArtistID3 should contain list of AlbumID3 Signed-off-by: Deluan --- server/subsonic/browsing.go | 2 +- server/subsonic/filter/filters.go | 4 ++-- server/subsonic/responses/responses.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/server/subsonic/browsing.go b/server/subsonic/browsing.go index 82bf50dc5..d46c7937d 100644 --- a/server/subsonic/browsing.go +++ b/server/subsonic/browsing.go @@ -424,7 +424,7 @@ func (api *Router) buildArtist(r *http.Request, artist *model.Artist) (*response return nil, err } - a.Album = slice.MapWithArg(albums, ctx, childFromAlbum) + a.Album = slice.MapWithArg(albums, ctx, buildAlbumID3) return a, nil } diff --git a/server/subsonic/filter/filters.go b/server/subsonic/filter/filters.go index 8a333c8e2..1b5416695 100644 --- a/server/subsonic/filter/filters.go +++ b/server/subsonic/filter/filters.go @@ -48,11 +48,11 @@ func AlbumsByArtist() Options { func AlbumsByArtistID(artistId string) Options { filters := []Sqlizer{ - persistence.Exists("json_tree(Participants, '$.albumartist')", Eq{"value": artistId}), + persistence.Exists("json_tree(participants, '$.albumartist')", Eq{"value": artistId}), } if conf.Server.Subsonic.ArtistParticipations { filters = append(filters, - persistence.Exists("json_tree(Participants, '$.artist')", Eq{"value": artistId}), + persistence.Exists("json_tree(participants, '$.artist')", Eq{"value": artistId}), ) } return addDefaultFilters(Options{ diff --git a/server/subsonic/responses/responses.go b/server/subsonic/responses/responses.go index c0f499ef2..0d22ef50b 100644 --- a/server/subsonic/responses/responses.go +++ b/server/subsonic/responses/responses.go @@ -284,7 +284,7 @@ type OpenSubsonicAlbumID3 struct { type ArtistWithAlbumsID3 struct { ArtistID3 - Album []Child `xml:"album" json:"album,omitempty"` + Album []AlbumID3 `xml:"album" json:"album,omitempty"` } type AlbumWithSongsID3 struct { From 1e1dce92b6a2a508f35a30d8ff8ac60274a780ce Mon Sep 17 00:00:00 2001 From: Xabi <888924+xabirequejo@users.noreply.github.com> Date: Sat, 22 Mar 2025 17:29:43 +0100 Subject: [PATCH 6/8] fix(ui): update Basque translation (#3864) * Update Basque localisation added missing strings * Update eu.json --- resources/i18n/eu.json | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/resources/i18n/eu.json b/resources/i18n/eu.json index a28e5751d..067310c14 100644 --- a/resources/i18n/eu.json +++ b/resources/i18n/eu.json @@ -216,6 +216,7 @@ "username": "Partekatzailea:", "url": "URLa", "description": "Deskribapena", + "downloadable": "Deskargatzea ahalbidetu?", "contents": "Edukia", "expiresAt": "Iraungitze-data:", "lastVisitedAt": "Azkenekoz bisitatu zen:", @@ -223,22 +224,24 @@ "format": "Formatua", "maxBitRate": "Gehienezko bit tasa", "updatedAt": "Eguneratze-data:", - "createdAt": "Sortze-data:", - "downloadable": "Deskargatzea ahalbidetu?" - } + "createdAt": "Sortze-data:" + }, + "notifications": {}, + "actions": {} }, "missing": { - "name": "", + "name": "Fitxategia falta da|||| Fitxategiak falta dira", + "empty": "Ez da fitxategirik falta", "fields": { - "path": "", - "size": "", - "updatedAt": "" + "path": "Bidea", + "size": "Tamaina", + "updatedAt": "Desagertze-data:" }, "actions": { - "remove": "" + "remove": "Kendu" }, "notifications": { - "removed": "" + "removed": "Faltan zeuden fitxategiak kendu dira" } } }, @@ -509,4 +512,4 @@ "current_song": "Uneko abestia" } } -} \ No newline at end of file +} From 63dc0e2062723171d2b54de3b7a232f5f6b6fb16 Mon Sep 17 00:00:00 2001 From: Nicolas Derive Date: Sat, 22 Mar 2025 17:31:32 +0100 Subject: [PATCH 7/8] =?UTF-8?q?fix(ui):=20update=20Fran=C3=A7ais,=20reorde?= =?UTF-8?q?r=20translation=20according=20to=20en.json=20template=20(#3839)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update french translation and reorder the file the same way as the en.json template, making comparison easier. --- resources/i18n/fr.json | 108 +++++++++++++++++++++++++++++------------ 1 file changed, 77 insertions(+), 31 deletions(-) diff --git a/resources/i18n/fr.json b/resources/i18n/fr.json index 7f8403bc3..50bc0d449 100644 --- a/resources/i18n/fr.json +++ b/resources/i18n/fr.json @@ -25,8 +25,13 @@ "quality": "Qualité", "bpm": "BPM", "playDate": "Derniers joués", - "channels": "Canaux", - "createdAt": "Date d'ajout" + "createdAt": "Date d'ajout", + "grouping": "Regroupement", + "mood": "Humeur", + "participants": "Participants supplémentaires", + "tags": "Étiquettes supplémentaires", + "mappedTags": "Étiquettes correspondantes", + "rawTags": "Étiquettes brutes" }, "actions": { "addToQueue": "Ajouter à la file", @@ -46,29 +51,35 @@ "duration": "Durée", "songCount": "Nombre de pistes", "playCount": "Nombre d'écoutes", + "size": "Taille", "name": "Nom", "genre": "Genre", "compilation": "Compilation", "year": "Année", + "originalDate": "Original", + "releaseDate": "Sortie", + "releases": "Sortie |||| Sorties", + "released": "Sortie", "updatedAt": "Mis à jour le", "comment": "Commentaire", "rating": "Classement", "createdAt": "Date d'ajout", - "size": "Taille", - "originalDate": "Original", - "releaseDate": "Sortie", - "releases": "Sortie |||| Sorties", - "released": "Sortie" + "recordLabel": "Label", + "catalogNum": "Numéro de catalogue", + "releaseType": "Type", + "grouping": "Regroupement", + "media": "Média", + "mood": "Humeur" }, "actions": { "playAll": "Lire", "playNext": "Lire ensuite", "addToQueue": "Ajouter à la file", + "share": "Partager", "shuffle": "Mélanger", "addToPlaylist": "Ajouter à la playlist", "download": "Télécharger", - "info": "Plus d'informations", - "share": "Partager" + "info": "Plus d'informations" }, "lists": { "all": "Tous", @@ -86,10 +97,26 @@ "name": "Nom", "albumCount": "Nombre d'albums", "songCount": "Nombre de pistes", + "size": "Taille", "playCount": "Lectures", "rating": "Classement", "genre": "Genre", - "size": "Taille" + "role": "Rôle" + }, + "roles": { + "albumartist": "Artiste de l'album |||| Artistes de l'album", + "artist": "Artiste |||| Artistes", + "composer": "Compositeur |||| Compositeurs", + "conductor": "Chef d'orchestre |||| Chefs d'orchestre", + "lyricist": "Parolier |||| Paroliers", + "arranger": "Arrangeur |||| Arrangeurs", + "producer": "Producteur |||| Producteurs", + "director": "Réalisateur |||| Réalisateurs", + "engineer": "Ingénieur |||| Ingénieurs", + "mixer": "Mixeur |||| Mixeurs", + "remixer": "Remixeur |||| Remixeurs", + "djmixer": "Mixeur DJ |||| Mixeurs DJ", + "performer": "Interprète |||| Interprètes" } }, "user": { @@ -98,6 +125,7 @@ "userName": "Nom d'utilisateur", "isAdmin": "Administrateur", "lastLoginAt": "Dernière connexion", + "lastAccessAt": "Dernier accès", "updatedAt": "Dernière mise à jour", "name": "Nom", "password": "Mot de passe", @@ -105,8 +133,7 @@ "changePassword": "Changer le mot de passe ?", "currentPassword": "Mot de passe actuel", "newPassword": "Nouveau mot de passe", - "token": "Token", - "lastAccessAt": "Dernier accès" + "token": "Token" }, "helperTexts": { "name": "Les changements liés à votre nom ne seront reflétés qu'à la prochaine connexion" @@ -152,7 +179,7 @@ "public": "Publique", "updatedAt": "Mise à jour le", "createdAt": "Créée le", - "songCount": "Titres", + "songCount": "Morceaux", "comment": "Commentaire", "sync": "Import automatique", "path": "Importer depuis" @@ -188,6 +215,7 @@ "username": "Partagé(e) par", "url": "Lien URL", "description": "Description", + "downloadable": "Autoriser les téléchargements ?", "contents": "Contenu", "expiresAt": "Expire le", "lastVisitedAt": "Visité pour la dernière fois", @@ -195,8 +223,24 @@ "format": "Format", "maxBitRate": "Bitrate maximum", "updatedAt": "Mis à jour le", - "createdAt": "Créé le", - "downloadable": "Autoriser les téléchargements ?" + "createdAt": "Créé le" + }, + "notifications": {}, + "actions": {} + }, + "missing": { + "name": "Fichier manquant|||| Fichiers manquants", + "empty": "Aucun fichier manquant", + "fields": { + "path": "Chemin", + "size": "Taille", + "updatedAt": "A disparu le" + }, + "actions": { + "remove": "Supprimer" + }, + "notifications": { + "removed": "Fichier(s) manquant(s) supprimé(s)" } } }, @@ -235,6 +279,7 @@ "add": "Ajouter", "back": "Retour", "bulk_actions": "%{smart_count} sélectionné |||| %{smart_count} sélectionnés", + "bulk_actions_mobile": "1 |||| %{smart_count}", "cancel": "Annuler", "clear_input_value": "Vider le champ", "clone": "Dupliquer", @@ -258,7 +303,6 @@ "close_menu": "Fermer le menu", "unselect": "Désélectionner", "skip": "Ignorer", - "bulk_actions_mobile": "1 |||| %{smart_count}", "share": "Partager", "download": "Télécharger" }, @@ -273,10 +317,10 @@ "error": "Un problème est survenu", "list": "%{name}", "loading": "Chargement", - "not_found": "Page manquante", + "not_found": "Introuvable", "show": "%{name} #%{id}", "empty": "Pas encore de %{name}.", - "invite": "Voulez-vous en créer ?" + "invite": "Voulez-vous en créer un ?" }, "input": { "file": { @@ -353,29 +397,31 @@ "noPlaylistsAvailable": "Aucune playlist", "delete_user_title": "Supprimer l'utilisateur '%{name}'", "delete_user_content": "Êtes-vous sûr(e) de vouloir supprimer cet utilisateur et ses données associées (y compris ses playlists et préférences) ?", + "remove_missing_title": "Supprimer les fichiers manquants", + "remove_missing_content": "Êtes-vous sûr(e) de vouloir supprimer les fichiers manquants sélectionnés de la base de données ? Ceci supprimera définiviement toute référence à ceux-ci, y compris leurs nombres d'écoutes et leurs notations.", "notifications_blocked": "Votre navigateur bloque les notifications de ce site", "notifications_not_available": "Votre navigateur ne permet pas d'afficher les notifications sur le bureau ou vous n'accédez pas à Navidrome via HTTPS", "lastfmLinkSuccess": "Last.fm a été correctement relié et le scrobble a été activé", "lastfmLinkFailure": "Last.fm n'a pas pu être correctement relié", "lastfmUnlinkSuccess": "Last.fm n'est plus relié et le scrobble a été désactivé", "lastfmUnlinkFailure": "Erreur pendant la suppression du lien avec Last.fm", + "listenBrainzLinkSuccess": "La liaison et le scrobble avec ListenBrainz sont maintenant activés pour l'utilisateur : %{user}", + "listenBrainzLinkFailure": "Échec lors de la liaison avec ListenBrainz : %{error}", + "listenBrainzUnlinkSuccess": "La liaison et le scrobble avec ListenBrainz sont maintenant désactivés", + "listenBrainzUnlinkFailure": "Échec lors de la désactivation de la liaison avec ListenBrainz", "openIn": { "lastfm": "Ouvrir dans Last.fm", "musicbrainz": "Ouvrir dans MusicBrainz" }, "lastfmLink": "Lire plus...", - "listenBrainzLinkSuccess": "La liaison et le scrobble avec ListenBrainz sont maintenant activés pour l'utilisateur : %{user}", - "listenBrainzLinkFailure": "Échec lors de la liaison avec ListenBrainz : %{error}", - "listenBrainzUnlinkSuccess": "La liaison et le scrobble avec ListenBrainz sont maintenant désactivés", - "listenBrainzUnlinkFailure": "Échec lors de la désactivation de la liaison avec ListenBrainz", - "downloadOriginalFormat": "Télécharger au format original", "shareOriginalFormat": "Partager avec le format original", "shareDialogTitle": "Partager %{resource} '%{name}'", "shareBatchDialogTitle": "Partager 1 %{resource} |||| Partager %{smart_count} %{resource}", + "shareCopyToClipboard": "Copier vers le presse-papier : Ctrl+C, Enter", "shareSuccess": "Lien copié vers le presse-papier : %{url}", "shareFailure": "Erreur en copiant le lien %{url} vers le presse-papier", "downloadDialogTitle": "Télécharger %{resource} '%{name}' (%{size})", - "shareCopyToClipboard": "Copier vers le presse-papier : Ctrl+C, Enter" + "downloadOriginalFormat": "Télécharger au format original" }, "menu": { "library": "Bibliothèque", @@ -389,6 +435,7 @@ "language": "Langue", "defaultView": "Vue par défaut", "desktop_notifications": "Notifications de bureau", + "lastfmNotConfigured": "La clef API de Last.fm n'est pas configurée", "lastfmScrobbling": "Scrobbler vers Last.fm", "listenBrainzScrobbling": "Scrobbler vers ListenBrainz", "replaygain": "Mode ReplayGain", @@ -397,14 +444,13 @@ "none": "Désactivé", "album": "Utiliser le gain de l'album", "track": "Utiliser le gain des pistes" - }, - "lastfmNotConfigured": "La clef API de Last.fm n'est pas configurée" + } } }, "albumList": "Albums", - "about": "À propos", "playlists": "Playlists", - "sharedPlaylists": "Playlists partagées" + "sharedPlaylists": "Playlists partagées", + "about": "À propos" }, "player": { "playListsText": "File de lecture", @@ -459,10 +505,10 @@ "toggle_play": "Lecture/Pause", "prev_song": "Morceau précédent", "next_song": "Morceau suivant", + "current_song": "Aller à la chanson en cours", "vol_up": "Augmenter le volume", "vol_down": "Baisser le volume", - "toggle_love": "Ajouter/Enlever le morceau des favoris", - "current_song": "Aller à la chanson en cours" + "toggle_love": "Ajouter/Enlever le morceau des favoris" } } -} \ No newline at end of file +} From be7cb59dc5255b845f491326ce936e7ab6165819 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Sat, 22 Mar 2025 12:34:35 -0400 Subject: [PATCH 8/8] fix(scanner): allow disabling splitting with the `Tags` config option (#3869) Signed-off-by: Deluan --- model/metadata/map_participants.go | 12 ++++++++++-- model/tag_mappings.go | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/model/metadata/map_participants.go b/model/metadata/map_participants.go index 9305d8791..a871f64fa 100644 --- a/model/metadata/map_participants.go +++ b/model/metadata/map_participants.go @@ -176,7 +176,11 @@ func (md Metadata) getRoleValues(role model.TagName) []string { if len(values) == 0 { return nil } - if conf := model.TagRolesConf(); len(conf.Split) > 0 { + conf := model.TagMainMappings()[role] + if conf.Split == nil { + conf = model.TagRolesConf() + } + if len(conf.Split) > 0 { values = conf.SplitTagValue(values) return filterDuplicatedOrEmptyValues(values) } @@ -193,7 +197,11 @@ func (md Metadata) getArtistValues(single, multi model.TagName) []string { if len(vSingle) != 1 { return vSingle } - if conf := model.TagArtistsConf(); len(conf.Split) > 0 { + conf := model.TagMainMappings()[single] + if conf.Split == nil { + conf = model.TagArtistsConf() + } + if len(conf.Split) > 0 { vSingle = conf.SplitTagValue(vSingle) return filterDuplicatedOrEmptyValues(vSingle) } diff --git a/model/tag_mappings.go b/model/tag_mappings.go index d8caa0c5d..d11b58fdc 100644 --- a/model/tag_mappings.go +++ b/model/tag_mappings.go @@ -201,7 +201,7 @@ func loadTagMappings() { aliases = oldValue.Aliases } split := cfg.Split - if len(split) == 0 { + if split == nil { split = oldValue.Split } c := TagConf{