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)') }}
|
{{ trans('prompts.noCover','No cover') }}
+
+
+
{{ displayCurrent(field) }}
|
-
+
+
+
+
+
+
+
+
+
+
+
+
|