2020-01-15 00:23:29 +01:00
|
|
|
package persistence
|
2020-01-13 00:36:19 +01:00
|
|
|
|
|
|
|
import (
|
2020-01-28 14:22:17 +01:00
|
|
|
"context"
|
2020-06-11 05:11:43 +02:00
|
|
|
"fmt"
|
2020-01-31 23:56:02 +01:00
|
|
|
"os"
|
2020-06-12 19:26:46 +02:00
|
|
|
"path/filepath"
|
2020-10-05 17:52:09 +02:00
|
|
|
"strings"
|
2020-07-22 15:36:22 +02:00
|
|
|
"unicode/utf8"
|
2020-01-13 00:36:19 +01:00
|
|
|
|
2020-01-28 14:22:17 +01:00
|
|
|
. "github.com/Masterminds/squirrel"
|
2022-07-30 18:43:48 +02:00
|
|
|
"github.com/beego/beego/v2/client/orm"
|
2020-01-28 14:22:17 +01:00
|
|
|
"github.com/deluan/rest"
|
2020-01-31 23:56:02 +01:00
|
|
|
"github.com/navidrome/navidrome/log"
|
2020-01-24 01:44:08 +01:00
|
|
|
"github.com/navidrome/navidrome/model"
|
2020-01-13 00:36:19 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
type mediaFileRepository struct {
|
2020-01-28 14:22:17 +01:00
|
|
|
sqlRepository
|
2020-03-22 01:00:46 +01:00
|
|
|
sqlRestful
|
2020-01-13 00:36:19 +01:00
|
|
|
}
|
|
|
|
|
2022-07-30 18:43:48 +02:00
|
|
|
func NewMediaFileRepository(ctx context.Context, o orm.QueryExecutor) *mediaFileRepository {
|
2020-01-13 00:36:19 +01:00
|
|
|
r := &mediaFileRepository{}
|
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 = "media_file"
|
2020-02-05 20:12:13 +01:00
|
|
|
r.sortMappings = map[string]string{
|
2023-05-19 21:27:47 +02:00
|
|
|
"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",
|
2020-05-05 22:17:09 +02:00
|
|
|
"random": "RANDOM()",
|
2020-02-05 20:12:13 +01:00
|
|
|
}
|
2020-03-20 03:26:18 +01:00
|
|
|
r.filterMappings = map[string]filterFunc{
|
2021-07-25 00:54:22 +02:00
|
|
|
"id": idFilter(r.tableName),
|
2020-05-23 05:10:58 +02:00
|
|
|
"title": fullTextFilter,
|
|
|
|
"starred": booleanFilter,
|
2020-03-20 03:26:18 +01:00
|
|
|
}
|
2020-01-13 00:36:19 +01:00
|
|
|
return r
|
|
|
|
}
|
|
|
|
|
2021-07-16 17:03:28 +02:00
|
|
|
func (r *mediaFileRepository) CountAll(options ...model.QueryOptions) (int64, error) {
|
2021-07-17 01:41:49 +02:00
|
|
|
sql := r.newSelectWithAnnotation("media_file.id")
|
2021-07-17 02:20:33 +02:00
|
|
|
sql = r.withGenres(sql)
|
2021-07-17 01:41:49 +02:00
|
|
|
return r.count(sql, options...)
|
2020-01-13 00:36:19 +01:00
|
|
|
}
|
|
|
|
|
2021-07-16 17:03:28 +02:00
|
|
|
func (r *mediaFileRepository) Exists(id string) (bool, error) {
|
2021-07-25 00:54:22 +02:00
|
|
|
return r.exists(Select().Where(Eq{"media_file.id": id}))
|
2020-01-28 14:22:17 +01:00
|
|
|
}
|
|
|
|
|
2021-07-16 17:03:28 +02:00
|
|
|
func (r *mediaFileRepository) Put(m *model.MediaFile) error {
|
2020-04-24 16:13:59 +02:00
|
|
|
m.FullText = getFullText(m.Title, m.Album, m.Artist, m.AlbumArtist,
|
2020-05-12 17:17:22 +02:00
|
|
|
m.SortTitle, m.SortAlbumName, m.SortArtistName, m.SortAlbumArtistName, m.DiscSubtitle)
|
2020-01-31 21:35:06 +01:00
|
|
|
_, err := r.put(m.ID, m)
|
2021-07-16 17:03:28 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2021-08-01 07:21:20 +02:00
|
|
|
return r.updateGenres(m.ID, r.tableName, m.Genres)
|
2020-01-13 00:36:19 +01:00
|
|
|
}
|
|
|
|
|
2021-07-16 17:03:28 +02:00
|
|
|
func (r *mediaFileRepository) selectMediaFile(options ...model.QueryOptions) SelectBuilder {
|
2020-08-01 18:17:06 +02:00
|
|
|
sql := r.newSelectWithAnnotation("media_file.id", options...).Columns("media_file.*")
|
2021-07-17 02:20:33 +02:00
|
|
|
sql = r.withBookmark(sql, "media_file.id")
|
2021-10-29 15:47:12 +02: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("media_file.id")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return sql
|
2020-01-13 00:36:19 +01:00
|
|
|
}
|
|
|
|
|
2021-07-16 17:03:28 +02:00
|
|
|
func (r *mediaFileRepository) Get(id string) (*model.MediaFile, error) {
|
|
|
|
sel := r.selectMediaFile().Where(Eq{"media_file.id": id})
|
2020-05-22 21:23:42 +02:00
|
|
|
var res model.MediaFiles
|
|
|
|
if err := r.queryAll(sel, &res); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if len(res) == 0 {
|
|
|
|
return nil, model.ErrNotFound
|
|
|
|
}
|
2021-07-16 17:03:28 +02:00
|
|
|
err := r.loadMediaFileGenres(&res)
|
|
|
|
return &res[0], err
|
2020-01-13 00:36:19 +01:00
|
|
|
}
|
|
|
|
|
2021-07-16 17:03:28 +02:00
|
|
|
func (r *mediaFileRepository) GetAll(options ...model.QueryOptions) (model.MediaFiles, error) {
|
2021-07-17 02:20:33 +02:00
|
|
|
sq := r.selectMediaFile(options...)
|
2020-01-31 22:47:13 +01:00
|
|
|
res := model.MediaFiles{}
|
2020-01-28 14:22:17 +01:00
|
|
|
err := r.queryAll(sq, &res)
|
2021-07-16 17:03:28 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
err = r.loadMediaFileGenres(&res)
|
2020-01-28 14:22:17 +01:00
|
|
|
return res, err
|
2020-01-16 21:56:24 +01:00
|
|
|
}
|
|
|
|
|
2021-07-16 17:03:28 +02:00
|
|
|
func (r *mediaFileRepository) FindByPath(path string) (*model.MediaFile, error) {
|
2023-03-10 18:29:38 +01:00
|
|
|
sel := r.newSelect().Columns("*").Where(Like{"path": path})
|
2020-07-11 20:38:17 +02:00
|
|
|
var res model.MediaFiles
|
|
|
|
if err := r.queryAll(sel, &res); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if len(res) == 0 {
|
|
|
|
return nil, model.ErrNotFound
|
|
|
|
}
|
|
|
|
return &res[0], nil
|
|
|
|
}
|
|
|
|
|
2020-10-05 17:52:09 +02:00
|
|
|
func cleanPath(path string) string {
|
|
|
|
path = filepath.Clean(path)
|
|
|
|
if !strings.HasSuffix(path, string(os.PathSeparator)) {
|
2021-07-15 19:42:54 +02:00
|
|
|
path += string(os.PathSeparator)
|
2020-10-05 17:52:09 +02:00
|
|
|
}
|
|
|
|
return path
|
|
|
|
}
|
|
|
|
|
|
|
|
func pathStartsWith(path string) Eq {
|
|
|
|
substr := fmt.Sprintf("substr(path, 1, %d)", utf8.RuneCountInString(path))
|
|
|
|
return Eq{substr: path}
|
|
|
|
}
|
|
|
|
|
2020-07-11 20:38:17 +02:00
|
|
|
// FindAllByPath only return mediafiles that are direct children of requested path
|
2021-07-16 17:03:28 +02:00
|
|
|
func (r *mediaFileRepository) FindAllByPath(path string) (model.MediaFiles, error) {
|
2020-06-11 05:11:43 +02:00
|
|
|
// Query by path based on https://stackoverflow.com/a/13911906/653632
|
2020-10-05 17:52:09 +02:00
|
|
|
path = cleanPath(path)
|
2020-07-22 15:36:22 +02:00
|
|
|
pathLen := utf8.RuneCountInString(path)
|
2021-10-29 19:07:20 +02:00
|
|
|
sel0 := r.newSelect().Columns("media_file.*", fmt.Sprintf("substr(path, %d) AS item", pathLen+2)).
|
2020-07-14 00:37:48 +02:00
|
|
|
Where(pathStartsWith(path))
|
2020-06-11 05:11:43 +02:00
|
|
|
sel := r.newSelect().Columns("*", "item NOT GLOB '*"+string(os.PathSeparator)+"*' AS isLast").
|
|
|
|
Where(Eq{"isLast": 1}).FromSelect(sel0, "sel0")
|
|
|
|
|
2020-01-31 22:47:13 +01:00
|
|
|
res := model.MediaFiles{}
|
2020-01-28 14:22:17 +01:00
|
|
|
err := r.queryAll(sel, &res)
|
2020-06-11 05:11:43 +02:00
|
|
|
return res, err
|
2020-01-28 14:22:17 +01:00
|
|
|
}
|
|
|
|
|
2020-06-12 19:26:46 +02:00
|
|
|
// FindPathsRecursively returns a list of all subfolders of basePath, recursively
|
2021-07-16 17:03:28 +02:00
|
|
|
func (r *mediaFileRepository) FindPathsRecursively(basePath string) ([]string, error) {
|
2020-10-05 17:52:09 +02:00
|
|
|
path := cleanPath(basePath)
|
2020-06-12 19:26:46 +02:00
|
|
|
// Query based on https://stackoverflow.com/a/38330814/653632
|
2020-06-12 20:34:50 +02:00
|
|
|
sel := r.newSelect().Columns(fmt.Sprintf("distinct rtrim(path, replace(path, '%s', ''))", string(os.PathSeparator))).
|
2020-10-05 17:52:09 +02:00
|
|
|
Where(pathStartsWith(path))
|
2020-06-12 19:26:46 +02:00
|
|
|
var res []string
|
|
|
|
err := r.queryAll(sel, &res)
|
|
|
|
return res, err
|
|
|
|
}
|
|
|
|
|
2021-07-16 17:03:28 +02:00
|
|
|
func (r *mediaFileRepository) deleteNotInPath(basePath string) error {
|
2020-10-05 17:52:09 +02:00
|
|
|
path := cleanPath(basePath)
|
|
|
|
sel := Delete(r.tableName).Where(NotEq(pathStartsWith(path)))
|
2020-10-02 22:18:45 +02:00
|
|
|
c, err := r.executeSQL(sel)
|
|
|
|
if err == nil {
|
|
|
|
if c > 0 {
|
|
|
|
log.Debug(r.ctx, "Deleted dangling tracks", "totalDeleted", c)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-07-16 17:03:28 +02:00
|
|
|
func (r *mediaFileRepository) Delete(id string) error {
|
2020-01-28 14:22:17 +01:00
|
|
|
return r.delete(Eq{"id": id})
|
|
|
|
}
|
|
|
|
|
2020-06-12 19:26:46 +02:00
|
|
|
// DeleteByPath delete from the DB all mediafiles that are direct children of path
|
2021-07-16 17:03:28 +02:00
|
|
|
func (r *mediaFileRepository) DeleteByPath(basePath string) (int64, error) {
|
2020-10-05 17:52:09 +02:00
|
|
|
path := cleanPath(basePath)
|
2020-07-22 15:36:22 +02:00
|
|
|
pathLen := utf8.RuneCountInString(path)
|
2020-06-11 05:11:43 +02:00
|
|
|
del := Delete(r.tableName).
|
2020-07-14 00:37:48 +02:00
|
|
|
Where(And{pathStartsWith(path),
|
2020-07-22 15:36:22 +02:00
|
|
|
Eq{fmt.Sprintf("substr(path, %d) glob '*%s*'", pathLen+2, string(os.PathSeparator)): 0}})
|
2020-06-11 05:11:43 +02:00
|
|
|
log.Debug(r.ctx, "Deleting mediafiles by path", "path", path)
|
2020-07-12 17:48:57 +02:00
|
|
|
return r.executeSQL(del)
|
2020-01-19 02:59:20 +01:00
|
|
|
}
|
|
|
|
|
2021-11-06 01:24:41 +01:00
|
|
|
func (r *mediaFileRepository) removeNonAlbumArtistIds() error {
|
|
|
|
upd := Update(r.tableName).Set("artist_id", "").Where(notExists("artist", ConcatExpr("id = artist_id")))
|
2023-02-07 19:08:25 +01:00
|
|
|
log.Debug(r.ctx, "Removing non-album artist_ids")
|
2021-11-06 01:24:41 +01:00
|
|
|
_, err := r.executeSQL(upd)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-07-16 17:03:28 +02:00
|
|
|
func (r *mediaFileRepository) Search(q string, offset int, size int) (model.MediaFiles, error) {
|
2020-01-31 22:47:13 +01:00
|
|
|
results := model.MediaFiles{}
|
2020-01-31 21:35:06 +01:00
|
|
|
err := r.doSearch(q, offset, size, &results, "title")
|
2023-11-22 03:11:45 +01:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
err = r.loadMediaFileGenres(&results)
|
2020-01-28 14:22:17 +01:00
|
|
|
return results, err
|
|
|
|
}
|
|
|
|
|
2021-07-16 17:03:28 +02:00
|
|
|
func (r *mediaFileRepository) Count(options ...rest.QueryOptions) (int64, error) {
|
2020-01-28 14:22:17 +01:00
|
|
|
return r.CountAll(r.parseRestOptions(options...))
|
|
|
|
}
|
|
|
|
|
2021-07-16 17:03:28 +02:00
|
|
|
func (r *mediaFileRepository) Read(id string) (interface{}, error) {
|
2020-01-28 14:22:17 +01:00
|
|
|
return r.Get(id)
|
|
|
|
}
|
|
|
|
|
2021-07-16 17:03:28 +02:00
|
|
|
func (r *mediaFileRepository) ReadAll(options ...rest.QueryOptions) (interface{}, error) {
|
2020-01-28 14:22:17 +01:00
|
|
|
return r.GetAll(r.parseRestOptions(options...))
|
|
|
|
}
|
|
|
|
|
2021-07-16 17:03:28 +02:00
|
|
|
func (r *mediaFileRepository) EntityName() string {
|
2020-01-28 14:22:17 +01:00
|
|
|
return "mediafile"
|
|
|
|
}
|
|
|
|
|
2021-07-16 17:03:28 +02:00
|
|
|
func (r *mediaFileRepository) NewInstance() interface{} {
|
2020-05-22 21:23:42 +02:00
|
|
|
return &model.MediaFile{}
|
|
|
|
}
|
|
|
|
|
2020-01-15 04:22:34 +01:00
|
|
|
var _ model.MediaFileRepository = (*mediaFileRepository)(nil)
|
2020-01-28 14:22:17 +01:00
|
|
|
var _ model.ResourceRepository = (*mediaFileRepository)(nil)
|