2f5b2b5135
* fix(artwork): fallback mediafile cover art to disc artwork before album Changed the mediafile cover art fallback chain to go through disc artwork before album artwork (mediafile → disc → album). Previously, mediafiles without embedded art fell back directly to album cover, bypassing any disc-specific artwork. Renamed AlbumCoverArtID() to DiscCoverArtID() to encapsulate the disc-vs-album decision in a single method, used by both CoverArtID() and the mediafile artwork reader. Signed-off-by: Deluan <deluan@navidrome.org> * fix(artwork): fix cache invalidation for mediafile and album cover art Include imagesUpdatedAt from album folders in the mediafile artwork reader's cache key, so that when a cover image file changes on disk (without audio metadata changes) the mediafile cache properly invalidates. Also include CoverArtPriority unconditionally in the album artwork reader's cache key hash, so that changing the priority order with external services disabled correctly invalidates the album cache. * fix(artwork): skip disc artwork resolution for single-disc albums Single-disc albums with DiscNumber=1 were unnecessarily routed through discArtworkReader, which does extra DB queries only to fall through to album art anyway. Now only multi-disc albums use the disc fallback path. * refactor(artwork): restore AlbumCoverArtID as a separate method Extract AlbumCoverArtID back out of DiscCoverArtID so the single-disc fallback path in reader_mediafile can reference it by name instead of inlining the artwork ID construction. --------- Signed-off-by: Deluan <deluan@navidrome.org>
598 lines
23 KiB
Go
598 lines
23 KiB
Go
package model_test
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/navidrome/navidrome/conf"
|
|
"github.com/navidrome/navidrome/conf/configtest"
|
|
. "github.com/navidrome/navidrome/model"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
var _ = Describe("MediaFiles", func() {
|
|
var mfs MediaFiles
|
|
|
|
Describe("ToAlbum", func() {
|
|
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",
|
|
MbzReleaseGroupID: "MbzReleaseGroupID", Compilation: false, CatalogNum: "", Path: "/music1/file1.mp3", FolderID: "Folder1",
|
|
},
|
|
{
|
|
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",
|
|
MbzReleaseGroupID: "MbzReleaseGroupID",
|
|
Compilation: true, CatalogNum: "CatalogNum", HasCoverArt: true, Path: "/music2/file2.mp3", FolderID: "Folder2",
|
|
},
|
|
}
|
|
})
|
|
|
|
It("sets the single values correctly", func() {
|
|
album := mfs.ToAlbum()
|
|
Expect(album.ID).To(Equal("AlbumID"))
|
|
Expect(album.Name).To(Equal("Album"))
|
|
Expect(album.AlbumArtist).To(Equal("AlbumArtist"))
|
|
Expect(album.AlbumArtistID).To(Equal("AlbumArtistID"))
|
|
Expect(album.SortAlbumName).To(Equal("SortAlbumName"))
|
|
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.MbzReleaseGroupID).To(Equal("MbzReleaseGroupID"))
|
|
Expect(album.CatalogNum).To(Equal("CatalogNum"))
|
|
Expect(album.Compilation).To(BeTrue())
|
|
Expect(album.EmbedArtPath).To(Equal("/music2/file2.mp3"))
|
|
Expect(album.FolderIDs).To(ConsistOf("Folder1", "Folder2"))
|
|
})
|
|
})
|
|
Context("Aggregated attributes", func() {
|
|
When("we don't have any songs", func() {
|
|
BeforeEach(func() {
|
|
mfs = MediaFiles{}
|
|
})
|
|
It("returns an empty album", func() {
|
|
album := mfs.ToAlbum()
|
|
Expect(album.Duration).To(Equal(float32(0)))
|
|
Expect(album.Size).To(Equal(int64(0)))
|
|
Expect(album.MinYear).To(Equal(0))
|
|
Expect(album.MaxYear).To(Equal(0))
|
|
Expect(album.Date).To(BeEmpty())
|
|
Expect(album.UpdatedAt).To(BeZero())
|
|
Expect(album.CreatedAt).To(BeZero())
|
|
})
|
|
})
|
|
When("we have only one song", func() {
|
|
BeforeEach(func() {
|
|
mfs = MediaFiles{
|
|
{Duration: 100.2, Size: 1024, Year: 1985, Date: "1985-01-02", UpdatedAt: t("2022-12-19 09:30"), BirthTime: 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.Date).To(Equal("1985-01-02"))
|
|
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 with different dates", func() {
|
|
BeforeEach(func() {
|
|
mfs = MediaFiles{
|
|
{Duration: 100.2, Size: 1024, Year: 1985, Date: "1985-01-02", UpdatedAt: t("2022-12-19 09:30"), BirthTime: t("2022-12-19 08:30")},
|
|
{Duration: 200.2, Size: 2048, Year: 0, Date: "", UpdatedAt: t("2022-12-19 09:45"), BirthTime: t("2022-12-19 08:30")},
|
|
{Duration: 150.6, Size: 1000, Year: 1986, Date: "1986-01-02", UpdatedAt: t("2022-12-19 09:45"), BirthTime: 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.Date).To(BeEmpty())
|
|
Expect(album.UpdatedAt).To(Equal(t("2022-12-19 09:45")))
|
|
Expect(album.CreatedAt).To(Equal(t("2022-12-19 07:30")))
|
|
})
|
|
Context("MinYear", func() {
|
|
It("returns 0 when all values are 0", func() {
|
|
mfs = MediaFiles{{Year: 0}, {Year: 0}, {Year: 0}}
|
|
a := mfs.ToAlbum()
|
|
Expect(a.MinYear).To(Equal(0))
|
|
})
|
|
It("returns the smallest value from the list, not counting 0", func() {
|
|
mfs = MediaFiles{{Year: 2000}, {Year: 0}, {Year: 1999}}
|
|
a := mfs.ToAlbum()
|
|
Expect(a.MinYear).To(Equal(1999))
|
|
})
|
|
})
|
|
})
|
|
When("we have multiple songs with same dates", func() {
|
|
BeforeEach(func() {
|
|
mfs = MediaFiles{
|
|
{Duration: 100.2, Size: 1024, Year: 1985, Date: "1985-01-02", UpdatedAt: t("2022-12-19 09:30"), BirthTime: t("2022-12-19 08:30")},
|
|
{Duration: 200.2, Size: 2048, Year: 1985, Date: "1985-01-02", UpdatedAt: t("2022-12-19 09:45"), BirthTime: t("2022-12-19 08:30")},
|
|
{Duration: 150.6, Size: 1000, Year: 1985, Date: "1985-01-02", UpdatedAt: t("2022-12-19 09:45"), BirthTime: t("2022-12-19 07:30")},
|
|
}
|
|
})
|
|
It("sets the date field correctly", func() {
|
|
album := mfs.ToAlbum()
|
|
Expect(album.Date).To(Equal("1985-01-02"))
|
|
Expect(album.MinYear).To(Equal(1985))
|
|
Expect(album.MaxYear).To(Equal(1985))
|
|
})
|
|
})
|
|
DescribeTable("explicitStatus",
|
|
func(mfs MediaFiles, status string) {
|
|
Expect(mfs.ToAlbum().ExplicitStatus).To(Equal(status))
|
|
},
|
|
Entry("sets the album to clean when a clean song is present", MediaFiles{{ExplicitStatus: ""}, {ExplicitStatus: "c"}, {ExplicitStatus: ""}}, "c"),
|
|
Entry("sets the album to explicit when an explicit song is present", MediaFiles{{ExplicitStatus: ""}, {ExplicitStatus: "e"}, {ExplicitStatus: ""}}, "e"),
|
|
Entry("takes precedence of explicit songs over clean ones", MediaFiles{{ExplicitStatus: "e"}, {ExplicitStatus: "c"}, {ExplicitStatus: ""}}, "e"),
|
|
)
|
|
})
|
|
Context("Calculated attributes", func() {
|
|
Context("Discs", func() {
|
|
When("we have no discs info", func() {
|
|
BeforeEach(func() {
|
|
mfs = MediaFiles{{Album: "Album1"}, {Album: "Album1"}, {Album: "Album1"}}
|
|
})
|
|
It("adds 1 disc without subtitle", func() {
|
|
album := mfs.ToAlbum()
|
|
Expect(album.Discs).To(Equal(Discs{1: ""}))
|
|
})
|
|
})
|
|
When("we have only one disc", func() {
|
|
BeforeEach(func() {
|
|
mfs = MediaFiles{{DiscNumber: 1, DiscSubtitle: "DiscSubtitle"}}
|
|
})
|
|
It("sets the correct Discs", func() {
|
|
album := mfs.ToAlbum()
|
|
Expect(album.Discs).To(Equal(Discs{1: "DiscSubtitle"}))
|
|
})
|
|
})
|
|
When("we have multiple discs", func() {
|
|
BeforeEach(func() {
|
|
mfs = MediaFiles{{DiscNumber: 1, DiscSubtitle: "DiscSubtitle"}, {DiscNumber: 2, DiscSubtitle: "DiscSubtitle2"}, {DiscNumber: 1, DiscSubtitle: "DiscSubtitle"}}
|
|
})
|
|
It("sets the correct Discs", func() {
|
|
album := mfs.ToAlbum()
|
|
Expect(album.Discs).To(Equal(Discs{1: "DiscSubtitle", 2: "DiscSubtitle2"}))
|
|
})
|
|
})
|
|
})
|
|
|
|
Context("Genres/tags", func() {
|
|
When("we don't have any tags", func() {
|
|
BeforeEach(func() {
|
|
mfs = MediaFiles{{}}
|
|
})
|
|
It("sets the correct Genre", func() {
|
|
album := mfs.ToAlbum()
|
|
Expect(album.Tags).To(BeEmpty())
|
|
})
|
|
})
|
|
When("we have only one Genre", func() {
|
|
BeforeEach(func() {
|
|
mfs = MediaFiles{{Tags: Tags{"genre": []string{"Rock"}}}}
|
|
})
|
|
It("sets the correct Genre", func() {
|
|
album := mfs.ToAlbum()
|
|
Expect(album.Tags).To(HaveLen(1))
|
|
Expect(album.Tags).To(HaveKeyWithValue(TagGenre, []string{"Rock"}))
|
|
})
|
|
})
|
|
When("we have multiple Genres", func() {
|
|
BeforeEach(func() {
|
|
mfs = MediaFiles{
|
|
{Tags: Tags{"genre": []string{"Punk"}, "mood": []string{"Happy", "Chill"}}},
|
|
{Tags: Tags{"genre": []string{"Rock"}}},
|
|
{Tags: Tags{"genre": []string{"Alternative", "Rock"}}},
|
|
}
|
|
})
|
|
It("sets the correct Genre, sorted by frequency, then alphabetically", func() {
|
|
album := mfs.ToAlbum()
|
|
Expect(album.Tags).To(HaveLen(2))
|
|
Expect(album.Tags).To(HaveKeyWithValue(TagGenre, []string{"Rock", "Alternative", "Punk"}))
|
|
Expect(album.Tags).To(HaveKeyWithValue(TagMood, []string{"Chill", "Happy"}))
|
|
})
|
|
})
|
|
When("we have tags with mismatching case", func() {
|
|
BeforeEach(func() {
|
|
mfs = MediaFiles{
|
|
{Tags: Tags{"genre": []string{"synthwave"}}},
|
|
{Tags: Tags{"genre": []string{"Synthwave"}}},
|
|
}
|
|
})
|
|
It("normalizes the tags in just one", func() {
|
|
album := mfs.ToAlbum()
|
|
Expect(album.Tags).To(HaveLen(1))
|
|
Expect(album.Tags).To(HaveKeyWithValue(TagGenre, []string{"Synthwave"}))
|
|
})
|
|
})
|
|
})
|
|
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 comment", func() {
|
|
album := mfs.ToAlbum()
|
|
Expect(album.Comment).To(BeEmpty())
|
|
})
|
|
})
|
|
})
|
|
Context("Participants", func() {
|
|
var album Album
|
|
BeforeEach(func() {
|
|
mfs = MediaFiles{
|
|
{
|
|
Album: "Album1", AlbumArtistID: "AA1", AlbumArtist: "Display AlbumArtist1", Artist: "Artist1",
|
|
DiscSubtitle: "DiscSubtitle1", SortAlbumName: "SortAlbumName1",
|
|
Participants: Participants{
|
|
RoleAlbumArtist: ParticipantList{_p("AA1", "AlbumArtist1", "SortAlbumArtistName1")},
|
|
RoleArtist: ParticipantList{_p("A1", "Artist1", "SortArtistName1")},
|
|
},
|
|
},
|
|
{
|
|
Album: "Album1", AlbumArtistID: "AA1", AlbumArtist: "Display AlbumArtist1", Artist: "Artist2",
|
|
DiscSubtitle: "DiscSubtitle2", SortAlbumName: "SortAlbumName1",
|
|
Participants: Participants{
|
|
RoleAlbumArtist: ParticipantList{_p("AA1", "AlbumArtist1", "SortAlbumArtistName1")},
|
|
RoleArtist: ParticipantList{_p("A2", "Artist2", "SortArtistName2")},
|
|
RoleComposer: ParticipantList{_p("C1", "Composer1")},
|
|
},
|
|
},
|
|
}
|
|
album = mfs.ToAlbum()
|
|
})
|
|
It("gets all participants from all tracks", func() {
|
|
Expect(album.Participants).To(HaveKeyWithValue(RoleAlbumArtist, ParticipantList{_p("AA1", "AlbumArtist1", "SortAlbumArtistName1")}))
|
|
Expect(album.Participants).To(HaveKeyWithValue(RoleComposer, ParticipantList{_p("C1", "Composer1")}))
|
|
Expect(album.Participants).To(HaveKeyWithValue(RoleArtist, ParticipantList{
|
|
_p("A1", "Artist1", "SortArtistName1"), _p("A2", "Artist2", "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("uses the most frequent MbzAlbumID", func() {
|
|
album := mfs.ToAlbum()
|
|
Expect(album.MbzAlbumID).To(Equal("id1"))
|
|
})
|
|
})
|
|
})
|
|
Context("Album Art", func() {
|
|
When("we have media files with cover art from multiple discs", func() {
|
|
BeforeEach(func() {
|
|
mfs = MediaFiles{
|
|
{
|
|
Path: "Artist/Album/Disc2/01.mp3",
|
|
HasCoverArt: true,
|
|
DiscNumber: 2,
|
|
},
|
|
{
|
|
Path: "Artist/Album/Disc1/01.mp3",
|
|
HasCoverArt: true,
|
|
DiscNumber: 1,
|
|
},
|
|
{
|
|
Path: "Artist/Album/Disc3/01.mp3",
|
|
HasCoverArt: true,
|
|
DiscNumber: 3,
|
|
},
|
|
}
|
|
})
|
|
It("selects the cover art from the lowest disc number", func() {
|
|
album := mfs.ToAlbum()
|
|
Expect(album.EmbedArtPath).To(Equal("Artist/Album/Disc1/01.mp3"))
|
|
})
|
|
})
|
|
|
|
When("we have media files with cover art from the same disc number", func() {
|
|
BeforeEach(func() {
|
|
mfs = MediaFiles{
|
|
{
|
|
Path: "Artist/Album/Disc1/02.mp3",
|
|
HasCoverArt: true,
|
|
DiscNumber: 1,
|
|
},
|
|
{
|
|
Path: "Artist/Album/Disc1/01.mp3",
|
|
HasCoverArt: true,
|
|
DiscNumber: 1,
|
|
},
|
|
}
|
|
})
|
|
It("selects the cover art with the lowest path alphabetically", func() {
|
|
album := mfs.ToAlbum()
|
|
Expect(album.EmbedArtPath).To(Equal("Artist/Album/Disc1/01.mp3"))
|
|
})
|
|
})
|
|
|
|
When("we have media files with some missing cover art", func() {
|
|
BeforeEach(func() {
|
|
mfs = MediaFiles{
|
|
{
|
|
Path: "Artist/Album/Disc1/01.mp3",
|
|
HasCoverArt: false,
|
|
DiscNumber: 1,
|
|
},
|
|
{
|
|
Path: "Artist/Album/Disc2/01.mp3",
|
|
HasCoverArt: true,
|
|
DiscNumber: 2,
|
|
},
|
|
}
|
|
})
|
|
It("selects the file with cover art even if from a higher disc number", func() {
|
|
album := mfs.ToAlbum()
|
|
Expect(album.EmbedArtPath).To(Equal("Artist/Album/Disc2/01.mp3"))
|
|
})
|
|
})
|
|
|
|
When("we have media files with path names that don't correlate with disc numbers", func() {
|
|
BeforeEach(func() {
|
|
mfs = MediaFiles{
|
|
{
|
|
Path: "Artist/Album/file-z.mp3", // Path would be sorted last alphabetically
|
|
HasCoverArt: true,
|
|
DiscNumber: 1, // But it has lowest disc number
|
|
},
|
|
{
|
|
Path: "Artist/Album/file-a.mp3", // Path would be sorted first alphabetically
|
|
HasCoverArt: true,
|
|
DiscNumber: 2, // But it has higher disc number
|
|
},
|
|
{
|
|
Path: "Artist/Album/file-m.mp3",
|
|
HasCoverArt: true,
|
|
DiscNumber: 3,
|
|
},
|
|
}
|
|
})
|
|
It("selects the cover art from the lowest disc number regardless of path", func() {
|
|
album := mfs.ToAlbum()
|
|
Expect(album.EmbedArtPath).To(Equal("Artist/Album/file-z.mp3"))
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("ToM3U8", func() {
|
|
It("returns header only for empty MediaFiles", func() {
|
|
mfs = MediaFiles{}
|
|
result := mfs.ToM3U8("My Playlist", false)
|
|
Expect(result).To(Equal("#EXTM3U\n#PLAYLIST:My Playlist\n"))
|
|
})
|
|
|
|
DescribeTable("duration formatting",
|
|
func(duration float32, expected string) {
|
|
mfs = MediaFiles{{Title: "Song", Artist: "Artist", Duration: duration, Path: "song.mp3"}}
|
|
result := mfs.ToM3U8("Test", false)
|
|
Expect(result).To(ContainSubstring(expected))
|
|
},
|
|
Entry("zero duration", float32(0.0), "#EXTINF:0,"),
|
|
Entry("whole number", float32(120.0), "#EXTINF:120,"),
|
|
Entry("rounds 0.5 down", float32(180.5), "#EXTINF:180,"),
|
|
Entry("rounds 0.6 up", float32(240.6), "#EXTINF:241,"),
|
|
)
|
|
|
|
Context("multiple tracks", func() {
|
|
BeforeEach(func() {
|
|
mfs = MediaFiles{
|
|
{Title: "Song One", Artist: "Artist A", Duration: 120, Path: "a/song1.mp3", LibraryPath: "/music"},
|
|
{Title: "Song Two", Artist: "Artist B", Duration: 241, Path: "b/song2.mp3", LibraryPath: "/music"},
|
|
{Title: "Song with \"quotes\" & ampersands", Artist: "Artist with Ümläuts", Duration: 90, Path: "special/file.mp3", LibraryPath: "/música"},
|
|
}
|
|
})
|
|
|
|
DescribeTable("generates correct output",
|
|
func(absolutePaths bool, expectedContent string) {
|
|
result := mfs.ToM3U8("Multi Track", absolutePaths)
|
|
Expect(result).To(Equal(expectedContent))
|
|
},
|
|
Entry("relative paths",
|
|
false,
|
|
"#EXTM3U\n#PLAYLIST:Multi Track\n#EXTINF:120,Artist A - Song One\na/song1.mp3\n#EXTINF:241,Artist B - Song Two\nb/song2.mp3\n#EXTINF:90,Artist with Ümläuts - Song with \"quotes\" & ampersands\nspecial/file.mp3\n",
|
|
),
|
|
Entry("absolute paths",
|
|
true,
|
|
"#EXTM3U\n#PLAYLIST:Multi Track\n#EXTINF:120,Artist A - Song One\n/music/a/song1.mp3\n#EXTINF:241,Artist B - Song Two\n/music/b/song2.mp3\n#EXTINF:90,Artist with Ümläuts - Song with \"quotes\" & ampersands\n/música/special/file.mp3\n",
|
|
),
|
|
Entry("special characters",
|
|
false,
|
|
"#EXTM3U\n#PLAYLIST:Multi Track\n#EXTINF:120,Artist A - Song One\na/song1.mp3\n#EXTINF:241,Artist B - Song Two\nb/song2.mp3\n#EXTINF:90,Artist with Ümläuts - Song with \"quotes\" & ampersands\nspecial/file.mp3\n",
|
|
),
|
|
)
|
|
})
|
|
|
|
Context("path variations", func() {
|
|
It("handles different path structures", func() {
|
|
mfs = MediaFiles{
|
|
{Title: "Root", Artist: "Artist", Duration: 60, Path: "song.mp3", LibraryPath: "/lib"},
|
|
{Title: "Nested", Artist: "Artist", Duration: 60, Path: "deep/nested/song.mp3", LibraryPath: "/lib"},
|
|
}
|
|
|
|
relativeResult := mfs.ToM3U8("Test", false)
|
|
Expect(relativeResult).To(ContainSubstring("song.mp3\n"))
|
|
Expect(relativeResult).To(ContainSubstring("deep/nested/song.mp3\n"))
|
|
|
|
absoluteResult := mfs.ToM3U8("Test", true)
|
|
Expect(absoluteResult).To(ContainSubstring("/lib/song.mp3\n"))
|
|
Expect(absoluteResult).To(ContainSubstring("/lib/deep/nested/song.mp3\n"))
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
var _ = Describe("MediaFile", func() {
|
|
BeforeEach(func() {
|
|
DeferCleanup(configtest.SetupConfig())
|
|
conf.Server.EnableMediaFileCoverArt = true
|
|
})
|
|
DescribeTable("FullTitle",
|
|
func(enabled bool, tags Tags, expected string) {
|
|
conf.Server.Subsonic.AppendSubtitle = enabled
|
|
mf := MediaFile{Title: "Song", Tags: tags}
|
|
Expect(mf.FullTitle()).To(Equal(expected))
|
|
},
|
|
Entry("appends subtitle when enabled and tag is present", true, Tags{TagSubtitle: []string{"Live"}}, "Song (Live)"),
|
|
Entry("returns just title when disabled", false, Tags{TagSubtitle: []string{"Live"}}, "Song"),
|
|
Entry("returns just title when tag is absent", true, Tags{}, "Song"),
|
|
Entry("returns just title when tag is an empty slice", true, Tags{TagSubtitle: []string{}}, "Song"),
|
|
)
|
|
DescribeTable("FullAlbumName",
|
|
func(enabled bool, tags Tags, expected string) {
|
|
conf.Server.Subsonic.AppendAlbumVersion = enabled
|
|
mf := MediaFile{Album: "Album", Tags: tags}
|
|
Expect(mf.FullAlbumName()).To(Equal(expected))
|
|
},
|
|
Entry("appends version when enabled and tag is present", true, Tags{TagAlbumVersion: []string{"Deluxe Edition"}}, "Album (Deluxe Edition)"),
|
|
Entry("returns just album name when disabled", false, Tags{TagAlbumVersion: []string{"Deluxe Edition"}}, "Album"),
|
|
Entry("returns just album name when tag is absent", true, Tags{}, "Album"),
|
|
Entry("returns just album name when tag is an empty slice", true, Tags{TagAlbumVersion: []string{}}, "Album"),
|
|
)
|
|
Describe("CoverArtId", func() {
|
|
It("returns its own id if it HasCoverArt", func() {
|
|
mf := MediaFile{ID: "111", AlbumID: "1", HasCoverArt: true}
|
|
id := mf.CoverArtID()
|
|
Expect(id.Kind).To(Equal(KindMediaFileArtwork))
|
|
Expect(id.ID).To(Equal(mf.ID))
|
|
})
|
|
It("returns disc art id if HasCoverArt is false and DiscNumber > 0", func() {
|
|
mf := MediaFile{ID: "111", AlbumID: "1", HasCoverArt: false, DiscNumber: 2}
|
|
id := mf.CoverArtID()
|
|
Expect(id.Kind).To(Equal(KindDiscArtwork))
|
|
Expect(id.ID).To(Equal("1:2"))
|
|
})
|
|
It("returns its album id if HasCoverArt is false and DiscNumber is 0", func() {
|
|
mf := MediaFile{ID: "111", AlbumID: "1", HasCoverArt: false}
|
|
id := mf.CoverArtID()
|
|
Expect(id.Kind).To(Equal(KindAlbumArtwork))
|
|
Expect(id.ID).To(Equal(mf.AlbumID))
|
|
})
|
|
It("returns disc art id if EnableMediaFileCoverArt is disabled and DiscNumber > 0", func() {
|
|
conf.Server.EnableMediaFileCoverArt = false
|
|
mf := MediaFile{ID: "111", AlbumID: "1", HasCoverArt: true, DiscNumber: 3}
|
|
id := mf.CoverArtID()
|
|
Expect(id.Kind).To(Equal(KindDiscArtwork))
|
|
Expect(id.ID).To(Equal("1:3"))
|
|
})
|
|
It("returns its album id if EnableMediaFileCoverArt is disabled and DiscNumber is 0", func() {
|
|
conf.Server.EnableMediaFileCoverArt = false
|
|
mf := MediaFile{ID: "111", AlbumID: "1", HasCoverArt: true}
|
|
id := mf.CoverArtID()
|
|
Expect(id.Kind).To(Equal(KindAlbumArtwork))
|
|
Expect(id.ID).To(Equal(mf.AlbumID))
|
|
})
|
|
})
|
|
|
|
Describe("AudioCodec", func() {
|
|
It("returns normalized stored codec when available", func() {
|
|
mf := MediaFile{Codec: "AAC", Suffix: "m4a"}
|
|
Expect(mf.AudioCodec()).To(Equal("aac"))
|
|
})
|
|
|
|
It("returns stored codec lowercased", func() {
|
|
mf := MediaFile{Codec: "ALAC", Suffix: "m4a"}
|
|
Expect(mf.AudioCodec()).To(Equal("alac"))
|
|
})
|
|
|
|
DescribeTable("infers codec from suffix when Codec field is empty",
|
|
func(suffix string, bitDepth int, expected string) {
|
|
mf := MediaFile{Suffix: suffix, BitDepth: bitDepth}
|
|
Expect(mf.AudioCodec()).To(Equal(expected))
|
|
},
|
|
Entry("mp3", "mp3", 0, "mp3"),
|
|
Entry("mpga", "mpga", 0, "mp3"),
|
|
Entry("mp2", "mp2", 0, "mp2"),
|
|
Entry("ogg", "ogg", 0, "vorbis"),
|
|
Entry("oga", "oga", 0, "vorbis"),
|
|
Entry("opus", "opus", 0, "opus"),
|
|
Entry("mpc", "mpc", 0, "mpc"),
|
|
Entry("wma", "wma", 0, "wma"),
|
|
Entry("flac", "flac", 0, "flac"),
|
|
Entry("wav", "wav", 0, "pcm"),
|
|
Entry("aif", "aif", 0, "pcm"),
|
|
Entry("aiff", "aiff", 0, "pcm"),
|
|
Entry("aifc", "aifc", 0, "pcm"),
|
|
Entry("ape", "ape", 0, "ape"),
|
|
Entry("wv", "wv", 0, "wv"),
|
|
Entry("wvp", "wvp", 0, "wv"),
|
|
Entry("tta", "tta", 0, "tta"),
|
|
Entry("tak", "tak", 0, "tak"),
|
|
Entry("shn", "shn", 0, "shn"),
|
|
Entry("dsf", "dsf", 0, "dsd"),
|
|
Entry("dff", "dff", 0, "dsd"),
|
|
Entry("m4a with BitDepth=0 (AAC)", "m4a", 0, "aac"),
|
|
Entry("m4a with BitDepth>0 (ALAC)", "m4a", 16, "alac"),
|
|
Entry("m4b", "m4b", 0, "aac"),
|
|
Entry("m4p", "m4p", 0, "aac"),
|
|
Entry("m4r", "m4r", 0, "aac"),
|
|
Entry("unknown suffix", "xyz", 0, ""),
|
|
)
|
|
|
|
It("prefers stored codec over suffix inference", func() {
|
|
mf := MediaFile{Codec: "ALAC", Suffix: "m4a", BitDepth: 0}
|
|
Expect(mf.AudioCodec()).To(Equal("alac"))
|
|
})
|
|
})
|
|
|
|
})
|
|
|
|
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{}
|
|
}
|