2020-01-15 00:23:29 +01:00
|
|
|
package persistence
|
2020-01-13 03:59:06 +01:00
|
|
|
|
|
|
|
import (
|
2020-01-28 14:22:17 +01:00
|
|
|
"context"
|
2021-10-15 00:39:17 +02:00
|
|
|
"encoding/json"
|
|
|
|
"strings"
|
2020-04-11 22:45:21 +02:00
|
|
|
"time"
|
2020-01-13 03:59:06 +01:00
|
|
|
|
2020-01-28 14:22:17 +01:00
|
|
|
. "github.com/Masterminds/squirrel"
|
2020-01-13 03:59:06 +01:00
|
|
|
"github.com/astaxie/beego/orm"
|
2020-05-04 02:05:03 +02:00
|
|
|
"github.com/deluan/rest"
|
2020-04-11 22:45:21 +02:00
|
|
|
"github.com/navidrome/navidrome/log"
|
2020-01-24 01:44:08 +01:00
|
|
|
"github.com/navidrome/navidrome/model"
|
2020-01-13 03:59:06 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
type playlistRepository struct {
|
|
|
|
sqlRepository
|
2020-05-04 02:05:03 +02:00
|
|
|
sqlRestful
|
2020-01-13 03:59:06 +01:00
|
|
|
}
|
|
|
|
|
2021-10-15 00:39:17 +02:00
|
|
|
type dbPlaylist struct {
|
|
|
|
model.Playlist `structs:",flatten"`
|
|
|
|
RawRules string `structs:"rules"`
|
|
|
|
}
|
|
|
|
|
2020-01-28 14:22:17 +01:00
|
|
|
func NewPlaylistRepository(ctx context.Context, o orm.Ormer) model.PlaylistRepository {
|
2020-01-13 03:59:06 +01:00
|
|
|
r := &playlistRepository{}
|
2020-01-28 14:22:17 +01:00
|
|
|
r.ctx = ctx
|
2020-01-19 21:37:41 +01:00
|
|
|
r.ormer = o
|
2020-01-13 06:04:11 +01:00
|
|
|
r.tableName = "playlist"
|
2020-01-13 03:59:06 +01:00
|
|
|
return r
|
|
|
|
}
|
|
|
|
|
2020-05-17 03:42:52 +02:00
|
|
|
func (r *playlistRepository) userFilter() Sqlizer {
|
|
|
|
user := loggedUser(r.ctx)
|
|
|
|
if user.IsAdmin {
|
|
|
|
return And{}
|
|
|
|
}
|
|
|
|
return Or{
|
|
|
|
Eq{"public": true},
|
|
|
|
Eq{"owner": user.UserName},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-04 02:05:03 +02:00
|
|
|
func (r *playlistRepository) CountAll(options ...model.QueryOptions) (int64, error) {
|
2020-05-17 03:42:52 +02:00
|
|
|
sql := Select().Where(r.userFilter())
|
|
|
|
return r.count(sql, options...)
|
2020-01-28 14:22:17 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func (r *playlistRepository) Exists(id string) (bool, error) {
|
2020-05-17 03:42:52 +02:00
|
|
|
return r.exists(Select().Where(And{Eq{"id": id}, r.userFilter()}))
|
2020-01-28 14:22:17 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func (r *playlistRepository) Delete(id string) error {
|
2020-06-05 16:59:23 +02:00
|
|
|
usr := loggedUser(r.ctx)
|
|
|
|
if !usr.IsAdmin {
|
|
|
|
pls, err := r.Get(id)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if pls.Owner != usr.UserName {
|
|
|
|
return rest.ErrPermissionDenied
|
|
|
|
}
|
|
|
|
}
|
2020-06-10 23:00:15 +02:00
|
|
|
return r.delete(And{Eq{"id": id}, r.userFilter()})
|
2020-01-28 14:22:17 +01:00
|
|
|
}
|
|
|
|
|
2020-01-15 04:22:34 +01:00
|
|
|
func (r *playlistRepository) Put(p *model.Playlist) error {
|
2021-10-15 00:39:17 +02:00
|
|
|
pls := dbPlaylist{Playlist: *p}
|
|
|
|
if p.Rules != nil {
|
|
|
|
j, err := json.Marshal(p.Rules)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
pls.RawRules = string(j)
|
|
|
|
}
|
|
|
|
if pls.ID == "" {
|
|
|
|
pls.CreatedAt = time.Now()
|
2020-05-17 03:42:52 +02:00
|
|
|
} else {
|
2021-10-15 00:39:17 +02:00
|
|
|
ok, err := r.Exists(pls.ID)
|
2020-05-17 03:42:52 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if !ok {
|
|
|
|
return model.ErrNotAuthorized
|
|
|
|
}
|
2020-04-11 22:45:21 +02:00
|
|
|
}
|
2021-10-15 00:39:17 +02:00
|
|
|
pls.UpdatedAt = time.Now()
|
2020-05-08 19:57:32 +02:00
|
|
|
|
|
|
|
// Save tracks for later and set it to nil, to avoid trying to save it to the DB
|
2021-10-15 00:39:17 +02:00
|
|
|
tracks := pls.Tracks
|
|
|
|
pls.Tracks = nil
|
2020-05-08 19:57:32 +02:00
|
|
|
|
2021-10-15 00:39:17 +02:00
|
|
|
id, err := r.put(pls.ID, pls)
|
2020-05-08 19:57:32 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-05-17 03:42:52 +02:00
|
|
|
p.ID = id
|
2020-06-09 00:39:31 +02:00
|
|
|
|
|
|
|
// Only update tracks if they are specified
|
2021-10-15 00:39:17 +02:00
|
|
|
if tracks == nil {
|
|
|
|
return nil
|
2020-05-17 03:42:52 +02:00
|
|
|
}
|
2021-10-15 00:39:17 +02:00
|
|
|
return r.updateTracks(id, tracks)
|
2020-01-13 03:59:06 +01:00
|
|
|
}
|
|
|
|
|
2020-01-15 04:22:34 +01:00
|
|
|
func (r *playlistRepository) Get(id string) (*model.Playlist, error) {
|
2021-10-15 00:39:17 +02:00
|
|
|
return r.findBy(And{Eq{"id": id}, r.userFilter()}, true)
|
2020-01-22 00:10:29 +01:00
|
|
|
}
|
|
|
|
|
2020-07-11 20:38:17 +02:00
|
|
|
func (r *playlistRepository) FindByPath(path string) (*model.Playlist, error) {
|
2021-10-15 00:39:17 +02:00
|
|
|
return r.findBy(Eq{"path": path}, false)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *playlistRepository) FindByID(id string) (*model.Playlist, error) {
|
|
|
|
return r.findBy(And{Eq{"id": id}, r.userFilter()}, false)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *playlistRepository) findBy(sql Sqlizer, includeTracks bool) (*model.Playlist, error) {
|
|
|
|
sel := r.newSelect().Columns("*").Where(sql)
|
|
|
|
var pls []dbPlaylist
|
|
|
|
err := r.queryAll(sel, &pls)
|
2020-07-11 20:38:17 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2021-10-15 00:39:17 +02:00
|
|
|
if len(pls) == 0 {
|
|
|
|
return nil, model.ErrNotFound
|
|
|
|
}
|
|
|
|
|
|
|
|
return r.toModel(pls[0], includeTracks)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *playlistRepository) toModel(pls dbPlaylist, includeTracks bool) (*model.Playlist, error) {
|
|
|
|
var err error
|
|
|
|
if strings.TrimSpace(pls.RawRules) != "" {
|
|
|
|
r := model.SmartPlaylist{}
|
|
|
|
err = json.Unmarshal([]byte(pls.RawRules), &r)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
pls.Playlist.Rules = &r
|
|
|
|
} else {
|
|
|
|
pls.Playlist.Rules = nil
|
|
|
|
}
|
|
|
|
if includeTracks {
|
|
|
|
err = r.loadTracks(&pls)
|
|
|
|
}
|
|
|
|
return &pls.Playlist, err
|
2020-07-11 20:38:17 +02:00
|
|
|
}
|
|
|
|
|
2020-01-15 04:22:34 +01:00
|
|
|
func (r *playlistRepository) GetAll(options ...model.QueryOptions) (model.Playlists, error) {
|
2020-05-17 03:42:52 +02:00
|
|
|
sel := r.newSelect(options...).Columns("*").Where(r.userFilter())
|
2021-10-15 00:39:17 +02:00
|
|
|
var res []dbPlaylist
|
2020-01-28 14:22:17 +01:00
|
|
|
err := r.queryAll(sel, &res)
|
2021-10-15 00:39:17 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
playlists := make(model.Playlists, len(res))
|
|
|
|
for i, p := range res {
|
|
|
|
pls, err := r.toModel(p, false)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
playlists[i] = *pls
|
|
|
|
}
|
|
|
|
return playlists, err
|
2020-01-13 03:59:06 +01:00
|
|
|
}
|
|
|
|
|
2020-05-08 19:57:32 +02:00
|
|
|
func (r *playlistRepository) updateTracks(id string, tracks model.MediaFiles) error {
|
2020-05-16 02:47:15 +02:00
|
|
|
ids := make([]string, len(tracks))
|
|
|
|
for i := range tracks {
|
|
|
|
ids[i] = tracks[i].ID
|
2020-04-12 03:11:49 +02:00
|
|
|
}
|
2020-05-16 02:47:15 +02:00
|
|
|
return r.Tracks(id).Update(ids)
|
2020-05-08 19:57:32 +02:00
|
|
|
}
|
|
|
|
|
2021-10-15 00:39:17 +02:00
|
|
|
func (r *playlistRepository) loadTracks(pls *dbPlaylist) error {
|
2020-05-09 05:48:06 +02:00
|
|
|
tracksQuery := Select().From("playlist_tracks").
|
|
|
|
LeftJoin("annotation on ("+
|
|
|
|
"annotation.item_id = media_file_id"+
|
|
|
|
" AND annotation.item_type = 'media_file'"+
|
|
|
|
" AND annotation.user_id = '"+userId(r.ctx)+"')").
|
|
|
|
Columns("starred", "starred_at", "play_count", "play_date", "rating", "f.*").
|
|
|
|
Join("media_file f on f.id = media_file_id").
|
|
|
|
Where(Eq{"playlist_id": pls.ID}).OrderBy("playlist_tracks.id")
|
2020-05-17 03:42:52 +02:00
|
|
|
err := r.queryAll(tracksQuery, &pls.Tracks)
|
2020-05-08 19:57:32 +02:00
|
|
|
if err != nil {
|
|
|
|
log.Error("Error loading playlist tracks", "playlist", pls.Name, "id", pls.ID)
|
2020-04-11 22:45:21 +02:00
|
|
|
}
|
2021-07-16 17:03:28 +02:00
|
|
|
err = r.loadMediaFileGenres(&pls.Tracks)
|
2020-05-17 03:42:52 +02:00
|
|
|
return err
|
2020-04-11 22:45:21 +02:00
|
|
|
}
|
|
|
|
|
2020-05-04 02:05:03 +02:00
|
|
|
func (r *playlistRepository) Count(options ...rest.QueryOptions) (int64, error) {
|
|
|
|
return r.CountAll(r.parseRestOptions(options...))
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *playlistRepository) Read(id string) (interface{}, error) {
|
|
|
|
return r.Get(id)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *playlistRepository) ReadAll(options ...rest.QueryOptions) (interface{}, error) {
|
|
|
|
return r.GetAll(r.parseRestOptions(options...))
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *playlistRepository) EntityName() string {
|
|
|
|
return "playlist"
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *playlistRepository) NewInstance() interface{} {
|
|
|
|
return &model.Playlist{}
|
|
|
|
}
|
|
|
|
|
2020-05-12 03:27:00 +02:00
|
|
|
func (r *playlistRepository) Save(entity interface{}) (string, error) {
|
|
|
|
pls := entity.(*model.Playlist)
|
|
|
|
pls.Owner = loggedUser(r.ctx).UserName
|
|
|
|
err := r.Put(pls)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
return pls.ID, err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *playlistRepository) Update(entity interface{}, cols ...string) error {
|
|
|
|
pls := entity.(*model.Playlist)
|
2020-06-05 16:59:23 +02:00
|
|
|
usr := loggedUser(r.ctx)
|
|
|
|
if !usr.IsAdmin && pls.Owner != usr.UserName {
|
|
|
|
return rest.ErrPermissionDenied
|
|
|
|
}
|
2020-05-12 03:27:00 +02:00
|
|
|
err := r.Put(pls)
|
|
|
|
if err == model.ErrNotFound {
|
|
|
|
return rest.ErrNotFound
|
|
|
|
}
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-05-19 02:32:01 +02:00
|
|
|
func (r *playlistRepository) removeOrphans() error {
|
|
|
|
sel := Select("playlist_tracks.playlist_id as id", "p.name").From("playlist_tracks").
|
|
|
|
Join("playlist p on playlist_tracks.playlist_id = p.id").
|
|
|
|
LeftJoin("media_file mf on playlist_tracks.media_file_id = mf.id").
|
|
|
|
Where(Eq{"mf.id": nil}).
|
|
|
|
GroupBy("playlist_tracks.playlist_id")
|
|
|
|
|
|
|
|
var pls []struct{ Id, Name string }
|
|
|
|
err := r.queryAll(sel, &pls)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, pl := range pls {
|
|
|
|
log.Debug(r.ctx, "Cleaning-up orphan tracks from playlist", "id", pl.Id, "name", pl.Name)
|
|
|
|
del := Delete("playlist_tracks").Where(And{
|
|
|
|
ConcatExpr("media_file_id not in (select id from media_file)"),
|
|
|
|
Eq{"playlist_id": pl.Id},
|
|
|
|
})
|
|
|
|
n, err := r.executeSQL(del)
|
|
|
|
if n == 0 || err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
log.Debug(r.ctx, "Deleted tracks, now reordering", "id", pl.Id, "name", pl.Name, "deleted", n)
|
|
|
|
|
|
|
|
// To reorganize the playlist, just add an empty list of new tracks
|
2021-10-01 16:37:05 +02:00
|
|
|
tracks := r.Tracks(pl.Id)
|
|
|
|
if _, err := tracks.Add(nil); err != nil {
|
2020-05-19 02:32:01 +02:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-01-15 04:22:34 +01:00
|
|
|
var _ model.PlaylistRepository = (*playlistRepository)(nil)
|
2020-05-12 03:27:00 +02:00
|
|
|
var _ rest.Repository = (*playlistRepository)(nil)
|
|
|
|
var _ rest.Persistable = (*playlistRepository)(nil)
|