diff --git a/conf/inject_definitions.go b/conf/inject_definitions.go index 82b81c386..0ccd7eed9 100644 --- a/conf/inject_definitions.go +++ b/conf/inject_definitions.go @@ -28,9 +28,11 @@ func init() { utils.DefineSingleton(new(engine.Playlists), engine.NewPlaylists) utils.DefineSingleton(new(engine.Search), engine.NewSearch) + utils.DefineSingleton(new(scanner.CheckSumRepository), persistence.NewCheckSumRepository) + utils.DefineSingleton(new(scanner.Scanner), scanner.NewItunesScanner) + // Other dependencies utils.DefineSingleton(new(itunesbridge.ItunesControl), itunesbridge.NewItunesControl) - utils.DefineSingleton(new(scanner.Scanner), scanner.NewItunesScanner) utils.DefineSingleton(new(gomate.DB), func() gomate.DB { return gomate.NewLedisEmbeddedDB(persistence.Db()) }) diff --git a/persistence/checksum_repository.go b/persistence/checksum_repository.go new file mode 100644 index 000000000..6730188f9 --- /dev/null +++ b/persistence/checksum_repository.go @@ -0,0 +1,61 @@ +package persistence + +import ( + "errors" + + "github.com/astaxie/beego" + "github.com/deluan/gosonic/scanner" + "github.com/siddontang/ledisdb/ledis" +) + +var ( + keyName = []byte("checksums") +) + +type checkSumRepository struct { + data map[string]string +} + +func NewCheckSumRepository() scanner.CheckSumRepository { + r := &checkSumRepository{} + r.loadData() + return r +} + +func (r *checkSumRepository) loadData() { + r.data = make(map[string]string) + + pairs, err := Db().HGetAll(keyName) + if err != nil { + beego.Error("Error loading CheckSums:", err) + } + for _, p := range pairs { + r.data[string(p.Field)] = string(p.Value) + } +} + +func (r *checkSumRepository) Put(id, sum string) error { + if id == "" { + return errors.New("Id is required") + } + _, err := Db().HSet(keyName, []byte(id), []byte(sum)) + return err +} + +func (r *checkSumRepository) Get(id string) (string, error) { + return r.data[id], nil +} + +func (r *checkSumRepository) SetData(newSums map[string]string) error { + Db().HClear(keyName) + pairs := make([]ledis.FVPair, len(newSums)) + r.data = make(map[string]string) + i := 0 + for id, sum := range newSums { + p := ledis.FVPair{Field: []byte(id), Value: []byte(sum)} + pairs[i] = p + r.data[id] = sum + i++ + } + return Db().HMset(keyName, pairs...) +} diff --git a/scanner/itunes_scanner.go b/scanner/itunes_scanner.go index 16ffdc51f..55e23fa06 100644 --- a/scanner/itunes_scanner.go +++ b/scanner/itunes_scanner.go @@ -3,19 +3,16 @@ package scanner import ( "crypto/md5" "fmt" + "html" + "mime" "net/url" "os" "path/filepath" + "regexp" "strconv" "strings" "time" - "html" - - "regexp" - - "mime" - "github.com/astaxie/beego" "github.com/deluan/gosonic/domain" "github.com/deluan/itl" @@ -29,10 +26,18 @@ type ItunesScanner struct { playlists map[string]*domain.Playlist pplaylists map[string]plsRelation lastModifiedSince time.Time + checksumRepo CheckSumRepository + newSums map[string]string } -func NewItunesScanner() *ItunesScanner { - return &ItunesScanner{} +func NewItunesScanner(checksumRepo CheckSumRepository) *ItunesScanner { + return &ItunesScanner{checksumRepo: checksumRepo} +} + +type CheckSumRepository interface { + Put(id, sum string) error + Get(id string) (string, error) + SetData(newSums map[string]string) error } type plsRelation struct { @@ -57,6 +62,7 @@ func (s *ItunesScanner) ScanLibrary(lastModifiedSince time.Time, path string) (i s.artists = make(map[string]*domain.Artist) s.playlists = make(map[string]*domain.Playlist) s.pplaylists = make(map[string]plsRelation) + s.newSums = make(map[string]string) i := 0 for _, t := range l.Tracks { @@ -71,6 +77,12 @@ func (s *ItunesScanner) ScanLibrary(lastModifiedSince time.Time, path string) (i } } + if err := s.checksumRepo.SetData(s.newSums); err != nil { + beego.Error("Error saving checksums:", err) + } else { + beego.Debug("Saved", len(s.newSums), "checksums") + } + ignFolders, _ := beego.AppConfig.Bool("plsIgnoreFolders") ignPatterns := beego.AppConfig.Strings("plsIgnoredPatterns") for _, p := range l.Playlists { @@ -158,6 +170,9 @@ func (s *ItunesScanner) fullPath(pID string) string { } func (s *ItunesScanner) lastChangedDate(t *itl.Track) time.Time { + if s.hasChanged(t) { + return time.Now() + } allDates := []time.Time{t.DateModified, t.PlayDateUTC} c := time.Time{} for _, d := range allDates { @@ -247,6 +262,14 @@ func (s *ItunesScanner) collectAlbums(t *itl.Track, mf *domain.MediaFile, ar *do return al } +func (s *ItunesScanner) hasChanged(t *itl.Track) bool { + id := strconv.Itoa(t.TrackID) + oldSum, _ := s.checksumRepo.Get(id) + newSum := calcCheckSum(t) + s.newSums[id] = newSum + return oldSum != newSum +} + func (s *ItunesScanner) collectArtists(t *itl.Track) *domain.Artist { id := artistId(t) _, found := s.artists[id] @@ -317,4 +340,12 @@ func realArtistName(t *itl.Track) string { return t.Artist } +// Calc sum of stats fields (whose changes are not reflected in DataModified) +func calcCheckSum(t *itl.Track) string { + data := fmt.Sprint(t.DateModified, t.PlayCount, t.PlayDate, t.ArtworkCount, t.Loved, t.AlbumLoved, + t.Rating, t.AlbumRating, t.SkipCount, t.SkipDate) + return fmt.Sprintf("%x", md5.Sum([]byte(data))) + +} + var _ Scanner = (*ItunesScanner)(nil)