2020-01-16 22:53:48 +01:00
|
|
|
package scanner
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
2020-01-18 07:49:08 +01:00
|
|
|
"sort"
|
2020-01-16 22:53:48 +01:00
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
2020-08-03 05:17:13 +02:00
|
|
|
"github.com/deluan/navidrome/conf"
|
2020-10-25 17:00:21 +01:00
|
|
|
"github.com/deluan/navidrome/core"
|
2020-01-24 01:44:08 +01:00
|
|
|
"github.com/deluan/navidrome/log"
|
|
|
|
"github.com/deluan/navidrome/model"
|
2020-07-27 17:12:39 +02:00
|
|
|
"github.com/deluan/navidrome/model/request"
|
2020-09-04 17:08:16 +02:00
|
|
|
"github.com/deluan/navidrome/scanner/metadata"
|
2020-04-24 19:27:18 +02:00
|
|
|
"github.com/deluan/navidrome/utils"
|
2020-01-16 22:53:48 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
type TagScanner struct {
|
2020-10-25 17:00:21 +01:00
|
|
|
rootFolder string
|
|
|
|
ds model.DataStore
|
|
|
|
mapper *mediaFileMapper
|
|
|
|
plsSync *playlistSync
|
|
|
|
cnt *counters
|
|
|
|
cacheWarmer core.CacheWarmer
|
2020-01-16 22:53:48 +01:00
|
|
|
}
|
|
|
|
|
2020-10-25 17:00:21 +01:00
|
|
|
func NewTagScanner(rootFolder string, ds model.DataStore, cacheWarmer core.CacheWarmer) *TagScanner {
|
2020-01-16 22:53:48 +01:00
|
|
|
return &TagScanner{
|
2020-10-25 17:00:21 +01:00
|
|
|
rootFolder: rootFolder,
|
|
|
|
mapper: newMediaFileMapper(rootFolder),
|
|
|
|
plsSync: newPlaylistSync(ds),
|
|
|
|
ds: ds,
|
|
|
|
cacheWarmer: cacheWarmer,
|
2020-01-16 22:53:48 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-29 16:31:45 +01:00
|
|
|
type (
|
|
|
|
counters struct {
|
|
|
|
added int64
|
|
|
|
updated int64
|
|
|
|
deleted int64
|
|
|
|
playlists int64
|
|
|
|
}
|
|
|
|
dirMap map[string]dirStats
|
|
|
|
)
|
2020-06-11 23:36:09 +02:00
|
|
|
|
|
|
|
const (
|
2020-07-27 17:12:39 +02:00
|
|
|
// filesBatchSize used for batching file metadata extraction
|
2020-06-11 23:36:09 +02:00
|
|
|
filesBatchSize = 100
|
|
|
|
)
|
2020-06-11 01:30:04 +02:00
|
|
|
|
2020-10-29 16:31:45 +01:00
|
|
|
// TagScanner algorithm overview:
|
2020-07-27 17:12:39 +02:00
|
|
|
// Load all directories from the DB
|
2020-10-29 16:31:45 +01:00
|
|
|
// Traverse the music folder, collecting each subfolder's ModTime (self or any non-dir children, whichever is newer)
|
2020-07-27 17:12:39 +02:00
|
|
|
// For each changed folder: get all files from DB whose path starts with the changed folder (non-recursively), check each file:
|
2020-01-18 05:55:58 +01:00
|
|
|
// if file in folder is newer, update the one in DB
|
2020-07-27 17:12:39 +02:00
|
|
|
// if file in folder does not exists in DB, add it
|
|
|
|
// for each file in the DB that is not found in the folder, delete it from DB
|
2020-10-29 16:31:45 +01:00
|
|
|
// Compare directories in the fs with the ones in the DB to find deleted folders
|
|
|
|
// For each deleted folder: delete all files from DB whose path starts with the delete folder path (non-recursively)
|
2020-01-18 05:55:58 +01:00
|
|
|
// Create new albums/artists, update counters:
|
|
|
|
// collect all albumIDs and artistIDs from previous steps
|
|
|
|
// refresh the collected albums and artists with the metadata from the mediafiles
|
2020-07-27 17:12:39 +02:00
|
|
|
// For each changed folder, process playlists:
|
|
|
|
// If the playlist is not in the DB, import it, setting sync = true
|
|
|
|
// If the playlist is in the DB and sync == true, import it, or else skip it
|
|
|
|
// Delete all empty albums, delete all empty artists, clean-up playlists
|
2020-11-01 22:37:33 +01:00
|
|
|
func (s *TagScanner) Scan(ctx context.Context, lastModifiedSince time.Time, progress chan uint32) error {
|
2020-07-27 17:12:39 +02:00
|
|
|
ctx = s.withAdminUser(ctx)
|
2020-02-09 05:21:03 +01:00
|
|
|
start := time.Now()
|
2020-07-12 19:30:03 +02:00
|
|
|
|
2020-07-27 17:12:39 +02:00
|
|
|
allDBDirs, err := s.getDBDirTree(ctx)
|
2020-01-16 22:53:48 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-10-29 16:31:45 +01:00
|
|
|
allFSDirs := dirMap{}
|
|
|
|
var changedDirs []string
|
|
|
|
s.cnt = &counters{}
|
2020-07-27 17:12:39 +02:00
|
|
|
|
2020-10-31 05:06:28 +01:00
|
|
|
foldersFound, walkerError := s.getRootFolderWalker(ctx)
|
2020-10-29 16:31:45 +01:00
|
|
|
for {
|
|
|
|
folderStats, more := <-foldersFound
|
|
|
|
if !more {
|
|
|
|
break
|
|
|
|
}
|
2020-11-01 22:37:33 +01:00
|
|
|
progress <- folderStats.AudioFilesCount
|
2020-10-29 16:31:45 +01:00
|
|
|
allFSDirs[folderStats.Path] = folderStats
|
2020-01-16 22:53:48 +01:00
|
|
|
|
2020-10-30 18:14:53 +01:00
|
|
|
if s.folderHasChanged(ctx, folderStats, allDBDirs, lastModifiedSince) {
|
2020-10-29 16:31:45 +01:00
|
|
|
changedDirs = append(changedDirs, folderStats.Path)
|
2020-10-30 04:47:43 +01:00
|
|
|
log.Debug("Processing changed folder", "dir", folderStats.Path)
|
2020-10-29 16:31:45 +01:00
|
|
|
err := s.processChangedDir(ctx, folderStats.Path)
|
|
|
|
if err != nil {
|
2020-10-30 04:47:43 +01:00
|
|
|
log.Error("Error updating folder in the DB", "dir", folderStats.Path, err)
|
2020-10-29 16:31:45 +01:00
|
|
|
}
|
|
|
|
}
|
2020-02-03 02:12:46 +01:00
|
|
|
}
|
2020-01-16 22:53:48 +01:00
|
|
|
|
2020-10-31 05:06:28 +01:00
|
|
|
if err := <-walkerError; err != nil {
|
|
|
|
log.Error("Scan was interrupted by error. See errors above", err)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-10-30 14:39:36 +01:00
|
|
|
// If the media folder is empty, abort to avoid deleting all data
|
|
|
|
if len(allFSDirs) <= 1 {
|
|
|
|
log.Error(ctx, "Media Folder is empty. Aborting scan.", "folder", s.rootFolder)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-10-29 16:31:45 +01:00
|
|
|
deletedDirs := s.getDeletedDirs(ctx, allFSDirs, allDBDirs)
|
|
|
|
if len(deletedDirs)+len(changedDirs) == 0 {
|
|
|
|
log.Debug(ctx, "No changes found in Music Folder", "folder", s.rootFolder)
|
|
|
|
return nil
|
|
|
|
}
|
2020-01-16 22:53:48 +01:00
|
|
|
|
2020-07-27 17:12:39 +02:00
|
|
|
for _, dir := range deletedDirs {
|
|
|
|
err := s.processDeletedDir(ctx, dir)
|
2020-01-16 22:53:48 +01:00
|
|
|
if err != nil {
|
2020-10-30 04:47:43 +01:00
|
|
|
log.Error("Error removing deleted folder from DB", "dir", dir, err)
|
2020-01-16 22:53:48 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-29 16:31:45 +01:00
|
|
|
s.cnt.playlists = 0
|
2020-08-03 05:17:13 +02:00
|
|
|
if conf.Server.AutoImportPlaylists {
|
2020-10-29 16:31:45 +01:00
|
|
|
// Now that all mediafiles are imported/updated, search for and import/update playlists
|
2020-08-03 05:17:13 +02:00
|
|
|
u, _ := request.UserFrom(ctx)
|
|
|
|
for _, dir := range changedDirs {
|
|
|
|
info := allFSDirs[dir]
|
2020-10-29 16:31:45 +01:00
|
|
|
if info.HasPlaylist {
|
2020-08-03 05:17:13 +02:00
|
|
|
if !u.IsAdmin {
|
|
|
|
log.Warn("Playlists will not be imported, as there are no admin users yet, "+
|
|
|
|
"Please create an admin user first, and then update the playlists for them to be imported", "dir", dir)
|
|
|
|
} else {
|
2020-10-29 16:31:45 +01:00
|
|
|
s.cnt.playlists = s.plsSync.processPlaylists(ctx, dir)
|
2020-08-03 05:17:13 +02:00
|
|
|
}
|
2020-07-27 17:12:39 +02:00
|
|
|
}
|
|
|
|
}
|
2020-08-03 05:17:13 +02:00
|
|
|
} else {
|
|
|
|
log.Debug("Playlist auto-import is disabled")
|
2020-01-16 22:53:48 +01:00
|
|
|
}
|
|
|
|
|
2020-10-02 22:18:45 +02:00
|
|
|
err = s.ds.GC(log.NewContext(ctx), s.rootFolder)
|
2020-07-27 17:12:39 +02:00
|
|
|
log.Info("Finished processing Music Folder", "folder", s.rootFolder, "elapsed", time.Since(start),
|
2020-10-29 16:31:45 +01:00
|
|
|
"added", s.cnt.added, "updated", s.cnt.updated, "deleted", s.cnt.deleted, "playlistsImported", s.cnt.playlists)
|
2020-07-27 17:12:39 +02:00
|
|
|
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-10-31 05:06:28 +01:00
|
|
|
func (s *TagScanner) getRootFolderWalker(ctx context.Context) (walkResults, chan error) {
|
2020-07-27 17:12:39 +02:00
|
|
|
start := time.Now()
|
|
|
|
log.Trace(ctx, "Loading directory tree from music folder", "folder", s.rootFolder)
|
2020-10-29 16:31:45 +01:00
|
|
|
results := make(chan dirStats, 5000)
|
2020-10-31 05:06:28 +01:00
|
|
|
walkerError := make(chan error)
|
2020-10-29 16:31:45 +01:00
|
|
|
go func() {
|
2020-10-31 05:06:28 +01:00
|
|
|
err := walkDirTree(ctx, s.rootFolder, results)
|
|
|
|
if err != nil {
|
|
|
|
log.Error("There were errors reading directories from filesystem", err)
|
2020-10-29 16:31:45 +01:00
|
|
|
}
|
2020-10-31 05:06:28 +01:00
|
|
|
walkerError <- err
|
2020-10-30 04:47:43 +01:00
|
|
|
log.Debug("Finished reading directories from filesystem", "elapsed", time.Since(start))
|
2020-10-29 16:31:45 +01:00
|
|
|
}()
|
2020-10-31 05:06:28 +01:00
|
|
|
return results, walkerError
|
2020-07-27 17:12:39 +02:00
|
|
|
}
|
2020-01-16 22:53:48 +01:00
|
|
|
|
2020-07-27 17:12:39 +02:00
|
|
|
func (s *TagScanner) getDBDirTree(ctx context.Context) (map[string]struct{}, error) {
|
|
|
|
start := time.Now()
|
|
|
|
log.Trace(ctx, "Loading directory tree from database", "folder", s.rootFolder)
|
2020-06-12 19:26:46 +02:00
|
|
|
|
2020-07-27 17:12:39 +02:00
|
|
|
repo := s.ds.MediaFile(ctx)
|
|
|
|
dirs, err := repo.FindPathsRecursively(s.rootFolder)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
resp := map[string]struct{}{}
|
|
|
|
for _, d := range dirs {
|
|
|
|
resp[filepath.Clean(d)] = struct{}{}
|
|
|
|
}
|
2020-01-18 05:28:11 +01:00
|
|
|
|
2020-07-27 17:12:39 +02:00
|
|
|
log.Debug("Directory tree loaded from DB", "total", len(resp), "elapsed", time.Since(start))
|
|
|
|
return resp, nil
|
2020-01-16 22:53:48 +01:00
|
|
|
}
|
|
|
|
|
2020-10-30 18:14:53 +01:00
|
|
|
func (s *TagScanner) folderHasChanged(ctx context.Context, folder dirStats, dbDirs map[string]struct{}, lastModified time.Time) bool {
|
2020-10-29 16:31:45 +01:00
|
|
|
_, inDB := dbDirs[folder.Path]
|
2020-11-02 13:57:47 +01:00
|
|
|
// If is a new folder with at least one song OR it was modified after lastModified
|
|
|
|
return (!inDB && (folder.AudioFilesCount > 0)) || folder.ModTime.After(lastModified)
|
2020-07-27 17:12:39 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *TagScanner) getDeletedDirs(ctx context.Context, fsDirs dirMap, dbDirs map[string]struct{}) []string {
|
|
|
|
start := time.Now()
|
|
|
|
log.Trace(ctx, "Checking for deleted folders")
|
|
|
|
var deleted []string
|
|
|
|
|
|
|
|
for d := range dbDirs {
|
|
|
|
if _, ok := fsDirs[d]; !ok {
|
|
|
|
deleted = append(deleted, d)
|
|
|
|
}
|
2020-01-16 22:53:48 +01:00
|
|
|
}
|
2020-07-27 17:12:39 +02:00
|
|
|
|
|
|
|
sort.Strings(deleted)
|
|
|
|
log.Debug(ctx, "Finished deleted folders check", "total", len(deleted), "elapsed", time.Since(start))
|
|
|
|
return deleted
|
2020-01-16 22:53:48 +01:00
|
|
|
}
|
|
|
|
|
2020-07-27 17:12:39 +02:00
|
|
|
func (s *TagScanner) processDeletedDir(ctx context.Context, dir string) error {
|
|
|
|
start := time.Now()
|
|
|
|
buffer := newRefreshBuffer(ctx, s.ds)
|
|
|
|
|
|
|
|
mfs, err := s.ds.MediaFile(ctx).FindAllByPath(dir)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2020-06-11 01:30:04 +02:00
|
|
|
}
|
2020-07-27 17:12:39 +02:00
|
|
|
|
|
|
|
c, err := s.ds.MediaFile(ctx).DeleteByPath(dir)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2020-01-16 22:53:48 +01:00
|
|
|
}
|
2020-07-27 17:12:39 +02:00
|
|
|
s.cnt.deleted += c
|
|
|
|
|
|
|
|
for _, t := range mfs {
|
|
|
|
buffer.accumulate(t)
|
2020-10-25 17:00:21 +01:00
|
|
|
s.cacheWarmer.AddAlbum(ctx, t.AlbumID)
|
2020-07-27 17:12:39 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
err = buffer.flush()
|
2020-10-30 04:47:43 +01:00
|
|
|
log.Info(ctx, "Finished processing deleted folder", "dir", dir, "purged", len(mfs), "elapsed", time.Since(start))
|
2020-07-27 17:12:39 +02:00
|
|
|
return err
|
2020-01-16 22:53:48 +01:00
|
|
|
}
|
|
|
|
|
2020-07-27 17:12:39 +02:00
|
|
|
func (s *TagScanner) processChangedDir(ctx context.Context, dir string) error {
|
2020-01-16 22:53:48 +01:00
|
|
|
start := time.Now()
|
2020-07-27 17:12:39 +02:00
|
|
|
buffer := newRefreshBuffer(ctx, s.ds)
|
2020-01-16 22:53:48 +01:00
|
|
|
|
|
|
|
// Load folder's current tracks from DB into a map
|
|
|
|
currentTracks := map[string]model.MediaFile{}
|
2020-07-11 20:38:17 +02:00
|
|
|
ct, err := s.ds.MediaFile(ctx).FindAllByPath(dir)
|
2020-01-16 22:53:48 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
for _, t := range ct {
|
2020-02-14 02:18:17 +01:00
|
|
|
currentTracks[t.Path] = t
|
2020-01-16 22:53:48 +01:00
|
|
|
}
|
|
|
|
|
2020-02-14 02:18:17 +01:00
|
|
|
// Load tracks FileInfo from the folder
|
2020-07-27 17:12:39 +02:00
|
|
|
files, err := loadAllAudioFiles(dir)
|
2020-01-16 22:53:48 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-02-14 02:18:17 +01:00
|
|
|
// If no files to process, return
|
|
|
|
if len(files)+len(currentTracks) == 0 {
|
2020-01-18 07:49:08 +01:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-07-27 17:12:39 +02:00
|
|
|
orphanTracks := map[string]model.MediaFile{}
|
|
|
|
for k, v := range currentTracks {
|
|
|
|
orphanTracks[k] = v
|
|
|
|
}
|
|
|
|
|
|
|
|
// If track from folder is newer than the one in DB, select for update/insert in DB
|
|
|
|
log.Trace(ctx, "Processing changed folder", "dir", dir, "tracksInDB", len(currentTracks), "tracksInFolder", len(files))
|
2020-02-14 02:18:17 +01:00
|
|
|
var filesToUpdate []string
|
|
|
|
for filePath, info := range files {
|
|
|
|
c, ok := currentTracks[filePath]
|
2020-07-12 18:35:23 +02:00
|
|
|
if !ok {
|
|
|
|
filesToUpdate = append(filesToUpdate, filePath)
|
2020-07-27 17:12:39 +02:00
|
|
|
s.cnt.added++
|
2020-07-12 18:35:23 +02:00
|
|
|
}
|
|
|
|
if ok && info.ModTime().After(c.UpdatedAt) {
|
2020-02-14 02:18:17 +01:00
|
|
|
filesToUpdate = append(filesToUpdate, filePath)
|
2020-07-27 17:12:39 +02:00
|
|
|
s.cnt.updated++
|
2020-02-14 02:18:17 +01:00
|
|
|
}
|
2020-06-28 00:41:55 +02:00
|
|
|
|
2020-07-27 17:12:39 +02:00
|
|
|
// Force a refresh of the album and artist, to cater for cover art files
|
|
|
|
buffer.accumulate(c)
|
|
|
|
|
|
|
|
// Only leaves in orphanTracks the ones not found in the folder. After this loop any remaining orphanTracks
|
|
|
|
// are considered gone from the music folder and will be deleted from DB
|
|
|
|
delete(orphanTracks, filePath)
|
2020-02-14 02:18:17 +01:00
|
|
|
}
|
|
|
|
|
2020-01-16 22:53:48 +01:00
|
|
|
numUpdatedTracks := 0
|
|
|
|
numPurgedTracks := 0
|
2020-04-02 23:38:20 +02:00
|
|
|
|
|
|
|
if len(filesToUpdate) > 0 {
|
2020-07-27 17:12:39 +02:00
|
|
|
numUpdatedTracks, err = s.addOrUpdateTracksInDB(ctx, dir, currentTracks, filesToUpdate, buffer)
|
2020-06-11 01:30:04 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-27 17:12:39 +02:00
|
|
|
if len(orphanTracks) > 0 {
|
|
|
|
numPurgedTracks, err = s.deleteOrphanSongs(ctx, dir, orphanTracks, buffer)
|
2020-06-11 01:30:04 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
2020-07-27 17:12:39 +02:00
|
|
|
|
2020-10-25 17:00:21 +01:00
|
|
|
// Pre cache all changed album artwork
|
|
|
|
for albumID := range buffer.album {
|
|
|
|
s.cacheWarmer.AddAlbum(ctx, albumID)
|
|
|
|
}
|
|
|
|
|
2020-07-27 17:12:39 +02:00
|
|
|
err = buffer.flush()
|
|
|
|
log.Info(ctx, "Finished processing changed folder", "dir", dir, "updated", numUpdatedTracks,
|
|
|
|
"purged", numPurgedTracks, "elapsed", time.Since(start))
|
|
|
|
return err
|
2020-06-11 01:30:04 +02:00
|
|
|
}
|
|
|
|
|
2020-07-27 17:12:39 +02:00
|
|
|
func (s *TagScanner) deleteOrphanSongs(ctx context.Context, dir string, tracksToDelete map[string]model.MediaFile, buffer *refreshBuffer) (int, error) {
|
|
|
|
numPurgedTracks := 0
|
2020-01-16 22:53:48 +01:00
|
|
|
|
2020-07-27 17:12:39 +02:00
|
|
|
log.Debug(ctx, "Deleting orphan tracks from DB", "dir", dir, "numTracks", len(tracksToDelete))
|
|
|
|
// Remaining tracks from DB that are not in the folder are deleted
|
|
|
|
for _, ct := range tracksToDelete {
|
|
|
|
numPurgedTracks++
|
|
|
|
buffer.accumulate(ct)
|
|
|
|
if err := s.ds.MediaFile(ctx).Delete(ct.ID); err != nil {
|
|
|
|
return 0, err
|
2020-06-11 01:30:04 +02:00
|
|
|
}
|
2020-07-27 17:12:39 +02:00
|
|
|
s.cnt.deleted++
|
2020-01-16 22:53:48 +01:00
|
|
|
}
|
2020-07-27 17:12:39 +02:00
|
|
|
return numPurgedTracks, nil
|
2020-01-16 22:53:48 +01:00
|
|
|
}
|
2020-06-12 19:26:46 +02:00
|
|
|
|
2020-07-27 17:12:39 +02:00
|
|
|
func (s *TagScanner) addOrUpdateTracksInDB(ctx context.Context, dir string, currentTracks map[string]model.MediaFile, filesToUpdate []string, buffer *refreshBuffer) (int, error) {
|
|
|
|
numUpdatedTracks := 0
|
|
|
|
|
|
|
|
log.Trace(ctx, "Updating mediaFiles in DB", "dir", dir, "numFiles", len(filesToUpdate))
|
|
|
|
// Break the file list in chunks to avoid calling ffmpeg with too many parameters
|
|
|
|
chunks := utils.BreakUpStringSlice(filesToUpdate, filesBatchSize)
|
|
|
|
for _, chunk := range chunks {
|
|
|
|
// Load tracks Metadata from the folder
|
|
|
|
newTracks, err := s.loadTracks(chunk)
|
2020-06-12 19:26:46 +02:00
|
|
|
if err != nil {
|
2020-07-27 17:12:39 +02:00
|
|
|
return 0, err
|
2020-06-12 19:26:46 +02:00
|
|
|
}
|
|
|
|
|
2020-07-27 17:12:39 +02:00
|
|
|
// If track from folder is newer than the one in DB, update/insert in DB
|
|
|
|
log.Trace(ctx, "Updating mediaFiles in DB", "dir", dir, "files", chunk, "numFiles", len(chunk))
|
|
|
|
for i := range newTracks {
|
|
|
|
n := newTracks[i]
|
|
|
|
// Keep current annotations if the track is in the DB
|
|
|
|
if t, ok := currentTracks[n.Path]; ok {
|
|
|
|
n.Annotations = t.Annotations
|
2020-06-12 19:26:46 +02:00
|
|
|
}
|
2020-07-27 17:12:39 +02:00
|
|
|
err := s.ds.MediaFile(ctx).Put(&n)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
buffer.accumulate(n)
|
|
|
|
numUpdatedTracks++
|
2020-06-12 19:26:46 +02:00
|
|
|
}
|
|
|
|
}
|
2020-07-27 17:12:39 +02:00
|
|
|
return numUpdatedTracks, nil
|
2020-06-12 19:26:46 +02:00
|
|
|
}
|
2020-01-16 22:53:48 +01:00
|
|
|
|
2020-02-14 02:18:17 +01:00
|
|
|
func (s *TagScanner) loadTracks(filePaths []string) (model.MediaFiles, error) {
|
2020-09-04 17:08:16 +02:00
|
|
|
mds, err := metadata.Extract(filePaths...)
|
2020-02-13 16:03:52 +01:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2020-01-21 06:49:41 +01:00
|
|
|
var mfs model.MediaFiles
|
|
|
|
for _, md := range mds {
|
2020-07-17 16:27:30 +02:00
|
|
|
mf := s.mapper.toMediaFile(md)
|
2020-01-21 06:49:41 +01:00
|
|
|
mfs = append(mfs, mf)
|
2020-01-16 22:53:48 +01:00
|
|
|
}
|
2020-01-21 06:49:41 +01:00
|
|
|
return mfs, nil
|
2020-01-16 22:53:48 +01:00
|
|
|
}
|
|
|
|
|
2020-07-27 17:12:39 +02:00
|
|
|
func (s *TagScanner) withAdminUser(ctx context.Context) context.Context {
|
|
|
|
u, err := s.ds.User(ctx).FindFirstAdmin()
|
|
|
|
if err != nil {
|
|
|
|
log.Warn(ctx, "No admin user found!", err)
|
|
|
|
u = &model.User{}
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx = request.WithUsername(ctx, u.UserName)
|
|
|
|
return request.WithUser(ctx, *u)
|
|
|
|
}
|
|
|
|
|
|
|
|
func loadAllAudioFiles(dirPath string) (map[string]os.FileInfo, error) {
|
2020-07-16 23:42:26 +02:00
|
|
|
dir, err := os.Open(dirPath)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
files, err := dir.Readdir(-1)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
audioFiles := make(map[string]os.FileInfo)
|
|
|
|
for _, f := range files {
|
|
|
|
if f.IsDir() {
|
|
|
|
continue
|
|
|
|
}
|
2020-08-21 17:47:32 +02:00
|
|
|
if strings.HasPrefix(f.Name(), ".") {
|
|
|
|
continue
|
|
|
|
}
|
2020-07-16 23:42:26 +02:00
|
|
|
filePath := filepath.Join(dirPath, f.Name())
|
|
|
|
if !utils.IsAudioFile(filePath) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
fi, err := os.Stat(filePath)
|
|
|
|
if err != nil {
|
|
|
|
log.Error("Could not stat file", "filePath", filePath, err)
|
|
|
|
} else {
|
|
|
|
audioFiles[filePath] = fi
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return audioFiles, nil
|
|
|
|
}
|