mirror of
https://github.com/navidrome/navidrome.git
synced 2025-04-01 19:47:37 +03:00
* fix displayArtist logic Signed-off-by: Deluan <deluan@navidrome.org> * remove unneeded value Signed-off-by: Deluan <deluan@navidrome.org> * refactor Signed-off-by: Deluan <deluan@navidrome.org> * Use first albumartist if it cannot figure out the display name Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org>
166 lines
4.9 KiB
Go
166 lines
4.9 KiB
Go
package metadata
|
|
|
|
import (
|
|
"encoding/json"
|
|
"maps"
|
|
"math"
|
|
"strconv"
|
|
|
|
"github.com/navidrome/navidrome/log"
|
|
"github.com/navidrome/navidrome/model"
|
|
"github.com/navidrome/navidrome/model/id"
|
|
"github.com/navidrome/navidrome/utils/str"
|
|
)
|
|
|
|
func (md Metadata) ToMediaFile(libID int, folderID string) model.MediaFile {
|
|
mf := model.MediaFile{
|
|
LibraryID: libID,
|
|
FolderID: folderID,
|
|
Tags: maps.Clone(md.tags),
|
|
}
|
|
|
|
// Title and Album
|
|
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)
|
|
|
|
// Disc and Track info
|
|
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)
|
|
mf.BPM = int(math.Round(md.Float(model.TagBPM)))
|
|
mf.Lyrics = md.mapLyrics()
|
|
mf.ExplicitStatus = md.mapExplicitStatusTag()
|
|
|
|
// Dates
|
|
origDate := md.Date(model.TagOriginalDate)
|
|
mf.OriginalYear, mf.OriginalDate = origDate.Year(), string(origDate)
|
|
relDate := md.Date(model.TagReleaseDate)
|
|
mf.ReleaseYear, mf.ReleaseDate = relDate.Year(), string(relDate)
|
|
date := md.Date(model.TagRecordingDate)
|
|
mf.Year, mf.Date = date.Year(), string(date)
|
|
|
|
// MBIDs
|
|
mf.MbzRecordingID = md.String(model.TagMusicBrainzRecordingID)
|
|
mf.MbzReleaseTrackID = md.String(model.TagMusicBrainzTrackID)
|
|
mf.MbzAlbumID = md.String(model.TagMusicBrainzAlbumID)
|
|
mf.MbzReleaseGroupID = md.String(model.TagMusicBrainzReleaseGroupID)
|
|
|
|
// ReplayGain
|
|
mf.RGAlbumPeak = md.Float(model.TagReplayGainAlbumPeak, 1)
|
|
mf.RGAlbumGain = md.mapGain(model.TagReplayGainAlbumGain, model.TagR128AlbumGain)
|
|
mf.RGTrackPeak = md.Float(model.TagReplayGainTrackPeak, 1)
|
|
mf.RGTrackGain = md.mapGain(model.TagReplayGainTrackGain, model.TagR128TrackGain)
|
|
|
|
// General properties
|
|
mf.HasCoverArt = md.HasPicture()
|
|
mf.Duration = md.Length()
|
|
mf.BitRate = md.AudioProperties().BitRate
|
|
mf.SampleRate = md.AudioProperties().SampleRate
|
|
mf.BitDepth = md.AudioProperties().BitDepth
|
|
mf.Channels = md.AudioProperties().Channels
|
|
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)
|
|
|
|
// Persistent IDs
|
|
mf.PID = md.trackPID(mf)
|
|
mf.AlbumID = md.albumID(mf)
|
|
|
|
// BFR These IDs will go away once the UI handle multiple participants.
|
|
// BFR For Legacy Subsonic compatibility, we will set them in the API handlers
|
|
mf.ArtistID = mf.Participants.First(model.RoleArtist).ID
|
|
mf.AlbumArtistID = mf.Participants.First(model.RoleAlbumArtist).ID
|
|
|
|
// BFR What to do with sort/order artist names?
|
|
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
|
|
|
|
// Don't store tags that are first-class fields (and are not album-level tags) in the
|
|
// MediaFile struct. This is to avoid redundancy in the DB
|
|
//
|
|
// Remove all tags from the main section that are not flagged as album tags
|
|
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 {
|
|
getPID := createGetPID(id.NewHash)
|
|
return getPID(mf, md, pidConf)
|
|
}
|
|
|
|
func (md Metadata) mapGain(rg, r128 model.TagName) float64 {
|
|
v := md.Gain(rg)
|
|
if v != 0 {
|
|
return v
|
|
}
|
|
r128value := md.String(r128)
|
|
if r128value != "" {
|
|
var v, err = strconv.Atoi(r128value)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
// Convert Q7.8 to float
|
|
var value = float64(v) / 256.0
|
|
// Adding 5 dB to normalize with ReplayGain level
|
|
return value + 5
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (md Metadata) mapLyrics() string {
|
|
rawLyrics := md.Pairs(model.TagLyrics)
|
|
|
|
lyricList := make(model.LyricList, 0, len(rawLyrics))
|
|
|
|
for _, raw := range rawLyrics {
|
|
lang := raw.Key()
|
|
text := raw.Value()
|
|
|
|
lyrics, err := model.ToLyrics(lang, text)
|
|
if err != nil {
|
|
log.Warn("Unexpected failure occurred when parsing lyrics", "file", md.filePath, err)
|
|
continue
|
|
}
|
|
if !lyrics.IsEmpty() {
|
|
lyricList = append(lyricList, *lyrics)
|
|
}
|
|
}
|
|
|
|
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 ""
|
|
}
|
|
}
|