mirror of
https://github.com/navidrome/navidrome.git
synced 2025-12-23 23:18:05 -05:00
* fix: implement RecentlyAddedByModTime support for mediafiles Fixes #4046 by adding recently_added sort mapping to MediaFileRepository that respects the RecentlyAddedByModTime configuration setting. Previously, this feature only worked for albums, causing inconsistent behavior when clients requested tracks sorted by 'recently added'. Changes include: - Add mediaFileRecentlyAddedSort() function that returns 'updated_at' when RecentlyAddedByModTime=true, 'created_at' otherwise - Add 'recently_added' sort mapping to mediafile repository - Add comprehensive tests to verify both configuration scenarios This ensures consistent sorting behavior between albums and tracks when using the RecentlyAddedByModTime feature. * fix: update createdAt field to sort by recently added Modified the createdAt field in the SongList component to include a sortBy attribute set to "recently_added". This change ensures that the media files are displayed in the order they were added, improving the user experience when browsing through recently added items. Signed-off-by: Deluan <deluan@navidrome.org> * better testing Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
. "github.com/Masterminds/squirrel"
|
||||
"github.com/deluan/rest"
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/utils/slice"
|
||||
@@ -74,13 +75,14 @@ func NewMediaFileRepository(ctx context.Context, db dbx.Builder) model.MediaFile
|
||||
r.tableName = "media_file"
|
||||
r.registerModel(&model.MediaFile{}, mediaFileFilter())
|
||||
r.setSortMappings(map[string]string{
|
||||
"title": "order_title",
|
||||
"artist": "order_artist_name, order_album_name, release_date, disc_number, track_number",
|
||||
"album_artist": "order_album_artist_name, order_album_name, release_date, disc_number, track_number",
|
||||
"album": "order_album_name, album_id, disc_number, track_number, order_artist_name, title",
|
||||
"random": "random",
|
||||
"created_at": "media_file.created_at",
|
||||
"starred_at": "starred, starred_at",
|
||||
"title": "order_title",
|
||||
"artist": "order_artist_name, order_album_name, release_date, disc_number, track_number",
|
||||
"album_artist": "order_album_artist_name, order_album_name, release_date, disc_number, track_number",
|
||||
"album": "order_album_name, album_id, disc_number, track_number, order_artist_name, title",
|
||||
"random": "random",
|
||||
"created_at": "media_file.created_at",
|
||||
"recently_added": mediaFileRecentlyAddedSort(),
|
||||
"starred_at": "starred, starred_at",
|
||||
})
|
||||
return r
|
||||
}
|
||||
@@ -103,6 +105,13 @@ var mediaFileFilter = sync.OnceValue(func() map[string]filterFunc {
|
||||
return filters
|
||||
})
|
||||
|
||||
func mediaFileRecentlyAddedSort() string {
|
||||
if conf.Server.RecentlyAddedByModTime {
|
||||
return "media_file.updated_at"
|
||||
}
|
||||
return "media_file.created_at"
|
||||
}
|
||||
|
||||
func (r *mediaFileRepository) CountAll(options ...model.QueryOptions) (int64, error) {
|
||||
query := r.newSelect()
|
||||
query = r.withAnnotation(query, "media_file.id")
|
||||
|
||||
@@ -5,12 +5,15 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/squirrel"
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/conf/configtest"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/model/id"
|
||||
"github.com/navidrome/navidrome/model/request"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/pocketbase/dbx"
|
||||
)
|
||||
|
||||
var _ = Describe("MediaRepository", func() {
|
||||
@@ -155,4 +158,156 @@ var _ = Describe("MediaRepository", func() {
|
||||
Expect(mf.PlayCount).To(Equal(int64(1)))
|
||||
})
|
||||
})
|
||||
|
||||
Context("Sort options", func() {
|
||||
Context("recently_added sort", func() {
|
||||
var testMediaFiles []model.MediaFile
|
||||
|
||||
BeforeEach(func() {
|
||||
DeferCleanup(configtest.SetupConfig())
|
||||
|
||||
// Create test media files with specific timestamps
|
||||
testMediaFiles = []model.MediaFile{
|
||||
{
|
||||
ID: id.NewRandom(),
|
||||
LibraryID: 1,
|
||||
Title: "Old Song",
|
||||
Path: "/test/old.mp3",
|
||||
},
|
||||
{
|
||||
ID: id.NewRandom(),
|
||||
LibraryID: 1,
|
||||
Title: "Middle Song",
|
||||
Path: "/test/middle.mp3",
|
||||
},
|
||||
{
|
||||
ID: id.NewRandom(),
|
||||
LibraryID: 1,
|
||||
Title: "New Song",
|
||||
Path: "/test/new.mp3",
|
||||
},
|
||||
}
|
||||
|
||||
// Insert test data first
|
||||
for i := range testMediaFiles {
|
||||
Expect(mr.Put(&testMediaFiles[i])).To(Succeed())
|
||||
}
|
||||
|
||||
// Then manually update timestamps using direct SQL to bypass the repository logic
|
||||
db := GetDBXBuilder()
|
||||
|
||||
// Set specific timestamps for testing
|
||||
oldTime := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
middleTime := time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
newTime := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
// Update "Old Song": created long ago, updated recently
|
||||
_, err := db.Update("media_file",
|
||||
map[string]interface{}{
|
||||
"created_at": oldTime,
|
||||
"updated_at": newTime,
|
||||
},
|
||||
dbx.HashExp{"id": testMediaFiles[0].ID}).Execute()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Update "Middle Song": created and updated at the same middle time
|
||||
_, err = db.Update("media_file",
|
||||
map[string]interface{}{
|
||||
"created_at": middleTime,
|
||||
"updated_at": middleTime,
|
||||
},
|
||||
dbx.HashExp{"id": testMediaFiles[1].ID}).Execute()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Update "New Song": created recently, updated long ago
|
||||
_, err = db.Update("media_file",
|
||||
map[string]interface{}{
|
||||
"created_at": newTime,
|
||||
"updated_at": oldTime,
|
||||
},
|
||||
dbx.HashExp{"id": testMediaFiles[2].ID}).Execute()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
// Clean up test data
|
||||
for _, mf := range testMediaFiles {
|
||||
_ = mr.Delete(mf.ID)
|
||||
}
|
||||
})
|
||||
|
||||
When("RecentlyAddedByModTime is false", func() {
|
||||
var testRepo model.MediaFileRepository
|
||||
|
||||
BeforeEach(func() {
|
||||
conf.Server.RecentlyAddedByModTime = false
|
||||
// Create repository AFTER setting config
|
||||
ctx := log.NewContext(GinkgoT().Context())
|
||||
ctx = request.WithUser(ctx, model.User{ID: "userid"})
|
||||
testRepo = NewMediaFileRepository(ctx, GetDBXBuilder())
|
||||
})
|
||||
|
||||
It("sorts by created_at", func() {
|
||||
// Get results sorted by recently_added (should use created_at)
|
||||
results, err := testRepo.GetAll(model.QueryOptions{
|
||||
Sort: "recently_added",
|
||||
Order: "desc",
|
||||
Filters: squirrel.Eq{"media_file.id": []string{testMediaFiles[0].ID, testMediaFiles[1].ID, testMediaFiles[2].ID}},
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(results).To(HaveLen(3))
|
||||
|
||||
// Verify sorting by created_at (newest first in descending order)
|
||||
Expect(results[0].Title).To(Equal("New Song")) // created 2022
|
||||
Expect(results[1].Title).To(Equal("Middle Song")) // created 2021
|
||||
Expect(results[2].Title).To(Equal("Old Song")) // created 2020
|
||||
})
|
||||
|
||||
It("sorts in ascending order when specified", func() {
|
||||
// Get results sorted by recently_added in ascending order
|
||||
results, err := testRepo.GetAll(model.QueryOptions{
|
||||
Sort: "recently_added",
|
||||
Order: "asc",
|
||||
Filters: squirrel.Eq{"media_file.id": []string{testMediaFiles[0].ID, testMediaFiles[1].ID, testMediaFiles[2].ID}},
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(results).To(HaveLen(3))
|
||||
|
||||
// Verify sorting by created_at (oldest first)
|
||||
Expect(results[0].Title).To(Equal("Old Song")) // created 2020
|
||||
Expect(results[1].Title).To(Equal("Middle Song")) // created 2021
|
||||
Expect(results[2].Title).To(Equal("New Song")) // created 2022
|
||||
})
|
||||
})
|
||||
|
||||
When("RecentlyAddedByModTime is true", func() {
|
||||
var testRepo model.MediaFileRepository
|
||||
|
||||
BeforeEach(func() {
|
||||
conf.Server.RecentlyAddedByModTime = true
|
||||
// Create repository AFTER setting config
|
||||
ctx := log.NewContext(GinkgoT().Context())
|
||||
ctx = request.WithUser(ctx, model.User{ID: "userid"})
|
||||
testRepo = NewMediaFileRepository(ctx, GetDBXBuilder())
|
||||
})
|
||||
|
||||
It("sorts by updated_at", func() {
|
||||
// Get results sorted by recently_added (should use updated_at)
|
||||
results, err := testRepo.GetAll(model.QueryOptions{
|
||||
Sort: "recently_added",
|
||||
Order: "desc",
|
||||
Filters: squirrel.Eq{"media_file.id": []string{testMediaFiles[0].ID, testMediaFiles[1].ID, testMediaFiles[2].ID}},
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(results).To(HaveLen(3))
|
||||
|
||||
// Verify sorting by updated_at (newest first in descending order)
|
||||
Expect(results[0].Title).To(Equal("Old Song")) // updated 2022
|
||||
Expect(results[1].Title).To(Equal("Middle Song")) // updated 2021
|
||||
Expect(results[2].Title).To(Equal("New Song")) // updated 2020
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -182,7 +182,9 @@ const SongList = (props) => {
|
||||
),
|
||||
comment: <TextField source="comment" />,
|
||||
path: <PathField source="path" />,
|
||||
createdAt: <DateField source="createdAt" showTime />,
|
||||
createdAt: (
|
||||
<DateField source="createdAt" sortBy="recently_added" showTime />
|
||||
),
|
||||
}
|
||||
}, [isDesktop, classes.ratingField])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user