mirror of
https://github.com/navidrome/navidrome.git
synced 2025-12-23 15:08:04 -05:00
* fix(deps): update wazero dependencies to resolve issues Signed-off-by: Deluan <deluan@navidrome.org> * fix(deps): update wazero dependency to latest version Signed-off-by: Deluan <deluan@navidrome.org> * fix: correct track ordering when sorting playlists by album Fixed issue #3177 where tracks within multi-disc albums were displayed out of order when sorting playlists by album. The playlist track repository was using an incomplete sort mapping that only sorted by album name and artist, missing the critical disc_number and track_number fields. Changed the album sort mapping in playlist_track_repository from: order_album_name, order_album_artist_name to: order_album_name, order_album_artist_name, disc_number, track_number, order_artist_name, title This now matches the sorting used in the media file repository, ensuring tracks are sorted by: 1. Album name (groups by album) 2. Album artist (handles compilations) 3. Disc number (multi-disc album discs in order) 4. Track number (tracks within disc in order) 5. Artist name and title (edge cases with missing metadata) Added comprehensive tests with a multi-disc test album to verify correct sorting behavior. * chore: sync go.mod and go.sum with master * chore: align playlist album sort order with mediafile_repository (use album_id) * fix: clean up test playlist to prevent state leakage in randomized test runs --------- Signed-off-by: Deluan <deluan@navidrome.org>
272 lines
8.9 KiB
Go
272 lines
8.9 KiB
Go
package persistence
|
|
|
|
import (
|
|
"context"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
_ "github.com/mattn/go-sqlite3"
|
|
"github.com/navidrome/navidrome/conf"
|
|
"github.com/navidrome/navidrome/db"
|
|
"github.com/navidrome/navidrome/log"
|
|
"github.com/navidrome/navidrome/model"
|
|
"github.com/navidrome/navidrome/model/request"
|
|
"github.com/navidrome/navidrome/tests"
|
|
"github.com/navidrome/navidrome/utils/gg"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
"github.com/pocketbase/dbx"
|
|
)
|
|
|
|
func TestPersistence(t *testing.T) {
|
|
tests.Init(t, true)
|
|
|
|
//os.Remove("./test-123.db")
|
|
//conf.Server.DbPath = "./test-123.db"
|
|
conf.Server.DbPath = "file::memory:?cache=shared&_foreign_keys=on"
|
|
defer db.Init(context.Background())()
|
|
log.SetLevel(log.LevelFatal)
|
|
RegisterFailHandler(Fail)
|
|
RunSpecs(t, "Persistence Suite")
|
|
}
|
|
|
|
func mf(mf model.MediaFile) model.MediaFile {
|
|
mf.Tags = model.Tags{}
|
|
mf.LibraryID = 1
|
|
mf.LibraryPath = "music" // Default folder
|
|
mf.LibraryName = "Music Library"
|
|
mf.Participants = model.Participants{
|
|
model.RoleArtist: model.ParticipantList{
|
|
model.Participant{Artist: model.Artist{ID: mf.ArtistID, Name: mf.Artist}},
|
|
},
|
|
}
|
|
if mf.Lyrics == "" {
|
|
mf.Lyrics = "[]"
|
|
}
|
|
return mf
|
|
}
|
|
|
|
func al(al model.Album) model.Album {
|
|
al.LibraryID = 1
|
|
al.LibraryPath = "music"
|
|
al.LibraryName = "Music Library"
|
|
al.Discs = model.Discs{}
|
|
al.Tags = model.Tags{}
|
|
al.Participants = model.Participants{}
|
|
return al
|
|
}
|
|
|
|
var (
|
|
artistKraftwerk = model.Artist{ID: "2", Name: "Kraftwerk", OrderArtistName: "kraftwerk"}
|
|
artistBeatles = model.Artist{ID: "3", Name: "The Beatles", OrderArtistName: "beatles"}
|
|
testArtists = model.Artists{
|
|
artistKraftwerk,
|
|
artistBeatles,
|
|
}
|
|
)
|
|
|
|
var (
|
|
albumSgtPeppers = al(model.Album{ID: "101", Name: "Sgt Peppers", AlbumArtist: "The Beatles", OrderAlbumName: "sgt peppers", AlbumArtistID: "3", EmbedArtPath: p("/beatles/1/sgt/a day.mp3"), SongCount: 1, MaxYear: 1967})
|
|
albumAbbeyRoad = al(model.Album{ID: "102", Name: "Abbey Road", AlbumArtist: "The Beatles", OrderAlbumName: "abbey road", AlbumArtistID: "3", EmbedArtPath: p("/beatles/1/come together.mp3"), SongCount: 1, MaxYear: 1969})
|
|
albumRadioactivity = al(model.Album{ID: "103", Name: "Radioactivity", AlbumArtist: "Kraftwerk", OrderAlbumName: "radioactivity", AlbumArtistID: "2", EmbedArtPath: p("/kraft/radio/radio.mp3"), SongCount: 2})
|
|
albumMultiDisc = al(model.Album{ID: "104", Name: "Multi Disc Album", AlbumArtist: "Test Artist", OrderAlbumName: "multi disc album", AlbumArtistID: "1", EmbedArtPath: p("/test/multi/disc1/track1.mp3"), SongCount: 4})
|
|
testAlbums = model.Albums{
|
|
albumSgtPeppers,
|
|
albumAbbeyRoad,
|
|
albumRadioactivity,
|
|
albumMultiDisc,
|
|
}
|
|
)
|
|
|
|
var (
|
|
songDayInALife = mf(model.MediaFile{ID: "1001", Title: "A Day In A Life", ArtistID: "3", Artist: "The Beatles", AlbumID: "101", Album: "Sgt Peppers", Path: p("/beatles/1/sgt/a day.mp3")})
|
|
songComeTogether = mf(model.MediaFile{ID: "1002", Title: "Come Together", ArtistID: "3", Artist: "The Beatles", AlbumID: "102", Album: "Abbey Road", Path: p("/beatles/1/come together.mp3")})
|
|
songRadioactivity = mf(model.MediaFile{ID: "1003", Title: "Radioactivity", ArtistID: "2", Artist: "Kraftwerk", AlbumID: "103", Album: "Radioactivity", Path: p("/kraft/radio/radio.mp3")})
|
|
songAntenna = mf(model.MediaFile{ID: "1004", Title: "Antenna", ArtistID: "2", Artist: "Kraftwerk",
|
|
AlbumID: "103",
|
|
Path: p("/kraft/radio/antenna.mp3"),
|
|
RGAlbumGain: gg.P(1.0), RGAlbumPeak: gg.P(2.0), RGTrackGain: gg.P(3.0), RGTrackPeak: gg.P(4.0),
|
|
})
|
|
songAntennaWithLyrics = mf(model.MediaFile{
|
|
ID: "1005",
|
|
Title: "Antenna",
|
|
ArtistID: "2",
|
|
Artist: "Kraftwerk",
|
|
AlbumID: "103",
|
|
Lyrics: `[{"lang":"xxx","line":[{"value":"This is a set of lyrics"}],"synced":false}]`,
|
|
})
|
|
songAntenna2 = mf(model.MediaFile{ID: "1006", Title: "Antenna", ArtistID: "2", Artist: "Kraftwerk", AlbumID: "103"})
|
|
// Multi-disc album tracks (intentionally out of order to test sorting)
|
|
songDisc2Track11 = mf(model.MediaFile{ID: "2001", Title: "Disc 2 Track 11", ArtistID: "1", Artist: "Test Artist", AlbumID: "104", Album: "Multi Disc Album", DiscNumber: 2, TrackNumber: 11, Path: p("/test/multi/disc2/track11.mp3"), OrderAlbumName: "multi disc album", OrderArtistName: "test artist"})
|
|
songDisc1Track01 = mf(model.MediaFile{ID: "2002", Title: "Disc 1 Track 1", ArtistID: "1", Artist: "Test Artist", AlbumID: "104", Album: "Multi Disc Album", DiscNumber: 1, TrackNumber: 1, Path: p("/test/multi/disc1/track1.mp3"), OrderAlbumName: "multi disc album", OrderArtistName: "test artist"})
|
|
songDisc2Track01 = mf(model.MediaFile{ID: "2003", Title: "Disc 2 Track 1", ArtistID: "1", Artist: "Test Artist", AlbumID: "104", Album: "Multi Disc Album", DiscNumber: 2, TrackNumber: 1, Path: p("/test/multi/disc2/track1.mp3"), OrderAlbumName: "multi disc album", OrderArtistName: "test artist"})
|
|
songDisc1Track02 = mf(model.MediaFile{ID: "2004", Title: "Disc 1 Track 2", ArtistID: "1", Artist: "Test Artist", AlbumID: "104", Album: "Multi Disc Album", DiscNumber: 1, TrackNumber: 2, Path: p("/test/multi/disc1/track2.mp3"), OrderAlbumName: "multi disc album", OrderArtistName: "test artist"})
|
|
testSongs = model.MediaFiles{
|
|
songDayInALife,
|
|
songComeTogether,
|
|
songRadioactivity,
|
|
songAntenna,
|
|
songAntennaWithLyrics,
|
|
songAntenna2,
|
|
songDisc2Track11,
|
|
songDisc1Track01,
|
|
songDisc2Track01,
|
|
songDisc1Track02,
|
|
}
|
|
)
|
|
|
|
var (
|
|
radioWithoutHomePage = model.Radio{ID: "1235", StreamUrl: "https://example.com:8000/1/stream.mp3", HomePageUrl: "", Name: "No Homepage"}
|
|
radioWithHomePage = model.Radio{ID: "5010", StreamUrl: "https://example.com/stream.mp3", Name: "Example Radio", HomePageUrl: "https://example.com"}
|
|
testRadios = model.Radios{radioWithoutHomePage, radioWithHomePage}
|
|
)
|
|
|
|
var (
|
|
plsBest model.Playlist
|
|
plsCool model.Playlist
|
|
testPlaylists []*model.Playlist
|
|
)
|
|
|
|
var (
|
|
adminUser = model.User{ID: "userid", UserName: "userid", Name: "admin", Email: "admin@email.com", IsAdmin: true}
|
|
regularUser = model.User{ID: "2222", UserName: "regular-user", Name: "Regular User", Email: "regular@example.com"}
|
|
testUsers = model.Users{adminUser, regularUser}
|
|
)
|
|
|
|
func p(path string) string {
|
|
return filepath.FromSlash(path)
|
|
}
|
|
|
|
// Initialize test DB
|
|
// TODO Load this data setup from file(s)
|
|
var _ = BeforeSuite(func() {
|
|
conn := GetDBXBuilder()
|
|
ctx := log.NewContext(context.TODO())
|
|
ctx = request.WithUser(ctx, adminUser)
|
|
|
|
ur := NewUserRepository(ctx, conn)
|
|
for i := range testUsers {
|
|
err := ur.Put(&testUsers[i])
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
// Associate users with library 1 (default test library)
|
|
for i := range testUsers {
|
|
err := ur.SetUserLibraries(testUsers[i].ID, []int{1})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
alr := NewAlbumRepository(ctx, conn).(*albumRepository)
|
|
for i := range testAlbums {
|
|
a := testAlbums[i]
|
|
err := alr.Put(&a)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
arr := NewArtistRepository(ctx, conn)
|
|
for i := range testArtists {
|
|
a := testArtists[i]
|
|
err := arr.Put(&a)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
// Associate artists with library 1 (default test library)
|
|
lr := NewLibraryRepository(ctx, conn)
|
|
for i := range testArtists {
|
|
err := lr.AddArtist(1, testArtists[i].ID)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
mr := NewMediaFileRepository(ctx, conn)
|
|
for i := range testSongs {
|
|
err := mr.Put(&testSongs[i])
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
rar := NewRadioRepository(ctx, conn)
|
|
for i := range testRadios {
|
|
r := testRadios[i]
|
|
err := rar.Put(&r)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
plsBest = model.Playlist{
|
|
Name: "Best",
|
|
Comment: "No Comments",
|
|
OwnerID: "userid",
|
|
OwnerName: "userid",
|
|
Public: true,
|
|
SongCount: 2,
|
|
}
|
|
plsBest.AddMediaFilesByID([]string{"1001", "1003"})
|
|
plsCool = model.Playlist{Name: "Cool", OwnerID: "userid", OwnerName: "userid"}
|
|
plsCool.AddMediaFilesByID([]string{"1004"})
|
|
testPlaylists = []*model.Playlist{&plsBest, &plsCool}
|
|
|
|
pr := NewPlaylistRepository(ctx, conn)
|
|
for i := range testPlaylists {
|
|
err := pr.Put(testPlaylists[i])
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
// Prepare annotations
|
|
if err := arr.SetStar(true, artistBeatles.ID); err != nil {
|
|
panic(err)
|
|
}
|
|
ar, err := arr.Get(artistBeatles.ID)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
if ar == nil {
|
|
panic("artist not found after SetStar")
|
|
}
|
|
artistBeatles.Starred = true
|
|
artistBeatles.StarredAt = ar.StarredAt
|
|
testArtists[1] = artistBeatles
|
|
|
|
if err := alr.SetStar(true, albumRadioactivity.ID); err != nil {
|
|
panic(err)
|
|
}
|
|
al, err := alr.Get(albumRadioactivity.ID)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
if al == nil {
|
|
panic("album not found after SetStar")
|
|
}
|
|
albumRadioactivity.Starred = true
|
|
albumRadioactivity.StarredAt = al.StarredAt
|
|
testAlbums[2] = albumRadioactivity
|
|
|
|
if err := mr.SetStar(true, songComeTogether.ID); err != nil {
|
|
panic(err)
|
|
}
|
|
mf, err := mr.Get(songComeTogether.ID)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
songComeTogether.Starred = true
|
|
songComeTogether.StarredAt = mf.StarredAt
|
|
testSongs[1] = songComeTogether
|
|
})
|
|
|
|
func GetDBXBuilder() *dbx.DB {
|
|
return dbx.NewFromDB(db.Db(), db.Dialect)
|
|
}
|