🏡 index : quine-global/quintodrome.git

package model

import (
	"errors"
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/navidrome/navidrome/utils/slice"
)

type Kind struct {
	prefix string
	name   string
}

func (k Kind) String() string {
	return k.name
}

// Prefix is the short token used in artwork ids and the item_artwork.item_kind column.
func (k Kind) Prefix() string {
	return k.prefix
}

var (
	KindMediaFileArtwork = Kind{"mf", "media_file"}
	KindArtistArtwork    = Kind{"ar", "artist"}
	KindAlbumArtwork     = Kind{"al", "album"}
	KindPlaylistArtwork  = Kind{"pl", "playlist"}
	KindDiscArtwork      = Kind{"dc", "disc"}
	KindRadioArtwork     = Kind{"ra", "radio"}
)

var artworkKindMap = map[string]Kind{
	KindMediaFileArtwork.prefix: KindMediaFileArtwork,
	KindArtistArtwork.prefix:    KindArtistArtwork,
	KindAlbumArtwork.prefix:     KindAlbumArtwork,
	KindPlaylistArtwork.prefix:  KindPlaylistArtwork,
	KindDiscArtwork.prefix:      KindDiscArtwork,
	KindRadioArtwork.prefix:     KindRadioArtwork,
}

// KindPrefixes leaves the typed Kind domain for the item_kind column, or for a help string.
func KindPrefixes(kinds []Kind) []string {
	return slice.Map(kinds, func(k Kind) string { return k.prefix })
}

// 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
}

type ArtworkID struct {
	Kind       Kind
	ID         string
	Hash       string    // content-hash suffix; "" = unknown/none
	LastUpdate time.Time // legacy: populated only when parsing old _<hexTimestamp> tokens
}

func (id ArtworkID) String() string {
	if id.ID == "" {
		return ""
	}
	s := fmt.Sprintf("%s-%s", id.Kind.prefix, id.ID)
	if id.Hash != "" {
		return s + "_" + id.Hash
	}
	return s
}

func NewArtworkID(kind Kind, id string, lastUpdate *time.Time) ArtworkID {
	artID := ArtworkID{Kind: kind, ID: id}
	if lastUpdate != nil {
		artID.LastUpdate = *lastUpdate
	}
	return artID
}

func ParseArtworkID(id string) (ArtworkID, error) {
	parts := strings.SplitN(id, "-", 2)
	if len(parts) != 2 {
		return ArtworkID{}, errors.New("invalid artwork id")
	}
	kind, ok := artworkKindMap[parts[0]]
	if !ok {
		return ArtworkID{}, errors.New("invalid artwork kind")
	}
	parsedID := ArtworkID{
		Kind: kind,
		ID:   parts[1],
	}
	parts = strings.SplitN(parts[1], "_", 2)
	if len(parts) == 2 {
		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)
			}
		}
	}
	return parsedID, nil
}

// 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
}

func MustParseArtworkID(id string) ArtworkID {
	artID, err := ParseArtworkID(id)
	if err != nil {
		panic(artID)
	}
	return artID
}

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
}

func artworkIDFromAlbum(al Album) ArtworkID {
	return ArtworkID{Kind: KindAlbumArtwork, ID: al.ID, Hash: al.ImageHash}
}

func artworkIDFromMediaFile(mf MediaFile) ArtworkID {
	return ArtworkID{Kind: KindMediaFileArtwork, ID: mf.ID, Hash: mf.ImageHash}
}

func artworkIDFromPlaylist(pls Playlist) ArtworkID {
	return ArtworkID{Kind: KindPlaylistArtwork, ID: pls.ID, Hash: pls.ImageHash}
}

func artworkIDFromArtist(ar Artist) ArtworkID {
	return ArtworkID{Kind: KindArtistArtwork, ID: ar.ID, Hash: ar.ImageHash}
}

func artworkIDFromRadio(r Radio) ArtworkID {
	return ArtworkID{Kind: KindRadioArtwork, ID: r.ID, Hash: r.ImageHash}
}