From 536fae6a46ffdb6b76a6034fdbab9ffe14e4b81f Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Thu, 22 May 2025 11:51:23 +0200 Subject: [PATCH 1/8] ScrobblerLog.ReadHeader now accepts io.Reader --- internal/backends/scrobblerlog/scrobblerlog.go | 4 +--- pkg/scrobblerlog/parser.go | 8 ++++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/internal/backends/scrobblerlog/scrobblerlog.go b/internal/backends/scrobblerlog/scrobblerlog.go index db4e349..6454b7b 100644 --- a/internal/backends/scrobblerlog/scrobblerlog.go +++ b/internal/backends/scrobblerlog/scrobblerlog.go @@ -17,7 +17,6 @@ Scotty. If not, see . package scrobblerlog import ( - "bufio" "fmt" "os" "sort" @@ -105,8 +104,7 @@ func (b *ScrobblerLogBackend) StartImport() error { b.append = false } else { // Verify existing file is a scrobbler log - reader := bufio.NewReader(file) - if err = b.log.ReadHeader(reader); err != nil { + if err = b.log.ReadHeader(file); err != nil { file.Close() return err } diff --git a/pkg/scrobblerlog/parser.go b/pkg/scrobblerlog/parser.go index 6b9d1ba..8bad56d 100644 --- a/pkg/scrobblerlog/parser.go +++ b/pkg/scrobblerlog/parser.go @@ -94,7 +94,7 @@ func (l *ScrobblerLog) Parse(data io.Reader, ignoreSkipped bool) error { l.Records = make([]Record, 0) reader := bufio.NewReader(data) - err := l.ReadHeader(reader) + err := l.readHeader(reader) if err != nil { return err } @@ -173,7 +173,11 @@ func (l *ScrobblerLog) Append(data io.Writer, records []Record) (lastTimestamp t // Parses just the header of a scrobbler log file from the given reader. // // This function sets [ScrobblerLog.TZ] and [ScrobblerLog.Client]. -func (l *ScrobblerLog) ReadHeader(reader *bufio.Reader) error { +func (l *ScrobblerLog) ReadHeader(reader io.Reader) error { + return l.readHeader(bufio.NewReader(reader)) +} + +func (l *ScrobblerLog) readHeader(reader *bufio.Reader) error { // Skip header for i := 0; i < 3; i++ { line, _, err := reader.ReadLine() From 3b545a0fd6548bdcc89c5e096d013ba35eacd4fa Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Thu, 22 May 2025 08:48:37 +0200 Subject: [PATCH 2/8] Prepare using a context for export / import This will allow cancelling the export if the import fails before the export finished. For now the context isn't passed on to the actual export functions, hence there is not yet any cancellation happening. --- internal/backends/export.go | 7 ++++--- internal/backends/import.go | 35 ++++++++++++++++++++++++----------- internal/cli/transfer.go | 11 +++++++++-- 3 files changed, 37 insertions(+), 16 deletions(-) diff --git a/internal/backends/export.go b/internal/backends/export.go index 54daafb..0deebc6 100644 --- a/internal/backends/export.go +++ b/internal/backends/export.go @@ -16,6 +16,7 @@ Scotty. If not, see . package backends import ( + "context" "sync" "time" @@ -24,7 +25,7 @@ import ( type ExportProcessor[T models.ListensResult | models.LovesResult] interface { ExportBackend() models.Backend - Process(wg *sync.WaitGroup, oldestTimestamp time.Time, results chan T, progress chan models.TransferProgress) + Process(ctx context.Context, wg *sync.WaitGroup, oldestTimestamp time.Time, results chan T, progress chan models.TransferProgress) } type ListensExportProcessor struct { @@ -35,7 +36,7 @@ func (p ListensExportProcessor) ExportBackend() models.Backend { return p.Backend } -func (p ListensExportProcessor) Process(wg *sync.WaitGroup, oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) { +func (p ListensExportProcessor) Process(ctx context.Context, wg *sync.WaitGroup, oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) { wg.Add(1) defer wg.Done() defer close(results) @@ -50,7 +51,7 @@ func (p LovesExportProcessor) ExportBackend() models.Backend { return p.Backend } -func (p LovesExportProcessor) Process(wg *sync.WaitGroup, oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.TransferProgress) { +func (p LovesExportProcessor) Process(ctx context.Context, wg *sync.WaitGroup, oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.TransferProgress) { wg.Add(1) defer wg.Done() defer close(results) diff --git a/internal/backends/import.go b/internal/backends/import.go index 0a2e341..e7006bd 100644 --- a/internal/backends/import.go +++ b/internal/backends/import.go @@ -18,6 +18,7 @@ Scotty. If not, see . package backends import ( + "context" "sync" "go.uploadedlobster.com/scotty/internal/models" @@ -25,7 +26,7 @@ import ( type ImportProcessor[T models.ListensResult | models.LovesResult] interface { ImportBackend() models.ImportBackend - Process(wg *sync.WaitGroup, results chan T, out chan models.ImportResult, progress chan models.TransferProgress) + Process(ctx context.Context, wg *sync.WaitGroup, results chan T, out chan models.ImportResult, progress chan models.TransferProgress) Import(export T, result models.ImportResult, out chan models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) } @@ -37,8 +38,8 @@ func (p ListensImportProcessor) ImportBackend() models.ImportBackend { return p.Backend } -func (p ListensImportProcessor) Process(wg *sync.WaitGroup, results chan models.ListensResult, out chan models.ImportResult, progress chan models.TransferProgress) { - process(wg, p, results, out, progress) +func (p ListensImportProcessor) Process(ctx context.Context, wg *sync.WaitGroup, results chan models.ListensResult, out chan models.ImportResult, progress chan models.TransferProgress) { + process(ctx, wg, p, results, out, progress) } func (p ListensImportProcessor) Import(export models.ListensResult, result models.ImportResult, out chan models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) { @@ -66,8 +67,8 @@ func (p LovesImportProcessor) ImportBackend() models.ImportBackend { return p.Backend } -func (p LovesImportProcessor) Process(wg *sync.WaitGroup, results chan models.LovesResult, out chan models.ImportResult, progress chan models.TransferProgress) { - process(wg, p, results, out, progress) +func (p LovesImportProcessor) Process(ctx context.Context, wg *sync.WaitGroup, results chan models.LovesResult, out chan models.ImportResult, progress chan models.TransferProgress) { + process(ctx, wg, p, results, out, progress) } func (p LovesImportProcessor) Import(export models.LovesResult, result models.ImportResult, out chan models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) { @@ -87,7 +88,12 @@ func (p LovesImportProcessor) Import(export models.LovesResult, result models.Im return importResult, nil } -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) { +func process[R models.LovesResult | models.ListensResult, P ImportProcessor[R]]( + ctx context.Context, wg *sync.WaitGroup, + processor P, results chan R, + out chan models.ImportResult, + progress chan models.TransferProgress, +) { wg.Add(1) defer wg.Done() defer close(out) @@ -100,14 +106,21 @@ func process[R models.LovesResult | models.ListensResult, P ImportProcessor[R]]( } for exportResult := range results { - importResult, err := processor.Import(exportResult, result, out, progress) - result.Update(importResult) - if err != nil { + select { + case <-ctx.Done(): processor.ImportBackend().FinishImport() - out <- handleError(result, err, progress) + out <- handleError(result, ctx.Err(), progress) return + default: + importResult, err := processor.Import(exportResult, result, out, progress) + result.Update(importResult) + if err != nil { + processor.ImportBackend().FinishImport() + out <- handleError(result, err, progress) + return + } + progress <- p.FromImportResult(result, false) } - progress <- p.FromImportResult(result, false) } if err := processor.ImportBackend().FinishImport(); err != nil { diff --git a/internal/cli/transfer.go b/internal/cli/transfer.go index 62dd079..79be3f0 100644 --- a/internal/cli/transfer.go +++ b/internal/cli/transfer.go @@ -16,6 +16,7 @@ Scotty. If not, see . package cli import ( + "context" "errors" "fmt" "strconv" @@ -113,16 +114,22 @@ func (c *TransferCmd[E, I, R]) Transfer(exp backends.ExportProcessor[R], imp bac progressChan := make(chan models.TransferProgress) progress := setupProgressBars(progressChan) + ctx, cancel := context.WithCancel(context.Background()) wg := &sync.WaitGroup{} // Export from source exportChan := make(chan R, 1000) - go exp.Process(wg, timestamp, exportChan, progressChan) + go exp.Process(ctx, wg, timestamp, exportChan, progressChan) // Import into target resultChan := make(chan models.ImportResult) - go imp.Process(wg, exportChan, resultChan, progressChan) + go imp.Process(ctx, wg, exportChan, resultChan, progressChan) result := <-resultChan + + // Once import is done, the context can be cancelled + cancel() + + // Wait for all goroutines to finish wg.Wait() progress.close() From adfe3f5771f933bcb6012d24aa53a436370be677 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Thu, 22 May 2025 08:52:52 +0200 Subject: [PATCH 3/8] Use the transfer context also for the progress bars --- internal/cli/progress.go | 6 ++++-- internal/cli/transfer.go | 14 ++++++++++---- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/internal/cli/progress.go b/internal/cli/progress.go index e93ec18..db862a1 100644 --- a/internal/cli/progress.go +++ b/internal/cli/progress.go @@ -18,6 +18,7 @@ Scotty. If not, see . package cli import ( + "context" "sync" "time" @@ -39,9 +40,10 @@ type progressBarUpdater struct { importedItems int } -func setupProgressBars(updateChan chan models.TransferProgress) progressBarUpdater { +func setupProgressBars(ctx context.Context, updateChan chan models.TransferProgress) progressBarUpdater { wg := &sync.WaitGroup{} - p := mpb.New( + p := mpb.NewWithContext( + ctx, mpb.WithWaitGroup(wg), mpb.WithOutput(color.Output), // mpb.WithWidth(64), diff --git a/internal/cli/transfer.go b/internal/cli/transfer.go index 79be3f0..3aabb4b 100644 --- a/internal/cli/transfer.go +++ b/internal/cli/transfer.go @@ -110,11 +110,13 @@ func (c *TransferCmd[E, I, R]) Transfer(exp backends.ExportProcessor[R], imp bac } printTimestamp("From timestamp: %v (%v)", timestamp) + // Use a context with cancel to abort the transfer + ctx, cancel := context.WithCancel(context.Background()) + // Prepare progress bars progressChan := make(chan models.TransferProgress) - progress := setupProgressBars(progressChan) + progress := setupProgressBars(ctx, progressChan) - ctx, cancel := context.WithCancel(context.Background()) wg := &sync.WaitGroup{} // Export from source @@ -126,8 +128,12 @@ func (c *TransferCmd[E, I, R]) Transfer(exp backends.ExportProcessor[R], imp bac go imp.Process(ctx, wg, exportChan, resultChan, progressChan) result := <-resultChan - // Once import is done, the context can be cancelled - cancel() + // 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() From d1642b7f1f675a0eb72e2e60aaa2049cd8b55b87 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Thu, 22 May 2025 09:22:05 +0200 Subject: [PATCH 4/8] Make web service clients context aware --- internal/backends/deezer/client.go | 14 ++++---- internal/backends/deezer/client_test.go | 9 +++-- internal/backends/deezer/deezer.go | 9 +++-- internal/backends/funkwhale/client.go | 33 ++++++++++--------- internal/backends/funkwhale/client_test.go | 9 +++-- internal/backends/funkwhale/funkwhale.go | 7 ++-- internal/backends/listenbrainz/client.go | 18 ++++++---- internal/backends/listenbrainz/client_test.go | 20 +++++++---- .../backends/listenbrainz/listenbrainz.go | 27 ++++++++------- internal/backends/maloja/client.go | 9 +++-- internal/backends/maloja/client_test.go | 9 +++-- internal/backends/maloja/maloja.go | 8 +++-- internal/backends/spotify/client.go | 16 +++++---- internal/backends/spotify/client_test.go | 9 +++-- internal/backends/spotify/spotify.go | 7 ++-- 15 files changed, 128 insertions(+), 76 deletions(-) diff --git a/internal/backends/deezer/client.go b/internal/backends/deezer/client.go index 05264ae..3ab2b6c 100644 --- a/internal/backends/deezer/client.go +++ b/internal/backends/deezer/client.go @@ -1,5 +1,5 @@ /* -Copyright © 2023 Philipp Wolfer +Copyright © 2023-2025 Philipp Wolfer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -23,6 +23,7 @@ THE SOFTWARE. package deezer import ( + "context" "errors" "strconv" @@ -52,14 +53,14 @@ func NewClient(token oauth2.TokenSource) Client { } } -func (c Client) UserHistory(offset int, limit int) (result HistoryResult, err error) { +func (c Client) UserHistory(ctx context.Context, offset int, limit int) (result HistoryResult, err error) { const path = "/user/me/history" - return listRequest[HistoryResult](c, path, offset, limit) + return listRequest[HistoryResult](ctx, c, path, offset, limit) } -func (c Client) UserTracks(offset int, limit int) (TracksResult, error) { +func (c Client) UserTracks(ctx context.Context, offset int, limit int) (TracksResult, error) { const path = "/user/me/tracks" - return listRequest[TracksResult](c, path, offset, limit) + return listRequest[TracksResult](ctx, c, path, offset, limit) } func (c Client) setToken(req *resty.Request) error { @@ -72,8 +73,9 @@ func (c Client) setToken(req *resty.Request) error { return nil } -func listRequest[T Result](c Client, path string, offset int, limit int) (result T, err error) { +func listRequest[T Result](ctx context.Context, c Client, path string, offset int, limit int) (result T, err error) { request := c.HTTPClient.R(). + SetContext(ctx). SetQueryParams(map[string]string{ "index": strconv.Itoa(offset), "limit": strconv.Itoa(limit), diff --git a/internal/backends/deezer/client_test.go b/internal/backends/deezer/client_test.go index c90b01a..8b61804 100644 --- a/internal/backends/deezer/client_test.go +++ b/internal/backends/deezer/client_test.go @@ -1,5 +1,5 @@ /* -Copyright © 2023 Philipp Wolfer +Copyright © 2023-2025 Philipp Wolfer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -23,6 +23,7 @@ THE SOFTWARE. package deezer_test import ( + "context" "net/http" "testing" @@ -48,7 +49,8 @@ func TestGetUserHistory(t *testing.T) { "https://api.deezer.com/user/me/history", "testdata/user-history.json") - result, err := client.UserHistory(0, 2) + ctx := context.Background() + result, err := client.UserHistory(ctx, 0, 2) require.NoError(t, err) assert := assert.New(t) @@ -69,7 +71,8 @@ func TestGetUserTracks(t *testing.T) { "https://api.deezer.com/user/me/tracks", "testdata/user-tracks.json") - result, err := client.UserTracks(0, 2) + ctx := context.Background() + result, err := client.UserTracks(ctx, 0, 2) require.NoError(t, err) assert := assert.New(t) diff --git a/internal/backends/deezer/deezer.go b/internal/backends/deezer/deezer.go index 2209769..f3e3d37 100644 --- a/internal/backends/deezer/deezer.go +++ b/internal/backends/deezer/deezer.go @@ -16,6 +16,7 @@ Scotty. If not, see . package deezer import ( + "context" "fmt" "math" "net/url" @@ -78,6 +79,8 @@ func (b *DeezerApiBackend) OAuth2Setup(token oauth2.TokenSource) error { } func (b *DeezerApiBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) { + ctx := context.TODO() + // Choose a high offset, we attempt to search the loves backwards starting // at the oldest one. offset := math.MaxInt32 @@ -96,7 +99,7 @@ func (b *DeezerApiBackend) ExportListens(oldestTimestamp time.Time, results chan out: for { - result, err := b.client.UserHistory(offset, perPage) + result, err := b.client.UserHistory(ctx, offset, perPage) if err != nil { p.Export.Abort() progress <- p @@ -154,6 +157,8 @@ out: } func (b *DeezerApiBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.TransferProgress) { + ctx := context.TODO() + // Choose a high offset, we attempt to search the loves backwards starting // at the oldest one. offset := math.MaxInt32 @@ -168,7 +173,7 @@ func (b *DeezerApiBackend) ExportLoves(oldestTimestamp time.Time, results chan m out: for { - result, err := b.client.UserTracks(offset, perPage) + result, err := b.client.UserTracks(ctx, offset, perPage) if err != nil { p.Export.Abort() progress <- p diff --git a/internal/backends/funkwhale/client.go b/internal/backends/funkwhale/client.go index c231c94..3471612 100644 --- a/internal/backends/funkwhale/client.go +++ b/internal/backends/funkwhale/client.go @@ -1,5 +1,5 @@ /* -Copyright © 2023 Philipp Wolfer +Copyright © 2023-2025 Philipp Wolfer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -22,6 +22,7 @@ THE SOFTWARE. package funkwhale import ( + "context" "errors" "strconv" @@ -54,15 +55,10 @@ func NewClient(serverURL string, token string) Client { } } -func (c Client) GetHistoryListenings(user string, page int, perPage int) (result ListeningsResult, err error) { +func (c Client) GetHistoryListenings(ctx context.Context, user string, page int, perPage int) (result ListeningsResult, err error) { const path = "/api/v1/history/listenings" - response, err := c.HTTPClient.R(). - SetQueryParams(map[string]string{ - "username": user, - "page": strconv.Itoa(page), - "page_size": strconv.Itoa(perPage), - "ordering": "-creation_date", - }). + response, err := c.buildListRequest(ctx, page, perPage). + SetQueryParam("username", user). SetResult(&result). Get(path) @@ -73,14 +69,9 @@ func (c Client) GetHistoryListenings(user string, page int, perPage int) (result return } -func (c Client) GetFavoriteTracks(page int, perPage int) (result FavoriteTracksResult, err error) { +func (c Client) GetFavoriteTracks(ctx context.Context, page int, perPage int) (result FavoriteTracksResult, err error) { const path = "/api/v1/favorites/tracks" - response, err := c.HTTPClient.R(). - SetQueryParams(map[string]string{ - "page": strconv.Itoa(page), - "page_size": strconv.Itoa(perPage), - "ordering": "-creation_date", - }). + response, err := c.buildListRequest(ctx, page, perPage). SetResult(&result). Get(path) @@ -90,3 +81,13 @@ func (c Client) GetFavoriteTracks(page int, perPage int) (result FavoriteTracksR } return } + +func (c Client) buildListRequest(ctx context.Context, page int, perPage int) *resty.Request { + return c.HTTPClient.R(). + SetContext(ctx). + SetQueryParams(map[string]string{ + "page": strconv.Itoa(page), + "page_size": strconv.Itoa(perPage), + "ordering": "-creation_date", + }) +} diff --git a/internal/backends/funkwhale/client_test.go b/internal/backends/funkwhale/client_test.go index e850a4d..d6b04e0 100644 --- a/internal/backends/funkwhale/client_test.go +++ b/internal/backends/funkwhale/client_test.go @@ -1,5 +1,5 @@ /* -Copyright © 2023 Philipp Wolfer +Copyright © 2023-2025 Philipp Wolfer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -22,6 +22,7 @@ THE SOFTWARE. package funkwhale_test import ( + "context" "net/http" "testing" @@ -49,7 +50,8 @@ func TestGetHistoryListenings(t *testing.T) { "https://funkwhale.example.com/api/v1/history/listenings", "testdata/listenings.json") - result, err := client.GetHistoryListenings("outsidecontext", 0, 2) + ctx := context.Background() + result, err := client.GetHistoryListenings(ctx, "outsidecontext", 0, 2) require.NoError(t, err) assert := assert.New(t) @@ -73,7 +75,8 @@ func TestGetFavoriteTracks(t *testing.T) { "https://funkwhale.example.com/api/v1/favorites/tracks", "testdata/favorite-tracks.json") - result, err := client.GetFavoriteTracks(0, 2) + ctx := context.Background() + result, err := client.GetFavoriteTracks(ctx, 0, 2) require.NoError(t, err) assert := assert.New(t) diff --git a/internal/backends/funkwhale/funkwhale.go b/internal/backends/funkwhale/funkwhale.go index cd2f28e..434716f 100644 --- a/internal/backends/funkwhale/funkwhale.go +++ b/internal/backends/funkwhale/funkwhale.go @@ -17,6 +17,7 @@ Scotty. If not, see . package funkwhale import ( + "context" "sort" "time" @@ -61,6 +62,7 @@ func (b *FunkwhaleApiBackend) InitConfig(config *config.ServiceConfig) error { } func (b *FunkwhaleApiBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) { + ctx := context.TODO() page := 1 perPage := MaxItemsPerGet @@ -74,7 +76,7 @@ func (b *FunkwhaleApiBackend) ExportListens(oldestTimestamp time.Time, results c out: for { - result, err := b.client.GetHistoryListenings(b.username, page, perPage) + result, err := b.client.GetHistoryListenings(ctx, b.username, page, perPage) if err != nil { p.Export.Abort() progress <- p @@ -118,6 +120,7 @@ out: } func (b *FunkwhaleApiBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.TransferProgress) { + ctx := context.TODO() page := 1 perPage := MaxItemsPerGet @@ -131,7 +134,7 @@ func (b *FunkwhaleApiBackend) ExportLoves(oldestTimestamp time.Time, results cha out: for { - result, err := b.client.GetFavoriteTracks(page, perPage) + result, err := b.client.GetFavoriteTracks(ctx, page, perPage) if err != nil { p.Export.Abort() progress <- p diff --git a/internal/backends/listenbrainz/client.go b/internal/backends/listenbrainz/client.go index fff476c..d1a1fa6 100644 --- a/internal/backends/listenbrainz/client.go +++ b/internal/backends/listenbrainz/client.go @@ -1,5 +1,5 @@ /* -Copyright © 2023 Philipp Wolfer +Copyright © 2023-2025 Philipp Wolfer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -22,6 +22,7 @@ THE SOFTWARE. package listenbrainz import ( + "context" "errors" "strconv" "time" @@ -60,10 +61,11 @@ func NewClient(token string) Client { } } -func (c Client) GetListens(user string, maxTime time.Time, minTime time.Time) (result GetListensResult, err error) { +func (c Client) GetListens(ctx context.Context, user string, maxTime time.Time, minTime time.Time) (result GetListensResult, err error) { const path = "/user/{username}/listens" errorResult := ErrorResult{} response, err := c.HTTPClient.R(). + SetContext(ctx). SetPathParam("username", user). SetQueryParams(map[string]string{ "max_ts": strconv.FormatInt(maxTime.Unix(), 10), @@ -81,10 +83,11 @@ func (c Client) GetListens(user string, maxTime time.Time, minTime time.Time) (r return } -func (c Client) SubmitListens(listens ListenSubmission) (result StatusResult, err error) { +func (c Client) SubmitListens(ctx context.Context, listens ListenSubmission) (result StatusResult, err error) { const path = "/submit-listens" errorResult := ErrorResult{} response, err := c.HTTPClient.R(). + SetContext(ctx). SetBody(listens). SetResult(&result). SetError(&errorResult). @@ -97,10 +100,11 @@ func (c Client) SubmitListens(listens ListenSubmission) (result StatusResult, er return } -func (c Client) GetFeedback(user string, status int, offset int) (result GetFeedbackResult, err error) { +func (c Client) GetFeedback(ctx context.Context, user string, status int, offset int) (result GetFeedbackResult, err error) { const path = "/feedback/user/{username}/get-feedback" errorResult := ErrorResult{} response, err := c.HTTPClient.R(). + SetContext(ctx). SetPathParam("username", user). SetQueryParams(map[string]string{ "status": strconv.Itoa(status), @@ -119,10 +123,11 @@ func (c Client) GetFeedback(user string, status int, offset int) (result GetFeed return } -func (c Client) SendFeedback(feedback Feedback) (result StatusResult, err error) { +func (c Client) SendFeedback(ctx context.Context, feedback Feedback) (result StatusResult, err error) { const path = "/feedback/recording-feedback" errorResult := ErrorResult{} response, err := c.HTTPClient.R(). + SetContext(ctx). SetBody(feedback). SetResult(&result). SetError(&errorResult). @@ -135,10 +140,11 @@ func (c Client) SendFeedback(feedback Feedback) (result StatusResult, err error) return } -func (c Client) Lookup(recordingName string, artistName string) (result LookupResult, err error) { +func (c Client) Lookup(ctx context.Context, recordingName string, artistName string) (result LookupResult, err error) { const path = "/metadata/lookup" errorResult := ErrorResult{} response, err := c.HTTPClient.R(). + SetContext(ctx). SetQueryParams(map[string]string{ "recording_name": recordingName, "artist_name": artistName, diff --git a/internal/backends/listenbrainz/client_test.go b/internal/backends/listenbrainz/client_test.go index 2e841ae..45bb0de 100644 --- a/internal/backends/listenbrainz/client_test.go +++ b/internal/backends/listenbrainz/client_test.go @@ -1,5 +1,5 @@ /* -Copyright © 2023 Philipp Wolfer +Copyright © 2023-2025 Philipp Wolfer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -22,6 +22,7 @@ THE SOFTWARE. package listenbrainz_test import ( + "context" "net/http" "testing" "time" @@ -49,7 +50,9 @@ func TestGetListens(t *testing.T) { "https://api.listenbrainz.org/1/user/outsidecontext/listens", "testdata/listens.json") - result, err := client.GetListens("outsidecontext", time.Now(), time.Now().Add(-2*time.Hour)) + ctx := context.Background() + result, err := client.GetListens(ctx, "outsidecontext", + time.Now(), time.Now().Add(-2*time.Hour)) require.NoError(t, err) assert := assert.New(t) @@ -92,8 +95,8 @@ func TestSubmitListens(t *testing.T) { }, }, } - result, err := client.SubmitListens(listens) - require.NoError(t, err) + ctx := context.Background() + result, err := client.SubmitListens(ctx, listens) assert.Equal(t, "ok", result.Status) } @@ -107,7 +110,8 @@ func TestGetFeedback(t *testing.T) { "https://api.listenbrainz.org/1/feedback/user/outsidecontext/get-feedback", "testdata/feedback.json") - result, err := client.GetFeedback("outsidecontext", 1, 3) + ctx := context.Background() + result, err := client.GetFeedback(ctx, "outsidecontext", 1, 0) require.NoError(t, err) assert := assert.New(t) @@ -135,7 +139,8 @@ func TestSendFeedback(t *testing.T) { RecordingMBID: "c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12", Score: 1, } - result, err := client.SendFeedback(feedback) + ctx := context.Background() + result, err := client.SendFeedback(ctx, feedback) require.NoError(t, err) assert.Equal(t, "ok", result.Status) @@ -149,7 +154,8 @@ func TestLookup(t *testing.T) { "https://api.listenbrainz.org/1/metadata/lookup", "testdata/lookup.json") - result, err := client.Lookup("Paradise Lost", "Say Just Words") + ctx := context.Background() + result, err := client.Lookup(ctx, "Paradise Lost", "Say Just Words") require.NoError(t, err) assert := assert.New(t) diff --git a/internal/backends/listenbrainz/listenbrainz.go b/internal/backends/listenbrainz/listenbrainz.go index 61597d1..d622aff 100644 --- a/internal/backends/listenbrainz/listenbrainz.go +++ b/internal/backends/listenbrainz/listenbrainz.go @@ -17,6 +17,7 @@ Scotty. If not, see . package listenbrainz import ( + "context" "fmt" "sort" "time" @@ -73,6 +74,7 @@ func (b *ListenBrainzApiBackend) StartImport() error { return nil } func (b *ListenBrainzApiBackend) FinishImport() error { return nil } func (b *ListenBrainzApiBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) { + ctx := context.TODO() startTime := time.Now() minTime := oldestTimestamp if minTime.Unix() < 1 { @@ -87,7 +89,7 @@ func (b *ListenBrainzApiBackend) ExportListens(oldestTimestamp time.Time, result } for { - result, err := b.client.GetListens(b.username, time.Now(), minTime) + result, err := b.client.GetListens(ctx, b.username, time.Now(), minTime) if err != nil { p.Export.Abort() progress <- p @@ -135,6 +137,7 @@ func (b *ListenBrainzApiBackend) ExportListens(oldestTimestamp time.Time, result } func (b *ListenBrainzApiBackend) ImportListens(export models.ListensResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) { + ctx := context.TODO() total := len(export.Items) p := models.TransferProgress{}.FromImportResult(importResult, false) for i := 0; i < total; i += MaxListensPerRequest { @@ -151,7 +154,7 @@ func (b *ListenBrainzApiBackend) ImportListens(export models.ListensResult, impo for _, l := range listens { if b.checkDuplicates { - isDupe, err := b.checkDuplicateListen(l) + isDupe, err := b.checkDuplicateListen(ctx, l) p.Import.Elapsed += 1 progress <- p if err != nil { @@ -182,7 +185,7 @@ func (b *ListenBrainzApiBackend) ImportListens(export models.ListensResult, impo } if len(submission.Payload) > 0 { - _, err := b.client.SubmitListens(submission) + _, err := b.client.SubmitListens(ctx, submission) if err != nil { return importResult, err } @@ -199,12 +202,13 @@ func (b *ListenBrainzApiBackend) ImportListens(export models.ListensResult, impo } func (b *ListenBrainzApiBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.TransferProgress) { + ctx := context.TODO() exportChan := make(chan models.LovesResult) p := models.TransferProgress{ Export: &models.Progress{}, } - go b.exportLoves(oldestTimestamp, exportChan) + go b.exportLoves(ctx, oldestTimestamp, exportChan) for existingLoves := range exportChan { if existingLoves.Error != nil { p.Export.Abort() @@ -224,14 +228,14 @@ func (b *ListenBrainzApiBackend) ExportLoves(oldestTimestamp time.Time, results progress <- p } -func (b *ListenBrainzApiBackend) exportLoves(oldestTimestamp time.Time, results chan models.LovesResult) { +func (b *ListenBrainzApiBackend) exportLoves(ctx context.Context, oldestTimestamp time.Time, results chan models.LovesResult) { offset := 0 defer close(results) loves := make(models.LovesList, 0, 2*MaxItemsPerGet) out: for { - result, err := b.client.GetFeedback(b.username, 1, offset) + result, err := b.client.GetFeedback(ctx, b.username, 1, offset) if err != nil { results <- models.LovesResult{Error: err} return @@ -272,9 +276,10 @@ out: } func (b *ListenBrainzApiBackend) ImportLoves(export models.LovesResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) { + ctx := context.TODO() if len(b.existingMBIDs) == 0 { existingLovesChan := make(chan models.LovesResult) - go b.exportLoves(time.Unix(0, 0), existingLovesChan) + go b.exportLoves(ctx, time.Unix(0, 0), existingLovesChan) // TODO: Store MBIDs directly b.existingMBIDs = make(map[mbtypes.MBID]bool, MaxItemsPerGet) @@ -303,7 +308,7 @@ func (b *ListenBrainzApiBackend) ImportLoves(export models.LovesResult, importRe } if recordingMBID == "" { - lookup, err := b.client.Lookup(love.TrackName, love.ArtistName()) + lookup, err := b.client.Lookup(ctx, love.TrackName, love.ArtistName()) if err == nil { recordingMBID = lookup.RecordingMBID } @@ -315,7 +320,7 @@ func (b *ListenBrainzApiBackend) ImportLoves(export models.LovesResult, importRe if b.existingMBIDs[recordingMBID] { ok = true } else { - resp, err := b.client.SendFeedback(Feedback{ + resp, err := b.client.SendFeedback(ctx, Feedback{ RecordingMBID: recordingMBID, Score: 1, }) @@ -351,7 +356,7 @@ var defaultDuration = time.Duration(3 * time.Minute) const trackSimilarityThreshold = 0.9 -func (b *ListenBrainzApiBackend) checkDuplicateListen(listen models.Listen) (bool, error) { +func (b *ListenBrainzApiBackend) checkDuplicateListen(ctx context.Context, listen models.Listen) (bool, error) { // Find listens duration := listen.Duration if duration == 0 { @@ -359,7 +364,7 @@ func (b *ListenBrainzApiBackend) checkDuplicateListen(listen models.Listen) (boo } minTime := listen.ListenedAt.Add(-duration) maxTime := listen.ListenedAt.Add(duration) - candidates, err := b.client.GetListens(b.username, maxTime, minTime) + candidates, err := b.client.GetListens(ctx, b.username, maxTime, minTime) if err != nil { return false, err } diff --git a/internal/backends/maloja/client.go b/internal/backends/maloja/client.go index 249819a..b80cb56 100644 --- a/internal/backends/maloja/client.go +++ b/internal/backends/maloja/client.go @@ -1,5 +1,5 @@ /* -Copyright © 2023 Philipp Wolfer +Copyright © 2023-2025 Philipp Wolfer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -22,6 +22,7 @@ THE SOFTWARE. package maloja import ( + "context" "errors" "strconv" @@ -48,9 +49,10 @@ func NewClient(serverURL string, token string) Client { } } -func (c Client) GetScrobbles(page int, perPage int) (result GetScrobblesResult, err error) { +func (c Client) GetScrobbles(ctx context.Context, page int, perPage int) (result GetScrobblesResult, err error) { const path = "/apis/mlj_1/scrobbles" response, err := c.HTTPClient.R(). + SetContext(ctx). SetQueryParams(map[string]string{ "page": strconv.Itoa(page), "perpage": strconv.Itoa(perPage), @@ -65,10 +67,11 @@ func (c Client) GetScrobbles(page int, perPage int) (result GetScrobblesResult, return } -func (c Client) NewScrobble(scrobble NewScrobble) (result NewScrobbleResult, err error) { +func (c Client) NewScrobble(ctx context.Context, scrobble NewScrobble) (result NewScrobbleResult, err error) { const path = "/apis/mlj_1/newscrobble" scrobble.Key = c.token response, err := c.HTTPClient.R(). + SetContext(ctx). SetBody(scrobble). SetResult(&result). Post(path) diff --git a/internal/backends/maloja/client_test.go b/internal/backends/maloja/client_test.go index 54316a8..415f911 100644 --- a/internal/backends/maloja/client_test.go +++ b/internal/backends/maloja/client_test.go @@ -1,5 +1,5 @@ /* -Copyright © 2023 Philipp Wolfer +Copyright © 2023-2025 Philipp Wolfer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -22,6 +22,7 @@ THE SOFTWARE. package maloja_test import ( + "context" "net/http" "testing" @@ -48,7 +49,8 @@ func TestGetScrobbles(t *testing.T) { "https://maloja.example.com/apis/mlj_1/scrobbles", "testdata/scrobbles.json") - result, err := client.GetScrobbles(0, 2) + ctx := context.Background() + result, err := client.GetScrobbles(ctx, 0, 2) require.NoError(t, err) assert := assert.New(t) @@ -69,12 +71,13 @@ func TestNewScrobble(t *testing.T) { url := server + "/apis/mlj_1/newscrobble" httpmock.RegisterResponder("POST", url, responder) + ctx := context.Background() scrobble := maloja.NewScrobble{ Title: "Oweynagat", Artist: "Dool", Time: 1699574369, } - result, err := client.NewScrobble(scrobble) + result, err := client.NewScrobble(ctx, scrobble) require.NoError(t, err) assert.Equal(t, "success", result.Status) diff --git a/internal/backends/maloja/maloja.go b/internal/backends/maloja/maloja.go index 8968942..4a4965e 100644 --- a/internal/backends/maloja/maloja.go +++ b/internal/backends/maloja/maloja.go @@ -17,6 +17,7 @@ Scotty. If not, see . package maloja import ( + "context" "errors" "sort" "strings" @@ -64,6 +65,7 @@ func (b *MalojaApiBackend) StartImport() error { return nil } func (b *MalojaApiBackend) FinishImport() error { return nil } func (b *MalojaApiBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) { + ctx := context.TODO() page := 0 perPage := MaxItemsPerGet @@ -77,7 +79,7 @@ func (b *MalojaApiBackend) ExportListens(oldestTimestamp time.Time, results chan out: for { - result, err := b.client.GetScrobbles(page, perPage) + result, err := b.client.GetScrobbles(ctx, page, perPage) if err != nil { p.Export.Abort() progress <- p @@ -112,6 +114,8 @@ out: } func (b *MalojaApiBackend) ImportListens(export models.ListensResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) { + ctx := context.TODO() + p := models.TransferProgress{}.FromImportResult(importResult, false) for _, listen := range export.Items { scrobble := NewScrobble{ @@ -124,7 +128,7 @@ func (b *MalojaApiBackend) ImportListens(export models.ListensResult, importResu Nofix: b.nofix, } - resp, err := b.client.NewScrobble(scrobble) + resp, err := b.client.NewScrobble(ctx, scrobble) if err != nil { return importResult, err } else if resp.Status != "success" { diff --git a/internal/backends/spotify/client.go b/internal/backends/spotify/client.go index ff2b0a3..94d50ac 100644 --- a/internal/backends/spotify/client.go +++ b/internal/backends/spotify/client.go @@ -1,5 +1,5 @@ /* -Copyright © 2023 Philipp Wolfer +Copyright © 2023-2025 Philipp Wolfer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -59,17 +59,18 @@ func NewClient(token oauth2.TokenSource) Client { } } -func (c Client) RecentlyPlayedAfter(after time.Time, limit int) (RecentlyPlayedResult, error) { - return c.recentlyPlayed(&after, nil, limit) +func (c Client) RecentlyPlayedAfter(ctx context.Context, after time.Time, limit int) (RecentlyPlayedResult, error) { + return c.recentlyPlayed(ctx, &after, nil, limit) } -func (c Client) RecentlyPlayedBefore(before time.Time, limit int) (RecentlyPlayedResult, error) { - return c.recentlyPlayed(nil, &before, limit) +func (c Client) RecentlyPlayedBefore(ctx context.Context, before time.Time, limit int) (RecentlyPlayedResult, error) { + return c.recentlyPlayed(ctx, nil, &before, limit) } -func (c Client) recentlyPlayed(after *time.Time, before *time.Time, limit int) (result RecentlyPlayedResult, err error) { +func (c Client) recentlyPlayed(ctx context.Context, after *time.Time, before *time.Time, limit int) (result RecentlyPlayedResult, err error) { const path = "/me/player/recently-played" request := c.HTTPClient.R(). + SetContext(ctx). SetQueryParam("limit", strconv.Itoa(limit)). SetResult(&result) if after != nil { @@ -85,9 +86,10 @@ func (c Client) recentlyPlayed(after *time.Time, before *time.Time, limit int) ( return } -func (c Client) UserTracks(offset int, limit int) (result TracksResult, err error) { +func (c Client) UserTracks(ctx context.Context, offset int, limit int) (result TracksResult, err error) { const path = "/me/tracks" response, err := c.HTTPClient.R(). + SetContext(ctx). SetQueryParams(map[string]string{ "offset": strconv.Itoa(offset), "limit": strconv.Itoa(limit), diff --git a/internal/backends/spotify/client_test.go b/internal/backends/spotify/client_test.go index 78ff063..8135e1d 100644 --- a/internal/backends/spotify/client_test.go +++ b/internal/backends/spotify/client_test.go @@ -1,5 +1,5 @@ /* -Copyright © 2023 Philipp Wolfer +Copyright © 2023-2025 Philipp Wolfer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -22,6 +22,7 @@ THE SOFTWARE. package spotify_test import ( + "context" "net/http" "testing" "time" @@ -47,7 +48,8 @@ func TestRecentlyPlayedAfter(t *testing.T) { "https://api.spotify.com/v1/me/player/recently-played", "testdata/recently-played.json") - result, err := client.RecentlyPlayedAfter(time.Now(), 3) + ctx := context.Background() + result, err := client.RecentlyPlayedAfter(ctx, time.Now(), 3) require.NoError(t, err) assert := assert.New(t) @@ -67,7 +69,8 @@ func TestGetUserTracks(t *testing.T) { "https://api.spotify.com/v1/me/tracks", "testdata/user-tracks.json") - result, err := client.UserTracks(0, 2) + ctx := context.Background() + result, err := client.UserTracks(ctx, 0, 2) require.NoError(t, err) assert := assert.New(t) diff --git a/internal/backends/spotify/spotify.go b/internal/backends/spotify/spotify.go index 5d45087..73434b3 100644 --- a/internal/backends/spotify/spotify.go +++ b/internal/backends/spotify/spotify.go @@ -18,6 +18,7 @@ Scotty. If not, see . package spotify import ( + "context" "math" "net/url" "sort" @@ -96,6 +97,7 @@ func (b *SpotifyApiBackend) OAuth2Setup(token oauth2.TokenSource) error { } func (b *SpotifyApiBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) { + ctx := context.TODO() startTime := time.Now() minTime := oldestTimestamp @@ -107,7 +109,7 @@ func (b *SpotifyApiBackend) ExportListens(oldestTimestamp time.Time, results cha } for { - result, err := b.client.RecentlyPlayedAfter(minTime, MaxItemsPerGet) + result, err := b.client.RecentlyPlayedAfter(ctx, minTime, MaxItemsPerGet) if err != nil { p.Export.Abort() progress <- p @@ -163,6 +165,7 @@ func (b *SpotifyApiBackend) ExportListens(oldestTimestamp time.Time, results cha } func (b *SpotifyApiBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.TransferProgress) { + ctx := context.TODO() // Choose a high offset, we attempt to search the loves backwards starting // at the oldest one. offset := math.MaxInt32 @@ -178,7 +181,7 @@ func (b *SpotifyApiBackend) ExportLoves(oldestTimestamp time.Time, results chan out: for { - result, err := b.client.UserTracks(offset, perPage) + result, err := b.client.UserTracks(ctx, offset, perPage) if err != nil { p.Export.Abort() progress <- p From b5bca1d4abfb957f6f942f8dd9484147bccd7918 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Thu, 22 May 2025 09:24:06 +0200 Subject: [PATCH 5/8] Use context aware musicbrainzws2 --- go.mod | 2 +- go.sum | 4 ++-- internal/backends/listenbrainz/listenbrainz.go | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 47c7e88..db7ecc3 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/supersonic-app/go-subsonic v0.0.0-20241224013245-9b2841f3711d github.com/vbauerster/mpb/v8 v8.10.0 go.uploadedlobster.com/mbtypes v0.4.0 - go.uploadedlobster.com/musicbrainzws2 v0.14.0 + go.uploadedlobster.com/musicbrainzws2 v0.14.1-0.20250522060150-50bf4bea5400 golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 golang.org/x/oauth2 v0.30.0 golang.org/x/text v0.25.0 diff --git a/go.sum b/go.sum index 426e8c0..ef278f9 100644 --- a/go.sum +++ b/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.uploadedlobster.com/mbtypes v0.4.0 h1:D5asCgHsRWufj4Yn5u0IuH2J9z1UuYImYkYIp1Z1Q7s= go.uploadedlobster.com/mbtypes v0.4.0/go.mod h1:Bu1K1Hl77QTAE2Z7QKiW/JAp9KqYWQebkRRfG02dlZM= -go.uploadedlobster.com/musicbrainzws2 v0.14.0 h1:YaEtxNwLSNT1gzFipQ4XlaThNfXjBpzzb4I6WhIeUwg= -go.uploadedlobster.com/musicbrainzws2 v0.14.0/go.mod h1:T6sYE7ZHRH3mJWT3g9jdSUPKJLZubnBjKyjMPNdkgao= +go.uploadedlobster.com/musicbrainzws2 v0.14.1-0.20250522060150-50bf4bea5400 h1:wMJloSsyWjfXznQNjvsrqAeL61BGoil7t4H9hPt18fc= +go.uploadedlobster.com/musicbrainzws2 v0.14.1-0.20250522060150-50bf4bea5400/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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= diff --git a/internal/backends/listenbrainz/listenbrainz.go b/internal/backends/listenbrainz/listenbrainz.go index d622aff..9f269a2 100644 --- a/internal/backends/listenbrainz/listenbrainz.go +++ b/internal/backends/listenbrainz/listenbrainz.go @@ -251,7 +251,7 @@ out: // longer available and might have been merged. Try fetching details // from MusicBrainz. if feedback.TrackMetadata == nil { - track, err := b.lookupRecording(feedback.RecordingMBID) + track, err := b.lookupRecording(ctx, feedback.RecordingMBID) if err == nil { feedback.TrackMetadata = track } @@ -379,11 +379,11 @@ func (b *ListenBrainzApiBackend) checkDuplicateListen(ctx context.Context, liste return false, nil } -func (b *ListenBrainzApiBackend) lookupRecording(mbid mbtypes.MBID) (*Track, error) { +func (b *ListenBrainzApiBackend) lookupRecording(ctx context.Context, mbid mbtypes.MBID) (*Track, error) { filter := musicbrainzws2.IncludesFilter{ Includes: []string{"artist-credits"}, } - recording, err := b.mbClient.LookupRecording(mbid, filter) + recording, err := b.mbClient.LookupRecording(ctx, mbid, filter) if err != nil { return nil, err } From 26d9f5e840e357d94731210edf0337750c989eef Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Thu, 22 May 2025 11:09:39 +0200 Subject: [PATCH 6/8] Pass context to export backends --- internal/backends/deezer/deezer.go | 8 ++----- internal/backends/export.go | 4 ++-- internal/backends/funkwhale/funkwhale.go | 6 ++--- internal/backends/jspf/jspf.go | 5 ++-- internal/backends/lastfm/lastfm.go | 23 +++++++++++++++++-- .../backends/listenbrainz/listenbrainz.go | 6 ++--- internal/backends/maloja/maloja.go | 3 +-- .../backends/scrobblerlog/scrobblerlog.go | 3 ++- internal/backends/spotify/spotify.go | 6 ++--- .../backends/spotifyhistory/spotifyhistory.go | 16 ++++++++++--- internal/backends/subsonic/subsonic.go | 3 ++- internal/models/interfaces.go | 5 ++-- 12 files changed, 55 insertions(+), 33 deletions(-) diff --git a/internal/backends/deezer/deezer.go b/internal/backends/deezer/deezer.go index f3e3d37..c38f4e7 100644 --- a/internal/backends/deezer/deezer.go +++ b/internal/backends/deezer/deezer.go @@ -78,9 +78,7 @@ func (b *DeezerApiBackend) OAuth2Setup(token oauth2.TokenSource) error { return nil } -func (b *DeezerApiBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) { - ctx := context.TODO() - +func (b *DeezerApiBackend) ExportListens(ctx context.Context, oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) { // Choose a high offset, we attempt to search the loves backwards starting // at the oldest one. offset := math.MaxInt32 @@ -156,9 +154,7 @@ out: progress <- p } -func (b *DeezerApiBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.TransferProgress) { - ctx := context.TODO() - +func (b *DeezerApiBackend) ExportLoves(ctx context.Context, oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.TransferProgress) { // Choose a high offset, we attempt to search the loves backwards starting // at the oldest one. offset := math.MaxInt32 diff --git a/internal/backends/export.go b/internal/backends/export.go index 0deebc6..29ae595 100644 --- a/internal/backends/export.go +++ b/internal/backends/export.go @@ -40,7 +40,7 @@ func (p ListensExportProcessor) Process(ctx context.Context, wg *sync.WaitGroup, wg.Add(1) defer wg.Done() defer close(results) - p.Backend.ExportListens(oldestTimestamp, results, progress) + p.Backend.ExportListens(ctx, oldestTimestamp, results, progress) } type LovesExportProcessor struct { @@ -55,5 +55,5 @@ func (p LovesExportProcessor) Process(ctx context.Context, wg *sync.WaitGroup, o wg.Add(1) defer wg.Done() defer close(results) - p.Backend.ExportLoves(oldestTimestamp, results, progress) + p.Backend.ExportLoves(ctx, oldestTimestamp, results, progress) } diff --git a/internal/backends/funkwhale/funkwhale.go b/internal/backends/funkwhale/funkwhale.go index 434716f..d9632a6 100644 --- a/internal/backends/funkwhale/funkwhale.go +++ b/internal/backends/funkwhale/funkwhale.go @@ -61,8 +61,7 @@ func (b *FunkwhaleApiBackend) InitConfig(config *config.ServiceConfig) error { return nil } -func (b *FunkwhaleApiBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) { - ctx := context.TODO() +func (b *FunkwhaleApiBackend) ExportListens(ctx context.Context, oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) { page := 1 perPage := MaxItemsPerGet @@ -119,8 +118,7 @@ out: results <- models.ListensResult{Items: listens} } -func (b *FunkwhaleApiBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.TransferProgress) { - ctx := context.TODO() +func (b *FunkwhaleApiBackend) ExportLoves(ctx context.Context, oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.TransferProgress) { page := 1 perPage := MaxItemsPerGet diff --git a/internal/backends/jspf/jspf.go b/internal/backends/jspf/jspf.go index 0e200f2..77fed4b 100644 --- a/internal/backends/jspf/jspf.go +++ b/internal/backends/jspf/jspf.go @@ -18,6 +18,7 @@ Scotty. If not, see . package jspf import ( + "context" "errors" "os" "sort" @@ -93,7 +94,7 @@ func (b *JSPFBackend) FinishImport() error { return b.writeJSPF() } -func (b *JSPFBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) { +func (b *JSPFBackend) ExportListens(ctx context.Context, oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) { err := b.readJSPF() p := models.TransferProgress{ Export: &models.Progress{}, @@ -132,7 +133,7 @@ func (b *JSPFBackend) ImportListens(export models.ListensResult, importResult mo return importResult, nil } -func (b *JSPFBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.TransferProgress) { +func (b *JSPFBackend) ExportLoves(ctx context.Context, oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.TransferProgress) { err := b.readJSPF() p := models.TransferProgress{ Export: &models.Progress{}, diff --git a/internal/backends/lastfm/lastfm.go b/internal/backends/lastfm/lastfm.go index d262ada..3de75d1 100644 --- a/internal/backends/lastfm/lastfm.go +++ b/internal/backends/lastfm/lastfm.go @@ -16,6 +16,7 @@ Scotty. If not, see . package lastfm import ( + "context" "fmt" "net/url" "sort" @@ -88,7 +89,7 @@ func (b *LastfmApiBackend) OAuth2Setup(token oauth2.TokenSource) error { return nil } -func (b *LastfmApiBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) { +func (b *LastfmApiBackend) ExportListens(ctx context.Context, oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) { page := MaxPage minTime := oldestTimestamp perPage := MaxItemsPerGet @@ -102,6 +103,15 @@ func (b *LastfmApiBackend) ExportListens(oldestTimestamp time.Time, results chan out: for page > 0 { + select { + case <-ctx.Done(): + results <- models.ListensResult{Error: ctx.Err()} + p.Export.Abort() + progress <- p + return + default: + } + args := lastfm.P{ "user": b.username, "limit": MaxListensPerGet, @@ -258,7 +268,7 @@ func (b *LastfmApiBackend) ImportListens(export models.ListensResult, importResu return importResult, nil } -func (b *LastfmApiBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.TransferProgress) { +func (b *LastfmApiBackend) ExportLoves(ctx context.Context, oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.TransferProgress) { // Choose a high offset, we attempt to search the loves backwards starting // at the oldest one. page := 1 @@ -274,6 +284,15 @@ func (b *LastfmApiBackend) ExportLoves(oldestTimestamp time.Time, results chan m out: for { + select { + case <-ctx.Done(): + results <- models.LovesResult{Error: ctx.Err()} + p.Export.Abort() + progress <- p + return + default: + } + result, err := b.client.User.GetLovedTracks(lastfm.P{ "user": b.username, "limit": MaxItemsPerGet, diff --git a/internal/backends/listenbrainz/listenbrainz.go b/internal/backends/listenbrainz/listenbrainz.go index 9f269a2..ca1c0f0 100644 --- a/internal/backends/listenbrainz/listenbrainz.go +++ b/internal/backends/listenbrainz/listenbrainz.go @@ -73,8 +73,7 @@ func (b *ListenBrainzApiBackend) InitConfig(config *config.ServiceConfig) error func (b *ListenBrainzApiBackend) StartImport() error { return nil } func (b *ListenBrainzApiBackend) FinishImport() error { return nil } -func (b *ListenBrainzApiBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) { - ctx := context.TODO() +func (b *ListenBrainzApiBackend) ExportListens(ctx context.Context, oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) { startTime := time.Now() minTime := oldestTimestamp if minTime.Unix() < 1 { @@ -201,8 +200,7 @@ func (b *ListenBrainzApiBackend) ImportListens(export models.ListensResult, impo return importResult, nil } -func (b *ListenBrainzApiBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.TransferProgress) { - ctx := context.TODO() +func (b *ListenBrainzApiBackend) ExportLoves(ctx context.Context, oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.TransferProgress) { exportChan := make(chan models.LovesResult) p := models.TransferProgress{ Export: &models.Progress{}, diff --git a/internal/backends/maloja/maloja.go b/internal/backends/maloja/maloja.go index 4a4965e..8642924 100644 --- a/internal/backends/maloja/maloja.go +++ b/internal/backends/maloja/maloja.go @@ -64,8 +64,7 @@ func (b *MalojaApiBackend) InitConfig(config *config.ServiceConfig) error { func (b *MalojaApiBackend) StartImport() error { return nil } func (b *MalojaApiBackend) FinishImport() error { return nil } -func (b *MalojaApiBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) { - ctx := context.TODO() +func (b *MalojaApiBackend) ExportListens(ctx context.Context, oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) { page := 0 perPage := MaxItemsPerGet diff --git a/internal/backends/scrobblerlog/scrobblerlog.go b/internal/backends/scrobblerlog/scrobblerlog.go index 6454b7b..c7eb636 100644 --- a/internal/backends/scrobblerlog/scrobblerlog.go +++ b/internal/backends/scrobblerlog/scrobblerlog.go @@ -17,6 +17,7 @@ Scotty. If not, see . package scrobblerlog import ( + "context" "fmt" "os" "sort" @@ -129,7 +130,7 @@ func (b *ScrobblerLogBackend) FinishImport() error { return b.file.Close() } -func (b *ScrobblerLogBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) { +func (b *ScrobblerLogBackend) ExportListens(ctx context.Context, oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) { file, err := os.Open(b.filePath) p := models.TransferProgress{ Export: &models.Progress{}, diff --git a/internal/backends/spotify/spotify.go b/internal/backends/spotify/spotify.go index 73434b3..b00ebba 100644 --- a/internal/backends/spotify/spotify.go +++ b/internal/backends/spotify/spotify.go @@ -96,8 +96,7 @@ func (b *SpotifyApiBackend) OAuth2Setup(token oauth2.TokenSource) error { return nil } -func (b *SpotifyApiBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) { - ctx := context.TODO() +func (b *SpotifyApiBackend) ExportListens(ctx context.Context, oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) { startTime := time.Now() minTime := oldestTimestamp @@ -164,8 +163,7 @@ func (b *SpotifyApiBackend) ExportListens(oldestTimestamp time.Time, results cha progress <- p } -func (b *SpotifyApiBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.TransferProgress) { - ctx := context.TODO() +func (b *SpotifyApiBackend) ExportLoves(ctx context.Context, oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.TransferProgress) { // Choose a high offset, we attempt to search the loves backwards starting // at the oldest one. offset := math.MaxInt32 diff --git a/internal/backends/spotifyhistory/spotifyhistory.go b/internal/backends/spotifyhistory/spotifyhistory.go index d5c87bb..9a1ab2b 100644 --- a/internal/backends/spotifyhistory/spotifyhistory.go +++ b/internal/backends/spotifyhistory/spotifyhistory.go @@ -18,6 +18,7 @@ Scotty. If not, see . package spotifyhistory import ( + "context" "os" "path" "path/filepath" @@ -72,7 +73,7 @@ func (b *SpotifyHistoryBackend) InitConfig(config *config.ServiceConfig) error { return nil } -func (b *SpotifyHistoryBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) { +func (b *SpotifyHistoryBackend) ExportListens(ctx context.Context, oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.TransferProgress) { files, err := filepath.Glob(path.Join(b.dirPath, historyFileGlob)) p := models.TransferProgress{ Export: &models.Progress{}, @@ -89,11 +90,20 @@ func (b *SpotifyHistoryBackend) ExportListens(oldestTimestamp time.Time, results fileCount := int64(len(files)) p.Export.Total = fileCount for i, filePath := range files { - history, err := readHistoryFile(filePath) - if err != nil { + select { + case <-ctx.Done(): + results <- models.ListensResult{Error: ctx.Err()} p.Export.Abort() progress <- p + return + default: + } + + history, err := readHistoryFile(filePath) + if err != nil { results <- models.ListensResult{Error: err} + p.Export.Abort() + progress <- p return } listens := history.AsListenList(ListenListOptions{ diff --git a/internal/backends/subsonic/subsonic.go b/internal/backends/subsonic/subsonic.go index 2098688..aa1b1e3 100644 --- a/internal/backends/subsonic/subsonic.go +++ b/internal/backends/subsonic/subsonic.go @@ -17,6 +17,7 @@ Scotty. If not, see . package subsonic import ( + "context" "net/http" "sort" "time" @@ -63,7 +64,7 @@ func (b *SubsonicApiBackend) InitConfig(config *config.ServiceConfig) error { return nil } -func (b *SubsonicApiBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.TransferProgress) { +func (b *SubsonicApiBackend) ExportLoves(ctx context.Context, oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.TransferProgress) { err := b.client.Authenticate(b.password) p := models.TransferProgress{ Export: &models.Progress{}, diff --git a/internal/models/interfaces.go b/internal/models/interfaces.go index bb97dac..2b45d18 100644 --- a/internal/models/interfaces.go +++ b/internal/models/interfaces.go @@ -17,6 +17,7 @@ Scotty. If not, see . package models import ( + "context" "time" // "go.uploadedlobster.com/scotty/internal/auth" @@ -55,7 +56,7 @@ type ListensExport interface { // Returns a list of all listens newer then oldestTimestamp. // The returned list of listens is supposed to be ordered by the // Listen.ListenedAt timestamp, with the oldest entry first. - ExportListens(oldestTimestamp time.Time, results chan ListensResult, progress chan TransferProgress) + ExportListens(ctx context.Context, oldestTimestamp time.Time, results chan ListensResult, progress chan TransferProgress) } // Must be implemented by services supporting the import of listens. @@ -73,7 +74,7 @@ type LovesExport interface { // Returns a list of all loves newer then oldestTimestamp. // The returned list of listens is supposed to be ordered by the // Love.Created timestamp, with the oldest entry first. - ExportLoves(oldestTimestamp time.Time, results chan LovesResult, progress chan TransferProgress) + ExportLoves(ctx context.Context, oldestTimestamp time.Time, results chan LovesResult, progress chan TransferProgress) } // Must be implemented by services supporting the import of loves. From 4a66e3d43285ecf4179eb146f3346122b18e99e3 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Thu, 22 May 2025 11:20:09 +0200 Subject: [PATCH 7/8] Pass context to import backends --- internal/backends/dump/dump.go | 39 ++++++++++++------- internal/backends/import.go | 12 +++--- internal/backends/jspf/jspf.go | 30 +++++++++----- internal/backends/lastfm/lastfm.go | 16 +++++++- .../backends/listenbrainz/listenbrainz.go | 6 +-- internal/backends/maloja/maloja.go | 4 +- .../backends/scrobblerlog/scrobblerlog.go | 2 +- internal/models/interfaces.go | 4 +- 8 files changed, 71 insertions(+), 42 deletions(-) diff --git a/internal/backends/dump/dump.go b/internal/backends/dump/dump.go index add8711..14583f6 100644 --- a/internal/backends/dump/dump.go +++ b/internal/backends/dump/dump.go @@ -17,6 +17,7 @@ Scotty. If not, see . package dump import ( + "context" "fmt" "go.uploadedlobster.com/scotty/internal/config" @@ -36,27 +37,37 @@ func (b *DumpBackend) InitConfig(config *config.ServiceConfig) error { func (b *DumpBackend) StartImport() error { return nil } func (b *DumpBackend) FinishImport() error { return nil } -func (b *DumpBackend) ImportListens(export models.ListensResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) { +func (b *DumpBackend) ImportListens(ctx context.Context, export models.ListensResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) { for _, listen := range export.Items { - importResult.UpdateTimestamp(listen.ListenedAt) - importResult.ImportCount += 1 - msg := fmt.Sprintf("🎶 %v: \"%v\" by %v (%v)", - listen.ListenedAt, listen.TrackName, listen.ArtistName(), listen.RecordingMBID) - importResult.Log(models.Info, msg) - progress <- models.TransferProgress{}.FromImportResult(importResult, false) + select { + case <-ctx.Done(): + return importResult, ctx.Err() + default: + importResult.UpdateTimestamp(listen.ListenedAt) + importResult.ImportCount += 1 + msg := fmt.Sprintf("🎶 %v: \"%v\" by %v (%v)", + listen.ListenedAt, listen.TrackName, listen.ArtistName(), listen.RecordingMBID) + importResult.Log(models.Info, msg) + progress <- models.TransferProgress{}.FromImportResult(importResult, false) + } } return importResult, nil } -func (b *DumpBackend) ImportLoves(export models.LovesResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) { +func (b *DumpBackend) ImportLoves(ctx context.Context, export models.LovesResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) { for _, love := range export.Items { - importResult.UpdateTimestamp(love.Created) - importResult.ImportCount += 1 - msg := fmt.Sprintf("❤️ %v: \"%v\" by %v (%v)", - love.Created, love.TrackName, love.ArtistName(), love.RecordingMBID) - importResult.Log(models.Info, msg) - progress <- models.TransferProgress{}.FromImportResult(importResult, false) + select { + case <-ctx.Done(): + return importResult, ctx.Err() + default: + importResult.UpdateTimestamp(love.Created) + importResult.ImportCount += 1 + msg := fmt.Sprintf("❤️ %v: \"%v\" by %v (%v)", + love.Created, love.TrackName, love.ArtistName(), love.RecordingMBID) + importResult.Log(models.Info, msg) + progress <- models.TransferProgress{}.FromImportResult(importResult, false) + } } return importResult, nil diff --git a/internal/backends/import.go b/internal/backends/import.go index e7006bd..3d77b44 100644 --- a/internal/backends/import.go +++ b/internal/backends/import.go @@ -27,7 +27,7 @@ import ( type ImportProcessor[T models.ListensResult | models.LovesResult] interface { ImportBackend() models.ImportBackend Process(ctx context.Context, wg *sync.WaitGroup, results chan T, out chan models.ImportResult, progress chan models.TransferProgress) - Import(export T, result models.ImportResult, out chan models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) + Import(ctx context.Context, export T, result models.ImportResult, out chan models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) } type ListensImportProcessor struct { @@ -42,7 +42,7 @@ func (p ListensImportProcessor) Process(ctx context.Context, wg *sync.WaitGroup, process(ctx, wg, p, results, out, progress) } -func (p ListensImportProcessor) Import(export models.ListensResult, result models.ImportResult, out chan models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) { +func (p ListensImportProcessor) Import(ctx context.Context, export models.ListensResult, result models.ImportResult, out chan models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) { if export.Error != nil { return result, export.Error } @@ -52,7 +52,7 @@ func (p ListensImportProcessor) Import(export models.ListensResult, result model } else { result.TotalCount += len(export.Items) } - importResult, err := p.Backend.ImportListens(export, result, progress) + importResult, err := p.Backend.ImportListens(ctx, export, result, progress) if err != nil { return importResult, err } @@ -71,7 +71,7 @@ func (p LovesImportProcessor) Process(ctx context.Context, wg *sync.WaitGroup, r process(ctx, wg, p, results, out, progress) } -func (p LovesImportProcessor) Import(export models.LovesResult, result models.ImportResult, out chan models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) { +func (p LovesImportProcessor) Import(ctx context.Context, export models.LovesResult, result models.ImportResult, out chan models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) { if export.Error != nil { return result, export.Error } @@ -81,7 +81,7 @@ func (p LovesImportProcessor) Import(export models.LovesResult, result models.Im } else { result.TotalCount += len(export.Items) } - importResult, err := p.Backend.ImportLoves(export, result, progress) + importResult, err := p.Backend.ImportLoves(ctx, export, result, progress) if err != nil { return importResult, err } @@ -112,7 +112,7 @@ func process[R models.LovesResult | models.ListensResult, P ImportProcessor[R]]( out <- handleError(result, ctx.Err(), progress) return default: - importResult, err := processor.Import(exportResult, result, out, progress) + importResult, err := processor.Import(ctx, exportResult, result, out, progress) result.Update(importResult) if err != nil { processor.ImportBackend().FinishImport() diff --git a/internal/backends/jspf/jspf.go b/internal/backends/jspf/jspf.go index 77fed4b..a8a1929 100644 --- a/internal/backends/jspf/jspf.go +++ b/internal/backends/jspf/jspf.go @@ -121,12 +121,17 @@ func (b *JSPFBackend) ExportListens(ctx context.Context, oldestTimestamp time.Ti results <- models.ListensResult{Items: listens} } -func (b *JSPFBackend) ImportListens(export models.ListensResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) { +func (b *JSPFBackend) ImportListens(ctx context.Context, export models.ListensResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) { for _, listen := range export.Items { - track := listenAsTrack(listen) - b.playlist.Tracks = append(b.playlist.Tracks, track) - importResult.ImportCount += 1 - importResult.UpdateTimestamp(listen.ListenedAt) + select { + case <-ctx.Done(): + return importResult, ctx.Err() + default: + track := listenAsTrack(listen) + b.playlist.Tracks = append(b.playlist.Tracks, track) + importResult.ImportCount += 1 + importResult.UpdateTimestamp(listen.ListenedAt) + } } progress <- models.TransferProgress{}.FromImportResult(importResult, false) @@ -160,12 +165,17 @@ func (b *JSPFBackend) ExportLoves(ctx context.Context, oldestTimestamp time.Time results <- models.LovesResult{Items: loves} } -func (b *JSPFBackend) ImportLoves(export models.LovesResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) { +func (b *JSPFBackend) ImportLoves(ctx context.Context, export models.LovesResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) { for _, love := range export.Items { - track := loveAsTrack(love) - b.playlist.Tracks = append(b.playlist.Tracks, track) - importResult.ImportCount += 1 - importResult.UpdateTimestamp(love.Created) + select { + case <-ctx.Done(): + return importResult, ctx.Err() + default: + track := loveAsTrack(love) + b.playlist.Tracks = append(b.playlist.Tracks, track) + importResult.ImportCount += 1 + importResult.UpdateTimestamp(love.Created) + } } progress <- models.TransferProgress{}.FromImportResult(importResult, false) diff --git a/internal/backends/lastfm/lastfm.go b/internal/backends/lastfm/lastfm.go index 3de75d1..afc1fa3 100644 --- a/internal/backends/lastfm/lastfm.go +++ b/internal/backends/lastfm/lastfm.go @@ -192,9 +192,15 @@ out: progress <- p } -func (b *LastfmApiBackend) ImportListens(export models.ListensResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) { +func (b *LastfmApiBackend) ImportListens(ctx context.Context, export models.ListensResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) { total := len(export.Items) for i := 0; i < total; i += MaxListensPerSubmission { + select { + case <-ctx.Done(): + return importResult, ctx.Err() + default: + } + listens := export.Items[i:min(i+MaxListensPerSubmission, total)] count := len(listens) if count == 0 { @@ -354,8 +360,14 @@ out: results <- models.LovesResult{Items: loves, Total: totalCount} } -func (b *LastfmApiBackend) ImportLoves(export models.LovesResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) { +func (b *LastfmApiBackend) ImportLoves(ctx context.Context, export models.LovesResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) { for _, love := range export.Items { + select { + case <-ctx.Done(): + return importResult, ctx.Err() + default: + } + err := b.client.Track.Love(lastfm.P{ "track": love.TrackName, "artist": love.ArtistName(), diff --git a/internal/backends/listenbrainz/listenbrainz.go b/internal/backends/listenbrainz/listenbrainz.go index ca1c0f0..bf46c22 100644 --- a/internal/backends/listenbrainz/listenbrainz.go +++ b/internal/backends/listenbrainz/listenbrainz.go @@ -135,8 +135,7 @@ func (b *ListenBrainzApiBackend) ExportListens(ctx context.Context, oldestTimest progress <- p } -func (b *ListenBrainzApiBackend) ImportListens(export models.ListensResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) { - ctx := context.TODO() +func (b *ListenBrainzApiBackend) ImportListens(ctx context.Context, export models.ListensResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) { total := len(export.Items) p := models.TransferProgress{}.FromImportResult(importResult, false) for i := 0; i < total; i += MaxListensPerRequest { @@ -273,8 +272,7 @@ out: } } -func (b *ListenBrainzApiBackend) ImportLoves(export models.LovesResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) { - ctx := context.TODO() +func (b *ListenBrainzApiBackend) ImportLoves(ctx context.Context, export models.LovesResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) { if len(b.existingMBIDs) == 0 { existingLovesChan := make(chan models.LovesResult) go b.exportLoves(ctx, time.Unix(0, 0), existingLovesChan) diff --git a/internal/backends/maloja/maloja.go b/internal/backends/maloja/maloja.go index 8642924..f082d9b 100644 --- a/internal/backends/maloja/maloja.go +++ b/internal/backends/maloja/maloja.go @@ -112,9 +112,7 @@ out: results <- models.ListensResult{Items: listens} } -func (b *MalojaApiBackend) ImportListens(export models.ListensResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) { - ctx := context.TODO() - +func (b *MalojaApiBackend) ImportListens(ctx context.Context, export models.ListensResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) { p := models.TransferProgress{}.FromImportResult(importResult, false) for _, listen := range export.Items { scrobble := NewScrobble{ diff --git a/internal/backends/scrobblerlog/scrobblerlog.go b/internal/backends/scrobblerlog/scrobblerlog.go index c7eb636..6d331ce 100644 --- a/internal/backends/scrobblerlog/scrobblerlog.go +++ b/internal/backends/scrobblerlog/scrobblerlog.go @@ -167,7 +167,7 @@ func (b *ScrobblerLogBackend) ExportListens(ctx context.Context, oldestTimestamp results <- models.ListensResult{Items: listens} } -func (b *ScrobblerLogBackend) ImportListens(export models.ListensResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) { +func (b *ScrobblerLogBackend) ImportListens(ctx context.Context, export models.ListensResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) { records := make([]scrobblerlog.Record, len(export.Items)) for i, listen := range export.Items { records[i] = listenToRecord(listen) diff --git a/internal/models/interfaces.go b/internal/models/interfaces.go index 2b45d18..2f4beaf 100644 --- a/internal/models/interfaces.go +++ b/internal/models/interfaces.go @@ -64,7 +64,7 @@ type ListensImport interface { ImportBackend // Imports the given list of listens. - ImportListens(export ListensResult, importResult ImportResult, progress chan TransferProgress) (ImportResult, error) + ImportListens(ctx context.Context, export ListensResult, importResult ImportResult, progress chan TransferProgress) (ImportResult, error) } // Must be implemented by services supporting the export of loves. @@ -82,5 +82,5 @@ type LovesImport interface { ImportBackend // Imports the given list of loves. - ImportLoves(export LovesResult, importResult ImportResult, progress chan TransferProgress) (ImportResult, error) + ImportLoves(ctx context.Context, export LovesResult, importResult ImportResult, progress chan TransferProgress) (ImportResult, error) } From 20853f7601c80b1254c59d903b0e46667e98cecc Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Thu, 22 May 2025 14:13:31 +0200 Subject: [PATCH 8/8] Simplify context cancellation checks --- internal/backends/dump/dump.go | 38 +++++++++---------- internal/backends/import.go | 23 ++++++----- internal/backends/jspf/jspf.go | 30 +++++++-------- internal/backends/lastfm/lastfm.go | 24 ++++-------- .../backends/spotifyhistory/spotifyhistory.go | 6 +-- 5 files changed, 53 insertions(+), 68 deletions(-) diff --git a/internal/backends/dump/dump.go b/internal/backends/dump/dump.go index 14583f6..1fcd864 100644 --- a/internal/backends/dump/dump.go +++ b/internal/backends/dump/dump.go @@ -39,17 +39,16 @@ 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) { for _, listen := range export.Items { - select { - case <-ctx.Done(): - return importResult, ctx.Err() - default: - importResult.UpdateTimestamp(listen.ListenedAt) - importResult.ImportCount += 1 - msg := fmt.Sprintf("🎶 %v: \"%v\" by %v (%v)", - listen.ListenedAt, listen.TrackName, listen.ArtistName(), listen.RecordingMBID) - importResult.Log(models.Info, msg) - progress <- models.TransferProgress{}.FromImportResult(importResult, false) + if err := ctx.Err(); err != nil { + return importResult, err } + + importResult.UpdateTimestamp(listen.ListenedAt) + importResult.ImportCount += 1 + msg := fmt.Sprintf("🎶 %v: \"%v\" by %v (%v)", + listen.ListenedAt, listen.TrackName, listen.ArtistName(), listen.RecordingMBID) + importResult.Log(models.Info, msg) + progress <- models.TransferProgress{}.FromImportResult(importResult, false) } return importResult, nil @@ -57,17 +56,16 @@ func (b *DumpBackend) ImportListens(ctx context.Context, export models.ListensRe func (b *DumpBackend) ImportLoves(ctx context.Context, export models.LovesResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) { for _, love := range export.Items { - select { - case <-ctx.Done(): - return importResult, ctx.Err() - default: - importResult.UpdateTimestamp(love.Created) - importResult.ImportCount += 1 - msg := fmt.Sprintf("❤️ %v: \"%v\" by %v (%v)", - love.Created, love.TrackName, love.ArtistName(), love.RecordingMBID) - importResult.Log(models.Info, msg) - progress <- models.TransferProgress{}.FromImportResult(importResult, false) + if err := ctx.Err(); err != nil { + return importResult, err } + + importResult.UpdateTimestamp(love.Created) + importResult.ImportCount += 1 + msg := fmt.Sprintf("❤️ %v: \"%v\" by %v (%v)", + love.Created, love.TrackName, love.ArtistName(), love.RecordingMBID) + importResult.Log(models.Info, msg) + progress <- models.TransferProgress{}.FromImportResult(importResult, false) } return importResult, nil diff --git a/internal/backends/import.go b/internal/backends/import.go index 3d77b44..e7a6add 100644 --- a/internal/backends/import.go +++ b/internal/backends/import.go @@ -106,21 +106,20 @@ func process[R models.LovesResult | models.ListensResult, P ImportProcessor[R]]( } for exportResult := range results { - select { - case <-ctx.Done(): + if err := ctx.Err(); err != nil { processor.ImportBackend().FinishImport() - out <- handleError(result, ctx.Err(), progress) + out <- handleError(result, err, progress) return - default: - importResult, err := processor.Import(ctx, exportResult, result, out, progress) - result.Update(importResult) - if err != nil { - processor.ImportBackend().FinishImport() - out <- handleError(result, err, progress) - return - } - progress <- p.FromImportResult(result, false) } + + importResult, err := processor.Import(ctx, exportResult, result, out, progress) + result.Update(importResult) + if err != nil { + processor.ImportBackend().FinishImport() + out <- handleError(result, err, progress) + return + } + progress <- p.FromImportResult(result, false) } if err := processor.ImportBackend().FinishImport(); err != nil { diff --git a/internal/backends/jspf/jspf.go b/internal/backends/jspf/jspf.go index a8a1929..354640e 100644 --- a/internal/backends/jspf/jspf.go +++ b/internal/backends/jspf/jspf.go @@ -123,15 +123,14 @@ func (b *JSPFBackend) ExportListens(ctx context.Context, oldestTimestamp time.Ti func (b *JSPFBackend) ImportListens(ctx context.Context, export models.ListensResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) { for _, listen := range export.Items { - select { - case <-ctx.Done(): - return importResult, ctx.Err() - default: - track := listenAsTrack(listen) - b.playlist.Tracks = append(b.playlist.Tracks, track) - importResult.ImportCount += 1 - importResult.UpdateTimestamp(listen.ListenedAt) + if err := ctx.Err(); err != nil { + return importResult, err } + + track := listenAsTrack(listen) + b.playlist.Tracks = append(b.playlist.Tracks, track) + importResult.ImportCount += 1 + importResult.UpdateTimestamp(listen.ListenedAt) } progress <- models.TransferProgress{}.FromImportResult(importResult, false) @@ -167,15 +166,14 @@ func (b *JSPFBackend) ExportLoves(ctx context.Context, oldestTimestamp time.Time func (b *JSPFBackend) ImportLoves(ctx context.Context, export models.LovesResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) { for _, love := range export.Items { - select { - case <-ctx.Done(): - return importResult, ctx.Err() - default: - track := loveAsTrack(love) - b.playlist.Tracks = append(b.playlist.Tracks, track) - importResult.ImportCount += 1 - importResult.UpdateTimestamp(love.Created) + if err := ctx.Err(); err != nil { + return importResult, err } + + track := loveAsTrack(love) + b.playlist.Tracks = append(b.playlist.Tracks, track) + importResult.ImportCount += 1 + importResult.UpdateTimestamp(love.Created) } progress <- models.TransferProgress{}.FromImportResult(importResult, false) diff --git a/internal/backends/lastfm/lastfm.go b/internal/backends/lastfm/lastfm.go index afc1fa3..b34452e 100644 --- a/internal/backends/lastfm/lastfm.go +++ b/internal/backends/lastfm/lastfm.go @@ -103,13 +103,11 @@ func (b *LastfmApiBackend) ExportListens(ctx context.Context, oldestTimestamp ti out: for page > 0 { - select { - case <-ctx.Done(): - results <- models.ListensResult{Error: ctx.Err()} + if err := ctx.Err(); err != nil { + results <- models.ListensResult{Error: err} p.Export.Abort() progress <- p return - default: } args := lastfm.P{ @@ -195,10 +193,8 @@ out: func (b *LastfmApiBackend) ImportListens(ctx context.Context, export models.ListensResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) { total := len(export.Items) for i := 0; i < total; i += MaxListensPerSubmission { - select { - case <-ctx.Done(): - return importResult, ctx.Err() - default: + if err := ctx.Err(); err != nil { + return importResult, err } listens := export.Items[i:min(i+MaxListensPerSubmission, total)] @@ -290,13 +286,11 @@ func (b *LastfmApiBackend) ExportLoves(ctx context.Context, oldestTimestamp time out: for { - select { - case <-ctx.Done(): - results <- models.LovesResult{Error: ctx.Err()} + if err := ctx.Err(); err != nil { + results <- models.LovesResult{Error: err} p.Export.Abort() progress <- p return - default: } result, err := b.client.User.GetLovedTracks(lastfm.P{ @@ -362,10 +356,8 @@ out: func (b *LastfmApiBackend) ImportLoves(ctx context.Context, export models.LovesResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) { for _, love := range export.Items { - select { - case <-ctx.Done(): - return importResult, ctx.Err() - default: + if err := ctx.Err(); err != nil { + return importResult, err } err := b.client.Track.Love(lastfm.P{ diff --git a/internal/backends/spotifyhistory/spotifyhistory.go b/internal/backends/spotifyhistory/spotifyhistory.go index 9a1ab2b..76d0c9e 100644 --- a/internal/backends/spotifyhistory/spotifyhistory.go +++ b/internal/backends/spotifyhistory/spotifyhistory.go @@ -90,13 +90,11 @@ func (b *SpotifyHistoryBackend) ExportListens(ctx context.Context, oldestTimesta fileCount := int64(len(files)) p.Export.Total = fileCount for i, filePath := range files { - select { - case <-ctx.Done(): - results <- models.ListensResult{Error: ctx.Err()} + if err := ctx.Err(); err != nil { + results <- models.ListensResult{Error: err} p.Export.Abort() progress <- p return - default: } history, err := readHistoryFile(filePath)