navidrome/model/tag.go
Deluan c413d6510e fix(metadata): drop unused performer/plural-credit tag mappings
Per Copilot & Codex review on PR #5527:

- performercredit was registered as a plain string mapping, but PERFORMER
  is type:pair ('PERFORMER:instrument'). The credit form needs the same
  pair structure to be meaningful, and processPerformers doesn't read it
  either way. Better to remove the misleading mapping than ship dead
  config that suggests support we don't have.

- TagComposersCredit and TagLyricistsCredit (plural variants) were never
  consumed by any code path — only the singular TagComposerCredit and
  TagLyricistCredit are wired into roleMappings.

Both can be added back when the underlying feature is actually
implemented.
2026-05-24 22:24:06 -03:00

273 lines
7.6 KiB
Go

package model
import (
"cmp"
"crypto/md5"
"fmt"
"slices"
"strings"
"github.com/navidrome/navidrome/model/id"
"github.com/navidrome/navidrome/utils/slice"
)
type Tag struct {
ID string `json:"id,omitempty"`
TagName TagName `json:"tagName,omitempty"`
TagValue string `json:"tagValue,omitempty"`
AlbumCount int `json:"albumCount,omitempty"`
SongCount int `json:"songCount,omitempty"`
}
type TagList []Tag
func (l TagList) GroupByFrequency() Tags {
grouped := map[string]map[string]int{}
values := map[string]string{}
for _, t := range l {
if m, ok := grouped[string(t.TagName)]; !ok {
grouped[string(t.TagName)] = map[string]int{t.ID: 1}
} else {
m[t.ID]++
}
values[t.ID] = t.TagValue
}
tags := Tags{}
for name, counts := range grouped {
idList := make([]string, 0, len(counts))
for tid := range counts {
idList = append(idList, tid)
}
slices.SortFunc(idList, func(a, b string) int {
return cmp.Or(
cmp.Compare(counts[b], counts[a]),
cmp.Compare(values[a], values[b]),
)
})
tags[TagName(name)] = slice.Map(idList, func(id string) string { return values[id] })
}
return tags
}
func (t Tag) String() string {
return fmt.Sprintf("%s=%s", t.TagName, t.TagValue)
}
func NewTag(name TagName, value string) Tag {
name = name.ToLower()
hashID := tagID(name, value)
return Tag{
ID: hashID,
TagName: name,
TagValue: value,
}
}
func tagID(name TagName, value string) string {
return id.NewTagID(string(name), value)
}
type RawTags map[string][]string
type Tags map[TagName][]string
func (t Tags) Values(name TagName) []string {
return t[name]
}
func (t Tags) IDs() []string {
var ids []string
for name, tag := range t {
name = name.ToLower()
for _, v := range tag {
ids = append(ids, tagID(name, v))
}
}
return ids
}
func (t Tags) Flatten(name TagName) TagList {
var tags TagList
for _, v := range t[name] {
tags = append(tags, NewTag(name, v))
}
return tags
}
func (t Tags) FlattenAll() TagList {
var tags TagList
for name, values := range t {
for _, v := range values {
tags = append(tags, NewTag(name, v))
}
}
return tags
}
func (t Tags) Sort() {
for _, values := range t {
slices.Sort(values)
}
}
func (t Tags) Hash() []byte {
if len(t) == 0 {
return nil
}
ids := t.IDs()
slices.Sort(ids)
sum := md5.New()
sum.Write([]byte(strings.Join(ids, "|")))
return sum.Sum(nil)
}
func (t Tags) ToGenres() (string, Genres) {
values := t.Values("genre")
if len(values) == 0 {
return "", nil
}
genres := slice.Map(values, func(g string) Genre {
t := NewTag("genre", g)
return Genre{ID: t.ID, Name: g}
})
return genres[0].Name, genres
}
// Merge merges the tags from another Tags object into this one, removing any duplicates
func (t Tags) Merge(tags Tags) {
for name, values := range tags {
for _, v := range values {
t.Add(name, v)
}
}
}
func (t Tags) Add(name TagName, v string) {
if slices.Contains(t[name], v) {
return
}
t[name] = append(t[name], v)
}
type TagRepository interface {
Add(libraryID int, tags ...Tag) error
UpdateCounts() error
}
type TagName string
func (t TagName) ToLower() TagName {
return TagName(strings.ToLower(string(t)))
}
func (t TagName) String() string {
return string(t)
}
// Tag names, as defined in the mappings.yaml file
const (
TagAlbum TagName = "album"
TagTitle TagName = "title"
TagTrackNumber TagName = "track"
TagDiscNumber TagName = "disc"
TagTotalTracks TagName = "tracktotal"
TagTotalDiscs TagName = "disctotal"
TagDiscSubtitle TagName = "discsubtitle"
TagSubtitle TagName = "subtitle"
TagGenre TagName = "genre"
TagMood TagName = "mood"
TagComment TagName = "comment"
TagAlbumSort TagName = "albumsort"
TagAlbumVersion TagName = "albumversion"
TagTitleSort TagName = "titlesort"
TagCompilation TagName = "compilation"
TagGrouping TagName = "grouping"
TagLyrics TagName = "lyrics"
TagRecordLabel TagName = "recordlabel"
TagReleaseType TagName = "releasetype"
TagReleaseCountry TagName = "releasecountry"
TagMedia TagName = "media"
TagCatalogNumber TagName = "catalognumber"
TagISRC TagName = "isrc"
TagBPM TagName = "bpm"
TagExplicitStatus TagName = "explicitstatus"
// Dates and years
TagOriginalDate TagName = "originaldate"
TagReleaseDate TagName = "releasedate"
TagRecordingDate TagName = "recordingdate"
// Artists and roles
TagAlbumArtist TagName = "albumartist"
TagAlbumArtists TagName = "albumartists"
TagAlbumArtistSort TagName = "albumartistsort"
TagAlbumArtistsSort TagName = "albumartistssort"
TagAlbumArtistCredit TagName = "albumartistcredit"
TagAlbumArtistsCredit TagName = "albumartistscredit"
TagTrackArtist TagName = "artist"
TagTrackArtists TagName = "artists"
TagTrackArtistSort TagName = "artistsort"
TagTrackArtistsSort TagName = "artistssort"
TagTrackArtistCredit TagName = "artistcredit"
TagTrackArtistsCredit TagName = "artistscredit"
TagComposer TagName = "composer"
TagComposerSort TagName = "composersort"
TagComposerCredit TagName = "composercredit"
TagLyricist TagName = "lyricist"
TagLyricistSort TagName = "lyricistsort"
TagLyricistCredit TagName = "lyricistcredit"
TagDirector TagName = "director"
TagDirectorCredit TagName = "directorcredit"
TagProducer TagName = "producer"
TagProducerCredit TagName = "producercredit"
TagEngineer TagName = "engineer"
TagEngineerCredit TagName = "engineercredit"
TagMixer TagName = "mixer"
TagMixerCredit TagName = "mixercredit"
TagRemixer TagName = "remixer"
TagRemixerCredit TagName = "remixercredit"
TagDJMixer TagName = "djmixer"
TagDJMixerCredit TagName = "djmixercredit"
TagConductor TagName = "conductor"
TagConductorCredit TagName = "conductorcredit"
TagArranger TagName = "arranger"
TagArrangerCredit TagName = "arrangercredit"
TagPerformer TagName = "performer"
// Performer credits are not currently surfaced: performer tags are
// `type: pair` ('PERFORMER:instrument') and the matching credit
// representation hasn't been designed yet. Tracked separately.
// ReplayGain
TagReplayGainAlbumGain TagName = "replaygain_album_gain"
TagReplayGainAlbumPeak TagName = "replaygain_album_peak"
TagReplayGainTrackGain TagName = "replaygain_track_gain"
TagReplayGainTrackPeak TagName = "replaygain_track_peak"
TagR128AlbumGain TagName = "r128_album_gain"
TagR128TrackGain TagName = "r128_track_gain"
// MusicBrainz
TagMusicBrainzArtistID TagName = "musicbrainz_artistid"
TagMusicBrainzRecordingID TagName = "musicbrainz_recordingid"
TagMusicBrainzTrackID TagName = "musicbrainz_trackid"
TagMusicBrainzAlbumArtistID TagName = "musicbrainz_albumartistid"
TagMusicBrainzAlbumID TagName = "musicbrainz_albumid"
TagMusicBrainzReleaseGroupID TagName = "musicbrainz_releasegroupid"
TagMusicBrainzComposerID TagName = "musicbrainz_composerid"
TagMusicBrainzLyricistID TagName = "musicbrainz_lyricistid"
TagMusicBrainzDirectorID TagName = "musicbrainz_directorid"
TagMusicBrainzProducerID TagName = "musicbrainz_producerid"
TagMusicBrainzEngineerID TagName = "musicbrainz_engineerid"
TagMusicBrainzMixerID TagName = "musicbrainz_mixerid"
TagMusicBrainzRemixerID TagName = "musicbrainz_remixerid"
TagMusicBrainzDJMixerID TagName = "musicbrainz_djmixerid"
TagMusicBrainzConductorID TagName = "musicbrainz_conductorid"
TagMusicBrainzArrangerID TagName = "musicbrainz_arrangerid"
TagMusicBrainzPerformerID TagName = "musicbrainz_performerid"
)