/* Copyright © 2023 Philipp Wolfer This file is part of Scotty. Scotty is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Scotty is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Scotty. If not, see . */ package listenbrainz import ( "fmt" "sort" "time" "github.com/spf13/viper" "go.uploadedlobster.com/scotty/models" ) type ListenBrainzApiBackend struct { client Client username string existingMbids map[string]bool } func (b *ListenBrainzApiBackend) Name() string { return "listenbrainz" } func (b *ListenBrainzApiBackend) FromConfig(config *viper.Viper) models.Backend { b.client = NewClient(config.GetString("token")) b.client.MaxResults = MaxItemsPerGet b.username = config.GetString("username") return b } func (b *ListenBrainzApiBackend) StartImport() error { return nil } func (b *ListenBrainzApiBackend) FinishImport() error { return nil } func (b *ListenBrainzApiBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.Progress) { startTime := time.Now() maxTime := startTime minTime := time.Unix(0, 0) totalDuration := startTime.Sub(oldestTimestamp) defer close(results) // FIXME: Optimize by fetching the listens in reverse listen time order listens := make(models.ListensList, 0, 2*MaxItemsPerGet) p := models.Progress{Total: int64(totalDuration.Seconds())} out: for { result, err := b.client.GetListens(b.username, maxTime, minTime) if err != nil { progress <- p.Complete() results <- models.ListensResult{Error: err} return } count := len(result.Payload.Listens) if count == 0 { break } // Set maxTime to the oldest returned listen maxTime = time.Unix(result.Payload.Listens[count-1].ListenedAt, 0) remainingTime := maxTime.Sub(oldestTimestamp) for _, listen := range result.Payload.Listens { if listen.ListenedAt > oldestTimestamp.Unix() { listens = append(listens, listen.AsListen()) } else { // result contains listens older then oldestTimestamp, // we can stop requesting more p.Total = int64(startTime.Sub(time.Unix(listen.ListenedAt, 0)).Seconds()) break out } } p.Elapsed = int64(totalDuration.Seconds() - remainingTime.Seconds()) progress <- p } sort.Sort(listens) progress <- p.Complete() results <- models.ListensResult{Listens: listens, OldestTimestamp: oldestTimestamp} } func (b *ListenBrainzApiBackend) ImportListens(export models.ListensResult, importResult models.ImportResult, progress chan models.Progress) (models.ImportResult, error) { total := len(export.Listens) for i := 0; i < total; i += MaxListensPerRequest { listens := export.Listens[i:min(i+MaxItemsPerGet, total)] count := len(listens) if count == 0 { break } submission := ListenSubmission{ ListenType: Import, Payload: make([]Listen, 0, count), } for _, l := range listens { l.FillAdditionalInfo() listen := Listen{ ListenedAt: l.ListenedAt.Unix(), TrackMetadata: Track{ TrackName: l.TrackName, ReleaseName: l.ReleaseName, ArtistName: l.ArtistName(), AdditionalInfo: l.AdditionalInfo, }, } listen.TrackMetadata.AdditionalInfo["submission_client"] = "Scotty" submission.Payload = append(submission.Payload, listen) } _, err := b.client.SubmitListens(submission) if err != nil { return importResult, err } importResult.UpdateTimestamp(listens[count-1].ListenedAt) importResult.ImportCount += count progress <- models.Progress{}.FromImportResult(importResult) } return importResult, nil } func (b *ListenBrainzApiBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.Progress) { offset := 0 defer close(results) loves := make(models.LovesList, 0, 2*MaxItemsPerGet) p := models.Progress{} out: for { result, err := b.client.GetFeedback(b.username, 1, offset) if err != nil { progress <- p.Complete() results <- models.LovesResult{Error: err} return } count := len(result.Feedback) if count == 0 { break out } for _, feedback := range result.Feedback { love := feedback.AsLove() if love.Created.Unix() > oldestTimestamp.Unix() { loves = append(loves, love) p.Elapsed += 1 progress <- p } else { break out } } p.Total = int64(result.TotalCount) p.Elapsed += int64(count) offset += MaxItemsPerGet } sort.Sort(loves) progress <- p.Complete() results <- models.LovesResult{Loves: loves} } func (b *ListenBrainzApiBackend) ImportLoves(export models.LovesResult, importResult models.ImportResult, progress chan models.Progress) (models.ImportResult, error) { if len(b.existingMbids) == 0 { existingLovesChan := make(chan models.LovesResult) go b.ExportLoves(time.Unix(0, 0), existingLovesChan, progress) existingLoves := <-existingLovesChan if existingLoves.Error != nil { return importResult, existingLoves.Error } // TODO: Store MBIDs directly b.existingMbids = make(map[string]bool, len(existingLoves.Loves)) for _, love := range existingLoves.Loves { b.existingMbids[string(love.RecordingMbid)] = true } } for _, love := range export.Loves { recordingMbid := string(love.RecordingMbid) if recordingMbid == "" { lookup, err := b.client.Lookup(love.TrackName, love.ArtistName()) if err == nil { recordingMbid = lookup.RecordingMbid } } if recordingMbid != "" { ok := false errMsg := "" if b.existingMbids[recordingMbid] { ok = true } else { resp, err := b.client.SendFeedback(Feedback{ RecordingMbid: recordingMbid, Score: 1, }) ok = err == nil && resp.Status == "ok" if err != nil { errMsg = err.Error() } } if ok { importResult.UpdateTimestamp(love.Created) importResult.ImportCount += 1 } else { msg := fmt.Sprintf("Failed import of \"%s\" by %s: %v", love.TrackName, love.ArtistName(), errMsg) importResult.ImportErrors = append(importResult.ImportErrors, msg) } } progress <- models.Progress{}.FromImportResult(importResult) } return importResult, nil } func (lbListen Listen) AsListen() models.Listen { listen := models.Listen{ ListenedAt: time.Unix(lbListen.ListenedAt, 0), UserName: lbListen.UserName, Track: lbListen.TrackMetadata.AsTrack(), } return listen } func (f Feedback) AsLove() models.Love { recordingMbid := models.MBID(f.RecordingMbid) track := f.TrackMetadata if track == nil { track = &Track{} } love := models.Love{ UserName: f.UserName, RecordingMbid: recordingMbid, Created: time.Unix(f.Created, 0), Track: track.AsTrack(), } if love.Track.RecordingMbid == "" { love.Track.RecordingMbid = love.RecordingMbid } return love } func (t Track) AsTrack() models.Track { track := models.Track{ TrackName: t.TrackName, ReleaseName: t.ReleaseName, ArtistNames: []string{t.ArtistName}, Duration: t.Duration(), TrackNumber: t.TrackNumber(), DiscNumber: t.DiscNumber(), RecordingMbid: models.MBID(t.RecordingMbid()), ReleaseMbid: models.MBID(t.ReleaseMbid()), ReleaseGroupMbid: models.MBID(t.ReleaseGroupMbid()), ISRC: t.ISRC(), AdditionalInfo: t.AdditionalInfo, } if t.MbidMapping != nil && len(track.ArtistMbids) == 0 { for _, artistMbid := range t.MbidMapping.ArtistMbids { track.ArtistMbids = append(track.ArtistMbids, models.MBID(artistMbid)) } } return track }