mirror of
https://git.sr.ht/~phw/scotty
synced 2025-06-06 04:58:33 +02:00
Compare commits
No commits in common. "20853f7601c80b1254c59d903b0e46667e98cecc" and "97600d8190f80d0125a83849ad88e533433e8ef3" have entirely different histories.
20853f7601
...
97600d8190
29 changed files with 144 additions and 263 deletions
2
go.mod
2
go.mod
|
@ -22,7 +22,7 @@ require (
|
||||||
github.com/supersonic-app/go-subsonic v0.0.0-20241224013245-9b2841f3711d
|
github.com/supersonic-app/go-subsonic v0.0.0-20241224013245-9b2841f3711d
|
||||||
github.com/vbauerster/mpb/v8 v8.10.0
|
github.com/vbauerster/mpb/v8 v8.10.0
|
||||||
go.uploadedlobster.com/mbtypes v0.4.0
|
go.uploadedlobster.com/mbtypes v0.4.0
|
||||||
go.uploadedlobster.com/musicbrainzws2 v0.14.1-0.20250522060150-50bf4bea5400
|
go.uploadedlobster.com/musicbrainzws2 v0.14.0
|
||||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6
|
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6
|
||||||
golang.org/x/oauth2 v0.30.0
|
golang.org/x/oauth2 v0.30.0
|
||||||
golang.org/x/text v0.25.0
|
golang.org/x/text v0.25.0
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -132,8 +132,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
go.uploadedlobster.com/mbtypes v0.4.0 h1:D5asCgHsRWufj4Yn5u0IuH2J9z1UuYImYkYIp1Z1Q7s=
|
go.uploadedlobster.com/mbtypes v0.4.0 h1:D5asCgHsRWufj4Yn5u0IuH2J9z1UuYImYkYIp1Z1Q7s=
|
||||||
go.uploadedlobster.com/mbtypes v0.4.0/go.mod h1:Bu1K1Hl77QTAE2Z7QKiW/JAp9KqYWQebkRRfG02dlZM=
|
go.uploadedlobster.com/mbtypes v0.4.0/go.mod h1:Bu1K1Hl77QTAE2Z7QKiW/JAp9KqYWQebkRRfG02dlZM=
|
||||||
go.uploadedlobster.com/musicbrainzws2 v0.14.1-0.20250522060150-50bf4bea5400 h1:wMJloSsyWjfXznQNjvsrqAeL61BGoil7t4H9hPt18fc=
|
go.uploadedlobster.com/musicbrainzws2 v0.14.0 h1:YaEtxNwLSNT1gzFipQ4XlaThNfXjBpzzb4I6WhIeUwg=
|
||||||
go.uploadedlobster.com/musicbrainzws2 v0.14.1-0.20250522060150-50bf4bea5400/go.mod h1:T6sYE7ZHRH3mJWT3g9jdSUPKJLZubnBjKyjMPNdkgao=
|
go.uploadedlobster.com/musicbrainzws2 v0.14.0/go.mod h1:T6sYE7ZHRH3mJWT3g9jdSUPKJLZubnBjKyjMPNdkgao=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright © 2023-2025 Philipp Wolfer <phw@uploadedlobster.com>
|
Copyright © 2023 Philipp Wolfer <phw@uploadedlobster.com>
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
@ -23,7 +23,6 @@ THE SOFTWARE.
|
||||||
package deezer
|
package deezer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
@ -53,14 +52,14 @@ func NewClient(token oauth2.TokenSource) Client {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Client) UserHistory(ctx context.Context, offset int, limit int) (result HistoryResult, err error) {
|
func (c Client) UserHistory(offset int, limit int) (result HistoryResult, err error) {
|
||||||
const path = "/user/me/history"
|
const path = "/user/me/history"
|
||||||
return listRequest[HistoryResult](ctx, c, path, offset, limit)
|
return listRequest[HistoryResult](c, path, offset, limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Client) UserTracks(ctx context.Context, offset int, limit int) (TracksResult, error) {
|
func (c Client) UserTracks(offset int, limit int) (TracksResult, error) {
|
||||||
const path = "/user/me/tracks"
|
const path = "/user/me/tracks"
|
||||||
return listRequest[TracksResult](ctx, c, path, offset, limit)
|
return listRequest[TracksResult](c, path, offset, limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Client) setToken(req *resty.Request) error {
|
func (c Client) setToken(req *resty.Request) error {
|
||||||
|
@ -73,9 +72,8 @@ func (c Client) setToken(req *resty.Request) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func listRequest[T Result](ctx context.Context, c Client, path string, offset int, limit int) (result T, err error) {
|
func listRequest[T Result](c Client, path string, offset int, limit int) (result T, err error) {
|
||||||
request := c.HTTPClient.R().
|
request := c.HTTPClient.R().
|
||||||
SetContext(ctx).
|
|
||||||
SetQueryParams(map[string]string{
|
SetQueryParams(map[string]string{
|
||||||
"index": strconv.Itoa(offset),
|
"index": strconv.Itoa(offset),
|
||||||
"limit": strconv.Itoa(limit),
|
"limit": strconv.Itoa(limit),
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright © 2023-2025 Philipp Wolfer <phw@uploadedlobster.com>
|
Copyright © 2023 Philipp Wolfer <phw@uploadedlobster.com>
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
@ -23,7 +23,6 @@ THE SOFTWARE.
|
||||||
package deezer_test
|
package deezer_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -49,8 +48,7 @@ func TestGetUserHistory(t *testing.T) {
|
||||||
"https://api.deezer.com/user/me/history",
|
"https://api.deezer.com/user/me/history",
|
||||||
"testdata/user-history.json")
|
"testdata/user-history.json")
|
||||||
|
|
||||||
ctx := context.Background()
|
result, err := client.UserHistory(0, 2)
|
||||||
result, err := client.UserHistory(ctx, 0, 2)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
@ -71,8 +69,7 @@ func TestGetUserTracks(t *testing.T) {
|
||||||
"https://api.deezer.com/user/me/tracks",
|
"https://api.deezer.com/user/me/tracks",
|
||||||
"testdata/user-tracks.json")
|
"testdata/user-tracks.json")
|
||||||
|
|
||||||
ctx := context.Background()
|
result, err := client.UserTracks(0, 2)
|
||||||
result, err := client.UserTracks(ctx, 0, 2)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
|
@ -16,7 +16,6 @@ Scotty. If not, see <https://www.gnu.org/licenses/>.
|
||||||
package deezer
|
package deezer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
@ -78,7 +77,7 @@ func (b *DeezerApiBackend) OAuth2Setup(token oauth2.TokenSource) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *DeezerApiBackend) ExportListens(ctx context.Context, oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) {
|
func (b *DeezerApiBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) {
|
||||||
// Choose a high offset, we attempt to search the loves backwards starting
|
// Choose a high offset, we attempt to search the loves backwards starting
|
||||||
// at the oldest one.
|
// at the oldest one.
|
||||||
offset := math.MaxInt32
|
offset := math.MaxInt32
|
||||||
|
@ -97,7 +96,7 @@ func (b *DeezerApiBackend) ExportListens(ctx context.Context, oldestTimestamp ti
|
||||||
|
|
||||||
out:
|
out:
|
||||||
for {
|
for {
|
||||||
result, err := b.client.UserHistory(ctx, offset, perPage)
|
result, err := b.client.UserHistory(offset, perPage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.Export.Abort()
|
p.Export.Abort()
|
||||||
progress <- p
|
progress <- p
|
||||||
|
@ -154,7 +153,7 @@ out:
|
||||||
progress <- p
|
progress <- p
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *DeezerApiBackend) ExportLoves(ctx context.Context, oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.TransferProgress) {
|
func (b *DeezerApiBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.TransferProgress) {
|
||||||
// Choose a high offset, we attempt to search the loves backwards starting
|
// Choose a high offset, we attempt to search the loves backwards starting
|
||||||
// at the oldest one.
|
// at the oldest one.
|
||||||
offset := math.MaxInt32
|
offset := math.MaxInt32
|
||||||
|
@ -169,7 +168,7 @@ func (b *DeezerApiBackend) ExportLoves(ctx context.Context, oldestTimestamp time
|
||||||
|
|
||||||
out:
|
out:
|
||||||
for {
|
for {
|
||||||
result, err := b.client.UserTracks(ctx, offset, perPage)
|
result, err := b.client.UserTracks(offset, perPage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.Export.Abort()
|
p.Export.Abort()
|
||||||
progress <- p
|
progress <- p
|
||||||
|
|
|
@ -17,7 +17,6 @@ Scotty. If not, see <https://www.gnu.org/licenses/>.
|
||||||
package dump
|
package dump
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"go.uploadedlobster.com/scotty/internal/config"
|
"go.uploadedlobster.com/scotty/internal/config"
|
||||||
|
@ -37,12 +36,8 @@ func (b *DumpBackend) InitConfig(config *config.ServiceConfig) error {
|
||||||
func (b *DumpBackend) StartImport() error { return nil }
|
func (b *DumpBackend) StartImport() error { return nil }
|
||||||
func (b *DumpBackend) FinishImport() error { return nil }
|
func (b *DumpBackend) FinishImport() error { return nil }
|
||||||
|
|
||||||
func (b *DumpBackend) ImportListens(ctx context.Context, export models.ListensResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) {
|
func (b *DumpBackend) ImportListens(export models.ListensResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) {
|
||||||
for _, listen := range export.Items {
|
for _, listen := range export.Items {
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return importResult, err
|
|
||||||
}
|
|
||||||
|
|
||||||
importResult.UpdateTimestamp(listen.ListenedAt)
|
importResult.UpdateTimestamp(listen.ListenedAt)
|
||||||
importResult.ImportCount += 1
|
importResult.ImportCount += 1
|
||||||
msg := fmt.Sprintf("🎶 %v: \"%v\" by %v (%v)",
|
msg := fmt.Sprintf("🎶 %v: \"%v\" by %v (%v)",
|
||||||
|
@ -54,12 +49,8 @@ func (b *DumpBackend) ImportListens(ctx context.Context, export models.ListensRe
|
||||||
return importResult, nil
|
return importResult, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *DumpBackend) ImportLoves(ctx context.Context, export models.LovesResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) {
|
func (b *DumpBackend) ImportLoves(export models.LovesResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) {
|
||||||
for _, love := range export.Items {
|
for _, love := range export.Items {
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return importResult, err
|
|
||||||
}
|
|
||||||
|
|
||||||
importResult.UpdateTimestamp(love.Created)
|
importResult.UpdateTimestamp(love.Created)
|
||||||
importResult.ImportCount += 1
|
importResult.ImportCount += 1
|
||||||
msg := fmt.Sprintf("❤️ %v: \"%v\" by %v (%v)",
|
msg := fmt.Sprintf("❤️ %v: \"%v\" by %v (%v)",
|
||||||
|
|
|
@ -16,7 +16,6 @@ Scotty. If not, see <https://www.gnu.org/licenses/>.
|
||||||
package backends
|
package backends
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -25,7 +24,7 @@ import (
|
||||||
|
|
||||||
type ExportProcessor[T models.ListensResult | models.LovesResult] interface {
|
type ExportProcessor[T models.ListensResult | models.LovesResult] interface {
|
||||||
ExportBackend() models.Backend
|
ExportBackend() models.Backend
|
||||||
Process(ctx context.Context, wg *sync.WaitGroup, oldestTimestamp time.Time, results chan T, progress chan models.TransferProgress)
|
Process(wg *sync.WaitGroup, oldestTimestamp time.Time, results chan T, progress chan models.TransferProgress)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListensExportProcessor struct {
|
type ListensExportProcessor struct {
|
||||||
|
@ -36,11 +35,11 @@ func (p ListensExportProcessor) ExportBackend() models.Backend {
|
||||||
return p.Backend
|
return p.Backend
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p ListensExportProcessor) Process(ctx context.Context, wg *sync.WaitGroup, oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) {
|
func (p ListensExportProcessor) Process(wg *sync.WaitGroup, oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
defer close(results)
|
defer close(results)
|
||||||
p.Backend.ExportListens(ctx, oldestTimestamp, results, progress)
|
p.Backend.ExportListens(oldestTimestamp, results, progress)
|
||||||
}
|
}
|
||||||
|
|
||||||
type LovesExportProcessor struct {
|
type LovesExportProcessor struct {
|
||||||
|
@ -51,9 +50,9 @@ func (p LovesExportProcessor) ExportBackend() models.Backend {
|
||||||
return p.Backend
|
return p.Backend
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p LovesExportProcessor) Process(ctx context.Context, wg *sync.WaitGroup, oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.TransferProgress) {
|
func (p LovesExportProcessor) Process(wg *sync.WaitGroup, oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.TransferProgress) {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
defer close(results)
|
defer close(results)
|
||||||
p.Backend.ExportLoves(ctx, oldestTimestamp, results, progress)
|
p.Backend.ExportLoves(oldestTimestamp, results, progress)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright © 2023-2025 Philipp Wolfer <phw@uploadedlobster.com>
|
Copyright © 2023 Philipp Wolfer <phw@uploadedlobster.com>
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
@ -22,7 +22,6 @@ THE SOFTWARE.
|
||||||
package funkwhale
|
package funkwhale
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
@ -55,10 +54,15 @@ func NewClient(serverURL string, token string) Client {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Client) GetHistoryListenings(ctx context.Context, user string, page int, perPage int) (result ListeningsResult, err error) {
|
func (c Client) GetHistoryListenings(user string, page int, perPage int) (result ListeningsResult, err error) {
|
||||||
const path = "/api/v1/history/listenings"
|
const path = "/api/v1/history/listenings"
|
||||||
response, err := c.buildListRequest(ctx, page, perPage).
|
response, err := c.HTTPClient.R().
|
||||||
SetQueryParam("username", user).
|
SetQueryParams(map[string]string{
|
||||||
|
"username": user,
|
||||||
|
"page": strconv.Itoa(page),
|
||||||
|
"page_size": strconv.Itoa(perPage),
|
||||||
|
"ordering": "-creation_date",
|
||||||
|
}).
|
||||||
SetResult(&result).
|
SetResult(&result).
|
||||||
Get(path)
|
Get(path)
|
||||||
|
|
||||||
|
@ -69,25 +73,20 @@ func (c Client) GetHistoryListenings(ctx context.Context, user string, page int,
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Client) GetFavoriteTracks(ctx context.Context, page int, perPage int) (result FavoriteTracksResult, err error) {
|
func (c Client) GetFavoriteTracks(page int, perPage int) (result FavoriteTracksResult, err error) {
|
||||||
const path = "/api/v1/favorites/tracks"
|
const path = "/api/v1/favorites/tracks"
|
||||||
response, err := c.buildListRequest(ctx, page, perPage).
|
response, err := c.HTTPClient.R().
|
||||||
SetResult(&result).
|
|
||||||
Get(path)
|
|
||||||
|
|
||||||
if !response.IsSuccess() {
|
|
||||||
err = errors.New(response.String())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c Client) buildListRequest(ctx context.Context, page int, perPage int) *resty.Request {
|
|
||||||
return c.HTTPClient.R().
|
|
||||||
SetContext(ctx).
|
|
||||||
SetQueryParams(map[string]string{
|
SetQueryParams(map[string]string{
|
||||||
"page": strconv.Itoa(page),
|
"page": strconv.Itoa(page),
|
||||||
"page_size": strconv.Itoa(perPage),
|
"page_size": strconv.Itoa(perPage),
|
||||||
"ordering": "-creation_date",
|
"ordering": "-creation_date",
|
||||||
})
|
}).
|
||||||
|
SetResult(&result).
|
||||||
|
Get(path)
|
||||||
|
|
||||||
|
if !response.IsSuccess() {
|
||||||
|
err = errors.New(response.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright © 2023-2025 Philipp Wolfer <phw@uploadedlobster.com>
|
Copyright © 2023 Philipp Wolfer <phw@uploadedlobster.com>
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
@ -22,7 +22,6 @@ THE SOFTWARE.
|
||||||
package funkwhale_test
|
package funkwhale_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -50,8 +49,7 @@ func TestGetHistoryListenings(t *testing.T) {
|
||||||
"https://funkwhale.example.com/api/v1/history/listenings",
|
"https://funkwhale.example.com/api/v1/history/listenings",
|
||||||
"testdata/listenings.json")
|
"testdata/listenings.json")
|
||||||
|
|
||||||
ctx := context.Background()
|
result, err := client.GetHistoryListenings("outsidecontext", 0, 2)
|
||||||
result, err := client.GetHistoryListenings(ctx, "outsidecontext", 0, 2)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
@ -75,8 +73,7 @@ func TestGetFavoriteTracks(t *testing.T) {
|
||||||
"https://funkwhale.example.com/api/v1/favorites/tracks",
|
"https://funkwhale.example.com/api/v1/favorites/tracks",
|
||||||
"testdata/favorite-tracks.json")
|
"testdata/favorite-tracks.json")
|
||||||
|
|
||||||
ctx := context.Background()
|
result, err := client.GetFavoriteTracks(0, 2)
|
||||||
result, err := client.GetFavoriteTracks(ctx, 0, 2)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
|
@ -17,7 +17,6 @@ Scotty. If not, see <https://www.gnu.org/licenses/>.
|
||||||
package funkwhale
|
package funkwhale
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -61,7 +60,7 @@ func (b *FunkwhaleApiBackend) InitConfig(config *config.ServiceConfig) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *FunkwhaleApiBackend) ExportListens(ctx context.Context, oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) {
|
func (b *FunkwhaleApiBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) {
|
||||||
page := 1
|
page := 1
|
||||||
perPage := MaxItemsPerGet
|
perPage := MaxItemsPerGet
|
||||||
|
|
||||||
|
@ -75,7 +74,7 @@ func (b *FunkwhaleApiBackend) ExportListens(ctx context.Context, oldestTimestamp
|
||||||
|
|
||||||
out:
|
out:
|
||||||
for {
|
for {
|
||||||
result, err := b.client.GetHistoryListenings(ctx, b.username, page, perPage)
|
result, err := b.client.GetHistoryListenings(b.username, page, perPage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.Export.Abort()
|
p.Export.Abort()
|
||||||
progress <- p
|
progress <- p
|
||||||
|
@ -118,7 +117,7 @@ out:
|
||||||
results <- models.ListensResult{Items: listens}
|
results <- models.ListensResult{Items: listens}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *FunkwhaleApiBackend) ExportLoves(ctx context.Context, oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.TransferProgress) {
|
func (b *FunkwhaleApiBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.TransferProgress) {
|
||||||
page := 1
|
page := 1
|
||||||
perPage := MaxItemsPerGet
|
perPage := MaxItemsPerGet
|
||||||
|
|
||||||
|
@ -132,7 +131,7 @@ func (b *FunkwhaleApiBackend) ExportLoves(ctx context.Context, oldestTimestamp t
|
||||||
|
|
||||||
out:
|
out:
|
||||||
for {
|
for {
|
||||||
result, err := b.client.GetFavoriteTracks(ctx, page, perPage)
|
result, err := b.client.GetFavoriteTracks(page, perPage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.Export.Abort()
|
p.Export.Abort()
|
||||||
progress <- p
|
progress <- p
|
||||||
|
|
|
@ -18,7 +18,6 @@ Scotty. If not, see <https://www.gnu.org/licenses/>.
|
||||||
package backends
|
package backends
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"go.uploadedlobster.com/scotty/internal/models"
|
"go.uploadedlobster.com/scotty/internal/models"
|
||||||
|
@ -26,8 +25,8 @@ import (
|
||||||
|
|
||||||
type ImportProcessor[T models.ListensResult | models.LovesResult] interface {
|
type ImportProcessor[T models.ListensResult | models.LovesResult] interface {
|
||||||
ImportBackend() models.ImportBackend
|
ImportBackend() models.ImportBackend
|
||||||
Process(ctx context.Context, wg *sync.WaitGroup, results chan T, out chan models.ImportResult, progress chan models.TransferProgress)
|
Process(wg *sync.WaitGroup, results chan T, out chan models.ImportResult, progress chan models.TransferProgress)
|
||||||
Import(ctx context.Context, export T, result models.ImportResult, out chan models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error)
|
Import(export T, result models.ImportResult, out chan models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListensImportProcessor struct {
|
type ListensImportProcessor struct {
|
||||||
|
@ -38,11 +37,11 @@ func (p ListensImportProcessor) ImportBackend() models.ImportBackend {
|
||||||
return p.Backend
|
return p.Backend
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p ListensImportProcessor) Process(ctx context.Context, wg *sync.WaitGroup, results chan models.ListensResult, out chan models.ImportResult, progress chan models.TransferProgress) {
|
func (p ListensImportProcessor) Process(wg *sync.WaitGroup, results chan models.ListensResult, out chan models.ImportResult, progress chan models.TransferProgress) {
|
||||||
process(ctx, wg, p, results, out, progress)
|
process(wg, p, results, out, progress)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p ListensImportProcessor) Import(ctx context.Context, export models.ListensResult, result models.ImportResult, out chan models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) {
|
func (p ListensImportProcessor) Import(export models.ListensResult, result models.ImportResult, out chan models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) {
|
||||||
if export.Error != nil {
|
if export.Error != nil {
|
||||||
return result, export.Error
|
return result, export.Error
|
||||||
}
|
}
|
||||||
|
@ -52,7 +51,7 @@ func (p ListensImportProcessor) Import(ctx context.Context, export models.Listen
|
||||||
} else {
|
} else {
|
||||||
result.TotalCount += len(export.Items)
|
result.TotalCount += len(export.Items)
|
||||||
}
|
}
|
||||||
importResult, err := p.Backend.ImportListens(ctx, export, result, progress)
|
importResult, err := p.Backend.ImportListens(export, result, progress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return importResult, err
|
return importResult, err
|
||||||
}
|
}
|
||||||
|
@ -67,11 +66,11 @@ func (p LovesImportProcessor) ImportBackend() models.ImportBackend {
|
||||||
return p.Backend
|
return p.Backend
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p LovesImportProcessor) Process(ctx context.Context, wg *sync.WaitGroup, results chan models.LovesResult, out chan models.ImportResult, progress chan models.TransferProgress) {
|
func (p LovesImportProcessor) Process(wg *sync.WaitGroup, results chan models.LovesResult, out chan models.ImportResult, progress chan models.TransferProgress) {
|
||||||
process(ctx, wg, p, results, out, progress)
|
process(wg, p, results, out, progress)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p LovesImportProcessor) Import(ctx context.Context, export models.LovesResult, result models.ImportResult, out chan models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) {
|
func (p LovesImportProcessor) Import(export models.LovesResult, result models.ImportResult, out chan models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) {
|
||||||
if export.Error != nil {
|
if export.Error != nil {
|
||||||
return result, export.Error
|
return result, export.Error
|
||||||
}
|
}
|
||||||
|
@ -81,19 +80,14 @@ func (p LovesImportProcessor) Import(ctx context.Context, export models.LovesRes
|
||||||
} else {
|
} else {
|
||||||
result.TotalCount += len(export.Items)
|
result.TotalCount += len(export.Items)
|
||||||
}
|
}
|
||||||
importResult, err := p.Backend.ImportLoves(ctx, export, result, progress)
|
importResult, err := p.Backend.ImportLoves(export, result, progress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return importResult, err
|
return importResult, err
|
||||||
}
|
}
|
||||||
return importResult, nil
|
return importResult, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func process[R models.LovesResult | models.ListensResult, P ImportProcessor[R]](
|
func process[R models.LovesResult | models.ListensResult, P ImportProcessor[R]](wg *sync.WaitGroup, processor P, results chan R, out chan models.ImportResult, progress chan models.TransferProgress) {
|
||||||
ctx context.Context, wg *sync.WaitGroup,
|
|
||||||
processor P, results chan R,
|
|
||||||
out chan models.ImportResult,
|
|
||||||
progress chan models.TransferProgress,
|
|
||||||
) {
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
defer close(out)
|
defer close(out)
|
||||||
|
@ -106,13 +100,7 @@ func process[R models.LovesResult | models.ListensResult, P ImportProcessor[R]](
|
||||||
}
|
}
|
||||||
|
|
||||||
for exportResult := range results {
|
for exportResult := range results {
|
||||||
if err := ctx.Err(); err != nil {
|
importResult, err := processor.Import(exportResult, result, out, progress)
|
||||||
processor.ImportBackend().FinishImport()
|
|
||||||
out <- handleError(result, err, progress)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
importResult, err := processor.Import(ctx, exportResult, result, out, progress)
|
|
||||||
result.Update(importResult)
|
result.Update(importResult)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
processor.ImportBackend().FinishImport()
|
processor.ImportBackend().FinishImport()
|
||||||
|
|
|
@ -18,7 +18,6 @@ Scotty. If not, see <https://www.gnu.org/licenses/>.
|
||||||
package jspf
|
package jspf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
|
@ -94,7 +93,7 @@ func (b *JSPFBackend) FinishImport() error {
|
||||||
return b.writeJSPF()
|
return b.writeJSPF()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *JSPFBackend) ExportListens(ctx context.Context, oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) {
|
func (b *JSPFBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) {
|
||||||
err := b.readJSPF()
|
err := b.readJSPF()
|
||||||
p := models.TransferProgress{
|
p := models.TransferProgress{
|
||||||
Export: &models.Progress{},
|
Export: &models.Progress{},
|
||||||
|
@ -121,12 +120,8 @@ func (b *JSPFBackend) ExportListens(ctx context.Context, oldestTimestamp time.Ti
|
||||||
results <- models.ListensResult{Items: listens}
|
results <- models.ListensResult{Items: listens}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *JSPFBackend) ImportListens(ctx context.Context, export models.ListensResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) {
|
func (b *JSPFBackend) ImportListens(export models.ListensResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) {
|
||||||
for _, listen := range export.Items {
|
for _, listen := range export.Items {
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return importResult, err
|
|
||||||
}
|
|
||||||
|
|
||||||
track := listenAsTrack(listen)
|
track := listenAsTrack(listen)
|
||||||
b.playlist.Tracks = append(b.playlist.Tracks, track)
|
b.playlist.Tracks = append(b.playlist.Tracks, track)
|
||||||
importResult.ImportCount += 1
|
importResult.ImportCount += 1
|
||||||
|
@ -137,7 +132,7 @@ func (b *JSPFBackend) ImportListens(ctx context.Context, export models.ListensRe
|
||||||
return importResult, nil
|
return importResult, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *JSPFBackend) ExportLoves(ctx context.Context, oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.TransferProgress) {
|
func (b *JSPFBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.TransferProgress) {
|
||||||
err := b.readJSPF()
|
err := b.readJSPF()
|
||||||
p := models.TransferProgress{
|
p := models.TransferProgress{
|
||||||
Export: &models.Progress{},
|
Export: &models.Progress{},
|
||||||
|
@ -164,12 +159,8 @@ func (b *JSPFBackend) ExportLoves(ctx context.Context, oldestTimestamp time.Time
|
||||||
results <- models.LovesResult{Items: loves}
|
results <- models.LovesResult{Items: loves}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *JSPFBackend) ImportLoves(ctx context.Context, export models.LovesResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) {
|
func (b *JSPFBackend) ImportLoves(export models.LovesResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) {
|
||||||
for _, love := range export.Items {
|
for _, love := range export.Items {
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return importResult, err
|
|
||||||
}
|
|
||||||
|
|
||||||
track := loveAsTrack(love)
|
track := loveAsTrack(love)
|
||||||
b.playlist.Tracks = append(b.playlist.Tracks, track)
|
b.playlist.Tracks = append(b.playlist.Tracks, track)
|
||||||
importResult.ImportCount += 1
|
importResult.ImportCount += 1
|
||||||
|
|
|
@ -16,7 +16,6 @@ Scotty. If not, see <https://www.gnu.org/licenses/>.
|
||||||
package lastfm
|
package lastfm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"sort"
|
"sort"
|
||||||
|
@ -89,7 +88,7 @@ func (b *LastfmApiBackend) OAuth2Setup(token oauth2.TokenSource) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *LastfmApiBackend) ExportListens(ctx context.Context, oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) {
|
func (b *LastfmApiBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) {
|
||||||
page := MaxPage
|
page := MaxPage
|
||||||
minTime := oldestTimestamp
|
minTime := oldestTimestamp
|
||||||
perPage := MaxItemsPerGet
|
perPage := MaxItemsPerGet
|
||||||
|
@ -103,13 +102,6 @@ func (b *LastfmApiBackend) ExportListens(ctx context.Context, oldestTimestamp ti
|
||||||
|
|
||||||
out:
|
out:
|
||||||
for page > 0 {
|
for page > 0 {
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
results <- models.ListensResult{Error: err}
|
|
||||||
p.Export.Abort()
|
|
||||||
progress <- p
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
args := lastfm.P{
|
args := lastfm.P{
|
||||||
"user": b.username,
|
"user": b.username,
|
||||||
"limit": MaxListensPerGet,
|
"limit": MaxListensPerGet,
|
||||||
|
@ -190,13 +182,9 @@ out:
|
||||||
progress <- p
|
progress <- p
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *LastfmApiBackend) ImportListens(ctx context.Context, export models.ListensResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) {
|
func (b *LastfmApiBackend) ImportListens(export models.ListensResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) {
|
||||||
total := len(export.Items)
|
total := len(export.Items)
|
||||||
for i := 0; i < total; i += MaxListensPerSubmission {
|
for i := 0; i < total; i += MaxListensPerSubmission {
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return importResult, err
|
|
||||||
}
|
|
||||||
|
|
||||||
listens := export.Items[i:min(i+MaxListensPerSubmission, total)]
|
listens := export.Items[i:min(i+MaxListensPerSubmission, total)]
|
||||||
count := len(listens)
|
count := len(listens)
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
|
@ -270,7 +258,7 @@ func (b *LastfmApiBackend) ImportListens(ctx context.Context, export models.List
|
||||||
return importResult, nil
|
return importResult, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *LastfmApiBackend) ExportLoves(ctx context.Context, oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.TransferProgress) {
|
func (b *LastfmApiBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.TransferProgress) {
|
||||||
// Choose a high offset, we attempt to search the loves backwards starting
|
// Choose a high offset, we attempt to search the loves backwards starting
|
||||||
// at the oldest one.
|
// at the oldest one.
|
||||||
page := 1
|
page := 1
|
||||||
|
@ -286,13 +274,6 @@ func (b *LastfmApiBackend) ExportLoves(ctx context.Context, oldestTimestamp time
|
||||||
|
|
||||||
out:
|
out:
|
||||||
for {
|
for {
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
results <- models.LovesResult{Error: err}
|
|
||||||
p.Export.Abort()
|
|
||||||
progress <- p
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := b.client.User.GetLovedTracks(lastfm.P{
|
result, err := b.client.User.GetLovedTracks(lastfm.P{
|
||||||
"user": b.username,
|
"user": b.username,
|
||||||
"limit": MaxItemsPerGet,
|
"limit": MaxItemsPerGet,
|
||||||
|
@ -354,12 +335,8 @@ out:
|
||||||
results <- models.LovesResult{Items: loves, Total: totalCount}
|
results <- models.LovesResult{Items: loves, Total: totalCount}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *LastfmApiBackend) ImportLoves(ctx context.Context, export models.LovesResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) {
|
func (b *LastfmApiBackend) ImportLoves(export models.LovesResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) {
|
||||||
for _, love := range export.Items {
|
for _, love := range export.Items {
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return importResult, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err := b.client.Track.Love(lastfm.P{
|
err := b.client.Track.Love(lastfm.P{
|
||||||
"track": love.TrackName,
|
"track": love.TrackName,
|
||||||
"artist": love.ArtistName(),
|
"artist": love.ArtistName(),
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright © 2023-2025 Philipp Wolfer <phw@uploadedlobster.com>
|
Copyright © 2023 Philipp Wolfer <phw@uploadedlobster.com>
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
@ -22,7 +22,6 @@ THE SOFTWARE.
|
||||||
package listenbrainz
|
package listenbrainz
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
@ -61,11 +60,10 @@ func NewClient(token string) Client {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Client) GetListens(ctx context.Context, user string, maxTime time.Time, minTime time.Time) (result GetListensResult, err error) {
|
func (c Client) GetListens(user string, maxTime time.Time, minTime time.Time) (result GetListensResult, err error) {
|
||||||
const path = "/user/{username}/listens"
|
const path = "/user/{username}/listens"
|
||||||
errorResult := ErrorResult{}
|
errorResult := ErrorResult{}
|
||||||
response, err := c.HTTPClient.R().
|
response, err := c.HTTPClient.R().
|
||||||
SetContext(ctx).
|
|
||||||
SetPathParam("username", user).
|
SetPathParam("username", user).
|
||||||
SetQueryParams(map[string]string{
|
SetQueryParams(map[string]string{
|
||||||
"max_ts": strconv.FormatInt(maxTime.Unix(), 10),
|
"max_ts": strconv.FormatInt(maxTime.Unix(), 10),
|
||||||
|
@ -83,11 +81,10 @@ func (c Client) GetListens(ctx context.Context, user string, maxTime time.Time,
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Client) SubmitListens(ctx context.Context, listens ListenSubmission) (result StatusResult, err error) {
|
func (c Client) SubmitListens(listens ListenSubmission) (result StatusResult, err error) {
|
||||||
const path = "/submit-listens"
|
const path = "/submit-listens"
|
||||||
errorResult := ErrorResult{}
|
errorResult := ErrorResult{}
|
||||||
response, err := c.HTTPClient.R().
|
response, err := c.HTTPClient.R().
|
||||||
SetContext(ctx).
|
|
||||||
SetBody(listens).
|
SetBody(listens).
|
||||||
SetResult(&result).
|
SetResult(&result).
|
||||||
SetError(&errorResult).
|
SetError(&errorResult).
|
||||||
|
@ -100,11 +97,10 @@ func (c Client) SubmitListens(ctx context.Context, listens ListenSubmission) (re
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Client) GetFeedback(ctx context.Context, user string, status int, offset int) (result GetFeedbackResult, err error) {
|
func (c Client) GetFeedback(user string, status int, offset int) (result GetFeedbackResult, err error) {
|
||||||
const path = "/feedback/user/{username}/get-feedback"
|
const path = "/feedback/user/{username}/get-feedback"
|
||||||
errorResult := ErrorResult{}
|
errorResult := ErrorResult{}
|
||||||
response, err := c.HTTPClient.R().
|
response, err := c.HTTPClient.R().
|
||||||
SetContext(ctx).
|
|
||||||
SetPathParam("username", user).
|
SetPathParam("username", user).
|
||||||
SetQueryParams(map[string]string{
|
SetQueryParams(map[string]string{
|
||||||
"status": strconv.Itoa(status),
|
"status": strconv.Itoa(status),
|
||||||
|
@ -123,11 +119,10 @@ func (c Client) GetFeedback(ctx context.Context, user string, status int, offset
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Client) SendFeedback(ctx context.Context, feedback Feedback) (result StatusResult, err error) {
|
func (c Client) SendFeedback(feedback Feedback) (result StatusResult, err error) {
|
||||||
const path = "/feedback/recording-feedback"
|
const path = "/feedback/recording-feedback"
|
||||||
errorResult := ErrorResult{}
|
errorResult := ErrorResult{}
|
||||||
response, err := c.HTTPClient.R().
|
response, err := c.HTTPClient.R().
|
||||||
SetContext(ctx).
|
|
||||||
SetBody(feedback).
|
SetBody(feedback).
|
||||||
SetResult(&result).
|
SetResult(&result).
|
||||||
SetError(&errorResult).
|
SetError(&errorResult).
|
||||||
|
@ -140,11 +135,10 @@ func (c Client) SendFeedback(ctx context.Context, feedback Feedback) (result Sta
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Client) Lookup(ctx context.Context, recordingName string, artistName string) (result LookupResult, err error) {
|
func (c Client) Lookup(recordingName string, artistName string) (result LookupResult, err error) {
|
||||||
const path = "/metadata/lookup"
|
const path = "/metadata/lookup"
|
||||||
errorResult := ErrorResult{}
|
errorResult := ErrorResult{}
|
||||||
response, err := c.HTTPClient.R().
|
response, err := c.HTTPClient.R().
|
||||||
SetContext(ctx).
|
|
||||||
SetQueryParams(map[string]string{
|
SetQueryParams(map[string]string{
|
||||||
"recording_name": recordingName,
|
"recording_name": recordingName,
|
||||||
"artist_name": artistName,
|
"artist_name": artistName,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright © 2023-2025 Philipp Wolfer <phw@uploadedlobster.com>
|
Copyright © 2023 Philipp Wolfer <phw@uploadedlobster.com>
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
@ -22,7 +22,6 @@ THE SOFTWARE.
|
||||||
package listenbrainz_test
|
package listenbrainz_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
@ -50,9 +49,7 @@ func TestGetListens(t *testing.T) {
|
||||||
"https://api.listenbrainz.org/1/user/outsidecontext/listens",
|
"https://api.listenbrainz.org/1/user/outsidecontext/listens",
|
||||||
"testdata/listens.json")
|
"testdata/listens.json")
|
||||||
|
|
||||||
ctx := context.Background()
|
result, err := client.GetListens("outsidecontext", time.Now(), time.Now().Add(-2*time.Hour))
|
||||||
result, err := client.GetListens(ctx, "outsidecontext",
|
|
||||||
time.Now(), time.Now().Add(-2*time.Hour))
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
@ -95,8 +92,8 @@ func TestSubmitListens(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
ctx := context.Background()
|
result, err := client.SubmitListens(listens)
|
||||||
result, err := client.SubmitListens(ctx, listens)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "ok", result.Status)
|
assert.Equal(t, "ok", result.Status)
|
||||||
}
|
}
|
||||||
|
@ -110,8 +107,7 @@ func TestGetFeedback(t *testing.T) {
|
||||||
"https://api.listenbrainz.org/1/feedback/user/outsidecontext/get-feedback",
|
"https://api.listenbrainz.org/1/feedback/user/outsidecontext/get-feedback",
|
||||||
"testdata/feedback.json")
|
"testdata/feedback.json")
|
||||||
|
|
||||||
ctx := context.Background()
|
result, err := client.GetFeedback("outsidecontext", 1, 3)
|
||||||
result, err := client.GetFeedback(ctx, "outsidecontext", 1, 0)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
@ -139,8 +135,7 @@ func TestSendFeedback(t *testing.T) {
|
||||||
RecordingMBID: "c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12",
|
RecordingMBID: "c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12",
|
||||||
Score: 1,
|
Score: 1,
|
||||||
}
|
}
|
||||||
ctx := context.Background()
|
result, err := client.SendFeedback(feedback)
|
||||||
result, err := client.SendFeedback(ctx, feedback)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "ok", result.Status)
|
assert.Equal(t, "ok", result.Status)
|
||||||
|
@ -154,8 +149,7 @@ func TestLookup(t *testing.T) {
|
||||||
"https://api.listenbrainz.org/1/metadata/lookup",
|
"https://api.listenbrainz.org/1/metadata/lookup",
|
||||||
"testdata/lookup.json")
|
"testdata/lookup.json")
|
||||||
|
|
||||||
ctx := context.Background()
|
result, err := client.Lookup("Paradise Lost", "Say Just Words")
|
||||||
result, err := client.Lookup(ctx, "Paradise Lost", "Say Just Words")
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
|
@ -17,7 +17,6 @@ Scotty. If not, see <https://www.gnu.org/licenses/>.
|
||||||
package listenbrainz
|
package listenbrainz
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
@ -73,7 +72,7 @@ func (b *ListenBrainzApiBackend) InitConfig(config *config.ServiceConfig) error
|
||||||
func (b *ListenBrainzApiBackend) StartImport() error { return nil }
|
func (b *ListenBrainzApiBackend) StartImport() error { return nil }
|
||||||
func (b *ListenBrainzApiBackend) FinishImport() error { return nil }
|
func (b *ListenBrainzApiBackend) FinishImport() error { return nil }
|
||||||
|
|
||||||
func (b *ListenBrainzApiBackend) ExportListens(ctx context.Context, oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) {
|
func (b *ListenBrainzApiBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) {
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
minTime := oldestTimestamp
|
minTime := oldestTimestamp
|
||||||
if minTime.Unix() < 1 {
|
if minTime.Unix() < 1 {
|
||||||
|
@ -88,7 +87,7 @@ func (b *ListenBrainzApiBackend) ExportListens(ctx context.Context, oldestTimest
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
result, err := b.client.GetListens(ctx, b.username, time.Now(), minTime)
|
result, err := b.client.GetListens(b.username, time.Now(), minTime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.Export.Abort()
|
p.Export.Abort()
|
||||||
progress <- p
|
progress <- p
|
||||||
|
@ -135,7 +134,7 @@ func (b *ListenBrainzApiBackend) ExportListens(ctx context.Context, oldestTimest
|
||||||
progress <- p
|
progress <- p
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *ListenBrainzApiBackend) ImportListens(ctx context.Context, export models.ListensResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) {
|
func (b *ListenBrainzApiBackend) ImportListens(export models.ListensResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) {
|
||||||
total := len(export.Items)
|
total := len(export.Items)
|
||||||
p := models.TransferProgress{}.FromImportResult(importResult, false)
|
p := models.TransferProgress{}.FromImportResult(importResult, false)
|
||||||
for i := 0; i < total; i += MaxListensPerRequest {
|
for i := 0; i < total; i += MaxListensPerRequest {
|
||||||
|
@ -152,7 +151,7 @@ func (b *ListenBrainzApiBackend) ImportListens(ctx context.Context, export model
|
||||||
|
|
||||||
for _, l := range listens {
|
for _, l := range listens {
|
||||||
if b.checkDuplicates {
|
if b.checkDuplicates {
|
||||||
isDupe, err := b.checkDuplicateListen(ctx, l)
|
isDupe, err := b.checkDuplicateListen(l)
|
||||||
p.Import.Elapsed += 1
|
p.Import.Elapsed += 1
|
||||||
progress <- p
|
progress <- p
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -183,7 +182,7 @@ func (b *ListenBrainzApiBackend) ImportListens(ctx context.Context, export model
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(submission.Payload) > 0 {
|
if len(submission.Payload) > 0 {
|
||||||
_, err := b.client.SubmitListens(ctx, submission)
|
_, err := b.client.SubmitListens(submission)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return importResult, err
|
return importResult, err
|
||||||
}
|
}
|
||||||
|
@ -199,13 +198,13 @@ func (b *ListenBrainzApiBackend) ImportListens(ctx context.Context, export model
|
||||||
return importResult, nil
|
return importResult, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *ListenBrainzApiBackend) ExportLoves(ctx context.Context, oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.TransferProgress) {
|
func (b *ListenBrainzApiBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.TransferProgress) {
|
||||||
exportChan := make(chan models.LovesResult)
|
exportChan := make(chan models.LovesResult)
|
||||||
p := models.TransferProgress{
|
p := models.TransferProgress{
|
||||||
Export: &models.Progress{},
|
Export: &models.Progress{},
|
||||||
}
|
}
|
||||||
|
|
||||||
go b.exportLoves(ctx, oldestTimestamp, exportChan)
|
go b.exportLoves(oldestTimestamp, exportChan)
|
||||||
for existingLoves := range exportChan {
|
for existingLoves := range exportChan {
|
||||||
if existingLoves.Error != nil {
|
if existingLoves.Error != nil {
|
||||||
p.Export.Abort()
|
p.Export.Abort()
|
||||||
|
@ -225,14 +224,14 @@ func (b *ListenBrainzApiBackend) ExportLoves(ctx context.Context, oldestTimestam
|
||||||
progress <- p
|
progress <- p
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *ListenBrainzApiBackend) exportLoves(ctx context.Context, oldestTimestamp time.Time, results chan models.LovesResult) {
|
func (b *ListenBrainzApiBackend) exportLoves(oldestTimestamp time.Time, results chan models.LovesResult) {
|
||||||
offset := 0
|
offset := 0
|
||||||
defer close(results)
|
defer close(results)
|
||||||
loves := make(models.LovesList, 0, 2*MaxItemsPerGet)
|
loves := make(models.LovesList, 0, 2*MaxItemsPerGet)
|
||||||
|
|
||||||
out:
|
out:
|
||||||
for {
|
for {
|
||||||
result, err := b.client.GetFeedback(ctx, b.username, 1, offset)
|
result, err := b.client.GetFeedback(b.username, 1, offset)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
results <- models.LovesResult{Error: err}
|
results <- models.LovesResult{Error: err}
|
||||||
return
|
return
|
||||||
|
@ -248,7 +247,7 @@ out:
|
||||||
// longer available and might have been merged. Try fetching details
|
// longer available and might have been merged. Try fetching details
|
||||||
// from MusicBrainz.
|
// from MusicBrainz.
|
||||||
if feedback.TrackMetadata == nil {
|
if feedback.TrackMetadata == nil {
|
||||||
track, err := b.lookupRecording(ctx, feedback.RecordingMBID)
|
track, err := b.lookupRecording(feedback.RecordingMBID)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
feedback.TrackMetadata = track
|
feedback.TrackMetadata = track
|
||||||
}
|
}
|
||||||
|
@ -272,10 +271,10 @@ out:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *ListenBrainzApiBackend) ImportLoves(ctx context.Context, export models.LovesResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) {
|
func (b *ListenBrainzApiBackend) ImportLoves(export models.LovesResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) {
|
||||||
if len(b.existingMBIDs) == 0 {
|
if len(b.existingMBIDs) == 0 {
|
||||||
existingLovesChan := make(chan models.LovesResult)
|
existingLovesChan := make(chan models.LovesResult)
|
||||||
go b.exportLoves(ctx, time.Unix(0, 0), existingLovesChan)
|
go b.exportLoves(time.Unix(0, 0), existingLovesChan)
|
||||||
|
|
||||||
// TODO: Store MBIDs directly
|
// TODO: Store MBIDs directly
|
||||||
b.existingMBIDs = make(map[mbtypes.MBID]bool, MaxItemsPerGet)
|
b.existingMBIDs = make(map[mbtypes.MBID]bool, MaxItemsPerGet)
|
||||||
|
@ -304,7 +303,7 @@ func (b *ListenBrainzApiBackend) ImportLoves(ctx context.Context, export models.
|
||||||
}
|
}
|
||||||
|
|
||||||
if recordingMBID == "" {
|
if recordingMBID == "" {
|
||||||
lookup, err := b.client.Lookup(ctx, love.TrackName, love.ArtistName())
|
lookup, err := b.client.Lookup(love.TrackName, love.ArtistName())
|
||||||
if err == nil {
|
if err == nil {
|
||||||
recordingMBID = lookup.RecordingMBID
|
recordingMBID = lookup.RecordingMBID
|
||||||
}
|
}
|
||||||
|
@ -316,7 +315,7 @@ func (b *ListenBrainzApiBackend) ImportLoves(ctx context.Context, export models.
|
||||||
if b.existingMBIDs[recordingMBID] {
|
if b.existingMBIDs[recordingMBID] {
|
||||||
ok = true
|
ok = true
|
||||||
} else {
|
} else {
|
||||||
resp, err := b.client.SendFeedback(ctx, Feedback{
|
resp, err := b.client.SendFeedback(Feedback{
|
||||||
RecordingMBID: recordingMBID,
|
RecordingMBID: recordingMBID,
|
||||||
Score: 1,
|
Score: 1,
|
||||||
})
|
})
|
||||||
|
@ -352,7 +351,7 @@ var defaultDuration = time.Duration(3 * time.Minute)
|
||||||
|
|
||||||
const trackSimilarityThreshold = 0.9
|
const trackSimilarityThreshold = 0.9
|
||||||
|
|
||||||
func (b *ListenBrainzApiBackend) checkDuplicateListen(ctx context.Context, listen models.Listen) (bool, error) {
|
func (b *ListenBrainzApiBackend) checkDuplicateListen(listen models.Listen) (bool, error) {
|
||||||
// Find listens
|
// Find listens
|
||||||
duration := listen.Duration
|
duration := listen.Duration
|
||||||
if duration == 0 {
|
if duration == 0 {
|
||||||
|
@ -360,7 +359,7 @@ func (b *ListenBrainzApiBackend) checkDuplicateListen(ctx context.Context, liste
|
||||||
}
|
}
|
||||||
minTime := listen.ListenedAt.Add(-duration)
|
minTime := listen.ListenedAt.Add(-duration)
|
||||||
maxTime := listen.ListenedAt.Add(duration)
|
maxTime := listen.ListenedAt.Add(duration)
|
||||||
candidates, err := b.client.GetListens(ctx, b.username, maxTime, minTime)
|
candidates, err := b.client.GetListens(b.username, maxTime, minTime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
@ -375,11 +374,11 @@ func (b *ListenBrainzApiBackend) checkDuplicateListen(ctx context.Context, liste
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *ListenBrainzApiBackend) lookupRecording(ctx context.Context, mbid mbtypes.MBID) (*Track, error) {
|
func (b *ListenBrainzApiBackend) lookupRecording(mbid mbtypes.MBID) (*Track, error) {
|
||||||
filter := musicbrainzws2.IncludesFilter{
|
filter := musicbrainzws2.IncludesFilter{
|
||||||
Includes: []string{"artist-credits"},
|
Includes: []string{"artist-credits"},
|
||||||
}
|
}
|
||||||
recording, err := b.mbClient.LookupRecording(ctx, mbid, filter)
|
recording, err := b.mbClient.LookupRecording(mbid, filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright © 2023-2025 Philipp Wolfer <phw@uploadedlobster.com>
|
Copyright © 2023 Philipp Wolfer <phw@uploadedlobster.com>
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
@ -22,7 +22,6 @@ THE SOFTWARE.
|
||||||
package maloja
|
package maloja
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
@ -49,10 +48,9 @@ func NewClient(serverURL string, token string) Client {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Client) GetScrobbles(ctx context.Context, page int, perPage int) (result GetScrobblesResult, err error) {
|
func (c Client) GetScrobbles(page int, perPage int) (result GetScrobblesResult, err error) {
|
||||||
const path = "/apis/mlj_1/scrobbles"
|
const path = "/apis/mlj_1/scrobbles"
|
||||||
response, err := c.HTTPClient.R().
|
response, err := c.HTTPClient.R().
|
||||||
SetContext(ctx).
|
|
||||||
SetQueryParams(map[string]string{
|
SetQueryParams(map[string]string{
|
||||||
"page": strconv.Itoa(page),
|
"page": strconv.Itoa(page),
|
||||||
"perpage": strconv.Itoa(perPage),
|
"perpage": strconv.Itoa(perPage),
|
||||||
|
@ -67,11 +65,10 @@ func (c Client) GetScrobbles(ctx context.Context, page int, perPage int) (result
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Client) NewScrobble(ctx context.Context, scrobble NewScrobble) (result NewScrobbleResult, err error) {
|
func (c Client) NewScrobble(scrobble NewScrobble) (result NewScrobbleResult, err error) {
|
||||||
const path = "/apis/mlj_1/newscrobble"
|
const path = "/apis/mlj_1/newscrobble"
|
||||||
scrobble.Key = c.token
|
scrobble.Key = c.token
|
||||||
response, err := c.HTTPClient.R().
|
response, err := c.HTTPClient.R().
|
||||||
SetContext(ctx).
|
|
||||||
SetBody(scrobble).
|
SetBody(scrobble).
|
||||||
SetResult(&result).
|
SetResult(&result).
|
||||||
Post(path)
|
Post(path)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright © 2023-2025 Philipp Wolfer <phw@uploadedlobster.com>
|
Copyright © 2023 Philipp Wolfer <phw@uploadedlobster.com>
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
@ -22,7 +22,6 @@ THE SOFTWARE.
|
||||||
package maloja_test
|
package maloja_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -49,8 +48,7 @@ func TestGetScrobbles(t *testing.T) {
|
||||||
"https://maloja.example.com/apis/mlj_1/scrobbles",
|
"https://maloja.example.com/apis/mlj_1/scrobbles",
|
||||||
"testdata/scrobbles.json")
|
"testdata/scrobbles.json")
|
||||||
|
|
||||||
ctx := context.Background()
|
result, err := client.GetScrobbles(0, 2)
|
||||||
result, err := client.GetScrobbles(ctx, 0, 2)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
@ -71,13 +69,12 @@ func TestNewScrobble(t *testing.T) {
|
||||||
url := server + "/apis/mlj_1/newscrobble"
|
url := server + "/apis/mlj_1/newscrobble"
|
||||||
httpmock.RegisterResponder("POST", url, responder)
|
httpmock.RegisterResponder("POST", url, responder)
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
scrobble := maloja.NewScrobble{
|
scrobble := maloja.NewScrobble{
|
||||||
Title: "Oweynagat",
|
Title: "Oweynagat",
|
||||||
Artist: "Dool",
|
Artist: "Dool",
|
||||||
Time: 1699574369,
|
Time: 1699574369,
|
||||||
}
|
}
|
||||||
result, err := client.NewScrobble(ctx, scrobble)
|
result, err := client.NewScrobble(scrobble)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "success", result.Status)
|
assert.Equal(t, "success", result.Status)
|
||||||
|
|
|
@ -17,7 +17,6 @@ Scotty. If not, see <https://www.gnu.org/licenses/>.
|
||||||
package maloja
|
package maloja
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -64,7 +63,7 @@ func (b *MalojaApiBackend) InitConfig(config *config.ServiceConfig) error {
|
||||||
func (b *MalojaApiBackend) StartImport() error { return nil }
|
func (b *MalojaApiBackend) StartImport() error { return nil }
|
||||||
func (b *MalojaApiBackend) FinishImport() error { return nil }
|
func (b *MalojaApiBackend) FinishImport() error { return nil }
|
||||||
|
|
||||||
func (b *MalojaApiBackend) ExportListens(ctx context.Context, oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) {
|
func (b *MalojaApiBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) {
|
||||||
page := 0
|
page := 0
|
||||||
perPage := MaxItemsPerGet
|
perPage := MaxItemsPerGet
|
||||||
|
|
||||||
|
@ -78,7 +77,7 @@ func (b *MalojaApiBackend) ExportListens(ctx context.Context, oldestTimestamp ti
|
||||||
|
|
||||||
out:
|
out:
|
||||||
for {
|
for {
|
||||||
result, err := b.client.GetScrobbles(ctx, page, perPage)
|
result, err := b.client.GetScrobbles(page, perPage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.Export.Abort()
|
p.Export.Abort()
|
||||||
progress <- p
|
progress <- p
|
||||||
|
@ -112,7 +111,7 @@ out:
|
||||||
results <- models.ListensResult{Items: listens}
|
results <- models.ListensResult{Items: listens}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *MalojaApiBackend) ImportListens(ctx context.Context, export models.ListensResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) {
|
func (b *MalojaApiBackend) ImportListens(export models.ListensResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) {
|
||||||
p := models.TransferProgress{}.FromImportResult(importResult, false)
|
p := models.TransferProgress{}.FromImportResult(importResult, false)
|
||||||
for _, listen := range export.Items {
|
for _, listen := range export.Items {
|
||||||
scrobble := NewScrobble{
|
scrobble := NewScrobble{
|
||||||
|
@ -125,7 +124,7 @@ func (b *MalojaApiBackend) ImportListens(ctx context.Context, export models.List
|
||||||
Nofix: b.nofix,
|
Nofix: b.nofix,
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := b.client.NewScrobble(ctx, scrobble)
|
resp, err := b.client.NewScrobble(scrobble)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return importResult, err
|
return importResult, err
|
||||||
} else if resp.Status != "success" {
|
} else if resp.Status != "success" {
|
||||||
|
|
|
@ -17,7 +17,7 @@ Scotty. If not, see <https://www.gnu.org/licenses/>.
|
||||||
package scrobblerlog
|
package scrobblerlog
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"bufio"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
|
@ -105,7 +105,8 @@ func (b *ScrobblerLogBackend) StartImport() error {
|
||||||
b.append = false
|
b.append = false
|
||||||
} else {
|
} else {
|
||||||
// Verify existing file is a scrobbler log
|
// Verify existing file is a scrobbler log
|
||||||
if err = b.log.ReadHeader(file); err != nil {
|
reader := bufio.NewReader(file)
|
||||||
|
if err = b.log.ReadHeader(reader); err != nil {
|
||||||
file.Close()
|
file.Close()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -130,7 +131,7 @@ func (b *ScrobblerLogBackend) FinishImport() error {
|
||||||
return b.file.Close()
|
return b.file.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *ScrobblerLogBackend) ExportListens(ctx context.Context, oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) {
|
func (b *ScrobblerLogBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) {
|
||||||
file, err := os.Open(b.filePath)
|
file, err := os.Open(b.filePath)
|
||||||
p := models.TransferProgress{
|
p := models.TransferProgress{
|
||||||
Export: &models.Progress{},
|
Export: &models.Progress{},
|
||||||
|
@ -167,7 +168,7 @@ func (b *ScrobblerLogBackend) ExportListens(ctx context.Context, oldestTimestamp
|
||||||
results <- models.ListensResult{Items: listens}
|
results <- models.ListensResult{Items: listens}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *ScrobblerLogBackend) ImportListens(ctx context.Context, export models.ListensResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) {
|
func (b *ScrobblerLogBackend) ImportListens(export models.ListensResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) {
|
||||||
records := make([]scrobblerlog.Record, len(export.Items))
|
records := make([]scrobblerlog.Record, len(export.Items))
|
||||||
for i, listen := range export.Items {
|
for i, listen := range export.Items {
|
||||||
records[i] = listenToRecord(listen)
|
records[i] = listenToRecord(listen)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright © 2023-2025 Philipp Wolfer <phw@uploadedlobster.com>
|
Copyright © 2023 Philipp Wolfer <phw@uploadedlobster.com>
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
@ -59,18 +59,17 @@ func NewClient(token oauth2.TokenSource) Client {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Client) RecentlyPlayedAfter(ctx context.Context, after time.Time, limit int) (RecentlyPlayedResult, error) {
|
func (c Client) RecentlyPlayedAfter(after time.Time, limit int) (RecentlyPlayedResult, error) {
|
||||||
return c.recentlyPlayed(ctx, &after, nil, limit)
|
return c.recentlyPlayed(&after, nil, limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Client) RecentlyPlayedBefore(ctx context.Context, before time.Time, limit int) (RecentlyPlayedResult, error) {
|
func (c Client) RecentlyPlayedBefore(before time.Time, limit int) (RecentlyPlayedResult, error) {
|
||||||
return c.recentlyPlayed(ctx, nil, &before, limit)
|
return c.recentlyPlayed(nil, &before, limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Client) recentlyPlayed(ctx context.Context, after *time.Time, before *time.Time, limit int) (result RecentlyPlayedResult, err error) {
|
func (c Client) recentlyPlayed(after *time.Time, before *time.Time, limit int) (result RecentlyPlayedResult, err error) {
|
||||||
const path = "/me/player/recently-played"
|
const path = "/me/player/recently-played"
|
||||||
request := c.HTTPClient.R().
|
request := c.HTTPClient.R().
|
||||||
SetContext(ctx).
|
|
||||||
SetQueryParam("limit", strconv.Itoa(limit)).
|
SetQueryParam("limit", strconv.Itoa(limit)).
|
||||||
SetResult(&result)
|
SetResult(&result)
|
||||||
if after != nil {
|
if after != nil {
|
||||||
|
@ -86,10 +85,9 @@ func (c Client) recentlyPlayed(ctx context.Context, after *time.Time, before *ti
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Client) UserTracks(ctx context.Context, offset int, limit int) (result TracksResult, err error) {
|
func (c Client) UserTracks(offset int, limit int) (result TracksResult, err error) {
|
||||||
const path = "/me/tracks"
|
const path = "/me/tracks"
|
||||||
response, err := c.HTTPClient.R().
|
response, err := c.HTTPClient.R().
|
||||||
SetContext(ctx).
|
|
||||||
SetQueryParams(map[string]string{
|
SetQueryParams(map[string]string{
|
||||||
"offset": strconv.Itoa(offset),
|
"offset": strconv.Itoa(offset),
|
||||||
"limit": strconv.Itoa(limit),
|
"limit": strconv.Itoa(limit),
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright © 2023-2025 Philipp Wolfer <phw@uploadedlobster.com>
|
Copyright © 2023 Philipp Wolfer <phw@uploadedlobster.com>
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
@ -22,7 +22,6 @@ THE SOFTWARE.
|
||||||
package spotify_test
|
package spotify_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
@ -48,8 +47,7 @@ func TestRecentlyPlayedAfter(t *testing.T) {
|
||||||
"https://api.spotify.com/v1/me/player/recently-played",
|
"https://api.spotify.com/v1/me/player/recently-played",
|
||||||
"testdata/recently-played.json")
|
"testdata/recently-played.json")
|
||||||
|
|
||||||
ctx := context.Background()
|
result, err := client.RecentlyPlayedAfter(time.Now(), 3)
|
||||||
result, err := client.RecentlyPlayedAfter(ctx, time.Now(), 3)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
@ -69,8 +67,7 @@ func TestGetUserTracks(t *testing.T) {
|
||||||
"https://api.spotify.com/v1/me/tracks",
|
"https://api.spotify.com/v1/me/tracks",
|
||||||
"testdata/user-tracks.json")
|
"testdata/user-tracks.json")
|
||||||
|
|
||||||
ctx := context.Background()
|
result, err := client.UserTracks(0, 2)
|
||||||
result, err := client.UserTracks(ctx, 0, 2)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
|
@ -18,7 +18,6 @@ Scotty. If not, see <https://www.gnu.org/licenses/>.
|
||||||
package spotify
|
package spotify
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"math"
|
"math"
|
||||||
"net/url"
|
"net/url"
|
||||||
"sort"
|
"sort"
|
||||||
|
@ -96,7 +95,7 @@ func (b *SpotifyApiBackend) OAuth2Setup(token oauth2.TokenSource) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *SpotifyApiBackend) ExportListens(ctx context.Context, oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) {
|
func (b *SpotifyApiBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) {
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
minTime := oldestTimestamp
|
minTime := oldestTimestamp
|
||||||
|
|
||||||
|
@ -108,7 +107,7 @@ func (b *SpotifyApiBackend) ExportListens(ctx context.Context, oldestTimestamp t
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
result, err := b.client.RecentlyPlayedAfter(ctx, minTime, MaxItemsPerGet)
|
result, err := b.client.RecentlyPlayedAfter(minTime, MaxItemsPerGet)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.Export.Abort()
|
p.Export.Abort()
|
||||||
progress <- p
|
progress <- p
|
||||||
|
@ -163,7 +162,7 @@ func (b *SpotifyApiBackend) ExportListens(ctx context.Context, oldestTimestamp t
|
||||||
progress <- p
|
progress <- p
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *SpotifyApiBackend) ExportLoves(ctx context.Context, oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.TransferProgress) {
|
func (b *SpotifyApiBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.TransferProgress) {
|
||||||
// Choose a high offset, we attempt to search the loves backwards starting
|
// Choose a high offset, we attempt to search the loves backwards starting
|
||||||
// at the oldest one.
|
// at the oldest one.
|
||||||
offset := math.MaxInt32
|
offset := math.MaxInt32
|
||||||
|
@ -179,7 +178,7 @@ func (b *SpotifyApiBackend) ExportLoves(ctx context.Context, oldestTimestamp tim
|
||||||
|
|
||||||
out:
|
out:
|
||||||
for {
|
for {
|
||||||
result, err := b.client.UserTracks(ctx, offset, perPage)
|
result, err := b.client.UserTracks(offset, perPage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.Export.Abort()
|
p.Export.Abort()
|
||||||
progress <- p
|
progress <- p
|
||||||
|
|
|
@ -18,7 +18,6 @@ Scotty. If not, see <https://www.gnu.org/licenses/>.
|
||||||
package spotifyhistory
|
package spotifyhistory
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
@ -73,7 +72,7 @@ func (b *SpotifyHistoryBackend) InitConfig(config *config.ServiceConfig) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *SpotifyHistoryBackend) ExportListens(ctx context.Context, oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) {
|
func (b *SpotifyHistoryBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) {
|
||||||
files, err := filepath.Glob(path.Join(b.dirPath, historyFileGlob))
|
files, err := filepath.Glob(path.Join(b.dirPath, historyFileGlob))
|
||||||
p := models.TransferProgress{
|
p := models.TransferProgress{
|
||||||
Export: &models.Progress{},
|
Export: &models.Progress{},
|
||||||
|
@ -90,18 +89,11 @@ func (b *SpotifyHistoryBackend) ExportListens(ctx context.Context, oldestTimesta
|
||||||
fileCount := int64(len(files))
|
fileCount := int64(len(files))
|
||||||
p.Export.Total = fileCount
|
p.Export.Total = fileCount
|
||||||
for i, filePath := range files {
|
for i, filePath := range files {
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
results <- models.ListensResult{Error: err}
|
|
||||||
p.Export.Abort()
|
|
||||||
progress <- p
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
history, err := readHistoryFile(filePath)
|
history, err := readHistoryFile(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
results <- models.ListensResult{Error: err}
|
|
||||||
p.Export.Abort()
|
p.Export.Abort()
|
||||||
progress <- p
|
progress <- p
|
||||||
|
results <- models.ListensResult{Error: err}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
listens := history.AsListenList(ListenListOptions{
|
listens := history.AsListenList(ListenListOptions{
|
||||||
|
|
|
@ -17,7 +17,6 @@ Scotty. If not, see <https://www.gnu.org/licenses/>.
|
||||||
package subsonic
|
package subsonic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
@ -64,7 +63,7 @@ func (b *SubsonicApiBackend) InitConfig(config *config.ServiceConfig) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *SubsonicApiBackend) ExportLoves(ctx context.Context, oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.TransferProgress) {
|
func (b *SubsonicApiBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.TransferProgress) {
|
||||||
err := b.client.Authenticate(b.password)
|
err := b.client.Authenticate(b.password)
|
||||||
p := models.TransferProgress{
|
p := models.TransferProgress{
|
||||||
Export: &models.Progress{},
|
Export: &models.Progress{},
|
||||||
|
|
|
@ -18,7 +18,6 @@ Scotty. If not, see <https://www.gnu.org/licenses/>.
|
||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -40,10 +39,9 @@ type progressBarUpdater struct {
|
||||||
importedItems int
|
importedItems int
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupProgressBars(ctx context.Context, updateChan chan models.TransferProgress) progressBarUpdater {
|
func setupProgressBars(updateChan chan models.TransferProgress) progressBarUpdater {
|
||||||
wg := &sync.WaitGroup{}
|
wg := &sync.WaitGroup{}
|
||||||
p := mpb.NewWithContext(
|
p := mpb.New(
|
||||||
ctx,
|
|
||||||
mpb.WithWaitGroup(wg),
|
mpb.WithWaitGroup(wg),
|
||||||
mpb.WithOutput(color.Output),
|
mpb.WithOutput(color.Output),
|
||||||
// mpb.WithWidth(64),
|
// mpb.WithWidth(64),
|
||||||
|
|
|
@ -16,7 +16,6 @@ Scotty. If not, see <https://www.gnu.org/licenses/>.
|
||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -110,32 +109,20 @@ func (c *TransferCmd[E, I, R]) Transfer(exp backends.ExportProcessor[R], imp bac
|
||||||
}
|
}
|
||||||
printTimestamp("From timestamp: %v (%v)", timestamp)
|
printTimestamp("From timestamp: %v (%v)", timestamp)
|
||||||
|
|
||||||
// Use a context with cancel to abort the transfer
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
|
|
||||||
// Prepare progress bars
|
// Prepare progress bars
|
||||||
progressChan := make(chan models.TransferProgress)
|
progressChan := make(chan models.TransferProgress)
|
||||||
progress := setupProgressBars(ctx, progressChan)
|
progress := setupProgressBars(progressChan)
|
||||||
|
|
||||||
wg := &sync.WaitGroup{}
|
wg := &sync.WaitGroup{}
|
||||||
|
|
||||||
// Export from source
|
// Export from source
|
||||||
exportChan := make(chan R, 1000)
|
exportChan := make(chan R, 1000)
|
||||||
go exp.Process(ctx, wg, timestamp, exportChan, progressChan)
|
go exp.Process(wg, timestamp, exportChan, progressChan)
|
||||||
|
|
||||||
// Import into target
|
// Import into target
|
||||||
resultChan := make(chan models.ImportResult)
|
resultChan := make(chan models.ImportResult)
|
||||||
go imp.Process(ctx, wg, exportChan, resultChan, progressChan)
|
go imp.Process(wg, exportChan, resultChan, progressChan)
|
||||||
result := <-resultChan
|
result := <-resultChan
|
||||||
|
|
||||||
// If the import has errored, the context can be cancelled immediately
|
|
||||||
if result.Error != nil {
|
|
||||||
cancel()
|
|
||||||
} else {
|
|
||||||
defer cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for all goroutines to finish
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
progress.close()
|
progress.close()
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,6 @@ Scotty. If not, see <https://www.gnu.org/licenses/>.
|
||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
// "go.uploadedlobster.com/scotty/internal/auth"
|
// "go.uploadedlobster.com/scotty/internal/auth"
|
||||||
|
@ -56,7 +55,7 @@ type ListensExport interface {
|
||||||
// Returns a list of all listens newer then oldestTimestamp.
|
// Returns a list of all listens newer then oldestTimestamp.
|
||||||
// The returned list of listens is supposed to be ordered by the
|
// The returned list of listens is supposed to be ordered by the
|
||||||
// Listen.ListenedAt timestamp, with the oldest entry first.
|
// Listen.ListenedAt timestamp, with the oldest entry first.
|
||||||
ExportListens(ctx context.Context, oldestTimestamp time.Time, results chan ListensResult, progress chan TransferProgress)
|
ExportListens(oldestTimestamp time.Time, results chan ListensResult, progress chan TransferProgress)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Must be implemented by services supporting the import of listens.
|
// Must be implemented by services supporting the import of listens.
|
||||||
|
@ -64,7 +63,7 @@ type ListensImport interface {
|
||||||
ImportBackend
|
ImportBackend
|
||||||
|
|
||||||
// Imports the given list of listens.
|
// Imports the given list of listens.
|
||||||
ImportListens(ctx context.Context, export ListensResult, importResult ImportResult, progress chan TransferProgress) (ImportResult, error)
|
ImportListens(export ListensResult, importResult ImportResult, progress chan TransferProgress) (ImportResult, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Must be implemented by services supporting the export of loves.
|
// Must be implemented by services supporting the export of loves.
|
||||||
|
@ -74,7 +73,7 @@ type LovesExport interface {
|
||||||
// Returns a list of all loves newer then oldestTimestamp.
|
// Returns a list of all loves newer then oldestTimestamp.
|
||||||
// The returned list of listens is supposed to be ordered by the
|
// The returned list of listens is supposed to be ordered by the
|
||||||
// Love.Created timestamp, with the oldest entry first.
|
// Love.Created timestamp, with the oldest entry first.
|
||||||
ExportLoves(ctx context.Context, oldestTimestamp time.Time, results chan LovesResult, progress chan TransferProgress)
|
ExportLoves(oldestTimestamp time.Time, results chan LovesResult, progress chan TransferProgress)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Must be implemented by services supporting the import of loves.
|
// Must be implemented by services supporting the import of loves.
|
||||||
|
@ -82,5 +81,5 @@ type LovesImport interface {
|
||||||
ImportBackend
|
ImportBackend
|
||||||
|
|
||||||
// Imports the given list of loves.
|
// Imports the given list of loves.
|
||||||
ImportLoves(ctx context.Context, export LovesResult, importResult ImportResult, progress chan TransferProgress) (ImportResult, error)
|
ImportLoves(export LovesResult, importResult ImportResult, progress chan TransferProgress) (ImportResult, error)
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,7 +94,7 @@ func (l *ScrobblerLog) Parse(data io.Reader, ignoreSkipped bool) error {
|
||||||
l.Records = make([]Record, 0)
|
l.Records = make([]Record, 0)
|
||||||
|
|
||||||
reader := bufio.NewReader(data)
|
reader := bufio.NewReader(data)
|
||||||
err := l.readHeader(reader)
|
err := l.ReadHeader(reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -173,11 +173,7 @@ func (l *ScrobblerLog) Append(data io.Writer, records []Record) (lastTimestamp t
|
||||||
// Parses just the header of a scrobbler log file from the given reader.
|
// Parses just the header of a scrobbler log file from the given reader.
|
||||||
//
|
//
|
||||||
// This function sets [ScrobblerLog.TZ] and [ScrobblerLog.Client].
|
// This function sets [ScrobblerLog.TZ] and [ScrobblerLog.Client].
|
||||||
func (l *ScrobblerLog) ReadHeader(reader io.Reader) error {
|
func (l *ScrobblerLog) ReadHeader(reader *bufio.Reader) error {
|
||||||
return l.readHeader(bufio.NewReader(reader))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *ScrobblerLog) readHeader(reader *bufio.Reader) error {
|
|
||||||
// Skip header
|
// Skip header
|
||||||
for i := 0; i < 3; i++ {
|
for i := 0; i < 3; i++ {
|
||||||
line, _, err := reader.ReadLine()
|
line, _, err := reader.ReadLine()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue