fix(scanner): split comma-joined ABS sidecar genres into separate tags
Audiobookshelf-style exports join genres into one comma-string
(["Epic, Action & Adventure, Military, Sword & Sorcery"]). The
smart-genre picker already split candidates for the genre column,
but the tags assembly used the raw unsplit array — so the whole
string landed as ONE element of media_items.tags and rendered as a
single tag everywhere.
Use pickSmartGenre's split candidates for tags too, exactly as the
function's own comment documents ('split parts included stay in
tags'). Commas inside a single subject heading remain legal within
one tag (EPUB dc:subject path unchanged).
This commit is contained in:
1 parent
ecfd7276ab
commit
ab2a5568ae
2 files changed
+34
-3
No files matched your search
@@ -1884,11 +1884,17 @@ func extractAudiobookshelfSidecar(path string) *MediaMetadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Smart genre (A2c §2.5): most-specific canonical genre wins the genre
|
// Smart genre (A2c §2.5): most-specific canonical genre wins the genre
|
||||||
// column; every candidate (split parts included) stays in tags.
|
// column; every candidate (split parts included) stays in tags. The
|
||||||
if genre, _ := pickSmartGenre(sidecar.Genres); genre != "" {
|
// split is the point: ABS-style exports join genres into one
|
||||||
|
// comma-string ("Epic, Action & Adventure, Military") which must land
|
||||||
|
// as separate tags, while commas inside a single subject heading stay
|
||||||
|
// legal within one tag.
|
||||||
|
genre, all := pickSmartGenre(sidecar.Genres)
|
||||||
|
if genre != "" {
|
||||||
metadata.Genre = genre
|
metadata.Genre = genre
|
||||||
}
|
}
|
||||||
if tags := append(append([]string{}, sidecar.Genres...), sidecar.Tags...); len(tags) > 0 {
|
tags := append(append([]string{}, all...), sidecar.Tags...)
|
||||||
|
if len(tags) > 0 {
|
||||||
metadata.Tags = utils.NormalizeTags(tags)
|
metadata.Tags = utils.NormalizeTags(tags)
|
||||||
}
|
}
|
||||||
if sidecar.Language != nil {
|
if sidecar.Language != nil {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"image/jpeg"
|
"image/jpeg"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -196,6 +197,30 @@ func TestExtractAudiobookshelfSidecar(t *testing.T) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "joined genre string splits into separate tags (ABS export)",
|
||||||
|
json: `{"title": "Joined Genres", "genres": ["Epic, Action & Adventure, Military, Sword & Sorcery"]}`,
|
||||||
|
validate: func(t *testing.T, m *MediaMetadata) {
|
||||||
|
want := []string{"Epic", "Action & Adventure", "Military", "Sword & Sorcery"}
|
||||||
|
if len(m.Tags) != len(want) {
|
||||||
|
t.Fatalf("Tags = %v, want %v", m.Tags, want)
|
||||||
|
}
|
||||||
|
for i, w := range want {
|
||||||
|
if m.Tags[i] != w {
|
||||||
|
t.Errorf("Tags[%d] = %q, want %q", i, m.Tags[i], w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inGenre := false
|
||||||
|
for _, w := range want {
|
||||||
|
if strings.EqualFold(m.Genre, w) {
|
||||||
|
inGenre = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !inGenre || m.Genre == "" {
|
||||||
|
t.Errorf("Genre = %q, want a case-insensitive match of %v", m.Genre, want)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "sparse sidecar (real-world Audiobookshelf export)",
|
name: "sparse sidecar (real-world Audiobookshelf export)",
|
||||||
json: `{"title": "Sparse (1234)", "authors": ["X"], "tags": [], "description": null}`,
|
json: `{"title": "Sparse (1234)", "authors": ["X"], "tags": [], "description": null}`,
|
||||||
|
|||||||
Reference in new issue
Block a user