navidrome/scanner/refresher.go

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

152 lines
3.9 KiB
Go
Raw Normal View History

2022-12-19 18:08:39 +01:00
package scanner
import (
"context"
"fmt"
"path/filepath"
"strings"
"time"
2022-12-19 18:08:39 +01:00
"github.com/Masterminds/squirrel"
"github.com/navidrome/navidrome/core/artwork"
2022-12-19 18:08:39 +01:00
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/utils"
"github.com/navidrome/navidrome/utils/slice"
2023-01-17 23:26:48 +01:00
"golang.org/x/exp/maps"
2022-12-19 18:08:39 +01:00
)
2022-12-21 17:30:19 +01:00
// refresher is responsible for rolling up mediafiles attributes into albums attributes,
// and albums attributes into artists attributes. This is done by accumulating all album and artist IDs
// found during scan, and "refreshing" the albums and artists when flush is called.
//
// The actual mappings happen in MediaFiles.ToAlbum() and Albums.ToAlbumArtist()
2022-12-19 18:08:39 +01:00
type refresher struct {
2022-12-23 18:28:22 +01:00
ds model.DataStore
album map[string]struct{}
artist map[string]struct{}
dirMap dirMap
cacheWarmer artwork.CacheWarmer
2022-12-19 18:08:39 +01:00
}
func newRefresher(ds model.DataStore, cw artwork.CacheWarmer, dirMap dirMap) *refresher {
2022-12-19 18:08:39 +01:00
return &refresher{
2022-12-23 18:28:22 +01:00
ds: ds,
album: map[string]struct{}{},
artist: map[string]struct{}{},
dirMap: dirMap,
cacheWarmer: cw,
2022-12-19 18:08:39 +01:00
}
}
2022-12-21 17:30:19 +01:00
func (r *refresher) accumulate(mf model.MediaFile) {
2022-12-19 18:08:39 +01:00
if mf.AlbumID != "" {
2022-12-21 17:30:19 +01:00
r.album[mf.AlbumID] = struct{}{}
2022-12-19 18:08:39 +01:00
}
if mf.AlbumArtistID != "" {
2022-12-21 17:30:19 +01:00
r.artist[mf.AlbumArtistID] = struct{}{}
2022-12-19 18:08:39 +01:00
}
}
2022-12-23 18:28:22 +01:00
func (r *refresher) flush(ctx context.Context) error {
err := r.flushMap(ctx, r.album, "album", r.refreshAlbums)
2022-12-21 17:30:19 +01:00
if err != nil {
return err
}
2023-01-17 23:26:48 +01:00
r.album = map[string]struct{}{}
2022-12-23 18:28:22 +01:00
err = r.flushMap(ctx, r.artist, "artist", r.refreshArtists)
2022-12-21 17:30:19 +01:00
if err != nil {
return err
}
2022-12-23 18:28:22 +01:00
r.artist = map[string]struct{}{}
2022-12-21 17:30:19 +01:00
return nil
}
2022-12-23 18:28:22 +01:00
type refreshCallbackFunc = func(ctx context.Context, ids ...string) error
2022-12-19 18:08:39 +01:00
2022-12-23 18:28:22 +01:00
func (r *refresher) flushMap(ctx context.Context, m map[string]struct{}, entity string, refresh refreshCallbackFunc) error {
2022-12-19 18:08:39 +01:00
if len(m) == 0 {
return nil
}
2023-01-17 23:26:48 +01:00
ids := maps.Keys(m)
2022-12-19 18:08:39 +01:00
chunks := utils.BreakUpStringSlice(ids, 100)
for _, chunk := range chunks {
2022-12-23 18:28:22 +01:00
err := refresh(ctx, chunk...)
2022-12-19 18:08:39 +01:00
if err != nil {
2022-12-23 18:28:22 +01:00
log.Error(ctx, fmt.Sprintf("Error writing %ss to the DB", entity), err)
2022-12-19 18:08:39 +01:00
return err
}
}
return nil
}
2022-12-23 18:28:22 +01:00
func (r *refresher) refreshAlbums(ctx context.Context, ids ...string) error {
mfs, err := r.ds.MediaFile(ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_id": ids}})
2022-12-19 18:08:39 +01:00
if err != nil {
return err
}
if len(mfs) == 0 {
return nil
}
2022-12-23 18:28:22 +01:00
repo := r.ds.Album(ctx)
2022-12-19 18:08:39 +01:00
grouped := slice.Group(mfs, func(m model.MediaFile) string { return m.AlbumID })
for _, group := range grouped {
songs := model.MediaFiles(group)
a := songs.ToAlbum()
var updatedAt time.Time
a.ImageFiles, updatedAt = r.getImageFiles(songs.Dirs())
if updatedAt.After(a.UpdatedAt) {
a.UpdatedAt = updatedAt
}
2022-12-19 18:08:39 +01:00
err := repo.Put(&a)
if err != nil {
return err
}
2022-12-23 18:28:22 +01:00
r.cacheWarmer.PreCache(a.CoverArtID())
2022-12-19 18:08:39 +01:00
}
return nil
}
func (r *refresher) getImageFiles(dirs []string) (string, time.Time) {
var imageFiles []string
var updatedAt time.Time
for _, dir := range dirs {
stats := r.dirMap[dir]
for _, img := range stats.Images {
imageFiles = append(imageFiles, filepath.Join(dir, img))
}
if stats.ImagesUpdatedAt.After(updatedAt) {
updatedAt = stats.ImagesUpdatedAt
}
}
return strings.Join(imageFiles, string(filepath.ListSeparator)), updatedAt
}
2022-12-23 18:28:22 +01:00
func (r *refresher) refreshArtists(ctx context.Context, ids ...string) error {
albums, err := r.ds.Album(ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_artist_id": ids}})
2022-12-19 18:08:39 +01:00
if err != nil {
return err
}
2022-12-21 17:30:19 +01:00
if len(albums) == 0 {
return nil
}
2022-12-23 18:28:22 +01:00
repo := r.ds.Artist(ctx)
2022-12-21 17:30:19 +01:00
grouped := slice.Group(albums, func(al model.Album) string { return al.AlbumArtistID })
for _, group := range grouped {
a := model.Albums(group).ToAlbumArtist()
// Force a external metadata lookup on next access
a.ExternalInfoUpdatedAt = time.Time{}
2022-12-21 17:30:19 +01:00
err := repo.Put(&a)
if err != nil {
return err
}
2023-01-13 20:30:26 +01:00
r.cacheWarmer.PreCache(a.CoverArtID())
2022-12-19 18:08:39 +01:00
}
return nil
}