mirror of
https://git.sr.ht/~phw/scotty
synced 2025-04-30 13:47:05 +02:00
Implemented progressbar for export/import
This commit is contained in:
parent
ab04eb1123
commit
6e330daf06
24 changed files with 590 additions and 239 deletions
|
@ -52,7 +52,7 @@ func ResolveBackend[T interface{}](config *viper.Viper) (T, error) {
|
|||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
implements, interfaceName := implementsInterface[T](backend)
|
||||
implements, interfaceName := ImplementsInterface[T](&backend)
|
||||
if implements {
|
||||
result = backend.(T)
|
||||
} else {
|
||||
|
@ -91,14 +91,14 @@ func resolveBackend(config *viper.Viper) (string, models.Backend, error) {
|
|||
backendName := config.GetString("backend")
|
||||
backendType := knownBackends[backendName]
|
||||
if backendType == nil {
|
||||
return backendName, nil, errors.New(fmt.Sprintf("Unknown backend %s", backendName))
|
||||
return backendName, nil, fmt.Errorf("Unknown backend %s", backendName)
|
||||
}
|
||||
return backendName, backendType().FromConfig(config), nil
|
||||
}
|
||||
|
||||
func implementsInterface[T interface{}](backend models.Backend) (bool, string) {
|
||||
func ImplementsInterface[T interface{}](backend *models.Backend) (bool, string) {
|
||||
expectedInterface := reflect.TypeOf((*T)(nil)).Elem()
|
||||
implements := backend != nil && reflect.TypeOf(backend).Implements(expectedInterface)
|
||||
implements := backend != nil && reflect.TypeOf(*backend).Implements(expectedInterface)
|
||||
return implements, expectedInterface.Name()
|
||||
}
|
||||
|
||||
|
@ -133,7 +133,7 @@ func getImportCapabilities(backend models.Backend) []Capability {
|
|||
}
|
||||
|
||||
func checkCapability[T interface{}](backend models.Backend, suffix string) (string, bool) {
|
||||
implements, name := implementsInterface[T](backend)
|
||||
implements, name := ImplementsInterface[T](&backend)
|
||||
if implements {
|
||||
cap, found := strings.CutSuffix(strings.ToLower(name), suffix)
|
||||
if found {
|
||||
|
|
|
@ -23,12 +23,19 @@ THE SOFTWARE.
|
|||
package backends_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.uploadedlobster.com/scotty/backends"
|
||||
"go.uploadedlobster.com/scotty/backends/dump"
|
||||
"go.uploadedlobster.com/scotty/backends/funkwhale"
|
||||
"go.uploadedlobster.com/scotty/backends/jspf"
|
||||
"go.uploadedlobster.com/scotty/backends/listenbrainz"
|
||||
"go.uploadedlobster.com/scotty/backends/maloja"
|
||||
"go.uploadedlobster.com/scotty/backends/scrobblerlog"
|
||||
"go.uploadedlobster.com/scotty/backends/subsonic"
|
||||
"go.uploadedlobster.com/scotty/models"
|
||||
)
|
||||
|
||||
|
@ -37,7 +44,7 @@ func TestResolveBackend(t *testing.T) {
|
|||
config.Set("backend", "dump")
|
||||
backend, err := backends.ResolveBackend[models.ListensImport](config)
|
||||
assert.NoError(t, err)
|
||||
assert.IsType(t, dump.DumpBackend{}, backend)
|
||||
assert.IsType(t, &dump.DumpBackend{}, backend)
|
||||
}
|
||||
|
||||
func TestResolveBackendUnknown(t *testing.T) {
|
||||
|
@ -69,3 +76,39 @@ func TestGetBackends(t *testing.T) {
|
|||
// If we got here the "dump" backend was not included
|
||||
t.Errorf("GetBackends() did not return expected bacend \"dump\"")
|
||||
}
|
||||
|
||||
func TestImplementsInterfaces(t *testing.T) {
|
||||
expectInterface[models.ListensImport](t, &dump.DumpBackend{})
|
||||
expectInterface[models.LovesImport](t, &dump.DumpBackend{})
|
||||
|
||||
expectInterface[models.ListensExport](t, &funkwhale.FunkwhaleApiBackend{})
|
||||
// expectInterface[models.ListensImport](t, &funkwhale.FunkwhaleApiBackend{})
|
||||
expectInterface[models.LovesExport](t, &funkwhale.FunkwhaleApiBackend{})
|
||||
// expectInterface[models.LovesImport](t, &funkwhale.FunkwhaleApiBackend{})
|
||||
|
||||
// expectInterface[models.ListensExport](t, &jspf.JspfBackend{})
|
||||
// expectInterface[models.ListensImport](t, &jspf.JspfBackend{})
|
||||
// expectInterface[models.LovesExport](t, &jspf.JspfBackend{})
|
||||
expectInterface[models.LovesImport](t, &jspf.JspfBackend{})
|
||||
|
||||
expectInterface[models.ListensExport](t, &listenbrainz.ListenBrainzApiBackend{})
|
||||
// expectInterface[models.ListensImport](t, &listenbrainz.ListenBrainzApiBackend{})
|
||||
expectInterface[models.LovesExport](t, &listenbrainz.ListenBrainzApiBackend{})
|
||||
expectInterface[models.LovesImport](t, &listenbrainz.ListenBrainzApiBackend{})
|
||||
|
||||
expectInterface[models.ListensExport](t, &maloja.MalojaApiBackend{})
|
||||
expectInterface[models.ListensImport](t, &maloja.MalojaApiBackend{})
|
||||
|
||||
expectInterface[models.ListensExport](t, &scrobblerlog.ScrobblerLogBackend{})
|
||||
expectInterface[models.ListensImport](t, &scrobblerlog.ScrobblerLogBackend{})
|
||||
|
||||
expectInterface[models.LovesExport](t, &subsonic.SubsonicApiBackend{})
|
||||
// expectInterface[models.LovesImport](t, &subsonic.SubsonicApiBackend{})
|
||||
}
|
||||
|
||||
func expectInterface[T interface{}](t *testing.T, backend models.Backend) {
|
||||
ok, name := backends.ImplementsInterface[T](&backend)
|
||||
if !ok {
|
||||
t.Errorf("%v expected to implement %v", reflect.TypeOf(backend).Name(), name)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,55 +22,39 @@ THE SOFTWARE.
|
|||
package dump
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"go.uploadedlobster.com/scotty/models"
|
||||
)
|
||||
|
||||
type DumpBackend struct{}
|
||||
|
||||
func (b DumpBackend) FromConfig(config *viper.Viper) models.Backend {
|
||||
func (b *DumpBackend) FromConfig(config *viper.Viper) models.Backend {
|
||||
return b
|
||||
}
|
||||
|
||||
func (b DumpBackend) ImportListens(results chan models.ListensResult, oldestTimestamp time.Time) (models.ImportResult, error) {
|
||||
importResult := models.ImportResult{
|
||||
LastTimestamp: oldestTimestamp,
|
||||
}
|
||||
for result := range results {
|
||||
if result.Error != nil {
|
||||
return importResult, result.Error
|
||||
}
|
||||
func (b *DumpBackend) Init() error { return nil }
|
||||
func (b *DumpBackend) Finish() error { return nil }
|
||||
|
||||
importResult.TotalCount += len(result.Listens)
|
||||
for _, listen := range result.Listens {
|
||||
importResult.UpdateTimestamp(listen.ListenedAt)
|
||||
importResult.ImportCount += 1
|
||||
fmt.Printf("🎶 %v: \"%v\" by %v (%v)\n",
|
||||
listen.ListenedAt, listen.TrackName, listen.ArtistName(), listen.RecordingMbid)
|
||||
}
|
||||
func (b *DumpBackend) ImportListens(export models.ListensResult, importResult models.ImportResult, progress chan models.Progress) (models.ImportResult, error) {
|
||||
for _, listen := range export.Listens {
|
||||
importResult.UpdateTimestamp(listen.ListenedAt)
|
||||
importResult.ImportCount += 1
|
||||
progress <- models.Progress{}.FromImportResult(importResult)
|
||||
// fmt.Printf("🎶 %v: \"%v\" by %v (%v)\n",
|
||||
// listen.ListenedAt, listen.TrackName, listen.ArtistName(), listen.RecordingMbid)
|
||||
}
|
||||
|
||||
return importResult, nil
|
||||
}
|
||||
|
||||
func (b DumpBackend) ImportLoves(results chan models.LovesResult, oldestTimestamp time.Time) (models.ImportResult, error) {
|
||||
importResult := models.ImportResult{
|
||||
LastTimestamp: oldestTimestamp,
|
||||
func (b *DumpBackend) ImportLoves(export models.LovesResult, importResult models.ImportResult, progress chan models.Progress) (models.ImportResult, error) {
|
||||
for _, love := range export.Loves {
|
||||
importResult.UpdateTimestamp(love.Created)
|
||||
importResult.ImportCount += 1
|
||||
progress <- models.Progress{}.FromImportResult(importResult)
|
||||
// fmt.Printf("❤️ %v: \"%v\" by %v (%v)\n",
|
||||
// love.Created, love.TrackName, love.ArtistName(), love.RecordingMbid)
|
||||
}
|
||||
for result := range results {
|
||||
if result.Error != nil {
|
||||
return importResult, result.Error
|
||||
}
|
||||
|
||||
importResult.TotalCount += len(result.Loves)
|
||||
for _, love := range result.Loves {
|
||||
importResult.UpdateTimestamp(love.Created)
|
||||
importResult.ImportCount += 1
|
||||
fmt.Printf("❤️ %v: \"%v\" by %v (%v)\n",
|
||||
love.Created, love.TrackName, love.ArtistName(), love.RecordingMbid)
|
||||
}
|
||||
}
|
||||
return importResult, nil
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ type FunkwhaleApiBackend struct {
|
|||
username string
|
||||
}
|
||||
|
||||
func (b FunkwhaleApiBackend) FromConfig(config *viper.Viper) models.Backend {
|
||||
func (b *FunkwhaleApiBackend) FromConfig(config *viper.Viper) models.Backend {
|
||||
b.client = NewClient(
|
||||
config.GetString("server-url"),
|
||||
config.GetString("token"),
|
||||
|
@ -45,19 +45,22 @@ func (b FunkwhaleApiBackend) FromConfig(config *viper.Viper) models.Backend {
|
|||
return b
|
||||
}
|
||||
|
||||
func (b FunkwhaleApiBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult) {
|
||||
func (b *FunkwhaleApiBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.Progress) {
|
||||
page := 1
|
||||
perPage := MaxItemsPerGet
|
||||
|
||||
defer close(results)
|
||||
defer close(progress)
|
||||
|
||||
// We need to gather the full list of listens in order to sort them
|
||||
listens := make(models.ListensList, 0, 2*MaxItemsPerGet)
|
||||
listens := make(models.ListensList, 0, 2*perPage)
|
||||
p := models.Progress{Total: int64(perPage)}
|
||||
|
||||
out:
|
||||
for {
|
||||
result, err := b.client.GetHistoryListenings(b.username, page, perPage)
|
||||
if err != nil {
|
||||
results <- models.ListensResult{Error: err}
|
||||
close(results)
|
||||
}
|
||||
|
||||
count := len(result.Results)
|
||||
|
@ -68,6 +71,7 @@ out:
|
|||
for _, fwListen := range result.Results {
|
||||
listen := fwListen.ToListen()
|
||||
if listen.ListenedAt.Unix() > oldestTimestamp.Unix() {
|
||||
p.Elapsed += 1
|
||||
listens = append(listens, listen)
|
||||
} else {
|
||||
break out
|
||||
|
@ -76,25 +80,31 @@ out:
|
|||
|
||||
if result.Next == "" {
|
||||
// No further results
|
||||
p.Total = p.Elapsed
|
||||
p.Total -= int64(perPage - count)
|
||||
break out
|
||||
}
|
||||
|
||||
p.Total += int64(perPage)
|
||||
progress <- p
|
||||
page += 1
|
||||
}
|
||||
|
||||
sort.Sort(listens)
|
||||
progress <- p.Complete()
|
||||
results <- models.ListensResult{Listens: listens}
|
||||
close(results)
|
||||
}
|
||||
|
||||
func (b FunkwhaleApiBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult) {
|
||||
func (b *FunkwhaleApiBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.Progress) {
|
||||
page := 1
|
||||
perPage := MaxItemsPerGet
|
||||
|
||||
defer close(results)
|
||||
defer close(progress)
|
||||
|
||||
// We need to gather the full list of listens in order to sort them
|
||||
loves := make(models.LovesList, 0, 2*MaxItemsPerGet)
|
||||
loves := make(models.LovesList, 0, 2*perPage)
|
||||
p := models.Progress{Total: int64(perPage)}
|
||||
|
||||
out:
|
||||
for {
|
||||
|
@ -112,6 +122,7 @@ out:
|
|||
for _, favorite := range result.Results {
|
||||
love := favorite.ToLove()
|
||||
if love.Created.Unix() > oldestTimestamp.Unix() {
|
||||
p.Elapsed += 1
|
||||
loves = append(loves, love)
|
||||
} else {
|
||||
break out
|
||||
|
@ -123,10 +134,13 @@ out:
|
|||
break out
|
||||
}
|
||||
|
||||
p.Total += int64(perPage)
|
||||
progress <- p
|
||||
page += 1
|
||||
}
|
||||
|
||||
sort.Sort(loves)
|
||||
progress <- p.Complete()
|
||||
results <- models.LovesResult{Loves: loves}
|
||||
}
|
||||
|
||||
|
|
|
@ -35,8 +35,8 @@ import (
|
|||
func TestFromConfig(t *testing.T) {
|
||||
config := viper.New()
|
||||
config.Set("token", "thetoken")
|
||||
backend := funkwhale.FunkwhaleApiBackend{}.FromConfig(config)
|
||||
assert.IsType(t, funkwhale.FunkwhaleApiBackend{}, backend)
|
||||
backend := (&funkwhale.FunkwhaleApiBackend{}).FromConfig(config)
|
||||
assert.IsType(t, &funkwhale.FunkwhaleApiBackend{}, backend)
|
||||
}
|
||||
|
||||
func TestFunkwhaleListeningToListen(t *testing.T) {
|
||||
|
|
|
@ -36,42 +36,34 @@ type JspfBackend struct {
|
|||
title string
|
||||
creator string
|
||||
identifier string
|
||||
tracks []Track
|
||||
}
|
||||
|
||||
func (b JspfBackend) FromConfig(config *viper.Viper) models.Backend {
|
||||
func (b *JspfBackend) FromConfig(config *viper.Viper) models.Backend {
|
||||
b.filePath = config.GetString("file-path")
|
||||
b.title = config.GetString("title")
|
||||
b.creator = config.GetString("username")
|
||||
b.identifier = config.GetString("identifier")
|
||||
b.tracks = make([]Track, 0)
|
||||
return b
|
||||
}
|
||||
|
||||
func (b JspfBackend) ImportLoves(results chan models.LovesResult, oldestTimestamp time.Time) (models.ImportResult, error) {
|
||||
importResult := models.ImportResult{
|
||||
LastTimestamp: oldestTimestamp,
|
||||
func (b *JspfBackend) Init() error { return nil }
|
||||
func (b *JspfBackend) Finish() error {
|
||||
err := b.writeJspf(b.tracks)
|
||||
return err
|
||||
}
|
||||
|
||||
func (b *JspfBackend) ImportLoves(export models.LovesResult, importResult models.ImportResult, progress chan models.Progress) (models.ImportResult, error) {
|
||||
for _, love := range export.Loves {
|
||||
track := loveToTrack(love)
|
||||
b.tracks = append(b.tracks, track)
|
||||
importResult.ImportCount += 1
|
||||
importResult.UpdateTimestamp(love.Created)
|
||||
}
|
||||
|
||||
tracks := make([]Track, 0, importResult.TotalCount)
|
||||
for result := range results {
|
||||
if result.Error != nil {
|
||||
return importResult, result.Error
|
||||
}
|
||||
|
||||
importResult.TotalCount += len(result.Loves)
|
||||
for _, love := range result.Loves {
|
||||
track := loveToTrack(love)
|
||||
tracks = append(tracks, track)
|
||||
oldestTimestamp = love.Created
|
||||
importResult.ImportCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
err := b.writeJspf(tracks)
|
||||
if err != nil {
|
||||
importResult.UpdateTimestamp(oldestTimestamp)
|
||||
importResult.ImportCount = len(tracks)
|
||||
}
|
||||
return importResult, err
|
||||
progress <- models.Progress{}.FromImportResult(importResult)
|
||||
return importResult, nil
|
||||
}
|
||||
|
||||
func loveToTrack(love models.Love) Track {
|
||||
|
|
|
@ -36,6 +36,6 @@ func TestFromConfig(t *testing.T) {
|
|||
config.Set("title", "My Playlist")
|
||||
config.Set("username", "outsidecontext")
|
||||
config.Set("identifier", "http://example.com/playlist1")
|
||||
backend := scrobblerlog.ScrobblerLogBackend{}.FromConfig(config)
|
||||
assert.IsType(t, scrobblerlog.ScrobblerLogBackend{}, backend)
|
||||
backend := (&scrobblerlog.ScrobblerLogBackend{}).FromConfig(config)
|
||||
assert.IsType(t, &scrobblerlog.ScrobblerLogBackend{}, backend)
|
||||
}
|
||||
|
|
|
@ -31,25 +31,34 @@ import (
|
|||
)
|
||||
|
||||
type ListenBrainzApiBackend struct {
|
||||
client Client
|
||||
username string
|
||||
client Client
|
||||
username string
|
||||
existingMbids map[string]bool
|
||||
}
|
||||
|
||||
func (b ListenBrainzApiBackend) FromConfig(config *viper.Viper) models.Backend {
|
||||
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) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult) {
|
||||
maxTime := time.Now()
|
||||
func (b *ListenBrainzApiBackend) Init() error { return nil }
|
||||
func (b *ListenBrainzApiBackend) Finish() 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)
|
||||
defer close(progress)
|
||||
|
||||
// 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 {
|
||||
|
@ -66,6 +75,7 @@ out:
|
|||
|
||||
// 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() {
|
||||
|
@ -73,19 +83,26 @@ out:
|
|||
} 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)
|
||||
results <- models.ListensResult{Listens: listens}
|
||||
progress <- p.Complete()
|
||||
results <- models.ListensResult{Listens: listens, OldestTimestamp: oldestTimestamp}
|
||||
}
|
||||
|
||||
func (b ListenBrainzApiBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult) {
|
||||
func (b *ListenBrainzApiBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.Progress) {
|
||||
offset := 0
|
||||
defer close(results)
|
||||
defer close(progress)
|
||||
loves := make(models.LovesList, 0, 2*MaxItemsPerGet)
|
||||
p := models.Progress{}
|
||||
|
||||
out:
|
||||
for {
|
||||
|
@ -104,84 +121,77 @@ out:
|
|||
love := feedback.ToLove()
|
||||
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(results chan models.LovesResult, oldestTimestamp time.Time) (models.ImportResult, error) {
|
||||
importResult := models.ImportResult{
|
||||
LastTimestamp: oldestTimestamp,
|
||||
ImportErrors: make([]string, 0),
|
||||
}
|
||||
|
||||
existingLovesChan := make(chan models.LovesResult)
|
||||
go b.ExportLoves(time.Unix(0, 0), existingLovesChan)
|
||||
existingLoves := <-existingLovesChan
|
||||
if existingLoves.Error != nil {
|
||||
results <- models.LovesResult{Error: existingLoves.Error}
|
||||
close(results)
|
||||
}
|
||||
|
||||
existingMbids := make(map[string]bool, len(existingLoves.Loves))
|
||||
for _, love := range existingLoves.Loves {
|
||||
existingMbids[string(love.RecordingMbid)] = true
|
||||
}
|
||||
|
||||
for result := range results {
|
||||
if result.Error != nil {
|
||||
return importResult, result.Error
|
||||
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
|
||||
}
|
||||
|
||||
importResult.TotalCount += len(result.Loves)
|
||||
// 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 result.Loves {
|
||||
if love.Created.Unix() <= oldestTimestamp.Unix() {
|
||||
continue
|
||||
}
|
||||
for _, love := range export.Loves {
|
||||
recordingMbid := string(love.RecordingMbid)
|
||||
|
||||
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 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)
|
||||
}
|
||||
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
|
||||
|
|
|
@ -35,8 +35,8 @@ import (
|
|||
func TestFromConfig(t *testing.T) {
|
||||
config := viper.New()
|
||||
config.Set("token", "thetoken")
|
||||
backend := listenbrainz.ListenBrainzApiBackend{}.FromConfig(config)
|
||||
assert.IsType(t, listenbrainz.ListenBrainzApiBackend{}, backend)
|
||||
backend := (&listenbrainz.ListenBrainzApiBackend{}).FromConfig(config)
|
||||
assert.IsType(t, &listenbrainz.ListenBrainzApiBackend{}, backend)
|
||||
}
|
||||
|
||||
func TestListenBrainzListenToListen(t *testing.T) {
|
||||
|
|
|
@ -36,7 +36,7 @@ type MalojaApiBackend struct {
|
|||
nofix bool
|
||||
}
|
||||
|
||||
func (b MalojaApiBackend) FromConfig(config *viper.Viper) models.Backend {
|
||||
func (b *MalojaApiBackend) FromConfig(config *viper.Viper) models.Backend {
|
||||
b.client = NewClient(
|
||||
config.GetString("server-url"),
|
||||
config.GetString("token"),
|
||||
|
@ -45,14 +45,19 @@ func (b MalojaApiBackend) FromConfig(config *viper.Viper) models.Backend {
|
|||
return b
|
||||
}
|
||||
|
||||
func (b MalojaApiBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult) {
|
||||
func (b *MalojaApiBackend) Init() error { return nil }
|
||||
func (b *MalojaApiBackend) Finish() error { return nil }
|
||||
|
||||
func (b *MalojaApiBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.Progress) {
|
||||
page := 0
|
||||
perPage := MaxItemsPerGet
|
||||
|
||||
defer close(results)
|
||||
defer close(progress)
|
||||
|
||||
// We need to gather the full list of listens in order to sort them
|
||||
listens := make(models.ListensList, 0, 2*perPage)
|
||||
p := models.Progress{Total: int64(perPage)}
|
||||
|
||||
out:
|
||||
for {
|
||||
|
@ -69,55 +74,45 @@ out:
|
|||
|
||||
for _, scrobble := range result.List {
|
||||
if scrobble.ListenedAt > oldestTimestamp.Unix() {
|
||||
p.Elapsed += 1
|
||||
listens = append(listens, scrobble.ToListen())
|
||||
} else {
|
||||
break out
|
||||
}
|
||||
}
|
||||
|
||||
p.Total += int64(perPage)
|
||||
progress <- p
|
||||
page += 1
|
||||
}
|
||||
|
||||
sort.Sort(listens)
|
||||
progress <- p.Complete()
|
||||
results <- models.ListensResult{Listens: listens}
|
||||
}
|
||||
|
||||
func (b MalojaApiBackend) ImportListens(results chan models.ListensResult, oldestTimestamp time.Time) (models.ImportResult, error) {
|
||||
importResult := models.ImportResult{
|
||||
LastTimestamp: oldestTimestamp,
|
||||
}
|
||||
|
||||
for result := range results {
|
||||
if result.Error != nil {
|
||||
return importResult, result.Error
|
||||
func (b *MalojaApiBackend) ImportListens(export models.ListensResult, importResult models.ImportResult, progress chan models.Progress) (models.ImportResult, error) {
|
||||
for _, listen := range export.Listens {
|
||||
scrobble := NewScrobble{
|
||||
Title: listen.TrackName,
|
||||
Artists: listen.ArtistNames,
|
||||
Album: listen.ReleaseName,
|
||||
Duration: int64(listen.PlaybackDuration.Seconds()),
|
||||
Length: int64(listen.Duration.Seconds()),
|
||||
Time: listen.ListenedAt.Unix(),
|
||||
Nofix: b.nofix,
|
||||
}
|
||||
|
||||
importResult.TotalCount += len(result.Listens)
|
||||
for _, listen := range result.Listens {
|
||||
if listen.ListenedAt.Unix() <= oldestTimestamp.Unix() {
|
||||
break
|
||||
}
|
||||
|
||||
scrobble := NewScrobble{
|
||||
Title: listen.TrackName,
|
||||
Artists: listen.ArtistNames,
|
||||
Album: listen.ReleaseName,
|
||||
Duration: int64(listen.PlaybackDuration.Seconds()),
|
||||
Length: int64(listen.Duration.Seconds()),
|
||||
Time: listen.ListenedAt.Unix(),
|
||||
Nofix: b.nofix,
|
||||
}
|
||||
|
||||
resp, err := b.client.NewScrobble(scrobble)
|
||||
if err != nil {
|
||||
return importResult, err
|
||||
} else if resp.Status != "success" {
|
||||
return importResult, errors.New(resp.Error.Description)
|
||||
}
|
||||
|
||||
importResult.UpdateTimestamp(listen.ListenedAt)
|
||||
importResult.ImportCount += 1
|
||||
resp, err := b.client.NewScrobble(scrobble)
|
||||
if err != nil {
|
||||
return importResult, err
|
||||
} else if resp.Status != "success" {
|
||||
return importResult, errors.New(resp.Error.Description)
|
||||
}
|
||||
|
||||
importResult.UpdateTimestamp(listen.ListenedAt)
|
||||
importResult.ImportCount += 1
|
||||
progress <- models.Progress{}.FromImportResult(importResult)
|
||||
}
|
||||
|
||||
return importResult, nil
|
||||
|
|
|
@ -33,8 +33,8 @@ import (
|
|||
func TestFromConfig(t *testing.T) {
|
||||
config := viper.New()
|
||||
config.Set("token", "thetoken")
|
||||
backend := maloja.MalojaApiBackend{}.FromConfig(config)
|
||||
assert.IsType(t, maloja.MalojaApiBackend{}, backend)
|
||||
backend := (&maloja.MalojaApiBackend{}).FromConfig(config)
|
||||
assert.IsType(t, &maloja.MalojaApiBackend{}, backend)
|
||||
}
|
||||
|
||||
func TestScrobbleToListen(t *testing.T) {
|
||||
|
|
107
backends/process.go
Normal file
107
backends/process.go
Normal file
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
Copyright © 2023 Philipp Wolfer <phw@uploadedlobster.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
|
||||
package backends
|
||||
|
||||
import "go.uploadedlobster.com/scotty/models"
|
||||
|
||||
func ProcessListensImports(importer models.ListensImport, results chan models.ListensResult, out chan models.ImportResult, progress chan models.Progress) {
|
||||
defer close(out)
|
||||
defer close(progress)
|
||||
result := models.ImportResult{}
|
||||
|
||||
err := importer.Init()
|
||||
if err != nil {
|
||||
handleError(result, err, out, progress)
|
||||
return
|
||||
}
|
||||
|
||||
for exportResult := range results {
|
||||
if exportResult.Error != nil {
|
||||
handleError(result, exportResult.Error, out, progress)
|
||||
return
|
||||
}
|
||||
|
||||
result.TotalCount += len(exportResult.Listens)
|
||||
importResult, err := importer.ImportListens(exportResult, result, progress)
|
||||
if err != nil {
|
||||
handleError(importResult, err, out, progress)
|
||||
return
|
||||
}
|
||||
|
||||
result.Update(importResult)
|
||||
progress <- models.Progress{}.FromImportResult(result)
|
||||
}
|
||||
|
||||
err = importer.Finish()
|
||||
if err != nil {
|
||||
handleError(result, err, out, progress)
|
||||
return
|
||||
}
|
||||
|
||||
progress <- models.Progress{}.FromImportResult(result).Complete()
|
||||
out <- result
|
||||
}
|
||||
|
||||
func ProcessLovesImports(importer models.LovesImport, results chan models.LovesResult, out chan models.ImportResult, progress chan models.Progress) {
|
||||
defer close(out)
|
||||
defer close(progress)
|
||||
result := models.ImportResult{}
|
||||
|
||||
err := importer.Init()
|
||||
if err != nil {
|
||||
handleError(result, err, out, progress)
|
||||
return
|
||||
}
|
||||
|
||||
for exportResult := range results {
|
||||
if exportResult.Error != nil {
|
||||
handleError(result, exportResult.Error, out, progress)
|
||||
return
|
||||
}
|
||||
|
||||
result.TotalCount += len(exportResult.Loves)
|
||||
importResult, err := importer.ImportLoves(exportResult, result, progress)
|
||||
if err != nil {
|
||||
handleError(importResult, err, out, progress)
|
||||
return
|
||||
}
|
||||
|
||||
result.Update(importResult)
|
||||
progress <- models.Progress{}.FromImportResult(result)
|
||||
}
|
||||
|
||||
err = importer.Finish()
|
||||
if err != nil {
|
||||
handleError(result, err, out, progress)
|
||||
return
|
||||
}
|
||||
|
||||
progress <- models.Progress{}.FromImportResult(result).Complete()
|
||||
out <- result
|
||||
}
|
||||
|
||||
func handleError(result models.ImportResult, err error, out chan models.ImportResult, progress chan models.Progress) {
|
||||
result.Error = err
|
||||
progress <- models.Progress{}.FromImportResult(result).Complete()
|
||||
out <- result
|
||||
}
|
|
@ -33,16 +33,44 @@ import (
|
|||
type ScrobblerLogBackend struct {
|
||||
filePath string
|
||||
includeSkipped bool
|
||||
file *os.File
|
||||
log ScrobblerLog
|
||||
}
|
||||
|
||||
func (b ScrobblerLogBackend) FromConfig(config *viper.Viper) models.Backend {
|
||||
func (b *ScrobblerLogBackend) FromConfig(config *viper.Viper) models.Backend {
|
||||
b.filePath = config.GetString("file-path")
|
||||
b.includeSkipped = config.GetBool("include-skipped")
|
||||
return b
|
||||
}
|
||||
|
||||
func (b ScrobblerLogBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult) {
|
||||
func (b *ScrobblerLogBackend) Init() error {
|
||||
file, err := os.Create(b.filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.log = ScrobblerLog{
|
||||
Timezone: "UNKNOWN",
|
||||
Client: "Rockbox unknown $Revision$",
|
||||
}
|
||||
|
||||
err = WriteHeader(file, &b.log)
|
||||
if err != nil {
|
||||
file.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
b.file = file
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *ScrobblerLogBackend) Finish() error {
|
||||
return b.file.Close()
|
||||
}
|
||||
|
||||
func (b *ScrobblerLogBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.Progress) {
|
||||
defer close(results)
|
||||
defer close(progress)
|
||||
file, err := os.Open(b.filePath)
|
||||
if err != nil {
|
||||
results <- models.ListensResult{Error: err}
|
||||
|
@ -60,45 +88,19 @@ func (b ScrobblerLogBackend) ExportListens(oldestTimestamp time.Time, results ch
|
|||
|
||||
listens := log.Listens.NewerThan(oldestTimestamp)
|
||||
sort.Sort(listens)
|
||||
progress <- models.Progress{Elapsed: int64(len(listens))}.Complete()
|
||||
results <- models.ListensResult{Listens: listens}
|
||||
}
|
||||
|
||||
func (b ScrobblerLogBackend) ImportListens(results chan models.ListensResult, oldestTimestamp time.Time) (models.ImportResult, error) {
|
||||
importResult := models.ImportResult{
|
||||
LastTimestamp: oldestTimestamp,
|
||||
}
|
||||
|
||||
file, err := os.Create(b.filePath)
|
||||
func (b *ScrobblerLogBackend) ImportListens(export models.ListensResult, importResult models.ImportResult, progress chan models.Progress) (models.ImportResult, error) {
|
||||
lastTimestamp, err := Write(b.file, export.Listens)
|
||||
if err != nil {
|
||||
return importResult, err
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
log := ScrobblerLog{
|
||||
Timezone: "UNKNOWN",
|
||||
Client: "Rockbox unknown $Revision$",
|
||||
}
|
||||
|
||||
err = WriteHeader(file, &log)
|
||||
if err != nil {
|
||||
return importResult, err
|
||||
}
|
||||
|
||||
for result := range results {
|
||||
if result.Error != nil {
|
||||
return importResult, result.Error
|
||||
}
|
||||
|
||||
importResult.TotalCount += len(result.Listens)
|
||||
lastTimestamp, err := Write(file, result.Listens)
|
||||
if err != nil {
|
||||
return importResult, err
|
||||
}
|
||||
|
||||
importResult.UpdateTimestamp(lastTimestamp)
|
||||
importResult.ImportCount += len(result.Listens)
|
||||
}
|
||||
importResult.UpdateTimestamp(lastTimestamp)
|
||||
importResult.ImportCount = len(export.Listens)
|
||||
progress <- models.Progress{}.FromImportResult(importResult)
|
||||
|
||||
return importResult, nil
|
||||
}
|
||||
|
|
|
@ -32,6 +32,6 @@ import (
|
|||
func TestFromConfig(t *testing.T) {
|
||||
config := viper.New()
|
||||
config.Set("token", "thetoken")
|
||||
backend := scrobblerlog.ScrobblerLogBackend{}.FromConfig(config)
|
||||
assert.IsType(t, scrobblerlog.ScrobblerLogBackend{}, backend)
|
||||
backend := (&scrobblerlog.ScrobblerLogBackend{}).FromConfig(config)
|
||||
assert.IsType(t, &scrobblerlog.ScrobblerLogBackend{}, backend)
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ type SubsonicApiBackend struct {
|
|||
password string
|
||||
}
|
||||
|
||||
func (b SubsonicApiBackend) FromConfig(config *viper.Viper) models.Backend {
|
||||
func (b *SubsonicApiBackend) FromConfig(config *viper.Viper) models.Backend {
|
||||
b.client = subsonic.Client{
|
||||
Client: &http.Client{},
|
||||
BaseUrl: config.GetString("server-url"),
|
||||
|
@ -47,8 +47,9 @@ func (b SubsonicApiBackend) FromConfig(config *viper.Viper) models.Backend {
|
|||
return b
|
||||
}
|
||||
|
||||
func (b SubsonicApiBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult) {
|
||||
func (b *SubsonicApiBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.Progress) {
|
||||
defer close(results)
|
||||
defer close(progress)
|
||||
err := b.client.Authenticate(b.password)
|
||||
if err != nil {
|
||||
results <- models.LovesResult{Error: err}
|
||||
|
@ -61,10 +62,11 @@ func (b SubsonicApiBackend) ExportLoves(oldestTimestamp time.Time, results chan
|
|||
return
|
||||
}
|
||||
|
||||
progress <- models.Progress{Elapsed: int64(len(starred.Song))}.Complete()
|
||||
results <- models.LovesResult{Loves: b.filterSongs(starred.Song, oldestTimestamp)}
|
||||
}
|
||||
|
||||
func (b SubsonicApiBackend) filterSongs(songs []*subsonic.Child, oldestTimestamp time.Time) models.LovesList {
|
||||
func (b *SubsonicApiBackend) filterSongs(songs []*subsonic.Child, oldestTimestamp time.Time) models.LovesList {
|
||||
loves := make(models.LovesList, len(songs))
|
||||
for i, song := range songs {
|
||||
love := SongToLove(*song, b.client.User)
|
||||
|
|
|
@ -35,8 +35,8 @@ func TestFromConfig(t *testing.T) {
|
|||
config := viper.New()
|
||||
config.Set("server-url", "https://subsonic.example.com")
|
||||
config.Set("token", "thetoken")
|
||||
backend := subsonic.SubsonicApiBackend{}.FromConfig(config)
|
||||
assert.IsType(t, subsonic.SubsonicApiBackend{}, backend)
|
||||
backend := (&subsonic.SubsonicApiBackend{}).FromConfig(config)
|
||||
assert.IsType(t, &subsonic.SubsonicApiBackend{}, backend)
|
||||
}
|
||||
|
||||
func TestSongToLove(t *testing.T) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue