mirror of
https://github.com/navidrome/navidrome.git
synced 2025-07-13 23:21:21 +03:00
* docs: add testing and logging guidelines to AGENTS.md Signed-off-by: Deluan <deluan@navidrome.org> * Introduce TagISRC and update ISRC handling * fix: update .gitignore to exclude executable files and bin directory Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org>
258 lines
6.7 KiB
Go
258 lines
6.7 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"`
|
|
MediaFileCount 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) {
|
|
for _, existing := range t[name] {
|
|
if existing == v {
|
|
return
|
|
}
|
|
}
|
|
t[name] = append(t[name], v)
|
|
}
|
|
|
|
type TagRepository interface {
|
|
Add(...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"
|
|
TagTrackArtist TagName = "artist"
|
|
TagTrackArtists TagName = "artists"
|
|
TagTrackArtistSort TagName = "artistsort"
|
|
TagTrackArtistsSort TagName = "artistssort"
|
|
TagComposer TagName = "composer"
|
|
TagComposerSort TagName = "composersort"
|
|
TagLyricist TagName = "lyricist"
|
|
TagLyricistSort TagName = "lyricistsort"
|
|
TagDirector TagName = "director"
|
|
TagProducer TagName = "producer"
|
|
TagEngineer TagName = "engineer"
|
|
TagMixer TagName = "mixer"
|
|
TagRemixer TagName = "remixer"
|
|
TagDJMixer TagName = "djmixer"
|
|
TagConductor TagName = "conductor"
|
|
TagArranger TagName = "arranger"
|
|
TagPerformer TagName = "performer"
|
|
|
|
// 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"
|
|
)
|