navidrome/scanner/importer.go

325 lines
7.9 KiB
Go

package scanner
import (
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/cloudsonic/sonic-server/conf"
"github.com/cloudsonic/sonic-server/domain"
"github.com/cloudsonic/sonic-server/engine"
"github.com/cloudsonic/sonic-server/log"
"github.com/cloudsonic/sonic-server/utils"
)
type Scanner interface {
ScanLibrary(lastModifiedSince time.Time, path string) (int, error)
MediaFiles() map[string]*domain.MediaFile
Albums() map[string]*domain.Album
Artists() map[string]*domain.Artist
Playlists() map[string]*domain.Playlist
}
type tempIndex map[string]domain.ArtistInfo
type Importer struct {
scanner Scanner
mediaFolder string
mfRepo domain.MediaFileRepository
albumRepo domain.AlbumRepository
artistRepo domain.ArtistRepository
idxRepo domain.ArtistIndexRepository
plsRepo domain.PlaylistRepository
propertyRepo domain.PropertyRepository
search engine.Search
lastScan time.Time
lastCheck time.Time
}
func NewImporter(mediaFolder string, scanner Scanner, mfRepo domain.MediaFileRepository, albumRepo domain.AlbumRepository, artistRepo domain.ArtistRepository, idxRepo domain.ArtistIndexRepository, plsRepo domain.PlaylistRepository, propertyRepo domain.PropertyRepository, search engine.Search) *Importer {
return &Importer{
scanner: scanner,
mediaFolder: mediaFolder,
mfRepo: mfRepo,
albumRepo: albumRepo,
artistRepo: artistRepo,
idxRepo: idxRepo,
plsRepo: plsRepo,
propertyRepo: propertyRepo,
search: search,
}
}
func (i *Importer) CheckForUpdates(force bool) {
if force {
i.lastCheck = time.Time{}
}
i.startImport()
}
func (i *Importer) startImport() {
go func() {
info, err := os.Stat(i.mediaFolder)
if err != nil {
log.Error(err)
return
}
if i.lastCheck.After(info.ModTime()) {
return
}
i.lastCheck = time.Now()
i.scan()
}()
}
func (i *Importer) scan() {
i.lastScan = i.lastModifiedSince()
if i.lastScan.IsZero() {
log.Info("Starting first iTunes Library scan. This can take a while...")
}
total, err := i.scanner.ScanLibrary(i.lastScan, i.mediaFolder)
if err != nil {
log.Error("Error importing iTunes Library", err)
return
}
log.Debug("Found", "tracks", total,
"songs", len(i.scanner.MediaFiles()),
"albums", len(i.scanner.Albums()),
"artists", len(i.scanner.Artists()),
"playlists", len(i.scanner.Playlists()))
if err := i.importLibrary(); err != nil {
log.Error("Error persisting data", err)
}
if i.lastScan.IsZero() {
log.Info("Finished first iTunes Library import")
} else {
log.Debug("Finished updating tracks from iTunes Library")
}
}
func (i *Importer) lastModifiedSince() time.Time {
ms, err := i.propertyRepo.Get(domain.PropLastScan)
if err != nil {
log.Warn("Couldn't read LastScan", err)
return time.Time{}
}
if ms == "" {
log.Debug("First scan")
return time.Time{}
}
s, _ := strconv.ParseInt(ms, 10, 64)
return time.Unix(0, s*int64(time.Millisecond))
}
func (i *Importer) importLibrary() (err error) {
arc, _ := i.artistRepo.CountAll()
alc, _ := i.albumRepo.CountAll()
mfc, _ := i.mfRepo.CountAll()
plc, _ := i.plsRepo.CountAll()
log.Debug("Saving updated data")
mfs, mfu := i.importMediaFiles()
als, alu := i.importAlbums()
ars := i.importArtists()
pls := i.importPlaylists()
i.importArtistIndex()
log.Debug("Purging old data")
if deleted, err := i.mfRepo.PurgeInactive(mfs); err != nil {
log.Error(err)
} else {
i.search.RemoveMediaFile(deleted...)
}
if deleted, err := i.albumRepo.PurgeInactive(als); err != nil {
log.Error(err)
} else {
i.search.RemoveAlbum(deleted...)
}
if deleted, err := i.artistRepo.PurgeInactive(ars); err != nil {
log.Error("Deleting inactive artists", err)
} else {
i.search.RemoveArtist(deleted...)
}
if _, err := i.plsRepo.PurgeInactive(pls); err != nil {
log.Error(err)
}
arc2, _ := i.artistRepo.CountAll()
alc2, _ := i.albumRepo.CountAll()
mfc2, _ := i.mfRepo.CountAll()
plc2, _ := i.plsRepo.CountAll()
if arc != arc2 || alc != alc2 || mfc != mfc2 || plc != plc2 {
log.Info(fmt.Sprintf("Updated library totals: %d(%+d) artists, %d(%+d) albums, %d(%+d) songs, %d(%+d) playlists", arc2, arc2-arc, alc2, alc2-alc, mfc2, mfc2-mfc, plc2, plc2-plc))
}
if alu > 0 || mfu > 0 {
log.Info(fmt.Sprintf("Updated items: %d album(s), %d song(s)", alu, mfu))
}
if err == nil {
millis := time.Now().UnixNano() / int64(time.Millisecond)
i.propertyRepo.Put(domain.PropLastScan, fmt.Sprint(millis))
log.Debug("LastScan", "timestamp", millis)
}
return err
}
func (i *Importer) importMediaFiles() (domain.MediaFiles, int) {
mfs := make(domain.MediaFiles, len(i.scanner.MediaFiles()))
updates := 0
j := 0
for _, mf := range i.scanner.MediaFiles() {
mfs[j] = *mf
j++
if mf.UpdatedAt.Before(i.lastScan) {
continue
}
if mf.Starred {
original, err := i.mfRepo.Get(mf.ID)
if err != nil || !original.Starred {
mf.StarredAt = mf.UpdatedAt
} else {
mf.StarredAt = original.StarredAt
}
}
if err := i.mfRepo.Put(mf); err != nil {
log.Error(err)
}
if err := i.search.IndexMediaFile(mf); err != nil {
log.Error("Error indexing artist", err)
}
updates++
if !i.lastScan.IsZero() {
log.Debug(fmt.Sprintf(`-- Updated Track: "%s"`, mf.Title))
}
}
return mfs, updates
}
func (i *Importer) importAlbums() (domain.Albums, int) {
als := make(domain.Albums, len(i.scanner.Albums()))
updates := 0
j := 0
for _, al := range i.scanner.Albums() {
als[j] = *al
j++
if al.UpdatedAt.Before(i.lastScan) {
continue
}
if al.Starred {
original, err := i.albumRepo.Get(al.ID)
if err != nil || !original.Starred {
al.StarredAt = al.UpdatedAt
} else {
al.StarredAt = original.StarredAt
}
}
if err := i.albumRepo.Put(al); err != nil {
log.Error(err)
}
if err := i.search.IndexAlbum(al); err != nil {
log.Error("Error indexing artist", err)
}
updates++
if !i.lastScan.IsZero() {
log.Debug(fmt.Sprintf(`-- Updated Album: "%s" from "%s"`, al.Name, al.Artist))
}
}
return als, updates
}
func (i *Importer) importArtists() domain.Artists {
ars := make(domain.Artists, len(i.scanner.Artists()))
j := 0
for _, ar := range i.scanner.Artists() {
ars[j] = *ar
j++
if err := i.artistRepo.Put(ar); err != nil {
log.Error(err)
}
if err := i.search.IndexArtist(ar); err != nil {
log.Error("Error indexing artist", err)
}
}
return ars
}
func (i *Importer) importArtistIndex() {
indexGroups := utils.ParseIndexGroups(conf.Sonic.IndexGroups)
artistIndex := make(map[string]tempIndex)
for _, ar := range i.scanner.Artists() {
i.collectIndex(indexGroups, ar, artistIndex)
}
if err := i.saveIndex(artistIndex); err != nil {
log.Error(err)
}
}
func (i *Importer) importPlaylists() domain.Playlists {
pls := make(domain.Playlists, len(i.scanner.Playlists()))
j := 0
for _, pl := range i.scanner.Playlists() {
pl.Public = true
pl.Owner = conf.Sonic.User
pl.Comment = "Original: " + pl.FullPath
pls[j] = *pl
j++
if err := i.plsRepo.Put(pl); err != nil {
log.Error(err)
}
}
return pls
}
func (i *Importer) collectIndex(ig utils.IndexGroups, a *domain.Artist, artistIndex map[string]tempIndex) {
name := a.Name
indexName := strings.ToLower(utils.NoArticle(name))
if indexName == "" {
return
}
group := i.findGroup(ig, indexName)
artists := artistIndex[group]
if artists == nil {
artists = make(tempIndex)
artistIndex[group] = artists
}
artists[indexName] = domain.ArtistInfo{ArtistID: a.ID, Artist: a.Name, AlbumCount: a.AlbumCount}
}
func (i *Importer) findGroup(ig utils.IndexGroups, name string) string {
for k, v := range ig {
key := strings.ToLower(k)
if strings.HasPrefix(name, key) {
return v
}
}
return "#"
}
func (i *Importer) saveIndex(artistIndex map[string]tempIndex) error {
i.idxRepo.DeleteAll()
for k, temp := range artistIndex {
idx := &domain.ArtistIndex{ID: k}
for _, v := range temp {
idx.Artists = append(idx.Artists, v)
}
err := i.idxRepo.Put(idx)
if err != nil {
return err
}
}
return nil
}