Files
navidrome/persistence/album_repository.go
T
Alex Palaistras ac5d99c079 Check MIME type for cover on refresh, display
Files that match the `CoverArtPriority` setting will now be considered
eligible only if their extensions are of an 'image/*' MIME type (e.g.
'.png' for 'image/png', '.jpg' for 'image/jpeg'). This prevents matching
files that will likely not be valid during display.

In addition to the above, code for returning the cover image file from
scanned data will also check against the MIME type for the path stored,
instead of attempting to re-trace `CoverArtPriority` matches. This
simplifies the code and bypasses a number of edge-cases related to
inconsistent matching.
2020-06-24 20:48:42 -04:00

284 lines
7.4 KiB
Go

package persistence
import (
"context"
"mime"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
. "github.com/Masterminds/squirrel"
"github.com/astaxie/beego/orm"
"github.com/deluan/navidrome/conf"
"github.com/deluan/navidrome/consts"
"github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/model"
"github.com/deluan/rest"
)
type albumRepository struct {
sqlRepository
sqlRestful
}
func NewAlbumRepository(ctx context.Context, o orm.Ormer) model.AlbumRepository {
r := &albumRepository{}
r.ctx = ctx
r.ormer = o
r.tableName = "album"
r.sortMappings = map[string]string{
"name": "order_album_name",
"artist": "compilation asc, order_album_artist_name asc, order_album_name asc",
"random": "RANDOM()",
"max_year": "max_year asc, name, order_album_name asc",
}
r.filterMappings = map[string]filterFunc{
"name": fullTextFilter,
"compilation": booleanFilter,
"artist_id": artistFilter,
"year": yearFilter,
}
return r
}
func yearFilter(field 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(field string, value interface{}) Sqlizer {
return exists("media_file", And{
ConcatExpr("album_id=album.id"),
Or{
Eq{"artist_id": value},
Eq{"album_artist_id": value},
},
})
}
func (r *albumRepository) CountAll(options ...model.QueryOptions) (int64, error) {
return r.count(Select(), options...)
}
func (r *albumRepository) Exists(id string) (bool, error) {
return r.exists(Select().Where(Eq{"id": id}))
}
func (r *albumRepository) selectAlbum(options ...model.QueryOptions) SelectBuilder {
return r.newSelectWithAnnotation("album.id", options...).Columns("*")
}
func (r *albumRepository) Get(id string) (*model.Album, error) {
sq := r.selectAlbum().Where(Eq{"id": id})
var res model.Albums
if err := r.queryAll(sq, &res); err != nil {
return nil, err
}
if len(res) == 0 {
return nil, model.ErrNotFound
}
return &res[0], nil
}
func (r *albumRepository) FindByArtist(artistId string) (model.Albums, error) {
sq := r.selectAlbum().Where(Eq{"album_artist_id": artistId}).OrderBy("max_year")
res := model.Albums{}
err := r.queryAll(sq, &res)
return res, err
}
func (r *albumRepository) GetAll(options ...model.QueryOptions) (model.Albums, error) {
sq := r.selectAlbum(options...)
res := model.Albums{}
err := r.queryAll(sq, &res)
return res, err
}
// TODO Keep order when paginating
func (r *albumRepository) GetRandom(options ...model.QueryOptions) (model.Albums, error) {
sq := r.selectAlbum(options...)
sq = sq.OrderBy("RANDOM()")
results := model.Albums{}
err := r.queryAll(sq, &results)
return results, err
}
func (r *albumRepository) Refresh(ids ...string) error {
type refreshAlbum struct {
model.Album
CurrentId string
HasCoverArt bool
SongArtists string
Years string
DiscSubtitles string
}
var albums []refreshAlbum
sel := Select(`album_id as id, album as name, f.artist, f.album_artist, f.artist_id, f.album_artist_id,
f.sort_album_name, f.sort_artist_name, f.sort_album_artist_name,
f.order_album_name, f.order_album_artist_name,
f.compilation, f.genre, max(f.year) as max_year, sum(f.duration) as duration,
count(*) as song_count, a.id as current_id, f.id as cover_art_id, f.path as cover_art_path, f.has_cover_art,
group_concat(f.disc_subtitle, ' ') as disc_subtitles,
group_concat(f.artist, ' ') as song_artists, group_concat(f.year, ' ') as years`).
From("media_file f").
LeftJoin("album a on f.album_id = a.id").
Where(Eq{"f.album_id": ids}).GroupBy("album_id").OrderBy("f.id")
err := r.queryAll(sel, &albums)
if err != nil {
return err
}
toInsert := 0
toUpdate := 0
for _, al := range albums {
if !al.HasCoverArt || !strings.HasPrefix(conf.Server.CoverArtPriority, "embedded") {
if path := getCoverFromPath(al.CoverArtPath, al.HasCoverArt); path != "" {
al.CoverArtId = "al-" + al.ID
al.CoverArtPath = path
} else if !al.HasCoverArt {
al.CoverArtId = ""
}
}
if al.Compilation {
al.AlbumArtist = consts.VariousArtists
al.AlbumArtistID = consts.VariousArtistsID
}
if al.AlbumArtist == "" {
al.AlbumArtist = al.Artist
al.AlbumArtistID = al.ArtistID
}
al.MinYear = getMinYear(al.Years)
al.UpdatedAt = time.Now()
if al.CurrentId != "" {
toUpdate++
} else {
toInsert++
al.CreatedAt = time.Now()
}
al.FullText = getFullText(al.Name, al.Artist, al.AlbumArtist, al.SongArtists,
al.SortAlbumName, al.SortArtistName, al.SortAlbumArtistName, al.DiscSubtitles)
_, err := r.put(al.ID, al.Album)
if err != nil {
return err
}
}
if toInsert > 0 {
log.Debug(r.ctx, "Inserted new albums", "totalInserted", toInsert)
}
if toUpdate > 0 {
log.Debug(r.ctx, "Updated albums", "totalUpdated", toUpdate)
}
return err
}
func getMinYear(years string) int {
ys := strings.Fields(years)
sort.Strings(ys)
for _, y := range ys {
if y != "0" {
r, _ := strconv.Atoi(y)
return r
}
}
return 0
}
// GetCoverFromPath accepts a path to a file, and returns a path to an eligible cover image from the
// file's directory (as configured with CoverArtPriority). If no cover file is found, among
// available choices, or an error occurs, an empty string is returned. If HasEmbeddedCover is true,
// and 'embedded' is matched among eligible choices, GetCoverFromPath will return early with an
// empty path.
func getCoverFromPath(path string, hasEmbeddedCover bool) string {
n, err := os.Open(filepath.Dir(path))
if err != nil {
return ""
}
defer n.Close()
names, err := n.Readdirnames(-1)
if err != nil {
return ""
}
for _, p := range strings.Split(conf.Server.CoverArtPriority, ",") {
pat := strings.ToLower(strings.TrimSpace(p))
if pat == "embedded" {
if hasEmbeddedCover {
return ""
}
continue
}
for _, name := range names {
match, _ := filepath.Match(pat, strings.ToLower(name))
if match && isImageFile(filepath.Ext(name)) {
return filepath.Join(filepath.Dir(path), name)
}
}
}
return ""
}
func isImageFile(extension string) bool {
return strings.HasPrefix(mime.TypeByExtension(extension), "image/")
}
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) GetStarred(options ...model.QueryOptions) (model.Albums, error) {
sq := r.selectAlbum(options...).Where("starred = true")
starred := model.Albums{}
err := r.queryAll(sq, &starred)
return starred, err
}
func (r *albumRepository) Search(q string, offset int, size int) (model.Albums, error) {
results := model.Albums{}
err := r.doSearch(q, offset, size, &results, "name")
return results, 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)