diff --git a/persistence/mediafile_repository.go b/persistence/mediafile_repository.go
index eee6444c1..d12dd71ba 100644
--- a/persistence/mediafile_repository.go
+++ b/persistence/mediafile_repository.go
@@ -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")
diff --git a/persistence/mediafile_repository_test.go b/persistence/mediafile_repository_test.go
index 9dbb8080f..b364ca2e8 100644
--- a/persistence/mediafile_repository_test.go
+++ b/persistence/mediafile_repository_test.go
@@ -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
+ })
+ })
+
+ })
+ })
})
diff --git a/ui/src/song/SongList.jsx b/ui/src/song/SongList.jsx
index 2a2807964..f067e11d2 100644
--- a/ui/src/song/SongList.jsx
+++ b/ui/src/song/SongList.jsx
@@ -182,7 +182,9 @@ const SongList = (props) => {
),
comment: ,
path: ,
- createdAt: ,
+ createdAt: (
+
+ ),
}
}, [isDesktop, classes.ratingField])