package metadata
import (
"cmp"
"context"
"encoding/json"
"maps"
"math"
"slices"
"strconv"
"strings"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/utils/str"
)
func (md Metadata) ToMediaFile(lib model.Library, folderID string) model.MediaFile {
mf := model.MediaFile{
LibraryID: lib.ID,
FolderID: folderID,
Tags: maps.Clone(md.tags),
}
mf.Title = md.mapTrackTitle()
mf.Album = md.mapAlbumName()
mf.SortTitle = md.String(model.TagTitleSort)
mf.SortAlbumName = md.String(model.TagAlbumSort)
mf.OrderTitle = str.SanitizeFieldForSorting(mf.Title)
mf.OrderAlbumName = str.SanitizeFieldForSortingNoArticle(mf.Album)
mf.Compilation = md.Bool(model.TagCompilation)
mf.TrackNumber, _ = md.NumAndTotal(model.TagTrackNumber)
mf.DiscNumber, _ = md.NumAndTotal(model.TagDiscNumber)
mf.DiscSubtitle = md.String(model.TagDiscSubtitle)
mf.CatalogNum = md.String(model.TagCatalogNumber)
mf.Comment = md.String(model.TagComment)
if f := md.NullableFloat(model.TagBPM); f != nil {
if r := math.Round(*f); r > 0 && r <= math.MaxInt32 {
mf.BPM = new(int(r))
}
}
mf.Lyrics = md.mapLyrics()
mf.ExplicitStatus = md.mapExplicitStatusTag()
md.mapMovementNumbers(mf.Tags)
date, origDate, relDate := md.mapDates()
mf.OriginalYear, mf.OriginalDate = origDate.Year(), string(origDate)
mf.ReleaseYear, mf.ReleaseDate = relDate.Year(), string(relDate)
mf.Year, mf.Date = date.Year(), string(date)
mf.MbzRecordingID = md.String(model.TagMusicBrainzRecordingID)
mf.MbzReleaseTrackID = md.String(model.TagMusicBrainzTrackID)
mf.MbzAlbumID = md.String(model.TagMusicBrainzAlbumID)
mf.MbzReleaseGroupID = md.String(model.TagMusicBrainzReleaseGroupID)
mf.MbzAlbumType = md.String(model.TagReleaseType)
mf.RGAlbumPeak = md.NullableFloat(model.TagReplayGainAlbumPeak)
mf.RGAlbumGain = md.mapGain(model.TagReplayGainAlbumGain, model.TagR128AlbumGain)
mf.RGTrackPeak = md.NullableFloat(model.TagReplayGainTrackPeak)
mf.RGTrackGain = md.mapGain(model.TagReplayGainTrackGain, model.TagR128TrackGain)
mf.HasCoverArt = md.HasPicture()
mf.Duration = md.Length()
mf.BitRate = md.AudioProperties().BitRate
mf.SampleRate = md.AudioProperties().SampleRate
if bd := md.AudioProperties().BitDepth; bd > 0 {
mf.BitDepth = new(bd)
}
mf.Channels = md.AudioProperties().Channels
mf.Codec = md.AudioProperties().Codec
mf.Path = md.FilePath()
mf.Suffix = md.Suffix()
mf.Size = md.Size()
mf.BirthTime = md.BirthTime()
mf.UpdatedAt = md.ModTime()
mf.Participants = md.mapParticipants()
mf.Artist = md.mapDisplayArtist()
mf.AlbumArtist = md.mapDisplayAlbumArtist(mf)
pid := lib.EffectivePID()
mf.PID = md.trackPID(mf, pid)
mf.AlbumID = md.albumID(mf, pid.Album)
mf.ArtistID = mf.Participants.First(model.RoleArtist).ID
mf.AlbumArtistID = mf.Participants.First(model.RoleAlbumArtist).ID
mf.OrderArtistName = mf.Participants.First(model.RoleArtist).OrderArtistName
mf.OrderAlbumArtistName = mf.Participants.First(model.RoleAlbumArtist).OrderArtistName
mf.SortArtistName = mf.Participants.First(model.RoleArtist).SortArtistName
mf.SortAlbumArtistName = mf.Participants.First(model.RoleAlbumArtist).SortArtistName
for tag, conf := range model.TagMainMappings() {
if !conf.Album {
delete(mf.Tags, tag)
}
}
return mf
}
func (md Metadata) AlbumID(mf model.MediaFile, pidConf string) string {
return md.albumID(mf, pidConf)
}
func (md Metadata) mapGain(rg, r128 model.TagName) *float64 {
v := md.Gain(rg)
if v != nil {
return v
}
r128value := md.String(r128)
if r128value != "" {
var v, err = strconv.Atoi(r128value)
if err != nil {
return nil
}
value := float64(v) / 256.0
value += 5
return &value
}
return nil
}
func (md Metadata) mapMovementNumbers(tags model.Tags) {
values := md.Strings(model.TagMovementNumber)
if !slices.ContainsFunc(values, func(v string) bool { return strings.Contains(v, "/") }) {
return
}
numbers := make([]string, len(values))
totals := make([]string, len(values))
allHaveTotals := true
for i, v := range values {
var found bool
numbers[i], totals[i], found = strings.Cut(v, "/")
allHaveTotals = allHaveTotals && found
}
tags[model.TagMovementNumber] = numbers
if allHaveTotals && len(md.Strings(model.TagMovementTotal)) == 0 {
tags[model.TagMovementTotal] = totals
}
}
func (md Metadata) mapLyrics() string {
rawLyrics := md.Pairs(model.TagLyrics)
lyricList := make(model.LyricList, 0, len(rawLyrics))
ctx := log.NewContext(context.Background(), "file", md.filePath)
for _, raw := range rawLyrics {
lang := raw.Key()
text := raw.Value()
lyrics, err := model.ParseLyrics(ctx, "", lang, []byte(text))
if err != nil {
log.Warn(ctx, "Unexpected failure occurred when parsing lyrics", err)
continue
}
for _, lyric := range lyrics {
if !lyric.IsEmpty() {
lyricList = append(lyricList, lyric)
}
}
}
res, err := json.Marshal(lyricList)
if err != nil {
log.Warn("Unexpected error occurred when serializing lyrics", "file", md.filePath, err)
return ""
}
return string(res)
}
func (md Metadata) mapExplicitStatusTag() string {
switch md.first(model.TagExplicitStatus) {
case "1", "4":
return "e"
case "2":
return "c"
default:
return ""
}
}
func (md Metadata) mapDates() (date Date, originalDate Date, releaseDate Date) {
date = md.Date(model.TagRecordingDate)
originalDate = md.Date(model.TagOriginalDate)
releaseDate = md.Date(model.TagReleaseDate)
legacyMappings := (originalDate != "") &&
(releaseDate == "") &&
(date >= originalDate)
if legacyMappings {
return originalDate, originalDate, date
}
date = cmp.Or(date, originalDate, releaseDate)
return date, originalDate, releaseDate
}