2022-12-19 13:59:24 -05:00
|
|
|
package model
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
2023-02-08 13:49:05 -05:00
|
|
|
"strconv"
|
2022-12-19 13:59:24 -05:00
|
|
|
"strings"
|
2023-02-08 13:49:05 -05:00
|
|
|
"time"
|
2022-12-19 13:59:24 -05:00
|
|
|
)
|
|
|
|
|
|
2023-01-17 17:26:48 -05:00
|
|
|
type Kind struct {
|
|
|
|
|
prefix string
|
|
|
|
|
name string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (k Kind) String() string {
|
|
|
|
|
return k.name
|
|
|
|
|
}
|
2022-12-19 13:59:24 -05:00
|
|
|
|
2026-07-22 20:25:17 -04:00
|
|
|
// Prefix is the short token used in artwork ids and the item_artwork.item_kind column.
|
|
|
|
|
func (k Kind) Prefix() string {
|
|
|
|
|
return k.prefix
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-19 13:59:24 -05:00
|
|
|
var (
|
2023-01-17 17:26:48 -05:00
|
|
|
KindMediaFileArtwork = Kind{"mf", "media_file"}
|
|
|
|
|
KindArtistArtwork = Kind{"ar", "artist"}
|
|
|
|
|
KindAlbumArtwork = Kind{"al", "album"}
|
|
|
|
|
KindPlaylistArtwork = Kind{"pl", "playlist"}
|
feat(artwork): add per-disc cover art support (#5182)
* feat(artwork): add KindDiscArtwork and ParseDiscArtworkID
Add new disc artwork kind with 'dc' prefix for per-disc cover art
support. The composite ID format is albumID:discNumber, parsed by
the new ParseDiscArtworkID helper.
* feat(conf): add DiscArtPriority configuration option
Default: 'disc*.*, cd*.*, embedded'. Controls how per-disc cover
art is resolved, following the same pattern as CoverArtPriority
and ArtistArtPriority.
* feat(artwork): implement extractDiscNumber helper
Extracts disc number from filenames based on glob patterns by
parsing leading digits from the wildcard-matched portion.
Used for matching disc-specific artwork files like disc1.jpg.
* feat(artwork): implement fromDiscExternalFile source function
Disc-aware variant of fromExternalFile that filters image files
by disc number (extracted from filename) or folder association
(for multi-folder albums).
* feat(artwork): implement discArtworkReader
Resolves disc artwork using DiscArtPriority config patterns.
Supports glob patterns with disc number extraction, embedded
images from first track, and falls back to album cover art.
Handles both multi-folder and single-folder multi-disc albums.
* feat(artwork): register disc artwork reader in dispatcher
Add KindDiscArtwork case to getArtworkReader switch, routing
disc artwork requests to the new discArtworkReader.
* feat(subsonic): add CoverArt field to DiscTitle response
Implements OpenSubsonic PR #220: optional cover art ID in
DiscTitle responses for per-disc artwork support.
* feat(subsonic): populate CoverArt in DiscTitle responses
Each DiscTitle now includes a disc artwork ID (dc-albumID:discNum)
that clients can use with getCoverArt to retrieve per-disc artwork.
* style: fix file permission in test to satisfy gosec
* feat(ui): add disc cover art display and lightbox functionality
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: simplify disc artwork code
- Add DiscArtworkID constructor to encapsulate the "albumID:discNumber"
format in one place
- Convert fromDiscExternalFile to a method on discArtworkReader,
reducing parameter count from 6 to 2
- Remove unused rootFolder field from discArtworkReader
* style: fix prettier formatting in subsonic index
* style(ui): move cursor style to makeStyles in SongDatagrid
* feat(artwork): add discsubtitle option to DiscArtPriority
Allow matching disc cover art by the disc's subtitle/name.
When the "discsubtitle" keyword is in the priority list, image files
whose stem matches the disc subtitle (case-insensitive) are used.
This is useful for box sets with named discs (e.g., "The Blue Disc.jpg").
* feat(configuration): update discartpriority to include cover art options
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-13 18:33:18 -04:00
|
|
|
KindDiscArtwork = Kind{"dc", "disc"}
|
2026-03-18 18:57:33 -04:00
|
|
|
KindRadioArtwork = Kind{"ra", "radio"}
|
2022-12-19 13:59:24 -05:00
|
|
|
)
|
|
|
|
|
|
2023-01-17 17:26:48 -05:00
|
|
|
var artworkKindMap = map[string]Kind{
|
|
|
|
|
KindMediaFileArtwork.prefix: KindMediaFileArtwork,
|
|
|
|
|
KindArtistArtwork.prefix: KindArtistArtwork,
|
|
|
|
|
KindAlbumArtwork.prefix: KindAlbumArtwork,
|
|
|
|
|
KindPlaylistArtwork.prefix: KindPlaylistArtwork,
|
feat(artwork): add per-disc cover art support (#5182)
* feat(artwork): add KindDiscArtwork and ParseDiscArtworkID
Add new disc artwork kind with 'dc' prefix for per-disc cover art
support. The composite ID format is albumID:discNumber, parsed by
the new ParseDiscArtworkID helper.
* feat(conf): add DiscArtPriority configuration option
Default: 'disc*.*, cd*.*, embedded'. Controls how per-disc cover
art is resolved, following the same pattern as CoverArtPriority
and ArtistArtPriority.
* feat(artwork): implement extractDiscNumber helper
Extracts disc number from filenames based on glob patterns by
parsing leading digits from the wildcard-matched portion.
Used for matching disc-specific artwork files like disc1.jpg.
* feat(artwork): implement fromDiscExternalFile source function
Disc-aware variant of fromExternalFile that filters image files
by disc number (extracted from filename) or folder association
(for multi-folder albums).
* feat(artwork): implement discArtworkReader
Resolves disc artwork using DiscArtPriority config patterns.
Supports glob patterns with disc number extraction, embedded
images from first track, and falls back to album cover art.
Handles both multi-folder and single-folder multi-disc albums.
* feat(artwork): register disc artwork reader in dispatcher
Add KindDiscArtwork case to getArtworkReader switch, routing
disc artwork requests to the new discArtworkReader.
* feat(subsonic): add CoverArt field to DiscTitle response
Implements OpenSubsonic PR #220: optional cover art ID in
DiscTitle responses for per-disc artwork support.
* feat(subsonic): populate CoverArt in DiscTitle responses
Each DiscTitle now includes a disc artwork ID (dc-albumID:discNum)
that clients can use with getCoverArt to retrieve per-disc artwork.
* style: fix file permission in test to satisfy gosec
* feat(ui): add disc cover art display and lightbox functionality
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: simplify disc artwork code
- Add DiscArtworkID constructor to encapsulate the "albumID:discNumber"
format in one place
- Convert fromDiscExternalFile to a method on discArtworkReader,
reducing parameter count from 6 to 2
- Remove unused rootFolder field from discArtworkReader
* style: fix prettier formatting in subsonic index
* style(ui): move cursor style to makeStyles in SongDatagrid
* feat(artwork): add discsubtitle option to DiscArtPriority
Allow matching disc cover art by the disc's subtitle/name.
When the "discsubtitle" keyword is in the priority list, image files
whose stem matches the disc subtitle (case-insensitive) are used.
This is useful for box sets with named discs (e.g., "The Blue Disc.jpg").
* feat(configuration): update discartpriority to include cover art options
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-13 18:33:18 -04:00
|
|
|
KindDiscArtwork.prefix: KindDiscArtwork,
|
2026-03-18 18:57:33 -04:00
|
|
|
KindRadioArtwork.prefix: KindRadioArtwork,
|
2022-12-27 17:05:40 -05:00
|
|
|
}
|
|
|
|
|
|
refactor(artwork): thread model.Kind through the artwork API
Entity-level artwork queries now take a typed model.Kind instead of a bare
prefix string. GetItemArtwork, DeleteForItem(s), GetInfoForItems,
EnqueueStaleAbsent, hydrateItemImages, enqueueBackfillKind and artwork.Refresh
convert to the prefix string only at the two real boundaries: the SQL
item_kind column (kind.Prefix() inside each repo) and external string inputs
(a new model.ParseKind for the nativeapi URL param, which also validates it).
The Backfill/stale-absent kind slices, the resolve.go dispatch switch, and the
kind→resource / kind→table lookup maps now use the Kind vars directly. The
queue lifecycle methods (MarkFailed/Delete*) keep string kinds — they operate
on a dequeued item's raw ItemKind column, which stays a string field, always
populated via kind.Prefix().
Removes every bare "al"/"ar"/… prefix literal from non-test code (27 -> 0);
behavior is unchanged.
2026-07-24 16:58:23 -04:00
|
|
|
// ParseKind resolves an item_kind prefix (e.g. "al") to its Kind, reporting whether it was known.
|
|
|
|
|
// Use it at string boundaries — URL params, the item_kind column — to enter the typed Kind domain.
|
|
|
|
|
func ParseKind(prefix string) (Kind, bool) {
|
|
|
|
|
k, ok := artworkKindMap[prefix]
|
|
|
|
|
return k, ok
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-19 13:59:24 -05:00
|
|
|
type ArtworkID struct {
|
2023-02-08 13:49:05 -05:00
|
|
|
Kind Kind
|
|
|
|
|
ID string
|
2026-07-22 20:06:13 -04:00
|
|
|
Hash string // content-hash suffix; "" = unknown/none
|
|
|
|
|
LastUpdate time.Time // legacy: populated only when parsing old _<hexTimestamp> tokens
|
2022-12-19 13:59:24 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (id ArtworkID) String() string {
|
2022-12-27 16:36:13 -05:00
|
|
|
if id.ID == "" {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
2023-02-08 13:49:05 -05:00
|
|
|
s := fmt.Sprintf("%s-%s", id.Kind.prefix, id.ID)
|
2026-07-22 20:06:13 -04:00
|
|
|
if id.Hash != "" {
|
|
|
|
|
return s + "_" + id.Hash
|
2023-02-08 13:49:05 -05:00
|
|
|
}
|
2026-07-22 20:06:13 -04:00
|
|
|
return s
|
2022-12-19 13:59:24 -05:00
|
|
|
}
|
|
|
|
|
|
2023-02-08 13:49:05 -05:00
|
|
|
func NewArtworkID(kind Kind, id string, lastUpdate *time.Time) ArtworkID {
|
2026-07-22 20:06:13 -04:00
|
|
|
artID := ArtworkID{Kind: kind, ID: id}
|
2023-02-08 13:49:05 -05:00
|
|
|
if lastUpdate != nil {
|
|
|
|
|
artID.LastUpdate = *lastUpdate
|
|
|
|
|
}
|
|
|
|
|
return artID
|
2022-12-28 12:32:46 -05:00
|
|
|
}
|
|
|
|
|
|
2022-12-19 13:59:24 -05:00
|
|
|
func ParseArtworkID(id string) (ArtworkID, error) {
|
2022-12-27 17:05:40 -05:00
|
|
|
parts := strings.SplitN(id, "-", 2)
|
2022-12-27 11:36:23 -05:00
|
|
|
if len(parts) != 2 {
|
2022-12-19 13:59:24 -05:00
|
|
|
return ArtworkID{}, errors.New("invalid artwork id")
|
|
|
|
|
}
|
2023-02-08 13:49:05 -05:00
|
|
|
kind, ok := artworkKindMap[parts[0]]
|
|
|
|
|
if !ok {
|
2022-12-19 13:59:24 -05:00
|
|
|
return ArtworkID{}, errors.New("invalid artwork kind")
|
|
|
|
|
}
|
2023-02-08 13:49:05 -05:00
|
|
|
parsedID := ArtworkID{
|
|
|
|
|
Kind: kind,
|
|
|
|
|
ID: parts[1],
|
|
|
|
|
}
|
|
|
|
|
parts = strings.SplitN(parts[1], "_", 2)
|
|
|
|
|
if len(parts) == 2 {
|
2026-07-22 20:06:13 -04:00
|
|
|
parsedID.ID = parts[0]
|
|
|
|
|
suffix := parts[1]
|
|
|
|
|
switch {
|
|
|
|
|
// Hash detection must come first: a 16-hex value with the high bit set overflows int64.
|
|
|
|
|
case isImageHash(suffix):
|
|
|
|
|
parsedID.Hash = suffix
|
|
|
|
|
case suffix != "0":
|
|
|
|
|
if lastUpdate, err := strconv.ParseInt(suffix, 16, 64); err == nil {
|
|
|
|
|
parsedID.LastUpdate = time.Unix(lastUpdate, 0)
|
2023-02-08 13:49:05 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return parsedID, nil
|
2022-12-19 13:59:24 -05:00
|
|
|
}
|
|
|
|
|
|
2026-07-22 20:06:13 -04:00
|
|
|
// isImageHash reports whether s is a 16-char lowercase-hex XXH3-64 content hash.
|
|
|
|
|
func isImageHash(s string) bool {
|
|
|
|
|
if len(s) != 16 {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
for _, c := range s {
|
|
|
|
|
if !(c >= '0' && c <= '9' || c >= 'a' && c <= 'f') {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-20 11:27:40 -05:00
|
|
|
func MustParseArtworkID(id string) ArtworkID {
|
|
|
|
|
artID, err := ParseArtworkID(id)
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic(artID)
|
|
|
|
|
}
|
|
|
|
|
return artID
|
|
|
|
|
}
|
|
|
|
|
|
feat(artwork): add per-disc cover art support (#5182)
* feat(artwork): add KindDiscArtwork and ParseDiscArtworkID
Add new disc artwork kind with 'dc' prefix for per-disc cover art
support. The composite ID format is albumID:discNumber, parsed by
the new ParseDiscArtworkID helper.
* feat(conf): add DiscArtPriority configuration option
Default: 'disc*.*, cd*.*, embedded'. Controls how per-disc cover
art is resolved, following the same pattern as CoverArtPriority
and ArtistArtPriority.
* feat(artwork): implement extractDiscNumber helper
Extracts disc number from filenames based on glob patterns by
parsing leading digits from the wildcard-matched portion.
Used for matching disc-specific artwork files like disc1.jpg.
* feat(artwork): implement fromDiscExternalFile source function
Disc-aware variant of fromExternalFile that filters image files
by disc number (extracted from filename) or folder association
(for multi-folder albums).
* feat(artwork): implement discArtworkReader
Resolves disc artwork using DiscArtPriority config patterns.
Supports glob patterns with disc number extraction, embedded
images from first track, and falls back to album cover art.
Handles both multi-folder and single-folder multi-disc albums.
* feat(artwork): register disc artwork reader in dispatcher
Add KindDiscArtwork case to getArtworkReader switch, routing
disc artwork requests to the new discArtworkReader.
* feat(subsonic): add CoverArt field to DiscTitle response
Implements OpenSubsonic PR #220: optional cover art ID in
DiscTitle responses for per-disc artwork support.
* feat(subsonic): populate CoverArt in DiscTitle responses
Each DiscTitle now includes a disc artwork ID (dc-albumID:discNum)
that clients can use with getCoverArt to retrieve per-disc artwork.
* style: fix file permission in test to satisfy gosec
* feat(ui): add disc cover art display and lightbox functionality
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: simplify disc artwork code
- Add DiscArtworkID constructor to encapsulate the "albumID:discNumber"
format in one place
- Convert fromDiscExternalFile to a method on discArtworkReader,
reducing parameter count from 6 to 2
- Remove unused rootFolder field from discArtworkReader
* style: fix prettier formatting in subsonic index
* style(ui): move cursor style to makeStyles in SongDatagrid
* feat(artwork): add discsubtitle option to DiscArtPriority
Allow matching disc cover art by the disc's subtitle/name.
When the "discsubtitle" keyword is in the priority list, image files
whose stem matches the disc subtitle (case-insensitive) are used.
This is useful for box sets with named discs (e.g., "The Blue Disc.jpg").
* feat(configuration): update discartpriority to include cover art options
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-13 18:33:18 -04:00
|
|
|
func DiscArtworkID(albumID string, discNumber int) string {
|
|
|
|
|
return fmt.Sprintf("%s:%d", albumID, discNumber)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func ParseDiscArtworkID(id string) (albumID string, discNumber int, err error) {
|
|
|
|
|
parts := strings.SplitN(id, ":", 2)
|
|
|
|
|
if len(parts) != 2 || parts[1] == "" {
|
|
|
|
|
return "", 0, errors.New("invalid disc artwork id")
|
|
|
|
|
}
|
|
|
|
|
num, err := strconv.Atoi(parts[1])
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", 0, fmt.Errorf("invalid disc number in artwork id: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return parts[0], num, nil
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-19 13:59:24 -05:00
|
|
|
func artworkIDFromAlbum(al Album) ArtworkID {
|
2026-07-22 20:06:13 -04:00
|
|
|
return ArtworkID{Kind: KindAlbumArtwork, ID: al.ID, Hash: al.ImageHash}
|
2022-12-19 13:59:24 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func artworkIDFromMediaFile(mf MediaFile) ArtworkID {
|
2026-07-22 20:06:13 -04:00
|
|
|
return ArtworkID{Kind: KindMediaFileArtwork, ID: mf.ID, Hash: mf.ImageHash}
|
2022-12-19 13:59:24 -05:00
|
|
|
}
|
2022-12-28 13:37:13 -05:00
|
|
|
|
|
|
|
|
func artworkIDFromPlaylist(pls Playlist) ArtworkID {
|
2026-07-22 20:06:13 -04:00
|
|
|
return ArtworkID{Kind: KindPlaylistArtwork, ID: pls.ID, Hash: pls.ImageHash}
|
2022-12-28 13:37:13 -05:00
|
|
|
}
|
2022-12-31 16:58:07 -05:00
|
|
|
|
|
|
|
|
func artworkIDFromArtist(ar Artist) ArtworkID {
|
2026-07-22 20:06:13 -04:00
|
|
|
return ArtworkID{Kind: KindArtistArtwork, ID: ar.ID, Hash: ar.ImageHash}
|
2022-12-31 16:58:07 -05:00
|
|
|
}
|
2026-03-18 18:57:33 -04:00
|
|
|
|
|
|
|
|
func artworkIDFromRadio(r Radio) ArtworkID {
|
2026-07-22 20:06:13 -04:00
|
|
|
return ArtworkID{Kind: KindRadioArtwork, ID: r.ID, Hash: r.ImageHash}
|
2026-03-18 18:57:33 -04:00
|
|
|
}
|