mirror of
https://github.com/navidrome/navidrome.git
synced 2025-04-03 20:47:35 +03:00
Moved logic of collapsing songs into albums to model package
(it should really be called domain.... maybe will rename it later)
This commit is contained in:
parent
e03ccb3166
commit
28e7371d93
13 changed files with 507 additions and 26 deletions
|
@ -2,7 +2,14 @@ package model
|
|||
|
||||
import (
|
||||
"mime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/consts"
|
||||
"github.com/navidrome/navidrome/utils"
|
||||
"github.com/navidrome/navidrome/utils/number"
|
||||
"github.com/navidrome/navidrome/utils/slice"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
type MediaFile struct {
|
||||
|
@ -61,6 +68,107 @@ func (mf *MediaFile) ContentType() string {
|
|||
|
||||
type MediaFiles []MediaFile
|
||||
|
||||
func (mfs MediaFiles) ToAlbum() Album {
|
||||
a := Album{SongCount: len(mfs)}
|
||||
var fullText []string
|
||||
var albumArtistIds []string
|
||||
var songArtistIds []string
|
||||
var mbzAlbumIds []string
|
||||
var comments []string
|
||||
for _, m := range mfs {
|
||||
// We assume these attributes are all the same for all songs on an album
|
||||
a.ID = m.AlbumID
|
||||
a.Name = m.Album
|
||||
a.Artist = m.Artist
|
||||
a.ArtistID = m.ArtistID
|
||||
a.AlbumArtist = m.AlbumArtist
|
||||
a.AlbumArtistID = m.AlbumArtistID
|
||||
a.SortAlbumName = m.SortAlbumName
|
||||
a.SortArtistName = m.SortArtistName
|
||||
a.SortAlbumArtistName = m.SortAlbumArtistName
|
||||
a.OrderAlbumName = m.OrderAlbumName
|
||||
a.OrderAlbumArtistName = m.OrderAlbumArtistName
|
||||
a.MbzAlbumArtistID = m.MbzAlbumArtistID
|
||||
a.MbzAlbumType = m.MbzAlbumType
|
||||
a.MbzAlbumComment = m.MbzAlbumComment
|
||||
a.CatalogNum = m.CatalogNum
|
||||
a.Compilation = m.Compilation
|
||||
|
||||
// Calculated attributes based on aggregations
|
||||
a.Duration += m.Duration
|
||||
a.Size += m.Size
|
||||
if a.MinYear == 0 {
|
||||
a.MinYear = m.Year
|
||||
} else if m.Year > 0 {
|
||||
a.MinYear = number.Min(a.MinYear, m.Year)
|
||||
}
|
||||
a.MaxYear = number.Max(m.Year)
|
||||
a.UpdatedAt = newer(a.UpdatedAt, m.UpdatedAt)
|
||||
a.CreatedAt = older(a.CreatedAt, m.CreatedAt)
|
||||
a.Genres = append(a.Genres, m.Genres...)
|
||||
comments = append(comments, m.Comment)
|
||||
albumArtistIds = append(albumArtistIds, m.AlbumArtistID)
|
||||
songArtistIds = append(songArtistIds, m.ArtistID)
|
||||
mbzAlbumIds = append(mbzAlbumIds, m.MbzAlbumID)
|
||||
fullText = append(fullText,
|
||||
m.Album, m.AlbumArtist, m.Artist,
|
||||
m.SortAlbumName, m.SortAlbumArtistName, m.SortArtistName,
|
||||
m.DiscSubtitle)
|
||||
if m.HasCoverArt {
|
||||
// TODO CoverArtPriority
|
||||
a.CoverArtId = m.ID
|
||||
}
|
||||
}
|
||||
comments = slices.Compact(comments)
|
||||
if len(comments) == 1 {
|
||||
a.Comment = comments[0]
|
||||
}
|
||||
a.Genre = slice.MostFrequent(a.Genres).Name
|
||||
slices.SortFunc(a.Genres, func(a, b Genre) bool { return a.ID < b.ID })
|
||||
a.Genres = slices.Compact(a.Genres)
|
||||
a.FullText = " " + utils.SanitizeStrings(fullText...)
|
||||
a = fixAlbumArtist(a, albumArtistIds)
|
||||
songArtistIds = append(songArtistIds, a.AlbumArtistID, a.ArtistID)
|
||||
slices.Sort(songArtistIds)
|
||||
a.AllArtistIDs = strings.Join(slices.Compact(songArtistIds), " ")
|
||||
a.MbzAlbumID = slice.MostFrequent(mbzAlbumIds)
|
||||
return a
|
||||
}
|
||||
|
||||
func newer(t1, t2 time.Time) time.Time {
|
||||
if t1.After(t2) {
|
||||
return t1
|
||||
}
|
||||
return t2
|
||||
}
|
||||
|
||||
func older(t1, t2 time.Time) time.Time {
|
||||
if t1.IsZero() {
|
||||
return t2
|
||||
}
|
||||
if t1.After(t2) {
|
||||
return t2
|
||||
}
|
||||
return t1
|
||||
}
|
||||
|
||||
func fixAlbumArtist(a Album, albumArtistIds []string) Album {
|
||||
if !a.Compilation {
|
||||
if a.AlbumArtistID == "" {
|
||||
a.AlbumArtistID = a.ArtistID
|
||||
a.AlbumArtist = a.Artist
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
albumArtistIds = slices.Compact(albumArtistIds)
|
||||
if len(albumArtistIds) > 1 {
|
||||
a.AlbumArtist = consts.VariousArtists
|
||||
a.AlbumArtistID = consts.VariousArtistsID
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
type MediaFileRepository interface {
|
||||
CountAll(options ...QueryOptions) (int64, error)
|
||||
Exists(id string) (bool, error)
|
||||
|
|
53
model/mediafile_internal_test.go
Normal file
53
model/mediafile_internal_test.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"github.com/navidrome/navidrome/consts"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("fixAlbumArtist", func() {
|
||||
var album Album
|
||||
BeforeEach(func() {
|
||||
album = Album{}
|
||||
})
|
||||
Context("Non-Compilations", func() {
|
||||
BeforeEach(func() {
|
||||
album.Compilation = false
|
||||
album.Artist = "Sparks"
|
||||
album.ArtistID = "ar-123"
|
||||
})
|
||||
It("returns the track artist if no album artist is specified", func() {
|
||||
al := fixAlbumArtist(album, nil)
|
||||
Expect(al.AlbumArtistID).To(Equal("ar-123"))
|
||||
Expect(al.AlbumArtist).To(Equal("Sparks"))
|
||||
})
|
||||
It("returns the album artist if it is specified", func() {
|
||||
album.AlbumArtist = "Sparks Brothers"
|
||||
album.AlbumArtistID = "ar-345"
|
||||
al := fixAlbumArtist(album, nil)
|
||||
Expect(al.AlbumArtistID).To(Equal("ar-345"))
|
||||
Expect(al.AlbumArtist).To(Equal("Sparks Brothers"))
|
||||
})
|
||||
})
|
||||
Context("Compilations", func() {
|
||||
BeforeEach(func() {
|
||||
album.Compilation = true
|
||||
album.Name = "Sgt. Pepper Knew My Father"
|
||||
album.AlbumArtistID = "ar-000"
|
||||
album.AlbumArtist = "The Beatles"
|
||||
})
|
||||
|
||||
It("returns VariousArtists if there's more than one album artist", func() {
|
||||
al := fixAlbumArtist(album, []string{"ar-123", "ar-345"})
|
||||
Expect(al.AlbumArtistID).To(Equal(consts.VariousArtistsID))
|
||||
Expect(al.AlbumArtist).To(Equal(consts.VariousArtists))
|
||||
})
|
||||
|
||||
It("returns the sole album artist if they are the same", func() {
|
||||
al := fixAlbumArtist(album, []string{"ar-000", "ar-000"})
|
||||
Expect(al.AlbumArtistID).To(Equal("ar-000"))
|
||||
Expect(al.AlbumArtist).To(Equal("The Beatles"))
|
||||
})
|
||||
})
|
||||
})
|
219
model/mediafile_test.go
Normal file
219
model/mediafile_test.go
Normal file
|
@ -0,0 +1,219 @@
|
|||
package model_test
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
. "github.com/navidrome/navidrome/model"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("MediaFiles", func() {
|
||||
var mfs MediaFiles
|
||||
|
||||
Context("Simple attributes", func() {
|
||||
BeforeEach(func() {
|
||||
mfs = MediaFiles{
|
||||
{
|
||||
ID: "1", AlbumID: "AlbumID", Album: "Album", ArtistID: "ArtistID", Artist: "Artist", AlbumArtistID: "AlbumArtistID", AlbumArtist: "AlbumArtist",
|
||||
SortAlbumName: "SortAlbumName", SortArtistName: "SortArtistName", SortAlbumArtistName: "SortAlbumArtistName",
|
||||
OrderAlbumName: "OrderAlbumName", OrderAlbumArtistName: "OrderAlbumArtistName",
|
||||
MbzAlbumArtistID: "MbzAlbumArtistID", MbzAlbumType: "MbzAlbumType", MbzAlbumComment: "MbzAlbumComment",
|
||||
Compilation: false, CatalogNum: "",
|
||||
},
|
||||
{
|
||||
ID: "2", Album: "Album", ArtistID: "ArtistID", Artist: "Artist", AlbumArtistID: "AlbumArtistID", AlbumArtist: "AlbumArtist", AlbumID: "AlbumID",
|
||||
SortAlbumName: "SortAlbumName", SortArtistName: "SortArtistName", SortAlbumArtistName: "SortAlbumArtistName",
|
||||
OrderAlbumName: "OrderAlbumName", OrderArtistName: "OrderArtistName", OrderAlbumArtistName: "OrderAlbumArtistName",
|
||||
MbzAlbumArtistID: "MbzAlbumArtistID", MbzAlbumType: "MbzAlbumType", MbzAlbumComment: "MbzAlbumComment",
|
||||
Compilation: true, CatalogNum: "CatalogNum", HasCoverArt: true,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
It("sets the single values correctly", func() {
|
||||
album := mfs.ToAlbum()
|
||||
Expect(album.ID).To(Equal("AlbumID"))
|
||||
Expect(album.Name).To(Equal("Album"))
|
||||
Expect(album.Artist).To(Equal("Artist"))
|
||||
Expect(album.ArtistID).To(Equal("ArtistID"))
|
||||
Expect(album.AlbumArtist).To(Equal("AlbumArtist"))
|
||||
Expect(album.AlbumArtistID).To(Equal("AlbumArtistID"))
|
||||
Expect(album.SortAlbumName).To(Equal("SortAlbumName"))
|
||||
Expect(album.SortArtistName).To(Equal("SortArtistName"))
|
||||
Expect(album.SortAlbumArtistName).To(Equal("SortAlbumArtistName"))
|
||||
Expect(album.OrderAlbumName).To(Equal("OrderAlbumName"))
|
||||
Expect(album.OrderAlbumArtistName).To(Equal("OrderAlbumArtistName"))
|
||||
Expect(album.MbzAlbumArtistID).To(Equal("MbzAlbumArtistID"))
|
||||
Expect(album.MbzAlbumType).To(Equal("MbzAlbumType"))
|
||||
Expect(album.MbzAlbumComment).To(Equal("MbzAlbumComment"))
|
||||
Expect(album.CatalogNum).To(Equal("CatalogNum"))
|
||||
Expect(album.Compilation).To(BeTrue())
|
||||
Expect(album.CoverArtId).To(Equal("2"))
|
||||
})
|
||||
})
|
||||
Context("Aggregated attributes", func() {
|
||||
When("we have only one song", func() {
|
||||
BeforeEach(func() {
|
||||
mfs = MediaFiles{
|
||||
{Duration: 100.2, Size: 1024, Year: 1985, UpdatedAt: t("2022-12-19 09:30"), CreatedAt: t("2022-12-19 08:30")},
|
||||
}
|
||||
})
|
||||
It("calculates the aggregates correctly", func() {
|
||||
album := mfs.ToAlbum()
|
||||
Expect(album.Duration).To(Equal(float32(100.2)))
|
||||
Expect(album.Size).To(Equal(int64(1024)))
|
||||
Expect(album.MinYear).To(Equal(1985))
|
||||
Expect(album.MaxYear).To(Equal(1985))
|
||||
Expect(album.UpdatedAt).To(Equal(t("2022-12-19 09:30")))
|
||||
Expect(album.CreatedAt).To(Equal(t("2022-12-19 08:30")))
|
||||
})
|
||||
})
|
||||
When("we have multiple songs", func() {
|
||||
BeforeEach(func() {
|
||||
mfs = MediaFiles{
|
||||
{Duration: 100.2, Size: 1024, Year: 1985, UpdatedAt: t("2022-12-19 09:30"), CreatedAt: t("2022-12-19 08:30")},
|
||||
{Duration: 200.2, Size: 2048, Year: 0, UpdatedAt: t("2022-12-19 09:45"), CreatedAt: t("2022-12-19 08:30")},
|
||||
{Duration: 150.6, Size: 1000, Year: 1986, UpdatedAt: t("2022-12-19 09:45"), CreatedAt: t("2022-12-19 07:30")},
|
||||
}
|
||||
})
|
||||
It("calculates the aggregates correctly", func() {
|
||||
album := mfs.ToAlbum()
|
||||
Expect(album.Duration).To(Equal(float32(451.0)))
|
||||
Expect(album.Size).To(Equal(int64(4072)))
|
||||
Expect(album.MinYear).To(Equal(1985))
|
||||
Expect(album.MaxYear).To(Equal(1986))
|
||||
Expect(album.UpdatedAt).To(Equal(t("2022-12-19 09:45")))
|
||||
Expect(album.CreatedAt).To(Equal(t("2022-12-19 07:30")))
|
||||
})
|
||||
})
|
||||
})
|
||||
Context("Calculated attributes", func() {
|
||||
Context("Genres", func() {
|
||||
When("we have only one Genre", func() {
|
||||
BeforeEach(func() {
|
||||
mfs = MediaFiles{{Genres: Genres{{ID: "g1", Name: "Rock"}}}}
|
||||
})
|
||||
It("sets the correct Genre", func() {
|
||||
album := mfs.ToAlbum()
|
||||
Expect(album.Genre).To(Equal("Rock"))
|
||||
Expect(album.Genres).To(ConsistOf(Genre{ID: "g1", Name: "Rock"}))
|
||||
})
|
||||
})
|
||||
When("we have multiple Genres", func() {
|
||||
BeforeEach(func() {
|
||||
mfs = MediaFiles{{Genres: Genres{{ID: "g1", Name: "Rock"}, {ID: "g2", Name: "Punk"}, {ID: "g2", Name: "Alternative"}}}}
|
||||
})
|
||||
It("sets the correct Genre", func() {
|
||||
album := mfs.ToAlbum()
|
||||
Expect(album.Genre).To(Equal("Rock"))
|
||||
Expect(album.Genres).To(Equal(Genres{{ID: "g1", Name: "Rock"}, {ID: "g2", Name: "Punk"}, {ID: "g2", Name: "Alternative"}}))
|
||||
})
|
||||
})
|
||||
When("we have one predominant Genre", func() {
|
||||
var album Album
|
||||
BeforeEach(func() {
|
||||
mfs = MediaFiles{{Genres: Genres{{ID: "g2", Name: "Punk"}, {ID: "g1", Name: "Rock"}, {ID: "g2", Name: "Punk"}}}}
|
||||
album = mfs.ToAlbum()
|
||||
})
|
||||
It("sets the correct Genre", func() {
|
||||
Expect(album.Genre).To(Equal("Punk"))
|
||||
})
|
||||
It("removes duplications from Genres", func() {
|
||||
Expect(album.Genres).To(Equal(Genres{{ID: "g1", Name: "Rock"}, {ID: "g2", Name: "Punk"}}))
|
||||
})
|
||||
})
|
||||
})
|
||||
Context("Comments", func() {
|
||||
When("we have only one Comment", func() {
|
||||
BeforeEach(func() {
|
||||
mfs = MediaFiles{{Comment: "comment1"}}
|
||||
})
|
||||
It("sets the correct Comment", func() {
|
||||
album := mfs.ToAlbum()
|
||||
Expect(album.Comment).To(Equal("comment1"))
|
||||
})
|
||||
})
|
||||
When("we have multiple equal comments", func() {
|
||||
BeforeEach(func() {
|
||||
mfs = MediaFiles{{Comment: "comment1"}, {Comment: "comment1"}, {Comment: "comment1"}}
|
||||
})
|
||||
It("sets the correct Comment", func() {
|
||||
album := mfs.ToAlbum()
|
||||
Expect(album.Comment).To(Equal("comment1"))
|
||||
})
|
||||
})
|
||||
When("we have different comments", func() {
|
||||
BeforeEach(func() {
|
||||
mfs = MediaFiles{{Comment: "comment1"}, {Comment: "not the same"}, {Comment: "comment1"}}
|
||||
})
|
||||
It("sets the correct Genre", func() {
|
||||
album := mfs.ToAlbum()
|
||||
Expect(album.Comment).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
})
|
||||
Context("AllArtistIds", func() {
|
||||
BeforeEach(func() {
|
||||
mfs = MediaFiles{
|
||||
{AlbumArtistID: "22", ArtistID: "11"},
|
||||
{AlbumArtistID: "22", ArtistID: "33"},
|
||||
{AlbumArtistID: "22", ArtistID: "11"},
|
||||
}
|
||||
})
|
||||
It("removes duplications", func() {
|
||||
album := mfs.ToAlbum()
|
||||
Expect(album.AllArtistIDs).To(Equal("11 22 33"))
|
||||
})
|
||||
})
|
||||
Context("FullText", func() {
|
||||
BeforeEach(func() {
|
||||
mfs = MediaFiles{
|
||||
{
|
||||
Album: "Album1", AlbumArtist: "AlbumArtist1", Artist: "Artist1", DiscSubtitle: "DiscSubtitle1",
|
||||
SortAlbumName: "SortAlbumName1", SortAlbumArtistName: "SortAlbumArtistName1", SortArtistName: "SortArtistName1",
|
||||
},
|
||||
{
|
||||
Album: "Album1", AlbumArtist: "AlbumArtist1", Artist: "Artist2", DiscSubtitle: "DiscSubtitle2",
|
||||
SortAlbumName: "SortAlbumName1", SortAlbumArtistName: "SortAlbumArtistName1", SortArtistName: "SortArtistName2",
|
||||
},
|
||||
}
|
||||
})
|
||||
It("fills the fullText attribute correctly", func() {
|
||||
album := mfs.ToAlbum()
|
||||
Expect(album.FullText).To(Equal(" album1 albumartist1 artist1 artist2 discsubtitle1 discsubtitle2 sortalbumartistname1 sortalbumname1 sortartistname1 sortartistname2"))
|
||||
})
|
||||
})
|
||||
Context("MbzAlbumID", func() {
|
||||
When("we have only one MbzAlbumID", func() {
|
||||
BeforeEach(func() {
|
||||
mfs = MediaFiles{{MbzAlbumID: "id1"}}
|
||||
})
|
||||
It("sets the correct MbzAlbumID", func() {
|
||||
album := mfs.ToAlbum()
|
||||
Expect(album.MbzAlbumID).To(Equal("id1"))
|
||||
})
|
||||
})
|
||||
When("we have multiple MbzAlbumID", func() {
|
||||
BeforeEach(func() {
|
||||
mfs = MediaFiles{{MbzAlbumID: "id1"}, {MbzAlbumID: "id2"}, {MbzAlbumID: "id1"}}
|
||||
})
|
||||
It("sets the correct MbzAlbumID", func() {
|
||||
album := mfs.ToAlbum()
|
||||
Expect(album.MbzAlbumID).To(Equal("id1"))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
func t(v string) time.Time {
|
||||
var timeFormats = []string{"2006-01-02", "2006-01-02 15:04", "2006-01-02 15:04:05", "2006-01-02T15:04:05", "2006-01-02T15:04", "2006-01-02 15:04:05.999999999 -0700 MST"}
|
||||
for _, f := range timeFormats {
|
||||
t, err := time.ParseInLocation(f, v, time.UTC)
|
||||
if err == nil {
|
||||
return t.UTC()
|
||||
}
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue