2020-01-14 14:23:29 -09:00
|
|
|
package persistence
|
2020-01-12 14:55:55 -09:00
|
|
|
|
|
|
|
|
import (
|
2020-01-28 04:22:17 -09:00
|
|
|
"context"
|
2023-12-08 15:29:16 -09:00
|
|
|
"encoding/json"
|
2020-12-13 10:05:48 -09:00
|
|
|
"fmt"
|
2020-04-17 15:56:52 -08:00
|
|
|
"strings"
|
2020-01-12 14:55:55 -09:00
|
|
|
|
2020-01-28 04:22:17 -09:00
|
|
|
. "github.com/Masterminds/squirrel"
|
|
|
|
|
"github.com/deluan/rest"
|
2020-06-13 09:55:58 -08:00
|
|
|
"github.com/navidrome/navidrome/conf"
|
2020-01-31 05:53:19 -09:00
|
|
|
"github.com/navidrome/navidrome/log"
|
2020-01-23 15:44:08 -09:00
|
|
|
"github.com/navidrome/navidrome/model"
|
2023-12-09 09:52:17 -09:00
|
|
|
"github.com/pocketbase/dbx"
|
2020-01-12 14:55:55 -09:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type albumRepository struct {
|
2020-01-28 04:22:17 -09:00
|
|
|
sqlRepository
|
2020-01-12 14:55:55 -09:00
|
|
|
}
|
|
|
|
|
|
2023-12-08 15:29:16 -09:00
|
|
|
type dbAlbum struct {
|
|
|
|
|
*model.Album `structs:",flatten"`
|
2024-05-09 04:13:42 -08:00
|
|
|
Discs string `structs:"-" json:"discs"`
|
2023-12-08 15:29:16 -09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *dbAlbum) PostScan() error {
|
2024-04-28 09:51:57 -08:00
|
|
|
if a.Discs != "" {
|
|
|
|
|
return json.Unmarshal([]byte(a.Discs), &a.Album.Discs)
|
2023-12-08 15:29:16 -09:00
|
|
|
}
|
2024-04-28 09:51:57 -08:00
|
|
|
return nil
|
2023-12-08 15:29:16 -09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-28 09:51:57 -08:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-09 09:52:17 -09:00
|
|
|
func NewAlbumRepository(ctx context.Context, db dbx.Builder) model.AlbumRepository {
|
2020-01-12 14:55:55 -09:00
|
|
|
r := &albumRepository{}
|
2020-01-28 04:22:17 -09:00
|
|
|
r.ctx = ctx
|
2023-12-09 09:52:17 -09:00
|
|
|
r.db = db
|
2024-09-17 12:59:55 -08:00
|
|
|
r.tableName = "album"
|
2024-09-09 15:45:02 -08:00
|
|
|
r.registerModel(&model.Album{}, map[string]filterFunc{
|
2021-07-16 14:18:22 -08:00
|
|
|
"id": idFilter(r.tableName),
|
2020-07-28 04:49:28 -08:00
|
|
|
"name": fullTextFilter,
|
|
|
|
|
"compilation": booleanFilter,
|
|
|
|
|
"artist_id": artistFilter,
|
|
|
|
|
"year": yearFilter,
|
|
|
|
|
"recently_played": recentlyPlayedFilter,
|
2020-08-14 09:35:28 -08:00
|
|
|
"starred": booleanFilter,
|
2021-04-07 07:04:36 -08:00
|
|
|
"has_rating": hasRatingFilter,
|
2024-09-17 12:59:55 -08:00
|
|
|
"genre_id": eqFilter,
|
2024-09-09 15:45:02 -08:00
|
|
|
})
|
2023-12-11 16:37:11 -09:00
|
|
|
if conf.Server.PreferSortTags {
|
|
|
|
|
r.sortMappings = map[string]string{
|
2023-12-14 17:45:47 -09:00
|
|
|
"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",
|
2024-09-09 15:45:02 -08:00
|
|
|
"album_artist": "compilation asc, COALESCE(NULLIF(sort_album_artist_name,''),order_album_artist_name) asc, COALESCE(NULLIF(sort_album_name,''),order_album_name) asc",
|
2023-12-14 17:45:47 -09:00
|
|
|
"max_year": "coalesce(nullif(original_date,''), cast(max_year as text)), release_date, name, COALESCE(NULLIF(sort_album_name,''),order_album_name) asc",
|
2024-09-20 12:59:46 -08:00
|
|
|
"random": "random",
|
2023-12-14 17:45:47 -09:00
|
|
|
"recently_added": recentlyAddedSort(),
|
2024-09-19 09:05:26 -08:00
|
|
|
"starred_at": "starred, starred_at",
|
2023-12-14 17:45:47 -09:00
|
|
|
}
|
|
|
|
|
} 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",
|
2024-09-09 15:45:02 -08:00
|
|
|
"album_artist": "compilation asc, order_album_artist_name asc, order_album_name asc",
|
2023-12-14 17:45:47 -09:00
|
|
|
"max_year": "coalesce(nullif(original_date,''), cast(max_year as text)), release_date, name, order_album_name asc",
|
2024-09-20 12:59:46 -08:00
|
|
|
"random": "random",
|
2023-12-14 17:45:47 -09:00
|
|
|
"recently_added": recentlyAddedSort(),
|
2024-09-19 09:05:26 -08:00
|
|
|
"starred_at": "starred, starred_at",
|
2023-12-11 16:37:11 -09:00
|
|
|
}
|
|
|
|
|
}
|
2020-03-19 11:02:11 -08:00
|
|
|
|
2020-01-12 14:55:55 -09:00
|
|
|
return r
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-12 13:49:47 -09:00
|
|
|
func recentlyAddedSort() string {
|
|
|
|
|
if conf.Server.RecentlyAddedByModTime {
|
|
|
|
|
return "updated_at"
|
|
|
|
|
}
|
|
|
|
|
return "created_at"
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-28 09:51:57 -08:00
|
|
|
func recentlyPlayedFilter(string, interface{}) Sqlizer {
|
2020-07-28 04:49:28 -08:00
|
|
|
return Gt{"play_count": 0}
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-28 09:51:57 -08:00
|
|
|
func hasRatingFilter(string, interface{}) Sqlizer {
|
2021-04-07 07:04:36 -08:00
|
|
|
return Gt{"rating": 0}
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-28 09:51:57 -08:00
|
|
|
func yearFilter(_ string, value interface{}) Sqlizer {
|
2020-03-27 17:06:02 -08:00
|
|
|
return Or{
|
|
|
|
|
And{
|
|
|
|
|
Gt{"min_year": 0},
|
|
|
|
|
LtOrEq{"min_year": value},
|
|
|
|
|
GtOrEq{"max_year": value},
|
|
|
|
|
},
|
|
|
|
|
Eq{"max_year": value},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-28 09:51:57 -08:00
|
|
|
func artistFilter(_ string, value interface{}) Sqlizer {
|
2020-12-13 10:05:48 -09:00
|
|
|
return Like{"all_artist_ids": fmt.Sprintf("%%%s%%", value)}
|
2020-03-25 14:51:13 -08:00
|
|
|
}
|
|
|
|
|
|
2020-01-28 04:22:17 -09:00
|
|
|
func (r *albumRepository) CountAll(options ...model.QueryOptions) (int64, error) {
|
2021-07-16 16:20:33 -08:00
|
|
|
sql := r.newSelectWithAnnotation("album.id")
|
2023-11-25 19:08:20 -09:00
|
|
|
sql = r.withGenres(sql) // Required for filtering by genre
|
2021-07-16 15:41:49 -08:00
|
|
|
return r.count(sql, options...)
|
2020-01-28 04:22:17 -09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *albumRepository) Exists(id string) (bool, error) {
|
2021-07-16 16:20:33 -08:00
|
|
|
return r.exists(Select().Where(Eq{"album.id": id}))
|
2020-01-28 04:22:17 -09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *albumRepository) selectAlbum(options ...model.QueryOptions) SelectBuilder {
|
2021-07-16 16:20:33 -08:00
|
|
|
sql := r.newSelectWithAnnotation("album.id", options...).Columns("album.*")
|
2021-11-02 17:18:53 -08:00
|
|
|
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
|
2020-01-12 14:55:55 -09:00
|
|
|
}
|
|
|
|
|
|
2020-01-14 18:22:34 -09:00
|
|
|
func (r *albumRepository) Get(id string) (*model.Album, error) {
|
2021-07-16 16:20:33 -08:00
|
|
|
sq := r.selectAlbum().Where(Eq{"album.id": id})
|
2024-04-28 09:51:57 -08:00
|
|
|
var dba dbAlbums
|
2023-12-08 15:29:16 -09:00
|
|
|
if err := r.queryAll(sq, &dba); err != nil {
|
2020-01-12 14:55:55 -09:00
|
|
|
return nil, err
|
|
|
|
|
}
|
2023-12-08 15:29:16 -09:00
|
|
|
if len(dba) == 0 {
|
2020-05-22 11:23:42 -08:00
|
|
|
return nil, model.ErrNotFound
|
|
|
|
|
}
|
2024-04-28 09:51:57 -08:00
|
|
|
res := dba.toModels()
|
2024-04-28 10:31:15 -08:00
|
|
|
err := loadAllGenres(r, res)
|
2021-07-16 13:15:34 -08:00
|
|
|
return &res[0], err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *albumRepository) Put(m *model.Album) error {
|
2023-12-08 15:29:16 -09:00
|
|
|
_, err := r.put(m.ID, &dbAlbum{Album: m})
|
2021-07-16 13:15:34 -08:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2024-04-28 10:31:15 -08:00
|
|
|
return r.updateGenres(m.ID, m.Genres)
|
2020-01-12 14:55:55 -09:00
|
|
|
}
|
|
|
|
|
|
2020-01-14 18:22:34 -09:00
|
|
|
func (r *albumRepository) GetAll(options ...model.QueryOptions) (model.Albums, error) {
|
2021-11-02 17:18:53 -08:00
|
|
|
res, err := r.GetAllWithoutGenres(options...)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2024-04-28 10:31:15 -08:00
|
|
|
err = loadAllGenres(r, res)
|
2021-11-02 17:18:53 -08:00
|
|
|
return res, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *albumRepository) GetAllWithoutGenres(options ...model.QueryOptions) (model.Albums, error) {
|
2024-05-18 10:10:53 -08:00
|
|
|
r.resetSeededRandom(options)
|
2021-07-16 16:20:33 -08:00
|
|
|
sq := r.selectAlbum(options...)
|
2024-04-28 09:51:57 -08:00
|
|
|
var dba dbAlbums
|
2023-12-08 15:29:16 -09:00
|
|
|
err := r.queryAll(sq, &dba)
|
2021-07-16 13:15:34 -08:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2024-04-28 09:51:57 -08:00
|
|
|
return dba.toModels(), err
|
2020-01-12 14:55:55 -09:00
|
|
|
}
|
|
|
|
|
|
2020-05-18 16:32:01 -08:00
|
|
|
func (r *albumRepository) purgeEmpty() error {
|
2020-01-31 13:57:06 -09:00
|
|
|
del := Delete(r.tableName).Where("id not in (select distinct(album_id) from media_file)")
|
|
|
|
|
c, err := r.executeSQL(del)
|
2020-01-31 05:53:19 -09:00
|
|
|
if err == nil {
|
|
|
|
|
if c > 0 {
|
2020-01-31 13:57:06 -09:00
|
|
|
log.Debug(r.ctx, "Purged empty albums", "totalDeleted", c)
|
2020-01-31 05:53:19 -09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return err
|
2020-01-17 19:28:11 -09:00
|
|
|
}
|
|
|
|
|
|
2020-01-14 18:22:34 -09:00
|
|
|
func (r *albumRepository) Search(q string, offset int, size int) (model.Albums, error) {
|
2024-04-28 09:51:57 -08:00
|
|
|
var dba dbAlbums
|
2023-12-08 15:29:16 -09:00
|
|
|
err := r.doSearch(q, offset, size, &dba, "name")
|
2023-11-21 17:11:45 -09:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2024-04-28 09:51:57 -08:00
|
|
|
res := dba.toModels()
|
2024-04-28 10:31:15 -08:00
|
|
|
err = loadAllGenres(r, res)
|
2023-12-08 15:29:16 -09:00
|
|
|
return res, err
|
2020-01-28 04:22:17 -09:00
|
|
|
}
|
2020-01-13 11:41:14 -09:00
|
|
|
|
2020-01-28 04:22:17 -09:00
|
|
|
func (r *albumRepository) Count(options ...rest.QueryOptions) (int64, error) {
|
2024-09-09 15:45:02 -08:00
|
|
|
return r.CountAll(r.parseRestOptions(r.ctx, options...))
|
2020-01-28 04:22:17 -09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *albumRepository) Read(id string) (interface{}, error) {
|
|
|
|
|
return r.Get(id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *albumRepository) ReadAll(options ...rest.QueryOptions) (interface{}, error) {
|
2024-09-09 15:45:02 -08:00
|
|
|
return r.GetAll(r.parseRestOptions(r.ctx, options...))
|
2020-01-28 04:22:17 -09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *albumRepository) EntityName() string {
|
|
|
|
|
return "album"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *albumRepository) NewInstance() interface{} {
|
|
|
|
|
return &model.Album{}
|
2020-01-12 14:55:55 -09:00
|
|
|
}
|
|
|
|
|
|
2020-01-14 18:22:34 -09:00
|
|
|
var _ model.AlbumRepository = (*albumRepository)(nil)
|
2020-01-28 04:22:17 -09:00
|
|
|
var _ model.ResourceRepository = (*albumRepository)(nil)
|