From 8993c8031ad5cd3f3da7cc5738ecaf41e95d31b4 Mon Sep 17 00:00:00 2001 From: joelilas Date: Sun, 8 Feb 2026 16:50:29 +0100 Subject: [PATCH] fix: multivalue tags --- Dockerfile.full | 2 +- files/tags.go | 121 +++++++++ .../src/components/prompts/ModifyMetadata.vue | 241 +++++++++++++++++- frontend/src/utils/metadata.ts | 30 ++- http/data.go | 34 ++- http/metadata.go | 24 +- http/resource.go | 214 +++++++++++++++- 7 files changed, 640 insertions(+), 26 deletions(-) diff --git a/Dockerfile.full b/Dockerfile.full index 79e0ace9..5251a956 100644 --- a/Dockerfile.full +++ b/Dockerfile.full @@ -54,7 +54,7 @@ ENV GID=1000 RUN addgroup -g $GID user && \ adduser -D -u $UID -G user user && \ - apk add --no-cache ffmpeg + apk add --no-cache ffmpeg flac # Copy compiled binary from backend-builder COPY --from=backend-builder --chown=user:user /app/filebrowser /bin/filebrowser diff --git a/files/tags.go b/files/tags.go index 9ce74516..cb871e6f 100644 --- a/files/tags.go +++ b/files/tags.go @@ -5,6 +5,8 @@ import ( "encoding/base64" "io" "log" + "os/exec" + "path/filepath" "strings" "github.com/dhowden/tag" @@ -13,6 +15,7 @@ import ( // ReadAudioTags extracts common audio metadata from the file and stores it in // the FileInfo.Tags map. It attempts to extract common fields and includes // embedded picture(s) as base64 where present. +// Multi-valued tags like ARTISTS and ALBUMARTISTS are extracted as arrays. func (i *FileInfo) ReadAudioTags() error { if i.IsDir { return nil @@ -76,7 +79,20 @@ func (i *FileInfo) ReadAudioTags() error { } // Keep raw metadata map if available (some formats expose additional fields) + // Also extract multi-valued tags like ARTISTS and ALBUMARTISTS if raw := m.Raw(); raw != nil { + // First pass: collect multi-valued tags (ARTISTS, ALBUMARTISTS) + artists := extractMultiValuedTag(raw, "ARTISTS", "artists") + albumArtists := extractMultiValuedTag(raw, "ALBUMARTISTS", "albumartists") + + // Store multi-valued tags as arrays if present + if len(artists) > 0 { + tags["artists"] = artists + } + if len(albumArtists) > 0 { + tags["albumartists"] = albumArtists + } + for k, v := range raw { // Skip raw APIC entries (attached picture frame) to avoid // exposing large binary blobs. We already expose a friendly @@ -84,6 +100,11 @@ func (i *FileInfo) ReadAudioTags() error { if strings.EqualFold(k, "APIC") { continue } + // Skip multi-valued tags we already handled + kLower := strings.ToLower(k) + if kLower == "artists" || kLower == "albumartists" { + continue + } // avoid overwriting already set common fields if _, ok := tags[k]; !ok { tags[k] = v @@ -91,6 +112,19 @@ func (i *FileInfo) ReadAudioTags() error { } } + // For FLAC files, use metaflac to read multi-valued tags properly + // since dhowden/tag doesn't handle them correctly + if realPath := i.RealPath(); realPath != "" && strings.EqualFold(filepath.Ext(realPath), ".flac") { + if multiTags := readMultiValuedTagsWithMetaflac(realPath); multiTags != nil { + if artists, ok := multiTags["ARTISTS"]; ok && len(artists) > 0 { + tags["artists"] = artists + } + if albumArtists, ok := multiTags["ALBUMARTISTS"]; ok && len(albumArtists) > 0 { + tags["albumartists"] = albumArtists + } + } + } + // Attach tags map if len(tags) > 0 { i.Tags = tags @@ -98,3 +132,90 @@ func (i *FileInfo) ReadAudioTags() error { return nil } + +// extractMultiValuedTag extracts values for a multi-valued tag from raw metadata. +// It handles both Vorbis-style (multiple entries with same key) and ID3-style +// (single string that may need splitting) formats. +func extractMultiValuedTag(raw map[string]interface{}, keys ...string) []string { + var result []string + seen := make(map[string]bool) + + for _, key := range keys { + for rawKey, rawVal := range raw { + if !strings.EqualFold(rawKey, key) { + continue + } + // Handle different value types returned by the tag library + switch v := rawVal.(type) { + case string: + v = strings.TrimSpace(v) + if v != "" && !seen[v] { + result = append(result, v) + seen[v] = true + } + case []string: + for _, s := range v { + s = strings.TrimSpace(s) + if s != "" && !seen[s] { + result = append(result, s) + seen[s] = true + } + } + case []interface{}: + for _, item := range v { + if s, ok := item.(string); ok { + s = strings.TrimSpace(s) + if s != "" && !seen[s] { + result = append(result, s) + seen[s] = true + } + } + } + } + } + } + return result +} + +// readMultiValuedTagsWithMetaflac uses metaflac to read Vorbis comments from FLAC files. +// This properly handles multi-valued tags (multiple entries with same key). +func readMultiValuedTagsWithMetaflac(filepath string) map[string][]string { + if _, err := exec.LookPath("metaflac"); err != nil { + return nil + } + + // Use metaflac to export all tags + cmd := exec.Command("metaflac", "--export-tags-to=-", filepath) + out, err := cmd.Output() + if err != nil { + log.Printf("metaflac export failed for %s: %v", filepath, err) + return nil + } + + // Parse output: each line is TAG=VALUE + result := make(map[string][]string) + for _, line := range strings.Split(string(out), "\n") { + line = strings.TrimSpace(line) + if line == "" { + continue + } + idx := strings.Index(line, "=") + if idx < 0 { + continue + } + key := strings.ToUpper(strings.TrimSpace(line[:idx])) + value := strings.TrimSpace(line[idx+1:]) + if value == "" { + continue + } + // Collect multi-valued artist tags (ARTISTS, ARTIST, ALBUMARTISTS) + // Normalize key variations + if key == "ARTISTS" || key == "ARTIST" { + result["ARTISTS"] = append(result["ARTISTS"], value) + } else if key == "ALBUMARTISTS" || key == "ALBUMARTIST" || key == "ALBUM ARTIST" || key == "ALBUM_ARTIST" { + result["ALBUMARTISTS"] = append(result["ALBUMARTISTS"], value) + } + } + + return result +} diff --git a/frontend/src/components/prompts/ModifyMetadata.vue b/frontend/src/components/prompts/ModifyMetadata.vue index c8eb917b..394a2a4b 100644 --- a/frontend/src/components/prompts/ModifyMetadata.vue +++ b/frontend/src/components/prompts/ModifyMetadata.vue @@ -86,23 +86,64 @@ {{ fieldLabel(field) }} + {{ trans('prompts.multiValueHint','(multi-valued)') }} +
- + +