Deterministic pagination in random albums sort (#1841)

* Deterministic pagination in random albums sort

* Reseed on first random page

* Add unit tests

* Use rand in Subsonic API

* Use different seeds per user on SEEDEDRAND() SQLite3 function

* Small refactor

* Fix id mismatch

* Add seeded random to media_file (subsonic endpoint `getRandomSongs`)

* Refactor

* Remove unneeded import

---------

Co-authored-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Guilherme Souza 2024-05-18 15:10:53 -03:00 committed by GitHub
parent a9feeac793
commit 98218d045e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 110 additions and 8 deletions

View file

@ -75,7 +75,7 @@ func NewAlbumRepository(ctx context.Context, db dbx.Builder) model.AlbumReposito
"artist": "compilation asc, COALESCE(NULLIF(sort_album_artist_name,''),order_album_artist_name) asc, COALESCE(NULLIF(sort_album_name,''),order_album_name) asc",
"albumArtist": "compilation asc, COALESCE(NULLIF(sort_album_artist_name,''),order_album_artist_name) asc, COALESCE(NULLIF(sort_album_name,''),order_album_name) asc",
"max_year": "coalesce(nullif(original_date,''), cast(max_year as text)), release_date, name, COALESCE(NULLIF(sort_album_name,''),order_album_name) asc",
"random": "RANDOM()",
"random": r.seededRandomSort(),
"recently_added": recentlyAddedSort(),
}
} else {
@ -84,7 +84,7 @@ func NewAlbumRepository(ctx context.Context, db dbx.Builder) model.AlbumReposito
"artist": "compilation asc, order_album_artist_name asc, order_album_name asc",
"albumArtist": "compilation asc, order_album_artist_name asc, order_album_name asc",
"max_year": "coalesce(nullif(original_date,''), cast(max_year as text)), release_date, name, order_album_name asc",
"random": "RANDOM()",
"random": r.seededRandomSort(),
"recently_added": recentlyAddedSort(),
}
}
@ -180,6 +180,7 @@ func (r *albumRepository) GetAll(options ...model.QueryOptions) (model.Albums, e
}
func (r *albumRepository) GetAllWithoutGenres(options ...model.QueryOptions) (model.Albums, error) {
r.resetSeededRandom(options)
sq := r.selectAlbum(options...)
var dba dbAlbums
err := r.queryAll(sq, &dba)

View file

@ -36,7 +36,7 @@ func NewMediaFileRepository(ctx context.Context, db dbx.Builder) *mediaFileRepos
"title": "COALESCE(NULLIF(sort_title,''),title)",
"artist": "COALESCE(NULLIF(sort_artist_name,''),order_artist_name) asc, COALESCE(NULLIF(sort_album_name,''),order_album_name) asc, release_date asc, disc_number asc, track_number asc",
"album": "COALESCE(NULLIF(sort_album_name,''),order_album_name) asc, release_date asc, disc_number asc, track_number asc, COALESCE(NULLIF(sort_artist_name,''),order_artist_name) asc, COALESCE(NULLIF(sort_title,''),title) asc",
"random": "RANDOM()",
"random": r.seededRandomSort(),
"createdAt": "media_file.created_at",
}
} else {
@ -44,7 +44,7 @@ func NewMediaFileRepository(ctx context.Context, db dbx.Builder) *mediaFileRepos
"title": "order_title",
"artist": "order_artist_name asc, order_album_name asc, release_date asc, disc_number asc, track_number asc",
"album": "order_album_name asc, release_date asc, disc_number asc, track_number asc, order_artist_name asc, title asc",
"random": "RANDOM()",
"random": r.seededRandomSort(),
"createdAt": "media_file.created_at",
}
}
@ -102,6 +102,7 @@ func (r *mediaFileRepository) Get(id string) (*model.MediaFile, error) {
}
func (r *mediaFileRepository) GetAll(options ...model.QueryOptions) (model.MediaFiles, error) {
r.resetSeededRandom(options)
sq := r.selectMediaFile(options...)
res := model.MediaFiles{}
err := r.queryAll(sq, &res, options...)

View file

@ -14,6 +14,7 @@ import (
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request"
"github.com/navidrome/navidrome/utils/hasher"
"github.com/pocketbase/dbx"
)
@ -137,6 +138,18 @@ func (r sqlRepository) applyFilters(sq SelectBuilder, options ...model.QueryOpti
return sq
}
func (r sqlRepository) seededRandomSort() string {
u, _ := request.UserFrom(r.ctx)
return fmt.Sprintf("SEEDEDRAND('%s', id)", r.tableName+u.ID)
}
func (r sqlRepository) resetSeededRandom(options []model.QueryOptions) {
if len(options) > 0 && options[0].Offset == 0 && options[0].Sort == "random" {
u, _ := request.UserFrom(r.ctx)
hasher.Reseed(r.tableName + u.ID)
}
}
func (r sqlRepository) executeSQL(sq Sqlizer) (int64, error) {
query, args, err := r.toSQL(sq)
if err != nil {