package persistence import ( "context" "encoding/json" "fmt" "strings" . "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/pocketbase/dbx" ) type albumRepository struct { sqlRepository sqlRestful } type dbAlbum struct { *model.Album `structs:",flatten"` Discs string `structs:"-" json:"discs"` } func (a *dbAlbum) PostScan() error { if a.Discs != "" { return json.Unmarshal([]byte(a.Discs), &a.Album.Discs) } return nil } func (a *dbAlbum) PostMapArgs(m map[string]any) error { if len(a.Album.Discs) == 0 { m["discs"] = "{}" return nil } b, err := json.Marshal(a.Album.Discs) if err != nil { return err } m["discs"] = string(b) return nil } type dbAlbums []dbAlbum func (dba dbAlbums) toModels() model.Albums { res := make(model.Albums, len(dba)) for i := range dba { res[i] = *dba[i].Album } return res } func NewAlbumRepository(ctx context.Context, db dbx.Builder) model.AlbumRepository { r := &albumRepository{} r.ctx = ctx r.db = db r.tableName = "album" r.filterMappings = map[string]filterFunc{ "id": idFilter(r.tableName), "name": fullTextFilter, "compilation": booleanFilter, "artist_id": artistFilter, "year": yearFilter, "recently_played": recentlyPlayedFilter, "starred": booleanFilter, "has_rating": hasRatingFilter, } if conf.Server.PreferSortTags { r.sortMappings = map[string]string{ "name": "COALESCE(NULLIF(sort_album_name,''),order_album_name)", "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()", "recently_added": recentlyAddedSort(), } } else { r.sortMappings = map[string]string{ "name": "order_album_name asc, order_album_artist_name asc", "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()", "recently_added": recentlyAddedSort(), } } return r } func recentlyAddedSort() string { if conf.Server.RecentlyAddedByModTime { return "updated_at" } return "created_at" } func recentlyPlayedFilter(string, interface{}) Sqlizer { return Gt{"play_count": 0} } func hasRatingFilter(string, interface{}) Sqlizer { return Gt{"rating": 0} } func yearFilter(_ string, value interface{}) Sqlizer { return Or{ And{ Gt{"min_year": 0}, LtOrEq{"min_year": value}, GtOrEq{"max_year": value}, }, Eq{"max_year": value}, } } func artistFilter(_ string, value interface{}) Sqlizer { return Like{"all_artist_ids": fmt.Sprintf("%%%s%%", value)} } func (r *albumRepository) CountAll(options ...model.QueryOptions) (int64, error) { sql := r.newSelectWithAnnotation("album.id") sql = r.withGenres(sql) // Required for filtering by genre return r.count(sql, options...) } func (r *albumRepository) Exists(id string) (bool, error) { return r.exists(Select().Where(Eq{"album.id": id})) } func (r *albumRepository) selectAlbum(options ...model.QueryOptions) SelectBuilder { sql := r.newSelectWithAnnotation("album.id", options...).Columns("album.*") if len(options) > 0 && options[0].Filters != nil { s, _, _ := options[0].Filters.ToSql() // If there's any reference of genre in the filter, joins with genre if strings.Contains(s, "genre") { sql = r.withGenres(sql) // If there's no filter on genre_id, group the results by media_file.id if !strings.Contains(s, "genre_id") { sql = sql.GroupBy("album.id") } } } return sql } func (r *albumRepository) Get(id string) (*model.Album, error) { sq := r.selectAlbum().Where(Eq{"album.id": id}) var dba dbAlbums if err := r.queryAll(sq, &dba); err != nil { return nil, err } if len(dba) == 0 { return nil, model.ErrNotFound } res := dba.toModels() err := loadAllGenres(r, res) return &res[0], err } func (r *albumRepository) Put(m *model.Album) error { _, err := r.put(m.ID, &dbAlbum{Album: m}) if err != nil { return err } return r.updateGenres(m.ID, m.Genres) } func (r *albumRepository) GetAll(options ...model.QueryOptions) (model.Albums, error) { res, err := r.GetAllWithoutGenres(options...) if err != nil { return nil, err } err = loadAllGenres(r, res) return res, err } func (r *albumRepository) GetAllWithoutGenres(options ...model.QueryOptions) (model.Albums, error) { sq := r.selectAlbum(options...) var dba dbAlbums err := r.queryAll(sq, &dba) if err != nil { return nil, err } return dba.toModels(), err } func (r *albumRepository) purgeEmpty() error { del := Delete(r.tableName).Where("id not in (select distinct(album_id) from media_file)") c, err := r.executeSQL(del) if err == nil { if c > 0 { log.Debug(r.ctx, "Purged empty albums", "totalDeleted", c) } } return err } func (r *albumRepository) Search(q string, offset int, size int) (model.Albums, error) { var dba dbAlbums err := r.doSearch(q, offset, size, &dba, "name") if err != nil { return nil, err } res := dba.toModels() err = loadAllGenres(r, res) return res, err } func (r *albumRepository) Count(options ...rest.QueryOptions) (int64, error) { return r.CountAll(r.parseRestOptions(options...)) } func (r *albumRepository) Read(id string) (interface{}, error) { return r.Get(id) } func (r *albumRepository) ReadAll(options ...rest.QueryOptions) (interface{}, error) { return r.GetAll(r.parseRestOptions(options...)) } func (r *albumRepository) EntityName() string { return "album" } func (r *albumRepository) NewInstance() interface{} { return &model.Album{} } var _ model.AlbumRepository = (*albumRepository)(nil) var _ model.ResourceRepository = (*albumRepository)(nil)