From 4d07a39b64ce9d2a9926e46a78652a78a2dd9fd2 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 10 Dec 2023 00:24:39 +0100 Subject: [PATCH 01/82] jspf: implement append mode --- config.example.toml | 3 ++ internal/backends/jspf/jspf.go | 90 +++++++++++++++++++++++----------- 2 files changed, 65 insertions(+), 28 deletions(-) diff --git a/config.example.toml b/config.example.toml index b7b82fc..11930a2 100644 --- a/config.example.toml +++ b/config.example.toml @@ -59,6 +59,9 @@ append = true backend = "jspf" # The file path to the XSPF file file-path = "data/playlist.jspf" +# If true (default), new listens will be appended to the existing file. Set to +# false to overwrite the file and create a new JSPF playlist on every run. +append = true # Title of the playlist title = "My Playlist" # Creator of the playlist (only informational) diff --git a/internal/backends/jspf/jspf.go b/internal/backends/jspf/jspf.go index 944c6e6..b87b86a 100644 --- a/internal/backends/jspf/jspf.go +++ b/internal/backends/jspf/jspf.go @@ -28,11 +28,9 @@ import ( ) type JSPFBackend struct { - filePath string - title string - creator string - identifier string - tracks []jspf.Track + filePath string + playlist jspf.Playlist + append bool } func (b *JSPFBackend) Name() string { return "jspf" } @@ -42,6 +40,11 @@ func (b *JSPFBackend) Options() []models.BackendOption { Name: "file-path", Label: i18n.Tr("File path"), Type: models.String, + }, { + Name: "append", + Label: i18n.Tr("Append to file"), + Type: models.Bool, + Default: "true", }, { Name: "title", Label: i18n.Tr("Playlist title"), @@ -59,23 +62,37 @@ func (b *JSPFBackend) Options() []models.BackendOption { func (b *JSPFBackend) FromConfig(config *config.ServiceConfig) models.Backend { b.filePath = config.GetString("file-path") - b.title = config.GetString("title") - b.creator = config.GetString("username") - b.identifier = config.GetString("identifier") - b.tracks = make([]jspf.Track, 0) + b.append = true + if config.IsSet("append") { + b.append = config.GetBool("append") + } + b.playlist = jspf.Playlist{ + Title: config.GetString("title"), + Creator: config.GetString("username"), + Identifier: config.GetString("identifier"), + Tracks: make([]jspf.Track, 0), + Extension: map[string]any{ + jspf.MusicBrainzPlaylistExtensionId: jspf.MusicBrainzPlaylistExtension{ + LastModifiedAt: time.Now(), + Public: true, + }, + }, + } return b } -func (b *JSPFBackend) StartImport() error { return nil } +func (b *JSPFBackend) StartImport() error { + return b.readJSPF() +} + func (b *JSPFBackend) FinishImport() error { - err := b.writeJSPF(b.tracks) - return err + return b.writeJSPF() } func (b *JSPFBackend) ImportListens(export models.ListensResult, importResult models.ImportResult, progress chan models.Progress) (models.ImportResult, error) { for _, listen := range export.Items { track := listenAsTrack(listen) - b.tracks = append(b.tracks, track) + b.playlist.Tracks = append(b.playlist.Tracks, track) importResult.ImportCount += 1 importResult.UpdateTimestamp(listen.ListenedAt) } @@ -87,7 +104,7 @@ func (b *JSPFBackend) ImportListens(export models.ListensResult, importResult mo func (b *JSPFBackend) ImportLoves(export models.LovesResult, importResult models.ImportResult, progress chan models.Progress) (models.ImportResult, error) { for _, love := range export.Items { track := loveAsTrack(love) - b.tracks = append(b.tracks, track) + b.playlist.Tracks = append(b.playlist.Tracks, track) importResult.ImportCount += 1 importResult.UpdateTimestamp(love.Created) } @@ -162,21 +179,38 @@ func makeMusicBrainzExtension(t models.Track) jspf.MusicBrainzTrackExtension { return extension } -func (b JSPFBackend) writeJSPF(tracks []jspf.Track) error { +func (b *JSPFBackend) readJSPF() error { + if b.append { + file, err := os.Open(b.filePath) + if err != nil { + return err + } + + defer file.Close() + stat, err := file.Stat() + if err != nil { + return err + } + + if stat.Size() == 0 { + // Zero length file, treat as a new file + return nil + } else { + playlist := jspf.JSPF{} + err := playlist.Read(file) + if err != nil { + return err + } + b.playlist = playlist.Playlist + } + } + + return nil +} + +func (b *JSPFBackend) writeJSPF() error { playlist := jspf.JSPF{ - Playlist: jspf.Playlist{ - Title: b.title, - Creator: b.creator, - Identifier: b.identifier, - Date: time.Now(), - Tracks: tracks, - Extension: map[string]any{ - jspf.MusicBrainzPlaylistExtensionId: jspf.MusicBrainzPlaylistExtension{ - LastModifiedAt: time.Now(), - Public: true, - }, - }, - }, + Playlist: b.playlist, } file, err := os.Create(b.filePath) From acb0e9cb11fd837b8593c25691062351782771cf Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 10 Dec 2023 00:25:29 +0100 Subject: [PATCH 02/82] scrobblerlog: configuring should show append mode as enabled by default --- internal/backends/scrobblerlog/scrobblerlog.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/backends/scrobblerlog/scrobblerlog.go b/internal/backends/scrobblerlog/scrobblerlog.go index e09f100..2233421 100644 --- a/internal/backends/scrobblerlog/scrobblerlog.go +++ b/internal/backends/scrobblerlog/scrobblerlog.go @@ -47,9 +47,10 @@ func (b *ScrobblerLogBackend) Options() []models.BackendOption { Label: i18n.Tr("Include skipped listens"), Type: models.Bool, }, { - Name: "append", - Label: i18n.Tr("Append to file"), - Type: models.Bool, + Name: "append", + Label: i18n.Tr("Append to file"), + Type: models.Bool, + Default: "true", }} } From 6eaef181883efe961b2b86b446c6be6778a68414 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 10 Dec 2023 01:33:14 +0100 Subject: [PATCH 03/82] subsonic: only set tags if genre is non-empty --- internal/backends/subsonic/subsonic.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/backends/subsonic/subsonic.go b/internal/backends/subsonic/subsonic.go index 6560319..5b192ab 100644 --- a/internal/backends/subsonic/subsonic.go +++ b/internal/backends/subsonic/subsonic.go @@ -105,11 +105,14 @@ func SongAsLove(song subsonic.Child, username string) models.Love { ArtistNames: []string{song.Artist}, TrackNumber: song.Track, DiscNumber: song.DiscNumber, - Tags: []string{song.Genre}, AdditionalInfo: map[string]any{}, Duration: time.Duration(song.Duration * int(time.Second)), }, } + if song.Genre != "" { + love.Track.Tags = []string{song.Genre} + } + return love } From c4193f42a1eb1c9347cba9294731786767867855 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 10 Dec 2023 13:51:38 +0100 Subject: [PATCH 04/82] Code cleanup and missing error checks --- internal/auth/callback.go | 10 +++++++++- internal/backends/deezer/client.go | 5 ++++- internal/backends/scrobblerlog/parser.go | 2 +- internal/backends/scrobblerlog/parser_test.go | 2 +- internal/backends/scrobblerlog/scrobblerlog.go | 10 +++++----- internal/cli/common.go | 8 -------- internal/config/config.go | 5 +++-- 7 files changed, 23 insertions(+), 19 deletions(-) diff --git a/internal/auth/callback.go b/internal/auth/callback.go index 6ae0128..e49bfc7 100644 --- a/internal/auth/callback.go +++ b/internal/auth/callback.go @@ -17,6 +17,7 @@ package auth import ( "fmt" + "log" "net/http" "net/url" @@ -34,5 +35,12 @@ func RunOauth2CallbackServer(redirectURL url.URL, param string, responseChan cha } }) - go http.ListenAndServe(redirectURL.Host, nil) + go runServer(redirectURL.Host) +} + +func runServer(addr string) { + err := http.ListenAndServe(addr, nil) + if err != nil { + log.Fatal(err) + } } diff --git a/internal/backends/deezer/client.go b/internal/backends/deezer/client.go index 0d9cbb0..eccd188 100644 --- a/internal/backends/deezer/client.go +++ b/internal/backends/deezer/client.go @@ -79,7 +79,10 @@ func listRequest[T Result](c Client, path string, offset int, limit int) (result "limit": strconv.Itoa(limit), }). SetResult(&result) - c.setToken(request) + err = c.setToken(request) + if err != nil { + return + } response, err := request.Get(path) if response.StatusCode() != 200 { diff --git a/internal/backends/scrobblerlog/parser.go b/internal/backends/scrobblerlog/parser.go index a503d35..af891ac 100644 --- a/internal/backends/scrobblerlog/parser.go +++ b/internal/backends/scrobblerlog/parser.go @@ -105,7 +105,7 @@ func Write(data io.Writer, listens models.ListensList) (lastTimestamp time.Time, if !ok || rating == "" { rating = "L" } - tsvWriter.Write([]string{ + err = tsvWriter.Write([]string{ listen.ArtistName(), listen.ReleaseName, listen.TrackName, diff --git a/internal/backends/scrobblerlog/parser_test.go b/internal/backends/scrobblerlog/parser_test.go index 76b6c87..51d15c7 100644 --- a/internal/backends/scrobblerlog/parser_test.go +++ b/internal/backends/scrobblerlog/parser_test.go @@ -103,7 +103,7 @@ func TestWrite(t *testing.T) { require.NoError(t, err) lastTimestamp, err := scrobblerlog.Write(buffer, log.Listens) require.NoError(t, err) - result := string(buffer.Bytes()) + result := buffer.String() lines := strings.Split(result, "\n") assert.Equal(5, len(lines)) assert.Equal("#AUDIOSCROBBLER/1.1", lines[0]) diff --git a/internal/backends/scrobblerlog/scrobblerlog.go b/internal/backends/scrobblerlog/scrobblerlog.go index 2233421..09f081f 100644 --- a/internal/backends/scrobblerlog/scrobblerlog.go +++ b/internal/backends/scrobblerlog/scrobblerlog.go @@ -91,18 +91,18 @@ func (b *ScrobblerLogBackend) StartImport() error { } else { // Verify existing file is a scrobbler log reader := bufio.NewReader(file) - err = ReadHeader(reader, &b.log) - if err != nil { + if err = ReadHeader(reader, &b.log); err != nil { file.Close() return err } - file.Seek(0, 2) + if _, err = file.Seek(0, 2); err != nil { + return err + } } } if !b.append { - err = WriteHeader(file, &b.log) - if err != nil { + if err = WriteHeader(file, &b.log); err != nil { file.Close() return err } diff --git a/internal/cli/common.go b/internal/cli/common.go index 6c8786a..fb6fb02 100644 --- a/internal/cli/common.go +++ b/internal/cli/common.go @@ -24,11 +24,3 @@ func GetServiceConfigFromFlag(cmd *cobra.Command, flagName string) (config.Servi name := cmd.Flag(flagName).Value.String() return config.GetService(name) } - -func getInt64FromFlag(cmd *cobra.Command, flagName string) (result int64) { - result, err := cmd.Flags().GetInt64(flagName) - if err != nil { - result = 0 - } - return -} diff --git a/internal/config/config.go b/internal/config/config.go index f52de99..a529b92 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -61,7 +61,8 @@ func InitConfig(cfgFile string) error { // Create global config if it does not exist if viper.ConfigFileUsed() == "" && cfgFile == "" { if err := os.MkdirAll(configDir, 0750); err == nil { - viper.SafeWriteConfig() + // This call is expected to return an error if the file already exists + viper.SafeWriteConfig() //nolint:errcheck } } @@ -82,7 +83,7 @@ func WriteConfig(removedKeys ...string) error { configMap := viper.AllSettings() for _, key := range removedKeys { c := configMap - ok := true + var ok bool subKeys := strings.Split(key, ".") keyLen := len(subKeys) // Deep search the key in the config and delete the deepest key, if it exists From dd501df5c5a32181d22af3214fbb4736f66af174 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 10 Dec 2023 14:11:54 +0100 Subject: [PATCH 05/82] use go:embed to simplify testdata loading --- internal/backends/deezer/deezer_test.go | 17 ++++++++++------- internal/backends/deezer/models_test.go | 17 ++++++++++------- internal/backends/spotify/models_test.go | 9 +++++---- internal/backends/spotify/spotify_test.go | 17 ++++++++++------- 4 files changed, 35 insertions(+), 25 deletions(-) diff --git a/internal/backends/deezer/deezer_test.go b/internal/backends/deezer/deezer_test.go index c50d4a7..ac81402 100644 --- a/internal/backends/deezer/deezer_test.go +++ b/internal/backends/deezer/deezer_test.go @@ -16,8 +16,8 @@ Scotty. If not, see . package deezer_test import ( + _ "embed" "encoding/json" - "os" "testing" "time" @@ -28,6 +28,13 @@ import ( "go.uploadedlobster.com/scotty/internal/config" ) +var ( + //go:embed testdata/listen.json + testListen []byte + //go:embed testdata/track.json + testTrack []byte +) + func TestFromConfig(t *testing.T) { c := viper.New() c.Set("client-id", "someclientid") @@ -38,10 +45,8 @@ func TestFromConfig(t *testing.T) { } func TestListenAsListen(t *testing.T) { - data, err := os.ReadFile("testdata/listen.json") - require.NoError(t, err) track := deezer.Listen{} - err = json.Unmarshal(data, &track) + err := json.Unmarshal(testListen, &track) require.NoError(t, err) listen := track.AsListen() assert.Equal(t, time.Unix(1700753817, 0), listen.ListenedAt) @@ -55,10 +60,8 @@ func TestListenAsListen(t *testing.T) { } func TestLovedTrackAsLove(t *testing.T) { - data, err := os.ReadFile("testdata/track.json") - require.NoError(t, err) track := deezer.LovedTrack{} - err = json.Unmarshal(data, &track) + err := json.Unmarshal(testTrack, &track) require.NoError(t, err) love := track.AsLove() assert.Equal(t, time.Unix(1700743848, 0), love.Created) diff --git a/internal/backends/deezer/models_test.go b/internal/backends/deezer/models_test.go index 6a38b5c..0fc6ab4 100644 --- a/internal/backends/deezer/models_test.go +++ b/internal/backends/deezer/models_test.go @@ -16,8 +16,8 @@ Scotty. If not, see . package deezer_test import ( + _ "embed" "encoding/json" - "os" "testing" "github.com/stretchr/testify/assert" @@ -25,11 +25,16 @@ import ( "go.uploadedlobster.com/scotty/internal/backends/deezer" ) +var ( + //go:embed testdata/user-tracks.json + testUserTracks []byte + //go:embed testdata/user-history.json + testUserHistory []byte +) + func TestUserTracksResult(t *testing.T) { - data, err := os.ReadFile("testdata/user-tracks.json") - require.NoError(t, err) result := deezer.TracksResult{} - err = json.Unmarshal(data, &result) + err := json.Unmarshal(testUserTracks, &result) require.NoError(t, err) assert := assert.New(t) @@ -45,10 +50,8 @@ func TestUserTracksResult(t *testing.T) { } func TestUserHistoryResult(t *testing.T) { - data, err := os.ReadFile("testdata/user-history.json") - require.NoError(t, err) result := deezer.HistoryResult{} - err = json.Unmarshal(data, &result) + err := json.Unmarshal(testUserHistory, &result) require.NoError(t, err) assert := assert.New(t) diff --git a/internal/backends/spotify/models_test.go b/internal/backends/spotify/models_test.go index fcb4b6b..9d38b45 100644 --- a/internal/backends/spotify/models_test.go +++ b/internal/backends/spotify/models_test.go @@ -23,8 +23,8 @@ THE SOFTWARE. package spotify_test import ( + _ "embed" "encoding/json" - "os" "testing" "github.com/stretchr/testify/assert" @@ -32,11 +32,12 @@ import ( "go.uploadedlobster.com/scotty/internal/backends/spotify" ) +//go:embed testdata/recently-played.json +var testRecentlyPlayed []byte + func TestRecentlyPlayedResult(t *testing.T) { - data, err := os.ReadFile("testdata/recently-played.json") - require.NoError(t, err) result := spotify.RecentlyPlayedResult{} - err = json.Unmarshal(data, &result) + err := json.Unmarshal(testRecentlyPlayed, &result) require.NoError(t, err) assert := assert.New(t) diff --git a/internal/backends/spotify/spotify_test.go b/internal/backends/spotify/spotify_test.go index 496922a..bd7ff58 100644 --- a/internal/backends/spotify/spotify_test.go +++ b/internal/backends/spotify/spotify_test.go @@ -18,8 +18,8 @@ Scotty. If not, see . package spotify_test import ( + _ "embed" "encoding/json" - "os" "testing" "time" @@ -30,6 +30,13 @@ import ( "go.uploadedlobster.com/scotty/internal/config" ) +var ( + //go:embed testdata/listen.json + testListen []byte + //go:embed testdata/track.json + testTrack []byte +) + func TestFromConfig(t *testing.T) { c := viper.New() c.Set("client-id", "someclientid") @@ -40,10 +47,8 @@ func TestFromConfig(t *testing.T) { } func TestSpotifyListenAsListen(t *testing.T) { - data, err := os.ReadFile("testdata/listen.json") - require.NoError(t, err) spListen := spotify.Listen{} - err = json.Unmarshal(data, &spListen) + err := json.Unmarshal(testListen, &spListen) require.NoError(t, err) listen := spListen.AsListen() listenedAt, _ := time.Parse(time.RFC3339, "2023-11-21T15:24:33.361Z") @@ -65,10 +70,8 @@ func TestSpotifyListenAsListen(t *testing.T) { } func TestSavedTrackAsLove(t *testing.T) { - data, err := os.ReadFile("testdata/track.json") - require.NoError(t, err) track := spotify.SavedTrack{} - err = json.Unmarshal(data, &track) + err := json.Unmarshal(testTrack, &track) require.NoError(t, err) love := track.AsLove() created, _ := time.Parse(time.RFC3339, "2022-02-13T21:46:08Z") From a59a5429679a94dbf08dc9eaab239a20bc849b89 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 10 Dec 2023 14:17:49 +0100 Subject: [PATCH 06/82] moved OAuth2Authenticator to auth package --- cmd/service_add.go | 4 ++-- cmd/service_auth.go | 4 ++-- internal/auth/auth.go | 34 ++++++++++++++++++++++++++++++++++ internal/backends/auth.go | 2 +- internal/cli/auth.go | 3 +-- internal/models/interfaces.go | 15 +-------------- 6 files changed, 41 insertions(+), 21 deletions(-) create mode 100644 internal/auth/auth.go diff --git a/cmd/service_add.go b/cmd/service_add.go index 0bf671d..3360faa 100644 --- a/cmd/service_add.go +++ b/cmd/service_add.go @@ -27,11 +27,11 @@ import ( "github.com/manifoldco/promptui" "github.com/spf13/cobra" + "go.uploadedlobster.com/scotty/internal/auth" "go.uploadedlobster.com/scotty/internal/backends" "go.uploadedlobster.com/scotty/internal/cli" "go.uploadedlobster.com/scotty/internal/config" "go.uploadedlobster.com/scotty/internal/i18n" - "go.uploadedlobster.com/scotty/internal/models" ) var serviceAddCmd = &cobra.Command{ @@ -95,7 +95,7 @@ func init() { } func promptForAuth(service config.ServiceConfig) error { - backend, err := backends.ResolveBackend[models.OAuth2Authenticator](service) + backend, err := backends.ResolveBackend[auth.OAuth2Authenticator](service) if err != nil { // No authentication required, return return nil diff --git a/cmd/service_auth.go b/cmd/service_auth.go index ddab35d..0a075c5 100644 --- a/cmd/service_auth.go +++ b/cmd/service_auth.go @@ -18,9 +18,9 @@ package cmd import ( "github.com/spf13/cobra" + "go.uploadedlobster.com/scotty/internal/auth" "go.uploadedlobster.com/scotty/internal/backends" "go.uploadedlobster.com/scotty/internal/cli" - "go.uploadedlobster.com/scotty/internal/models" ) var serviceAuthCmd = &cobra.Command{ @@ -33,7 +33,7 @@ multiple services using the same backend but different authentication.`, Run: func(cmd *cobra.Command, args []string) { serviceConfig, err := cli.SelectService(cmd) cobra.CheckErr(err) - backend, err := backends.ResolveBackend[models.OAuth2Authenticator](serviceConfig) + backend, err := backends.ResolveBackend[auth.OAuth2Authenticator](serviceConfig) cobra.CheckErr(err) cli.AuthenticationFlow(serviceConfig, backend) }, diff --git a/internal/auth/auth.go b/internal/auth/auth.go new file mode 100644 index 0000000..5ba05af --- /dev/null +++ b/internal/auth/auth.go @@ -0,0 +1,34 @@ +/* +Copyright © 2023 Philipp Wolfer + +Scotty is free software: you can redistribute it and/or modify it under the +terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later version. + +Scotty is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +Scotty. If not, see . +*/ + +package auth + +import ( + "net/url" + + "go.uploadedlobster.com/scotty/internal/models" + "golang.org/x/oauth2" +) + +// Must be implemented by backends requiring OAuth2 authentication +type OAuth2Authenticator interface { + models.Backend + + // Returns OAuth2 config suitable for this backend + OAuth2Strategy(redirectUrl *url.URL) OAuth2Strategy + + // Setup the OAuth2 client + OAuth2Setup(token oauth2.TokenSource) error +} diff --git a/internal/backends/auth.go b/internal/backends/auth.go index d27efd6..c17e9ba 100644 --- a/internal/backends/auth.go +++ b/internal/backends/auth.go @@ -37,7 +37,7 @@ func BuildRedirectURL(config *viper.Viper, backend string) (*url.URL, error) { } func Authenticate(service string, backend models.Backend, db storage.Database, config *viper.Viper) (bool, error) { - authenticator, needAuth := backend.(models.OAuth2Authenticator) + authenticator, needAuth := backend.(auth.OAuth2Authenticator) if needAuth { redirectURL, err := BuildRedirectURL(config, backend.Name()) if err != nil { diff --git a/internal/cli/auth.go b/internal/cli/auth.go index fc5c889..828651a 100644 --- a/internal/cli/auth.go +++ b/internal/cli/auth.go @@ -26,12 +26,11 @@ import ( "go.uploadedlobster.com/scotty/internal/backends" "go.uploadedlobster.com/scotty/internal/config" "go.uploadedlobster.com/scotty/internal/i18n" - "go.uploadedlobster.com/scotty/internal/models" "go.uploadedlobster.com/scotty/internal/storage" "golang.org/x/oauth2" ) -func AuthenticationFlow(service config.ServiceConfig, backend models.OAuth2Authenticator) { +func AuthenticationFlow(service config.ServiceConfig, backend auth.OAuth2Authenticator) { redirectURL, err := backends.BuildRedirectURL(viper.GetViper(), backend.Name()) cobra.CheckErr(err) diff --git a/internal/models/interfaces.go b/internal/models/interfaces.go index cc52ead..cc19d8d 100644 --- a/internal/models/interfaces.go +++ b/internal/models/interfaces.go @@ -17,12 +17,10 @@ Scotty. If not, see . package models import ( - "net/url" "time" - "go.uploadedlobster.com/scotty/internal/auth" + // "go.uploadedlobster.com/scotty/internal/auth" "go.uploadedlobster.com/scotty/internal/config" - "golang.org/x/oauth2" ) // A listen service backend. @@ -85,14 +83,3 @@ type LovesImport interface { // Imports the given list of loves. ImportLoves(export LovesResult, importResult ImportResult, progress chan Progress) (ImportResult, error) } - -// Must be implemented by backends requiring OAuth2 authentication -type OAuth2Authenticator interface { - Backend - - // Returns OAuth2 config suitable for this backend - OAuth2Strategy(redirectUrl *url.URL) auth.OAuth2Strategy - - // Setup the OAuth2 client - OAuth2Setup(token oauth2.TokenSource) error -} From c4587b80aff3018e41ffc1276807a6763d75d2fd Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 10 Dec 2023 14:22:38 +0100 Subject: [PATCH 07/82] Introduce models.Entity type --- internal/backends/backends_test.go | 2 +- internal/cli/transfer.go | 4 ++-- internal/models/models.go | 6 ++++++ internal/storage/database.go | 5 +++-- internal/storage/database_test.go | 3 ++- internal/storage/models.go | 7 ++++--- 6 files changed, 18 insertions(+), 9 deletions(-) diff --git a/internal/backends/backends_test.go b/internal/backends/backends_test.go index ee71e6e..4d758eb 100644 --- a/internal/backends/backends_test.go +++ b/internal/backends/backends_test.go @@ -76,7 +76,7 @@ func TestGetBackends(t *testing.T) { } // If we got here the "dump" backend was not included - t.Errorf("GetBackends() did not return expected bacend \"dump\"") + t.Errorf("GetBackends() did not return expected backend \"dump\"") } func TestImplementsInterfaces(t *testing.T) { diff --git a/internal/cli/transfer.go b/internal/cli/transfer.go index 3816d96..5683bb6 100644 --- a/internal/cli/transfer.go +++ b/internal/cli/transfer.go @@ -38,7 +38,7 @@ func NewTransferCmd[ ]( cmd *cobra.Command, db *storage.Database, - entity string, + entity models.Entity, source string, target string, ) (TransferCmd[E, I, R], error) { @@ -57,7 +57,7 @@ func NewTransferCmd[ type TransferCmd[E models.Backend, I models.ImportBackend, R models.ListensResult | models.LovesResult] struct { cmd *cobra.Command db *storage.Database - entity string + entity models.Entity sourceName string targetName string ExpBackend E diff --git a/internal/models/models.go b/internal/models/models.go index 086b922..01830cb 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -27,6 +27,12 @@ import ( ) type MBID string +type Entity string + +const ( + Listens Entity = "listens" + Loves Entity = "loves" +) type AdditionalInfo map[string]any diff --git a/internal/storage/database.go b/internal/storage/database.go index 1548feb..d31a176 100644 --- a/internal/storage/database.go +++ b/internal/storage/database.go @@ -24,6 +24,7 @@ import ( "time" "github.com/glebarez/sqlite" + "go.uploadedlobster.com/scotty/internal/models" "golang.org/x/oauth2" "gorm.io/datatypes" "gorm.io/gorm" @@ -54,7 +55,7 @@ func New(dsn string) (db Database, err error) { return } -func (db Database) GetImportTimestamp(source string, target string, entity string) (time.Time, error) { +func (db Database) GetImportTimestamp(source string, target string, entity models.Entity) (time.Time, error) { result := ImportTimestamp{ SourceService: source, TargetService: target, @@ -64,7 +65,7 @@ func (db Database) GetImportTimestamp(source string, target string, entity strin return result.Timestamp, err } -func (db Database) SetImportTimestamp(source string, target string, entity string, timestamp time.Time) error { +func (db Database) SetImportTimestamp(source string, target string, entity models.Entity, timestamp time.Time) error { entry := ImportTimestamp{ SourceService: source, TargetService: target, diff --git a/internal/storage/database_test.go b/internal/storage/database_test.go index cd95149..606d334 100644 --- a/internal/storage/database_test.go +++ b/internal/storage/database_test.go @@ -23,6 +23,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.uploadedlobster.com/scotty/internal/models" "go.uploadedlobster.com/scotty/internal/storage" "golang.org/x/oauth2" ) @@ -33,7 +34,7 @@ func TestTimestampUpdate(t *testing.T) { source := "maloja" target := "funkwhale" - entity := "loves" + entity := models.Loves timestamp, err := db.GetImportTimestamp(source, target, entity) require.NoError(t, err) assert.Equal(t, time.Time{}, timestamp) diff --git a/internal/storage/models.go b/internal/storage/models.go index 9df85db..4b40e85 100644 --- a/internal/storage/models.go +++ b/internal/storage/models.go @@ -20,13 +20,14 @@ package storage import ( "time" + "go.uploadedlobster.com/scotty/internal/models" "gorm.io/datatypes" ) type ImportTimestamp struct { - SourceService string `gorm:"primaryKey"` - TargetService string `gorm:"primaryKey"` - Entity string `gorm:"primaryKey"` + SourceService string `gorm:"primaryKey"` + TargetService string `gorm:"primaryKey"` + Entity models.Entity `gorm:"primaryKey"` CreatedAt time.Time UpdatedAt time.Time Timestamp time.Time `gorm:"default:'1970-01-01T00:00:00'"` From 086bf256168e69e3ff89a4f06caafe1354595464 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 10 Dec 2023 14:25:05 +0100 Subject: [PATCH 08/82] update translations --- internal/translations/catalog.go | 22 +++++++++---------- .../translations/locales/de/out.gotext.json | 14 ++++++------ .../translations/locales/en/out.gotext.json | 18 +++++++-------- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/internal/translations/catalog.go b/internal/translations/catalog.go index 40dee94..3ceb6df 100644 --- a/internal/translations/catalog.go +++ b/internal/translations/catalog.go @@ -45,12 +45,12 @@ var messageKeyToIndex = map[string]int{ "Aborted": 15, "Access token": 26, "Access token received, you can use %v now.\n": 35, - "Append to file": 32, + "Append to file": 28, "Backend": 40, "Client ID": 22, "Client secret": 23, "Delete the service configuration \"%v\"?": 14, - "Disable auto correction of submitted listens": 30, + "Disable auto correction of submitted listens": 31, "During the import the following errors occurred:": 5, "Error: %v\n": 6, "Error: OAuth state mismatch": 34, @@ -59,10 +59,10 @@ var messageKeyToIndex = map[string]int{ "From timestamp: %v (%v)": 41, "Import failed, last reported timestamp was %v (%s)": 42, "Imported %v of %v %s into %v.": 4, - "Include skipped listens": 31, + "Include skipped listens": 32, "Latest timestamp: %v (%v)": 43, "No": 37, - "Playlist title": 28, + "Playlist title": 29, "Saved service %v using backend %v": 12, "Server URL": 24, "Service": 39, @@ -71,7 +71,7 @@ var messageKeyToIndex = map[string]int{ "The backend %v requires authentication. Authenticate now?": 13, "Token received, you can close this window now.": 19, "Transferring %s from %s to %s...": 3, - "Unique playlist identifier": 29, + "Unique playlist identifier": 30, "Updated service %v using backend %v\n": 17, "User name": 25, "Visit the URL for authorization: %v": 33, @@ -120,9 +120,9 @@ var enIndex = []uint32{ // 48 elements 0x000001c0, 0x000001dd, 0x0000020c, 0x00000220, 0x0000024f, 0x00000276, 0x0000028e, 0x00000298, 0x000002a6, 0x000002b1, 0x000002bb, 0x000002c8, - 0x000002d2, 0x000002e1, 0x000002fc, 0x00000329, + 0x000002d2, 0x000002e1, 0x000002f0, 0x0000030b, // Entry 20 - 3F - 0x00000341, 0x00000350, 0x00000377, 0x00000393, + 0x00000338, 0x00000350, 0x00000377, 0x00000393, 0x000003c6, 0x000003ca, 0x000003cd, 0x000003f0, 0x000003f8, 0x00000400, 0x0000041e, 0x00000457, 0x00000477, 0x000004aa, 0x000004cf, 0x000004f0, @@ -141,9 +141,9 @@ const enData string = "" + // Size: 1264 bytes "ing backend %[2]v\x04\x01\x09\x00\x0f\x02backend: %[1]v\x02Token receive" + "d, you can close this window now.\x02backend %[1]s does not implement %[" + "2]s\x02unknown backend \x22%[1]s\x22\x02Client ID\x02Client secret\x02Se" + - "rver URL\x02User name\x02Access token\x02File path\x02Playlist title\x02" + - "Unique playlist identifier\x02Disable auto correction of submitted liste" + - "ns\x02Include skipped listens\x02Append to file\x02Visit the URL for aut" + + "rver URL\x02User name\x02Access token\x02File path\x02Append to file\x02" + + "Playlist title\x02Unique playlist identifier\x02Disable auto correction " + + "of submitted listens\x02Include skipped listens\x02Visit the URL for aut" + "horization: %[1]v\x02Error: OAuth state mismatch\x04\x00\x01\x0a.\x02Acc" + "ess token received, you can use %[1]v now.\x02Yes\x02No\x02no existing s" + "ervice configurations\x02Service\x02Backend\x02From timestamp: %[1]v (%[" + @@ -152,4 +152,4 @@ const enData string = "" + // Size: 1264 bytes "te config\x02key must only consist of A-Za-z0-9_-\x02no service configur" + "ation \x22%[1]v\x22" - // Total table size 1883 bytes (1KiB); checksum: 6875B9DE + // Total table size 1883 bytes (1KiB); checksum: 4C56F9E5 diff --git a/internal/translations/locales/de/out.gotext.json b/internal/translations/locales/de/out.gotext.json index b608d02..a6059b7 100644 --- a/internal/translations/locales/de/out.gotext.json +++ b/internal/translations/locales/de/out.gotext.json @@ -240,6 +240,11 @@ "message": "File path", "translation": "" }, + { + "id": "Append to file", + "message": "Append to file", + "translation": "" + }, { "id": "Playlist title", "message": "Playlist title", @@ -260,11 +265,6 @@ "message": "Include skipped listens", "translation": "" }, - { - "id": "Append to file", - "message": "Append to file", - "translation": "" - }, { "id": "Visit the URL for authorization: {Url}", "message": "Visit the URL for authorization: {Url}", @@ -354,7 +354,7 @@ { "id": "Entity", "string": "%[1]s", - "type": "string", + "type": "go.uploadedlobster.com/scotty/internal/models.Entity", "underlyingType": "string", "argNum": 1, "expr": "c.entity" @@ -443,7 +443,7 @@ { "id": "Entity", "string": "%[3]s", - "type": "string", + "type": "go.uploadedlobster.com/scotty/internal/models.Entity", "underlyingType": "string", "argNum": 3, "expr": "c.entity" diff --git a/internal/translations/locales/en/out.gotext.json b/internal/translations/locales/en/out.gotext.json index 170000e..fb588de 100644 --- a/internal/translations/locales/en/out.gotext.json +++ b/internal/translations/locales/en/out.gotext.json @@ -282,6 +282,13 @@ "translatorComment": "Copied from source.", "fuzzy": true }, + { + "id": "Append to file", + "message": "Append to file", + "translation": "Append to file", + "translatorComment": "Copied from source.", + "fuzzy": true + }, { "id": "Playlist title", "message": "Playlist title", @@ -310,13 +317,6 @@ "translatorComment": "Copied from source.", "fuzzy": true }, - { - "id": "Append to file", - "message": "Append to file", - "translation": "Append to file", - "translatorComment": "Copied from source.", - "fuzzy": true - }, { "id": "Visit the URL for authorization: {Url}", "message": "Visit the URL for authorization: {Url}", @@ -423,7 +423,7 @@ { "id": "Entity", "string": "%[1]s", - "type": "string", + "type": "go.uploadedlobster.com/scotty/internal/models.Entity", "underlyingType": "string", "argNum": 1, "expr": "c.entity" @@ -518,7 +518,7 @@ { "id": "Entity", "string": "%[3]s", - "type": "string", + "type": "go.uploadedlobster.com/scotty/internal/models.Entity", "underlyingType": "string", "argNum": 3, "expr": "c.entity" From 78baba815432b79c0427f60965095d99cbd08551 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 10 Dec 2023 14:22:38 +0100 Subject: [PATCH 09/82] Introduce models.Entity type --- internal/models/models.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/internal/models/models.go b/internal/models/models.go index 01830cb..30b282a 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -34,6 +34,13 @@ const ( Loves Entity = "loves" ) +type Entity string + +const ( + Listens Entity = "listens" + Loves Entity = "loves" +) + type AdditionalInfo map[string]any type Track struct { From 53f7dbb5682a79f26fea7cce41bd35fb7e38ed7f Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 10 Dec 2023 14:48:38 +0100 Subject: [PATCH 10/82] update translations for weblate --- .../locales/de/messages.gotext.json | 398 ++++++++++++-- .../locales/en/messages.gotext.json | 501 ++++++++++++++++-- 2 files changed, 802 insertions(+), 97 deletions(-) diff --git a/internal/translations/locales/de/messages.gotext.json b/internal/translations/locales/de/messages.gotext.json index 0c41b77..a7a0848 100644 --- a/internal/translations/locales/de/messages.gotext.json +++ b/internal/translations/locales/de/messages.gotext.json @@ -2,18 +2,267 @@ "language": "de", "messages": [ { - "id": "Authenticate a service", - "message": "Authenticate a service", - "translation": "An einem Service anmelden" + "id": "export: {ExportCapabilities__}", + "message": "export: {ExportCapabilities__}", + "translation": "", + "placeholders": [ + { + "id": "ExportCapabilities__", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "strings.Join(info.ExportCapabilities, \", \")" + } + ] }, { - "id": "For backends requiring authentication this command can be used to authenticate.\n\nAuthentication is always done per configured service. That means you can have\nmultiple services using the same backend but different authentication.", - "message": "For backends requiring authentication this command can be used to authenticate.\n\nAuthentication is always done per configured service. That means you can have\nmultiple services using the same backend but different authentication.", + "id": "import: {ImportCapabilities__}", + "message": "import: {ImportCapabilities__}", + "translation": "", + "placeholders": [ + { + "id": "ImportCapabilities__", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "strings.Join(info.ImportCapabilities, \", \")" + } + ] + }, + { + "id": "Failed reading config: {Err}", + "message": "Failed reading config: {Err}", + "translation": "", + "placeholders": [ + { + "id": "Err", + "string": "%[1]v", + "type": "error", + "underlyingType": "interface{Error() string}", + "argNum": 1, + "expr": "err" + } + ] + }, + { + "id": "Service name", + "message": "Service name", "translation": "" }, { - "id": "failed loading service configuration", - "message": "failed loading service configuration", + "id": "a service with this name already exists", + "message": "a service with this name already exists", + "translation": "" + }, + { + "id": "Saved service {Name} using backend {Backend}", + "message": "Saved service {Name} using backend {Backend}", + "translation": "", + "placeholders": [ + { + "id": "Name", + "string": "%[1]v", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "service.Name" + }, + { + "id": "Backend", + "string": "%[2]v", + "type": "string", + "underlyingType": "string", + "argNum": 2, + "expr": "service.Backend" + } + ] + }, + { + "id": "The backend {Backend} requires authentication. Authenticate now?", + "message": "The backend {Backend} requires authentication. Authenticate now?", + "translation": "", + "placeholders": [ + { + "id": "Backend", + "string": "%[1]v", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "service.Backend" + } + ] + }, + { + "id": "Delete the service configuration \"{Service}\"?", + "message": "Delete the service configuration \"{Service}\"?", + "translation": "", + "placeholders": [ + { + "id": "Service", + "string": "%[1]v", + "type": "go.uploadedlobster.com/scotty/internal/config.ServiceConfig", + "underlyingType": "struct{Name string; Backend string; ConfigValues map[string]any}", + "argNum": 1, + "expr": "service" + } + ] + }, + { + "id": "Aborted", + "message": "Aborted", + "translation": "" + }, + { + "id": "Service \"{Name}\" deleted", + "message": "Service \"{Name}\" deleted", + "translation": "", + "placeholders": [ + { + "id": "Name", + "string": "%[1]v", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "service.Name" + } + ] + }, + { + "id": "Updated service {Name} using backend {Backend}", + "message": "Updated service {Name} using backend {Backend}", + "translation": "", + "placeholders": [ + { + "id": "Name", + "string": "%[1]v", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "service.Name" + }, + { + "id": "Backend", + "string": "%[2]v", + "type": "string", + "underlyingType": "string", + "argNum": 2, + "expr": "service.Backend" + } + ] + }, + { + "id": "backend: {Backend}", + "message": "backend: {Backend}", + "translation": "", + "placeholders": [ + { + "id": "Backend", + "string": "%[1]v", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "s.Backend" + } + ] + }, + { + "id": "Token received, you can close this window now.", + "message": "Token received, you can close this window now.", + "translation": "" + }, + { + "id": "backend {Backend} does not implement {InterfaceName}", + "message": "backend {Backend} does not implement {InterfaceName}", + "translation": "", + "placeholders": [ + { + "id": "Backend", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "config.Backend" + }, + { + "id": "InterfaceName", + "string": "%[2]s", + "type": "string", + "underlyingType": "string", + "argNum": 2, + "expr": "interfaceName" + } + ] + }, + { + "id": "unknown backend \"{BackendName}\"", + "message": "unknown backend \"{BackendName}\"", + "translation": "", + "placeholders": [ + { + "id": "BackendName", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "backendName" + } + ] + }, + { + "id": "Client ID", + "message": "Client ID", + "translation": "" + }, + { + "id": "Client secret", + "message": "Client secret", + "translation": "" + }, + { + "id": "Server URL", + "message": "Server URL", + "translation": "" + }, + { + "id": "User name", + "message": "User name", + "translation": "" + }, + { + "id": "Access token", + "message": "Access token", + "translation": "" + }, + { + "id": "File path", + "message": "File path", + "translation": "" + }, + { + "id": "Append to file", + "message": "Append to file", + "translation": "" + }, + { + "id": "Playlist title", + "message": "Playlist title", + "translation": "" + }, + { + "id": "Unique playlist identifier", + "message": "Unique playlist identifier", + "translation": "" + }, + { + "id": "Disable auto correction of submitted listens", + "message": "Disable auto correction of submitted listens", + "translation": "" + }, + { + "id": "Include skipped listens", + "message": "Include skipped listens", "translation": "" }, { @@ -47,15 +296,10 @@ "type": "string", "underlyingType": "string", "argNum": 1, - "expr": "serviceConfig.Name" + "expr": "service.Name" } ] }, - { - "id": "service configuration (required)", - "message": "service configuration (required)", - "translation": "Servicekonfiguration (notwendig)" - }, { "id": "exporting", "message": "exporting", @@ -77,6 +321,31 @@ "translatorComment": "Copied from source.", "fuzzy": true }, + { + "id": "Yes", + "message": "Yes", + "translation": "" + }, + { + "id": "No", + "message": "No", + "translation": "" + }, + { + "id": "no existing service configurations", + "message": "no existing service configurations", + "translation": "" + }, + { + "id": "Service", + "message": "Service", + "translation": "" + }, + { + "id": "Backend", + "message": "Backend", + "translation": "" + }, { "id": "Transferring {Entity} from {SourceName} to {TargetName}...", "message": "Transferring {Entity} from {SourceName} to {TargetName}...", @@ -85,7 +354,7 @@ { "id": "Entity", "string": "%[1]s", - "type": "string", + "type": "go.uploadedlobster.com/scotty/internal/models.Entity", "underlyingType": "string", "argNum": 1, "expr": "c.entity" @@ -109,48 +378,44 @@ ] }, { - "id": "From timestamp: {Timestamp} ({Unix})", - "message": "From timestamp: {Timestamp} ({Unix})", - "translation": "Ab Zeitstempel: {Timestamp} ({Unix})", + "id": "From timestamp: {Arg_1} ({Arg_2})", + "message": "From timestamp: {Arg_1} ({Arg_2})", + "translation": "", "placeholders": [ { - "id": "Timestamp", + "id": "Arg_1", "string": "%[1]v", - "type": "time.Time", - "underlyingType": "struct{wall uint64; ext int64; loc *time.Location}", - "argNum": 1, - "expr": "timestamp" + "type": "", + "underlyingType": "interface{}", + "argNum": 1 }, { - "id": "Unix", + "id": "Arg_2", "string": "%[2]v", - "type": "int64", - "underlyingType": "int64", - "argNum": 2, - "expr": "timestamp.Unix()" + "type": "", + "underlyingType": "interface{}", + "argNum": 2 } ] }, { - "id": "Import failed, last reported timestamp was {LastTimestamp} ({Unix})", - "message": "Import failed, last reported timestamp was {LastTimestamp} ({Unix})", - "translation": "Import fehlgeschlagen, der letzte Zeitstempel war {LastTimestamp} ({Unix})", + "id": "Import failed, last reported timestamp was {Arg_1} ({Arg_2})", + "message": "Import failed, last reported timestamp was {Arg_1} ({Arg_2})", + "translation": "", "placeholders": [ { - "id": "LastTimestamp", + "id": "Arg_1", "string": "%[1]v", - "type": "time.Time", - "underlyingType": "struct{wall uint64; ext int64; loc *time.Location}", - "argNum": 1, - "expr": "result.LastTimestamp" + "type": "", + "underlyingType": "interface{}", + "argNum": 1 }, { - "id": "Unix", - "string": "%[2]v", - "type": "int64", - "underlyingType": "int64", - "argNum": 2, - "expr": "result.LastTimestamp.Unix()" + "id": "Arg_2", + "string": "%[2]s", + "type": "", + "underlyingType": "string", + "argNum": 2 } ] }, @@ -178,7 +443,7 @@ { "id": "Entity", "string": "%[3]s", - "type": "string", + "type": "go.uploadedlobster.com/scotty/internal/models.Entity", "underlyingType": "string", "argNum": 3, "expr": "c.entity" @@ -214,25 +479,48 @@ ] }, { - "id": "Latest timestamp: {LastTimestamp} ({Unix})", - "message": "Latest timestamp: {LastTimestamp} ({Unix})", - "translation": "Neuester Zeitstempel: {LastTimestamp} ({Unix})", + "id": "Latest timestamp: {Arg_1} ({Arg_2})", + "message": "Latest timestamp: {Arg_1} ({Arg_2})", + "translation": "", "placeholders": [ { - "id": "LastTimestamp", + "id": "Arg_1", "string": "%[1]v", - "type": "time.Time", - "underlyingType": "struct{wall uint64; ext int64; loc *time.Location}", - "argNum": 1, - "expr": "result.LastTimestamp" + "type": "", + "underlyingType": "interface{}", + "argNum": 1 }, { - "id": "Unix", + "id": "Arg_2", "string": "%[2]v", - "type": "int64", - "underlyingType": "int64", - "argNum": 2, - "expr": "result.LastTimestamp.Unix()" + "type": "", + "underlyingType": "interface{}", + "argNum": 2 + } + ] + }, + { + "id": "no configuration file defined, cannot write config", + "message": "no configuration file defined, cannot write config", + "translation": "" + }, + { + "id": "key must only consist of A-Za-z0-9_-", + "message": "key must only consist of A-Za-z0-9_-", + "translation": "" + }, + { + "id": "no service configuration \"{Name}\"", + "message": "no service configuration \"{Name}\"", + "translation": "", + "placeholders": [ + { + "id": "Name", + "string": "%[1]v", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "name" } ] } diff --git a/internal/translations/locales/en/messages.gotext.json b/internal/translations/locales/en/messages.gotext.json index 0b8d9c4..03ce260 100644 --- a/internal/translations/locales/en/messages.gotext.json +++ b/internal/translations/locales/en/messages.gotext.json @@ -1,6 +1,363 @@ { "language": "en", "messages": [ + { + "id": "export: {ExportCapabilities__}", + "message": "export: {ExportCapabilities__}", + "translation": "export: {ExportCapabilities__}", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "ExportCapabilities__", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "strings.Join(info.ExportCapabilities, \", \")" + } + ], + "fuzzy": true + }, + { + "id": "import: {ImportCapabilities__}", + "message": "import: {ImportCapabilities__}", + "translation": "import: {ImportCapabilities__}", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "ImportCapabilities__", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "strings.Join(info.ImportCapabilities, \", \")" + } + ], + "fuzzy": true + }, + { + "id": "Failed reading config: {Err}", + "message": "Failed reading config: {Err}", + "translation": "Failed reading config: {Err}", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Err", + "string": "%[1]v", + "type": "error", + "underlyingType": "interface{Error() string}", + "argNum": 1, + "expr": "err" + } + ], + "fuzzy": true + }, + { + "id": "Service name", + "message": "Service name", + "translation": "Service name", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "a service with this name already exists", + "message": "a service with this name already exists", + "translation": "a service with this name already exists", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Saved service {Name} using backend {Backend}", + "message": "Saved service {Name} using backend {Backend}", + "translation": "Saved service {Name} using backend {Backend}", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Name", + "string": "%[1]v", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "service.Name" + }, + { + "id": "Backend", + "string": "%[2]v", + "type": "string", + "underlyingType": "string", + "argNum": 2, + "expr": "service.Backend" + } + ], + "fuzzy": true + }, + { + "id": "The backend {Backend} requires authentication. Authenticate now?", + "message": "The backend {Backend} requires authentication. Authenticate now?", + "translation": "The backend {Backend} requires authentication. Authenticate now?", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Backend", + "string": "%[1]v", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "service.Backend" + } + ], + "fuzzy": true + }, + { + "id": "Delete the service configuration \"{Service}\"?", + "message": "Delete the service configuration \"{Service}\"?", + "translation": "Delete the service configuration \"{Service}\"?", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Service", + "string": "%[1]v", + "type": "go.uploadedlobster.com/scotty/internal/config.ServiceConfig", + "underlyingType": "struct{Name string; Backend string; ConfigValues map[string]any}", + "argNum": 1, + "expr": "service" + } + ], + "fuzzy": true + }, + { + "id": "Aborted", + "message": "Aborted", + "translation": "Aborted", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Service \"{Name}\" deleted", + "message": "Service \"{Name}\" deleted", + "translation": "Service \"{Name}\" deleted", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Name", + "string": "%[1]v", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "service.Name" + } + ], + "fuzzy": true + }, + { + "id": "Updated service {Name} using backend {Backend}", + "message": "Updated service {Name} using backend {Backend}", + "translation": "Updated service {Name} using backend {Backend}", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Name", + "string": "%[1]v", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "service.Name" + }, + { + "id": "Backend", + "string": "%[2]v", + "type": "string", + "underlyingType": "string", + "argNum": 2, + "expr": "service.Backend" + } + ], + "fuzzy": true + }, + { + "id": "backend: {Backend}", + "message": "backend: {Backend}", + "translation": "backend: {Backend}", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Backend", + "string": "%[1]v", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "s.Backend" + } + ], + "fuzzy": true + }, + { + "id": "Token received, you can close this window now.", + "message": "Token received, you can close this window now.", + "translation": "Token received, you can close this window now.", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "backend {Backend} does not implement {InterfaceName}", + "message": "backend {Backend} does not implement {InterfaceName}", + "translation": "backend {Backend} does not implement {InterfaceName}", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Backend", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "config.Backend" + }, + { + "id": "InterfaceName", + "string": "%[2]s", + "type": "string", + "underlyingType": "string", + "argNum": 2, + "expr": "interfaceName" + } + ], + "fuzzy": true + }, + { + "id": "unknown backend \"{BackendName}\"", + "message": "unknown backend \"{BackendName}\"", + "translation": "unknown backend \"{BackendName}\"", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "BackendName", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "backendName" + } + ], + "fuzzy": true + }, + { + "id": "Client ID", + "message": "Client ID", + "translation": "Client ID", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Client secret", + "message": "Client secret", + "translation": "Client secret", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Server URL", + "message": "Server URL", + "translation": "Server URL", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "User name", + "message": "User name", + "translation": "User name", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Access token", + "message": "Access token", + "translation": "Access token", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "File path", + "message": "File path", + "translation": "File path", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Append to file", + "message": "Append to file", + "translation": "Append to file", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Playlist title", + "message": "Playlist title", + "translation": "Playlist title", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Unique playlist identifier", + "message": "Unique playlist identifier", + "translation": "Unique playlist identifier", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Disable auto correction of submitted listens", + "message": "Disable auto correction of submitted listens", + "translation": "Disable auto correction of submitted listens", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Include skipped listens", + "message": "Include skipped listens", + "translation": "Include skipped listens", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Visit the URL for authorization: {Url}", + "message": "Visit the URL for authorization: {Url}", + "translation": "Visit the URL for authorization: {Url}", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Url", + "string": "%[1]v", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "authUrl.Url" + } + ], + "fuzzy": true + }, + { + "id": "Error: OAuth state mismatch", + "message": "Error: OAuth state mismatch", + "translation": "Error: OAuth state mismatch", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Access token received, you can use {Name} now.", + "message": "Access token received, you can use {Name} now.", + "translation": "Access token received, you can use {Name} now.", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Name", + "string": "%[1]v", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "service.Name" + } + ], + "fuzzy": true + }, { "id": "exporting", "message": "exporting", @@ -22,6 +379,41 @@ "translatorComment": "Copied from source.", "fuzzy": true }, + { + "id": "Yes", + "message": "Yes", + "translation": "Yes", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "No", + "message": "No", + "translation": "No", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "no existing service configurations", + "message": "no existing service configurations", + "translation": "no existing service configurations", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Service", + "message": "Service", + "translation": "Service", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Backend", + "message": "Backend", + "translation": "Backend", + "translatorComment": "Copied from source.", + "fuzzy": true + }, { "id": "Transferring {Entity} from {SourceName} to {TargetName}...", "message": "Transferring {Entity} from {SourceName} to {TargetName}...", @@ -31,7 +423,7 @@ { "id": "Entity", "string": "%[1]s", - "type": "string", + "type": "go.uploadedlobster.com/scotty/internal/models.Entity", "underlyingType": "string", "argNum": 1, "expr": "c.entity" @@ -56,51 +448,47 @@ "fuzzy": true }, { - "id": "From timestamp: {Timestamp} ({Unix})", - "message": "From timestamp: {Timestamp} ({Unix})", - "translation": "From timestamp: {Timestamp} ({Unix})", + "id": "From timestamp: {Arg_1} ({Arg_2})", + "message": "From timestamp: {Arg_1} ({Arg_2})", + "translation": "From timestamp: {Arg_1} ({Arg_2})", "translatorComment": "Copied from source.", "placeholders": [ { - "id": "Timestamp", + "id": "Arg_1", "string": "%[1]v", - "type": "time.Time", - "underlyingType": "struct{wall uint64; ext int64; loc *time.Location}", - "argNum": 1, - "expr": "timestamp" + "type": "", + "underlyingType": "interface{}", + "argNum": 1 }, { - "id": "Unix", + "id": "Arg_2", "string": "%[2]v", - "type": "int64", - "underlyingType": "int64", - "argNum": 2, - "expr": "timestamp.Unix()" + "type": "", + "underlyingType": "interface{}", + "argNum": 2 } ], "fuzzy": true }, { - "id": "Import failed, last reported timestamp was {LastTimestamp} ({Unix})", - "message": "Import failed, last reported timestamp was {LastTimestamp} ({Unix})", - "translation": "Import failed, last reported timestamp was {LastTimestamp} ({Unix})", + "id": "Import failed, last reported timestamp was {Arg_1} ({Arg_2})", + "message": "Import failed, last reported timestamp was {Arg_1} ({Arg_2})", + "translation": "Import failed, last reported timestamp was {Arg_1} ({Arg_2})", "translatorComment": "Copied from source.", "placeholders": [ { - "id": "LastTimestamp", + "id": "Arg_1", "string": "%[1]v", - "type": "time.Time", - "underlyingType": "struct{wall uint64; ext int64; loc *time.Location}", - "argNum": 1, - "expr": "result.LastTimestamp" + "type": "", + "underlyingType": "interface{}", + "argNum": 1 }, { - "id": "Unix", - "string": "%[2]v", - "type": "int64", - "underlyingType": "int64", - "argNum": 2, - "expr": "result.LastTimestamp.Unix()" + "id": "Arg_2", + "string": "%[2]s", + "type": "", + "underlyingType": "string", + "argNum": 2 } ], "fuzzy": true @@ -130,7 +518,7 @@ { "id": "Entity", "string": "%[3]s", - "type": "string", + "type": "go.uploadedlobster.com/scotty/internal/models.Entity", "underlyingType": "string", "argNum": 3, "expr": "c.entity" @@ -171,26 +559,55 @@ "fuzzy": true }, { - "id": "Latest timestamp: {LastTimestamp} ({Unix})", - "message": "Latest timestamp: {LastTimestamp} ({Unix})", - "translation": "Latest timestamp: {LastTimestamp} ({Unix})", + "id": "Latest timestamp: {Arg_1} ({Arg_2})", + "message": "Latest timestamp: {Arg_1} ({Arg_2})", + "translation": "Latest timestamp: {Arg_1} ({Arg_2})", "translatorComment": "Copied from source.", "placeholders": [ { - "id": "LastTimestamp", + "id": "Arg_1", "string": "%[1]v", - "type": "time.Time", - "underlyingType": "struct{wall uint64; ext int64; loc *time.Location}", - "argNum": 1, - "expr": "result.LastTimestamp" + "type": "", + "underlyingType": "interface{}", + "argNum": 1 }, { - "id": "Unix", + "id": "Arg_2", "string": "%[2]v", - "type": "int64", - "underlyingType": "int64", - "argNum": 2, - "expr": "result.LastTimestamp.Unix()" + "type": "", + "underlyingType": "interface{}", + "argNum": 2 + } + ], + "fuzzy": true + }, + { + "id": "no configuration file defined, cannot write config", + "message": "no configuration file defined, cannot write config", + "translation": "no configuration file defined, cannot write config", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "key must only consist of A-Za-z0-9_-", + "message": "key must only consist of A-Za-z0-9_-", + "translation": "key must only consist of A-Za-z0-9_-", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "no service configuration \"{Name}\"", + "message": "no service configuration \"{Name}\"", + "translation": "no service configuration \"{Name}\"", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Name", + "string": "%[1]v", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "name" } ], "fuzzy": true From ba4825aae937c59c514c2ce53ce05f93372afdad Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 10 Dec 2023 14:17:29 +0000 Subject: [PATCH 11/82] Translated using Weblate (German) Currently translated at 100.0% (47 of 47 strings) Co-authored-by: Philipp Wolfer Translate-URL: https://translate.uploadedlobster.com/projects/scotty/app/de/ Translation: Scotty/app --- .../locales/de/messages.gotext.json | 92 +++++++++---------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/internal/translations/locales/de/messages.gotext.json b/internal/translations/locales/de/messages.gotext.json index a7a0848..8d1e08f 100644 --- a/internal/translations/locales/de/messages.gotext.json +++ b/internal/translations/locales/de/messages.gotext.json @@ -4,7 +4,7 @@ { "id": "export: {ExportCapabilities__}", "message": "export: {ExportCapabilities__}", - "translation": "", + "translation": "Export: {ExportCapabilities__}", "placeholders": [ { "id": "ExportCapabilities__", @@ -19,7 +19,7 @@ { "id": "import: {ImportCapabilities__}", "message": "import: {ImportCapabilities__}", - "translation": "", + "translation": "Import: {ImportCapabilities__}", "placeholders": [ { "id": "ImportCapabilities__", @@ -34,7 +34,7 @@ { "id": "Failed reading config: {Err}", "message": "Failed reading config: {Err}", - "translation": "", + "translation": "Fehler beim Lesen der Konfiguration: {Err}", "placeholders": [ { "id": "Err", @@ -49,17 +49,17 @@ { "id": "Service name", "message": "Service name", - "translation": "" + "translation": "Servicename" }, { "id": "a service with this name already exists", "message": "a service with this name already exists", - "translation": "" + "translation": "ein Service mit diesem Namen existiert bereits" }, { "id": "Saved service {Name} using backend {Backend}", "message": "Saved service {Name} using backend {Backend}", - "translation": "", + "translation": "Service {Name} mit dem Backend {Backend} gespeichert", "placeholders": [ { "id": "Name", @@ -82,7 +82,7 @@ { "id": "The backend {Backend} requires authentication. Authenticate now?", "message": "The backend {Backend} requires authentication. Authenticate now?", - "translation": "", + "translation": "Das Backend {Backend} erfordert Authentifizierung. Jetzt authentifizieren?", "placeholders": [ { "id": "Backend", @@ -97,7 +97,7 @@ { "id": "Delete the service configuration \"{Service}\"?", "message": "Delete the service configuration \"{Service}\"?", - "translation": "", + "translation": "Die Servicekonfiguration „{Service}“ löschen?", "placeholders": [ { "id": "Service", @@ -112,12 +112,12 @@ { "id": "Aborted", "message": "Aborted", - "translation": "" + "translation": "Abgebrochen" }, { "id": "Service \"{Name}\" deleted", "message": "Service \"{Name}\" deleted", - "translation": "", + "translation": "Service „{Name}“ gelöscht", "placeholders": [ { "id": "Name", @@ -132,7 +132,7 @@ { "id": "Updated service {Name} using backend {Backend}", "message": "Updated service {Name} using backend {Backend}", - "translation": "", + "translation": "Service {Name} mit dem Backend {Backend} aktualisiert", "placeholders": [ { "id": "Name", @@ -155,7 +155,7 @@ { "id": "backend: {Backend}", "message": "backend: {Backend}", - "translation": "", + "translation": "Backend: {Backend}", "placeholders": [ { "id": "Backend", @@ -170,12 +170,12 @@ { "id": "Token received, you can close this window now.", "message": "Token received, you can close this window now.", - "translation": "" + "translation": "Token erhalten, das Fenster kann jetzt geschlossen werden." }, { "id": "backend {Backend} does not implement {InterfaceName}", "message": "backend {Backend} does not implement {InterfaceName}", - "translation": "", + "translation": "das backend {Backend} implementiert {InterfaceName} nicht", "placeholders": [ { "id": "Backend", @@ -198,7 +198,7 @@ { "id": "unknown backend \"{BackendName}\"", "message": "unknown backend \"{BackendName}\"", - "translation": "", + "translation": "unbekanntes Backend „{BackendName}“", "placeholders": [ { "id": "BackendName", @@ -213,62 +213,62 @@ { "id": "Client ID", "message": "Client ID", - "translation": "" + "translation": "Client-ID" }, { "id": "Client secret", "message": "Client secret", - "translation": "" + "translation": "Client-Secret" }, { "id": "Server URL", "message": "Server URL", - "translation": "" + "translation": "Server-URL" }, { "id": "User name", "message": "User name", - "translation": "" + "translation": "Benutzername" }, { "id": "Access token", "message": "Access token", - "translation": "" + "translation": "Zugriffstoken" }, { "id": "File path", "message": "File path", - "translation": "" + "translation": "Dateipfad" }, { "id": "Append to file", "message": "Append to file", - "translation": "" + "translation": "An Datei anhängen" }, { "id": "Playlist title", "message": "Playlist title", - "translation": "" + "translation": "Titel der Playlist" }, { "id": "Unique playlist identifier", "message": "Unique playlist identifier", - "translation": "" + "translation": "Eindeutige Playlist-ID" }, { "id": "Disable auto correction of submitted listens", "message": "Disable auto correction of submitted listens", - "translation": "" + "translation": "Autokorrektur für übermittelte Titel deaktivieren" }, { "id": "Include skipped listens", "message": "Include skipped listens", - "translation": "" + "translation": "Übersprungene Titel einbeziehen" }, { "id": "Visit the URL for authorization: {Url}", "message": "Visit the URL for authorization: {Url}", - "translation": "", + "translation": "URL für Autorisierung öffnen: {Url}", "placeholders": [ { "id": "Url", @@ -283,12 +283,12 @@ { "id": "Error: OAuth state mismatch", "message": "Error: OAuth state mismatch", - "translation": "" + "translation": "Fehler: OAuth-State stimmt nicht überein" }, { "id": "Access token received, you can use {Name} now.", "message": "Access token received, you can use {Name} now.", - "translation": "", + "translation": "Zugriffstoken erhalten, {Name} kann jetzt verwendet werden.", "placeholders": [ { "id": "Name", @@ -303,48 +303,48 @@ { "id": "exporting", "message": "exporting", - "translation": "exportiere", "translatorComment": "Copied from source.", - "fuzzy": true + "fuzzy": true, + "translation": "exportiere" }, { "id": "importing", "message": "importing", - "translation": "importiere", "translatorComment": "Copied from source.", - "fuzzy": true + "fuzzy": true, + "translation": "importiere" }, { "id": "done", "message": "done", - "translation": "fertig", "translatorComment": "Copied from source.", - "fuzzy": true + "fuzzy": true, + "translation": "fertig" }, { "id": "Yes", "message": "Yes", - "translation": "" + "translation": "Ja" }, { "id": "No", "message": "No", - "translation": "" + "translation": "Nein" }, { "id": "no existing service configurations", "message": "no existing service configurations", - "translation": "" + "translation": "keine bestehenden Servicekonfigurationen" }, { "id": "Service", "message": "Service", - "translation": "" + "translation": "Service" }, { "id": "Backend", "message": "Backend", - "translation": "" + "translation": "Backend" }, { "id": "Transferring {Entity} from {SourceName} to {TargetName}...", @@ -380,7 +380,7 @@ { "id": "From timestamp: {Arg_1} ({Arg_2})", "message": "From timestamp: {Arg_1} ({Arg_2})", - "translation": "", + "translation": "Ab Zeitstempel: {Arg_1} ({Arg_2})", "placeholders": [ { "id": "Arg_1", @@ -401,7 +401,7 @@ { "id": "Import failed, last reported timestamp was {Arg_1} ({Arg_2})", "message": "Import failed, last reported timestamp was {Arg_1} ({Arg_2})", - "translation": "", + "translation": "Import fehlgeschlagen, letzter Zeitstempel war {Arg_1} ({Arg_2})", "placeholders": [ { "id": "Arg_1", @@ -481,7 +481,7 @@ { "id": "Latest timestamp: {Arg_1} ({Arg_2})", "message": "Latest timestamp: {Arg_1} ({Arg_2})", - "translation": "", + "translation": "Letzter Zeitstempel: {Arg_1} ({Arg_2})", "placeholders": [ { "id": "Arg_1", @@ -502,17 +502,17 @@ { "id": "no configuration file defined, cannot write config", "message": "no configuration file defined, cannot write config", - "translation": "" + "translation": "keine Konfigurationsdatei definiert, Konfiguration kann nicht geschrieben werden" }, { "id": "key must only consist of A-Za-z0-9_-", "message": "key must only consist of A-Za-z0-9_-", - "translation": "" + "translation": "Schlüssel darf nur die Zeichen A-Za-z0-9_- beinhalten" }, { "id": "no service configuration \"{Name}\"", "message": "no service configuration \"{Name}\"", - "translation": "", + "translation": "keine Servicekonfiguration „{Name}“", "placeholders": [ { "id": "Name", From 788fa3828df32885247dd0e859d45a1bfd650bd9 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 10 Dec 2023 15:19:11 +0100 Subject: [PATCH 12/82] fixed redeclared Entity --- internal/models/models.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/internal/models/models.go b/internal/models/models.go index 30b282a..01830cb 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -34,13 +34,6 @@ const ( Loves Entity = "loves" ) -type Entity string - -const ( - Listens Entity = "listens" - Loves Entity = "loves" -) - type AdditionalInfo map[string]any type Track struct { From c6be6c558f14733974a96f6428170cacb8a1f140 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 10 Dec 2023 15:24:15 +0100 Subject: [PATCH 13/82] update translations --- internal/backends/backends_test.go | 5 +- internal/translations/catalog.go | 208 ++++++++++-------- .../translations/locales/de/out.gotext.json | 80 +++---- 3 files changed, 157 insertions(+), 136 deletions(-) diff --git a/internal/backends/backends_test.go b/internal/backends/backends_test.go index 4d758eb..b6a6968 100644 --- a/internal/backends/backends_test.go +++ b/internal/backends/backends_test.go @@ -35,6 +35,7 @@ import ( "go.uploadedlobster.com/scotty/internal/backends/spotify" "go.uploadedlobster.com/scotty/internal/backends/subsonic" "go.uploadedlobster.com/scotty/internal/config" + "go.uploadedlobster.com/scotty/internal/i18n" "go.uploadedlobster.com/scotty/internal/models" ) @@ -52,7 +53,7 @@ func TestResolveBackendUnknown(t *testing.T) { c.Set("backend", "foo") service := config.NewServiceConfig("test", c) _, err := backends.ResolveBackend[models.ListensImport](service) - assert.EqualError(t, err, "unknown backend \"foo\"") + assert.EqualError(t, err, i18n.Tr("unknown backend \"%s\"", "foo")) } func TestResolveBackendInvalidInterface(t *testing.T) { @@ -60,7 +61,7 @@ func TestResolveBackendInvalidInterface(t *testing.T) { c.Set("backend", "dump") service := config.NewServiceConfig("test", c) _, err := backends.ResolveBackend[models.ListensExport](service) - assert.EqualError(t, err, "backend dump does not implement ListensExport") + assert.EqualError(t, err, i18n.Tr("backend %s does not implement %s", "dump", "ListensExport")) } func TestGetBackends(t *testing.T) { diff --git a/internal/translations/catalog.go b/internal/translations/catalog.go index 3ceb6df..e91524f 100644 --- a/internal/translations/catalog.go +++ b/internal/translations/catalog.go @@ -39,117 +39,137 @@ func init() { } var messageKeyToIndex = map[string]int{ - "\tbackend: %v": 18, - "\texport: %s": 7, - "\timport: %s\n": 8, - "Aborted": 15, - "Access token": 26, - "Access token received, you can use %v now.\n": 35, - "Append to file": 28, - "Backend": 40, - "Client ID": 22, - "Client secret": 23, - "Delete the service configuration \"%v\"?": 14, - "Disable auto correction of submitted listens": 31, - "During the import the following errors occurred:": 5, - "Error: %v\n": 6, - "Error: OAuth state mismatch": 34, - "Failed reading config: %v": 9, - "File path": 27, - "From timestamp: %v (%v)": 41, - "Import failed, last reported timestamp was %v (%s)": 42, - "Imported %v of %v %s into %v.": 4, - "Include skipped listens": 32, + "\tbackend: %v": 11, + "\texport: %s": 0, + "\timport: %s\n": 1, + "Aborted": 8, + "Access token": 19, + "Access token received, you can use %v now.\n": 28, + "Append to file": 21, + "Backend": 36, + "Client ID": 15, + "Client secret": 16, + "Delete the service configuration \"%v\"?": 7, + "Disable auto correction of submitted listens": 24, + "During the import the following errors occurred:": 41, + "Error: %v\n": 42, + "Error: OAuth state mismatch": 27, + "Failed reading config: %v": 2, + "File path": 20, + "From timestamp: %v (%v)": 38, + "Import failed, last reported timestamp was %v (%s)": 39, + "Imported %v of %v %s into %v.": 40, + "Include skipped listens": 25, "Latest timestamp: %v (%v)": 43, - "No": 37, - "Playlist title": 29, - "Saved service %v using backend %v": 12, - "Server URL": 24, - "Service": 39, - "Service \"%v\" deleted\n": 16, - "Service name": 10, - "The backend %v requires authentication. Authenticate now?": 13, - "Token received, you can close this window now.": 19, - "Transferring %s from %s to %s...": 3, - "Unique playlist identifier": 30, - "Updated service %v using backend %v\n": 17, - "User name": 25, - "Visit the URL for authorization: %v": 33, - "Yes": 36, - "a service with this name already exists": 11, - "backend %s does not implement %s": 20, - "done": 2, - "exporting": 0, - "importing": 1, + "No": 33, + "Playlist title": 22, + "Saved service %v using backend %v": 5, + "Server URL": 17, + "Service": 35, + "Service \"%v\" deleted\n": 9, + "Service name": 3, + "The backend %v requires authentication. Authenticate now?": 6, + "Token received, you can close this window now.": 12, + "Transferring %s from %s to %s...": 37, + "Unique playlist identifier": 23, + "Updated service %v using backend %v\n": 10, + "User name": 18, + "Visit the URL for authorization: %v": 26, + "Yes": 32, + "a service with this name already exists": 4, + "backend %s does not implement %s": 13, + "done": 31, + "exporting": 29, + "importing": 30, "key must only consist of A-Za-z0-9_-": 45, "no configuration file defined, cannot write config": 44, - "no existing service configurations": 38, + "no existing service configurations": 34, "no service configuration \"%v\"": 46, - "unknown backend \"%s\"": 21, + "unknown backend \"%s\"": 14, } var deIndex = []uint32{ // 48 elements // Entry 0 - 1F - 0x00000000, 0x0000000b, 0x00000016, 0x0000001d, - 0x00000046, 0x00000071, 0x000000a8, 0x000000bb, - 0x000000bb, 0x000000bb, 0x000000bb, 0x000000bb, - 0x000000bb, 0x000000bb, 0x000000bb, 0x000000bb, - 0x000000bb, 0x000000bb, 0x000000bb, 0x000000bb, - 0x000000bb, 0x000000bb, 0x000000bb, 0x000000bb, - 0x000000bb, 0x000000bb, 0x000000bb, 0x000000bb, - 0x000000bb, 0x000000bb, 0x000000bb, 0x000000bb, + 0x00000000, 0x00000013, 0x00000027, 0x00000052, + 0x0000005e, 0x0000008d, 0x000000bd, 0x00000104, + 0x00000133, 0x0000013f, 0x00000162, 0x00000198, + 0x000001ac, 0x000001e7, 0x00000213, 0x00000233, + 0x0000023d, 0x0000024b, 0x00000256, 0x00000263, + 0x00000271, 0x0000027b, 0x0000028e, 0x000002a1, + 0x000002b8, 0x000002ec, 0x0000030d, 0x00000333, + 0x0000035d, 0x0000039d, 0x000003a8, 0x000003b3, // Entry 20 - 3F - 0x000000bb, 0x000000bb, 0x000000bb, 0x000000bb, - 0x000000bb, 0x000000bb, 0x000000bb, 0x000000bb, - 0x000000bb, 0x000000bb, 0x000000bb, 0x000000bb, - 0x000000bb, 0x000000bb, 0x000000bb, 0x000000bb, + 0x000003ba, 0x000003bd, 0x000003c2, 0x000003eb, + 0x000003f3, 0x000003fb, 0x00000424, 0x00000442, + 0x0000047f, 0x000004aa, 0x000004e1, 0x000004f4, + 0x00000517, 0x00000568, 0x0000059f, 0x000005c6, } // Size: 216 bytes -const deData string = "" + // Size: 187 bytes - "\x02exportiere\x02importiere\x02fertig\x02Übertrage %[1]s von %[2]s nach" + - " %[3]s...\x02%[1]v von %[2]v %[3]s in %[4]v importiert.\x02Während des I" + - "mports sind folgende Fehler aufgetreten:\x04\x00\x01\x0a\x0e\x02Fehler: " + - "%[1]v" +const deData string = "" + // Size: 1478 bytes + "\x04\x01\x09\x00\x0e\x02Export: %[1]s\x04\x01\x09\x01\x0a\x0e\x02Import:" + + " %[1]s\x02Fehler beim Lesen der Konfiguration: %[1]v\x02Servicename\x02e" + + "in Service mit diesem Namen existiert bereits\x02Service %[1]v mit dem B" + + "ackend %[2]v gespeichert\x02Das Backend %[1]v erfordert Authentifizierun" + + "g. Jetzt authentifizieren?\x02Die Servicekonfiguration „%[1]v“ löschen?" + + "\x02Abgebrochen\x04\x00\x01\x0a\x1e\x02Service „%[1]v“ gelöscht\x04\x00" + + "\x01\x0a1\x02Service %[1]v mit dem Backend %[2]v aktualisiert\x04\x01" + + "\x09\x00\x0f\x02Backend: %[1]v\x02Token erhalten, das Fenster kann jetzt" + + " geschlossen werden.\x02das backend %[1]s implementiert %[2]s nicht\x02u" + + "nbekanntes Backend „%[1]s“\x02Client-ID\x02Client-Secret\x02Server-URL" + + "\x02Benutzername\x02Zugriffstoken\x02Dateipfad\x02An Datei anhängen\x02T" + + "itel der Playlist\x02Eindeutige Playlist-ID\x02Autokorrektur für übermit" + + "telte Titel deaktivieren\x02Übersprungene Titel einbeziehen\x02URL für A" + + "utorisierung öffnen: %[1]v\x02Fehler: OAuth-State stimmt nicht überein" + + "\x04\x00\x01\x0a;\x02Zugriffstoken erhalten, %[1]v kann jetzt verwendet " + + "werden.\x02exportiere\x02importiere\x02fertig\x02Ja\x02Nein\x02keine bes" + + "tehenden Servicekonfigurationen\x02Service\x02Backend\x02Übertrage %[1]s" + + " von %[2]s nach %[3]s...\x02Ab Zeitstempel: %[1]v (%[2]v)\x02Import fehl" + + "geschlagen, letzter Zeitstempel war %[1]v (%[2]s)\x02%[1]v von %[2]v %[3" + + "]s in %[4]v importiert.\x02Während des Imports sind folgende Fehler aufg" + + "etreten:\x04\x00\x01\x0a\x0e\x02Fehler: %[1]v\x02Letzter Zeitstempel: %[" + + "1]v (%[2]v)\x02keine Konfigurationsdatei definiert, Konfiguration kann n" + + "icht geschrieben werden\x02Schlüssel darf nur die Zeichen A-Za-z0-9_- be" + + "inhalten\x02keine Servicekonfiguration „%[1]v“" var enIndex = []uint32{ // 48 elements // Entry 0 - 1F - 0x00000000, 0x0000000a, 0x00000014, 0x00000019, - 0x00000043, 0x0000006d, 0x0000009e, 0x000000b0, - 0x000000c3, 0x000000d7, 0x000000f4, 0x00000101, - 0x00000129, 0x00000151, 0x0000018e, 0x000001b8, - 0x000001c0, 0x000001dd, 0x0000020c, 0x00000220, - 0x0000024f, 0x00000276, 0x0000028e, 0x00000298, - 0x000002a6, 0x000002b1, 0x000002bb, 0x000002c8, - 0x000002d2, 0x000002e1, 0x000002f0, 0x0000030b, + 0x00000000, 0x00000013, 0x00000027, 0x00000044, + 0x00000051, 0x00000079, 0x000000a1, 0x000000de, + 0x00000108, 0x00000110, 0x0000012d, 0x0000015c, + 0x00000170, 0x0000019f, 0x000001c6, 0x000001de, + 0x000001e8, 0x000001f6, 0x00000201, 0x0000020b, + 0x00000218, 0x00000222, 0x00000231, 0x00000240, + 0x0000025b, 0x00000288, 0x000002a0, 0x000002c7, + 0x000002e3, 0x00000316, 0x00000320, 0x0000032a, // Entry 20 - 3F - 0x00000338, 0x00000350, 0x00000377, 0x00000393, - 0x000003c6, 0x000003ca, 0x000003cd, 0x000003f0, - 0x000003f8, 0x00000400, 0x0000041e, 0x00000457, + 0x0000032f, 0x00000333, 0x00000336, 0x00000359, + 0x00000361, 0x00000369, 0x00000393, 0x000003b1, + 0x000003ea, 0x00000414, 0x00000445, 0x00000457, 0x00000477, 0x000004aa, 0x000004cf, 0x000004f0, } // Size: 216 bytes const enData string = "" + // Size: 1264 bytes - "\x02exporting\x02importing\x02done\x02Transferring %[1]s from %[2]s to %" + - "[3]s...\x02Imported %[1]v of %[2]v %[3]s into %[4]v.\x02During the impor" + - "t the following errors occurred:\x04\x00\x01\x0a\x0d\x02Error: %[1]v\x04" + - "\x01\x09\x00\x0e\x02export: %[1]s\x04\x01\x09\x01\x0a\x0e\x02import: %[1" + - "]s\x02Failed reading config: %[1]v\x02Service name\x02a service with thi" + - "s name already exists\x02Saved service %[1]v using backend %[2]v\x02The " + - "backend %[1]v requires authentication. Authenticate now?\x02Delete the s" + - "ervice configuration \x22%[1]v\x22?\x02Aborted\x04\x00\x01\x0a\x18\x02Se" + - "rvice \x22%[1]v\x22 deleted\x04\x00\x01\x0a*\x02Updated service %[1]v us" + - "ing backend %[2]v\x04\x01\x09\x00\x0f\x02backend: %[1]v\x02Token receive" + - "d, you can close this window now.\x02backend %[1]s does not implement %[" + - "2]s\x02unknown backend \x22%[1]s\x22\x02Client ID\x02Client secret\x02Se" + - "rver URL\x02User name\x02Access token\x02File path\x02Append to file\x02" + - "Playlist title\x02Unique playlist identifier\x02Disable auto correction " + - "of submitted listens\x02Include skipped listens\x02Visit the URL for aut" + - "horization: %[1]v\x02Error: OAuth state mismatch\x04\x00\x01\x0a.\x02Acc" + - "ess token received, you can use %[1]v now.\x02Yes\x02No\x02no existing s" + - "ervice configurations\x02Service\x02Backend\x02From timestamp: %[1]v (%[" + - "2]v)\x02Import failed, last reported timestamp was %[1]v (%[2]s)\x02Late" + - "st timestamp: %[1]v (%[2]v)\x02no configuration file defined, cannot wri" + - "te config\x02key must only consist of A-Za-z0-9_-\x02no service configur" + - "ation \x22%[1]v\x22" + "\x04\x01\x09\x00\x0e\x02export: %[1]s\x04\x01\x09\x01\x0a\x0e\x02import:" + + " %[1]s\x02Failed reading config: %[1]v\x02Service name\x02a service with" + + " this name already exists\x02Saved service %[1]v using backend %[2]v\x02" + + "The backend %[1]v requires authentication. Authenticate now?\x02Delete t" + + "he service configuration \x22%[1]v\x22?\x02Aborted\x04\x00\x01\x0a\x18" + + "\x02Service \x22%[1]v\x22 deleted\x04\x00\x01\x0a*\x02Updated service %[" + + "1]v using backend %[2]v\x04\x01\x09\x00\x0f\x02backend: %[1]v\x02Token r" + + "eceived, you can close this window now.\x02backend %[1]s does not implem" + + "ent %[2]s\x02unknown backend \x22%[1]s\x22\x02Client ID\x02Client secret" + + "\x02Server URL\x02User name\x02Access token\x02File path\x02Append to fi" + + "le\x02Playlist title\x02Unique playlist identifier\x02Disable auto corre" + + "ction of submitted listens\x02Include skipped listens\x02Visit the URL f" + + "or authorization: %[1]v\x02Error: OAuth state mismatch\x04\x00\x01\x0a." + + "\x02Access token received, you can use %[1]v now.\x02exporting\x02import" + + "ing\x02done\x02Yes\x02No\x02no existing service configurations\x02Servic" + + "e\x02Backend\x02Transferring %[1]s from %[2]s to %[3]s...\x02From timest" + + "amp: %[1]v (%[2]v)\x02Import failed, last reported timestamp was %[1]v (" + + "%[2]s)\x02Imported %[1]v of %[2]v %[3]s into %[4]v.\x02During the import" + + " the following errors occurred:\x04\x00\x01\x0a\x0d\x02Error: %[1]v\x02L" + + "atest timestamp: %[1]v (%[2]v)\x02no configuration file defined, cannot " + + "write config\x02key must only consist of A-Za-z0-9_-\x02no service confi" + + "guration \x22%[1]v\x22" - // Total table size 1883 bytes (1KiB); checksum: 4C56F9E5 + // Total table size 3174 bytes (3KiB); checksum: 18BB58D4 diff --git a/internal/translations/locales/de/out.gotext.json b/internal/translations/locales/de/out.gotext.json index a6059b7..94494ee 100644 --- a/internal/translations/locales/de/out.gotext.json +++ b/internal/translations/locales/de/out.gotext.json @@ -4,7 +4,7 @@ { "id": "export: {ExportCapabilities__}", "message": "export: {ExportCapabilities__}", - "translation": "", + "translation": "Export: {ExportCapabilities__}", "placeholders": [ { "id": "ExportCapabilities__", @@ -19,7 +19,7 @@ { "id": "import: {ImportCapabilities__}", "message": "import: {ImportCapabilities__}", - "translation": "", + "translation": "Import: {ImportCapabilities__}", "placeholders": [ { "id": "ImportCapabilities__", @@ -34,7 +34,7 @@ { "id": "Failed reading config: {Err}", "message": "Failed reading config: {Err}", - "translation": "", + "translation": "Fehler beim Lesen der Konfiguration: {Err}", "placeholders": [ { "id": "Err", @@ -49,17 +49,17 @@ { "id": "Service name", "message": "Service name", - "translation": "" + "translation": "Servicename" }, { "id": "a service with this name already exists", "message": "a service with this name already exists", - "translation": "" + "translation": "ein Service mit diesem Namen existiert bereits" }, { "id": "Saved service {Name} using backend {Backend}", "message": "Saved service {Name} using backend {Backend}", - "translation": "", + "translation": "Service {Name} mit dem Backend {Backend} gespeichert", "placeholders": [ { "id": "Name", @@ -82,7 +82,7 @@ { "id": "The backend {Backend} requires authentication. Authenticate now?", "message": "The backend {Backend} requires authentication. Authenticate now?", - "translation": "", + "translation": "Das Backend {Backend} erfordert Authentifizierung. Jetzt authentifizieren?", "placeholders": [ { "id": "Backend", @@ -97,7 +97,7 @@ { "id": "Delete the service configuration \"{Service}\"?", "message": "Delete the service configuration \"{Service}\"?", - "translation": "", + "translation": "Die Servicekonfiguration „{Service}“ löschen?", "placeholders": [ { "id": "Service", @@ -112,12 +112,12 @@ { "id": "Aborted", "message": "Aborted", - "translation": "" + "translation": "Abgebrochen" }, { "id": "Service \"{Name}\" deleted", "message": "Service \"{Name}\" deleted", - "translation": "", + "translation": "Service „{Name}“ gelöscht", "placeholders": [ { "id": "Name", @@ -132,7 +132,7 @@ { "id": "Updated service {Name} using backend {Backend}", "message": "Updated service {Name} using backend {Backend}", - "translation": "", + "translation": "Service {Name} mit dem Backend {Backend} aktualisiert", "placeholders": [ { "id": "Name", @@ -155,7 +155,7 @@ { "id": "backend: {Backend}", "message": "backend: {Backend}", - "translation": "", + "translation": "Backend: {Backend}", "placeholders": [ { "id": "Backend", @@ -170,12 +170,12 @@ { "id": "Token received, you can close this window now.", "message": "Token received, you can close this window now.", - "translation": "" + "translation": "Token erhalten, das Fenster kann jetzt geschlossen werden." }, { "id": "backend {Backend} does not implement {InterfaceName}", "message": "backend {Backend} does not implement {InterfaceName}", - "translation": "", + "translation": "das backend {Backend} implementiert {InterfaceName} nicht", "placeholders": [ { "id": "Backend", @@ -198,7 +198,7 @@ { "id": "unknown backend \"{BackendName}\"", "message": "unknown backend \"{BackendName}\"", - "translation": "", + "translation": "unbekanntes Backend „{BackendName}“", "placeholders": [ { "id": "BackendName", @@ -213,62 +213,62 @@ { "id": "Client ID", "message": "Client ID", - "translation": "" + "translation": "Client-ID" }, { "id": "Client secret", "message": "Client secret", - "translation": "" + "translation": "Client-Secret" }, { "id": "Server URL", "message": "Server URL", - "translation": "" + "translation": "Server-URL" }, { "id": "User name", "message": "User name", - "translation": "" + "translation": "Benutzername" }, { "id": "Access token", "message": "Access token", - "translation": "" + "translation": "Zugriffstoken" }, { "id": "File path", "message": "File path", - "translation": "" + "translation": "Dateipfad" }, { "id": "Append to file", "message": "Append to file", - "translation": "" + "translation": "An Datei anhängen" }, { "id": "Playlist title", "message": "Playlist title", - "translation": "" + "translation": "Titel der Playlist" }, { "id": "Unique playlist identifier", "message": "Unique playlist identifier", - "translation": "" + "translation": "Eindeutige Playlist-ID" }, { "id": "Disable auto correction of submitted listens", "message": "Disable auto correction of submitted listens", - "translation": "" + "translation": "Autokorrektur für übermittelte Titel deaktivieren" }, { "id": "Include skipped listens", "message": "Include skipped listens", - "translation": "" + "translation": "Übersprungene Titel einbeziehen" }, { "id": "Visit the URL for authorization: {Url}", "message": "Visit the URL for authorization: {Url}", - "translation": "", + "translation": "URL für Autorisierung öffnen: {Url}", "placeholders": [ { "id": "Url", @@ -283,12 +283,12 @@ { "id": "Error: OAuth state mismatch", "message": "Error: OAuth state mismatch", - "translation": "" + "translation": "Fehler: OAuth-State stimmt nicht überein" }, { "id": "Access token received, you can use {Name} now.", "message": "Access token received, you can use {Name} now.", - "translation": "", + "translation": "Zugriffstoken erhalten, {Name} kann jetzt verwendet werden.", "placeholders": [ { "id": "Name", @@ -324,27 +324,27 @@ { "id": "Yes", "message": "Yes", - "translation": "" + "translation": "Ja" }, { "id": "No", "message": "No", - "translation": "" + "translation": "Nein" }, { "id": "no existing service configurations", "message": "no existing service configurations", - "translation": "" + "translation": "keine bestehenden Servicekonfigurationen" }, { "id": "Service", "message": "Service", - "translation": "" + "translation": "Service" }, { "id": "Backend", "message": "Backend", - "translation": "" + "translation": "Backend" }, { "id": "Transferring {Entity} from {SourceName} to {TargetName}...", @@ -380,7 +380,7 @@ { "id": "From timestamp: {Arg_1} ({Arg_2})", "message": "From timestamp: {Arg_1} ({Arg_2})", - "translation": "", + "translation": "Ab Zeitstempel: {Arg_1} ({Arg_2})", "placeholders": [ { "id": "Arg_1", @@ -401,7 +401,7 @@ { "id": "Import failed, last reported timestamp was {Arg_1} ({Arg_2})", "message": "Import failed, last reported timestamp was {Arg_1} ({Arg_2})", - "translation": "", + "translation": "Import fehlgeschlagen, letzter Zeitstempel war {Arg_1} ({Arg_2})", "placeholders": [ { "id": "Arg_1", @@ -481,7 +481,7 @@ { "id": "Latest timestamp: {Arg_1} ({Arg_2})", "message": "Latest timestamp: {Arg_1} ({Arg_2})", - "translation": "", + "translation": "Letzter Zeitstempel: {Arg_1} ({Arg_2})", "placeholders": [ { "id": "Arg_1", @@ -502,17 +502,17 @@ { "id": "no configuration file defined, cannot write config", "message": "no configuration file defined, cannot write config", - "translation": "" + "translation": "keine Konfigurationsdatei definiert, Konfiguration kann nicht geschrieben werden" }, { "id": "key must only consist of A-Za-z0-9_-", "message": "key must only consist of A-Za-z0-9_-", - "translation": "" + "translation": "Schlüssel darf nur die Zeichen A-Za-z0-9_- beinhalten" }, { "id": "no service configuration \"{Name}\"", "message": "no service configuration \"{Name}\"", - "translation": "", + "translation": "keine Servicekonfiguration „{Name}“", "placeholders": [ { "id": "Name", From be1cfdac9e57db1b04e43f0c3ff2b56b1e8b1de9 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 10 Dec 2023 16:15:09 +0100 Subject: [PATCH 14/82] allow datetime string as --timestamp parameter --- README.md | 4 ++-- cmd/beam_listens.go | 4 +--- cmd/beam_loves.go | 4 +--- internal/cli/transfer.go | 26 +++++++++++++++++++++----- 4 files changed, 25 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 414f498..a6de18d 100644 --- a/README.md +++ b/README.md @@ -104,12 +104,12 @@ Imported 4 of 4 loves into listenbrainz. Latest timestamp: 2023-11-23 14:44:46 +0100 CET (1700747086) ``` -Scotty will remember the latest timestamp for which it transferred data between the two services. The next time you run `scotty beam loves deezer listenbrainz` it will only consider tracks loved after the previous import. If you for some reason want to override this and start importing at an earlier time again, you can specify an earlier start time with the `--timestamp` parameter, which expects a Unix timestamp. +Scotty will remember the latest timestamp for which it transferred data between the two services. The next time you run `scotty beam loves deezer listenbrainz` it will only consider tracks loved after the previous import. If you for some reason want to override this and start importing at an earlier time again, you can specify an earlier start time with the `--timestamp` parameter, which can be either a Unix timestamp (seconds since 1970-01-01 00:00:00) or a date time string like "2023-12-10 16:12:00". For example to import listens starting at a specific timestamp use: ``` -scotty beam listens deezer listenbrainz --timestamp 1701872784 +scotty beam listens deezer listenbrainz --timestamp "2023-12-06 14:26:24" ``` diff --git a/cmd/beam_listens.go b/cmd/beam_listens.go index 149319f..fe567f0 100644 --- a/cmd/beam_listens.go +++ b/cmd/beam_listens.go @@ -17,8 +17,6 @@ Scotty. If not, see . package cmd import ( - "math" - "github.com/spf13/cobra" "go.uploadedlobster.com/scotty/internal/backends" "go.uploadedlobster.com/scotty/internal/cli" @@ -60,5 +58,5 @@ func init() { // Cobra supports local flags which will only run when this command // is called directly, e.g.: // beamListensCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") - beamListensCmd.Flags().Int64P("timestamp", "t", math.MinInt64, "Only import listens newer then given Unix timestamp") + beamListensCmd.Flags().StringP("timestamp", "t", "", "only import listens newer then given timestamp") } diff --git a/cmd/beam_loves.go b/cmd/beam_loves.go index 19063db..5f75d70 100644 --- a/cmd/beam_loves.go +++ b/cmd/beam_loves.go @@ -17,8 +17,6 @@ Scotty. If not, see . package cmd import ( - "math" - "github.com/spf13/cobra" "go.uploadedlobster.com/scotty/internal/backends" "go.uploadedlobster.com/scotty/internal/cli" @@ -60,5 +58,5 @@ func init() { // Cobra supports local flags which will only run when this command // is called directly, e.g.: // beamLovesCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") - beamLovesCmd.Flags().Int64P("timestamp", "t", math.MinInt64, "Only import loves newer then given Unix timestamp") + beamLovesCmd.Flags().StringP("timestamp", "t", "", "only import loves newer then given timestamp") } diff --git a/internal/cli/transfer.go b/internal/cli/transfer.go index 5683bb6..1af552e 100644 --- a/internal/cli/transfer.go +++ b/internal/cli/transfer.go @@ -16,8 +16,8 @@ Scotty. If not, see . package cli import ( + "errors" "fmt" - "math" "strconv" "sync" "time" @@ -155,13 +155,29 @@ func (c *TransferCmd[E, I, R]) Transfer(exp backends.ExportProcessor[R], imp bac } func (c *TransferCmd[E, I, R]) timestamp() (time.Time, error) { - flagValue, err := c.cmd.Flags().GetInt64("timestamp") - if err == nil && flagValue > math.MinInt64 { - return time.Unix(flagValue, 0), nil - } else { + flagValue, err := c.cmd.Flags().GetString("timestamp") + if err != nil { + return time.Time{}, err + } + + // No timestamp given, read from database + if flagValue == "" { timestamp, err := c.db.GetImportTimestamp(c.sourceName, c.targetName, c.entity) return timestamp, err } + + // Try using given value as a Unix timestamp + if timestamp, err := strconv.ParseInt(flagValue, 10, 64); err == nil { + return time.Unix(timestamp, 0), nil + } + + // Try to parse datetime string + for _, format := range []string{time.DateTime, time.RFC3339} { + if t, err := time.Parse(format, flagValue); err == nil { + return t, nil + } + } + return time.Time{}, errors.New(i18n.Tr("invalid timestamp string \"%v\"", flagValue)) } func (c *TransferCmd[E, I, R]) updateTimestamp(result models.ImportResult, oldTimestamp time.Time) error { From c4da3a40cce53bf3bef955c48b66244d6c2c46d2 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Mon, 11 Dec 2023 22:48:08 +0100 Subject: [PATCH 15/82] Added util.Min and util.Max helpers --- internal/backends/deezer/deezer.go | 11 +++------ internal/backends/spotify/spotify.go | 6 ++--- internal/util/util.go | 34 ++++++++++++++++++++++++++++ internal/util/util_test.go | 34 ++++++++++++++++++++++++++++ 4 files changed, 73 insertions(+), 12 deletions(-) create mode 100644 internal/util/util.go create mode 100644 internal/util/util_test.go diff --git a/internal/backends/deezer/deezer.go b/internal/backends/deezer/deezer.go index c796af3..896b348 100644 --- a/internal/backends/deezer/deezer.go +++ b/internal/backends/deezer/deezer.go @@ -26,6 +26,7 @@ import ( "go.uploadedlobster.com/scotty/internal/config" "go.uploadedlobster.com/scotty/internal/i18n" "go.uploadedlobster.com/scotty/internal/models" + "go.uploadedlobster.com/scotty/internal/util" "golang.org/x/oauth2" ) @@ -105,10 +106,7 @@ out: // and continue. if offset >= result.Total { p.Total = int64(result.Total) - offset = result.Total - perPage - if offset < 0 { - offset = 0 - } + offset = util.Max(result.Total-perPage, 0) continue } @@ -177,10 +175,7 @@ out: if offset >= result.Total { p.Total = int64(result.Total) totalCount = result.Total - offset = result.Total - perPage - if offset < 0 { - offset = 0 - } + offset = util.Max(result.Total-perPage, 0) continue } diff --git a/internal/backends/spotify/spotify.go b/internal/backends/spotify/spotify.go index 9b623cb..b9a51e2 100644 --- a/internal/backends/spotify/spotify.go +++ b/internal/backends/spotify/spotify.go @@ -28,6 +28,7 @@ import ( "go.uploadedlobster.com/scotty/internal/config" "go.uploadedlobster.com/scotty/internal/i18n" "go.uploadedlobster.com/scotty/internal/models" + "go.uploadedlobster.com/scotty/internal/util" "golang.org/x/oauth2" "golang.org/x/oauth2/spotify" ) @@ -183,10 +184,7 @@ out: if offset >= result.Total { p.Total = int64(result.Total) totalCount = result.Total - offset = result.Total - perPage - if offset < 0 { - offset = 0 - } + offset = util.Max(result.Total-perPage, 0) continue } diff --git a/internal/util/util.go b/internal/util/util.go new file mode 100644 index 0000000..99826a1 --- /dev/null +++ b/internal/util/util.go @@ -0,0 +1,34 @@ +/* +Copyright © 2023 Philipp Wolfer + +Scotty is free software: you can redistribute it and/or modify it under the +terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later version. + +Scotty is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +Scotty. If not, see . +*/ + +package util + +import "golang.org/x/exp/constraints" + +func Max[T constraints.Ordered](m, n T) T { + if n > m { + return n + } else { + return m + } +} + +func Min[T constraints.Ordered](m, n T) T { + if n < m { + return n + } else { + return m + } +} diff --git a/internal/util/util_test.go b/internal/util/util_test.go new file mode 100644 index 0000000..638e3a6 --- /dev/null +++ b/internal/util/util_test.go @@ -0,0 +1,34 @@ +/* +Copyright © 2023 Philipp Wolfer + +Scotty is free software: you can redistribute it and/or modify it under the +terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later version. + +Scotty is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +Scotty. If not, see . +*/ + +package util_test + +import ( + "fmt" + + "go.uploadedlobster.com/scotty/internal/util" +) + +func MaxExample() { + v := util.Max(2, 5) + fmt.Print(v) + // Output: 5 +} + +func MinExample() { + v := util.Min(2, 5) + fmt.Print(v) + // Output: 2 +} From 6ac2b4f14281ffa20b076c49698d151c61e525fe Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Mon, 11 Dec 2023 23:21:29 +0100 Subject: [PATCH 16/82] Moved ratelimit to pkg --- internal/backends/funkwhale/client.go | 2 +- internal/backends/listenbrainz/client.go | 2 +- internal/backends/spotify/client.go | 2 +- {internal => pkg}/ratelimit/httpheader.go | 9 +++++++++ 4 files changed, 12 insertions(+), 3 deletions(-) rename {internal => pkg}/ratelimit/httpheader.go (82%) diff --git a/internal/backends/funkwhale/client.go b/internal/backends/funkwhale/client.go index b757d6f..39071e7 100644 --- a/internal/backends/funkwhale/client.go +++ b/internal/backends/funkwhale/client.go @@ -26,8 +26,8 @@ import ( "strconv" "github.com/go-resty/resty/v2" - "go.uploadedlobster.com/scotty/internal/ratelimit" "go.uploadedlobster.com/scotty/internal/version" + "go.uploadedlobster.com/scotty/pkg/ratelimit" ) const MaxItemsPerGet = 50 diff --git a/internal/backends/listenbrainz/client.go b/internal/backends/listenbrainz/client.go index aa30b78..72257bf 100644 --- a/internal/backends/listenbrainz/client.go +++ b/internal/backends/listenbrainz/client.go @@ -27,8 +27,8 @@ import ( "time" "github.com/go-resty/resty/v2" - "go.uploadedlobster.com/scotty/internal/ratelimit" "go.uploadedlobster.com/scotty/internal/version" + "go.uploadedlobster.com/scotty/pkg/ratelimit" ) const ( diff --git a/internal/backends/spotify/client.go b/internal/backends/spotify/client.go index 08c00f3..7bbcf48 100644 --- a/internal/backends/spotify/client.go +++ b/internal/backends/spotify/client.go @@ -29,8 +29,8 @@ import ( "time" "github.com/go-resty/resty/v2" - "go.uploadedlobster.com/scotty/internal/ratelimit" "go.uploadedlobster.com/scotty/internal/version" + "go.uploadedlobster.com/scotty/pkg/ratelimit" "golang.org/x/oauth2" ) diff --git a/internal/ratelimit/httpheader.go b/pkg/ratelimit/httpheader.go similarity index 82% rename from internal/ratelimit/httpheader.go rename to pkg/ratelimit/httpheader.go index 91a05b5..3f2552c 100644 --- a/internal/ratelimit/httpheader.go +++ b/pkg/ratelimit/httpheader.go @@ -29,6 +29,15 @@ const ( MaxWaitTimeSeconds = 60 ) +// Implements rate HTTP header based limiting for resty. +// +// This works with servers that return the status code 429 (Too Many Requests) +// and an HTTP header indicating the time in seconds until rate limit resets. +// Common headers used are "X-RateLimit-Reset-In" or "Retry-After". +// +// Usage: +// +// ratelimit.EnableHTTPHeaderRateLimit(client, "Retry-After") func EnableHTTPHeaderRateLimit(client *resty.Client, resetInHeader string) { client.SetRetryCount(RetryCount) client.AddRetryCondition( From 7666ca53a7a1e344bba164220e4888cbfb5c6e34 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sat, 13 Jan 2024 13:18:52 +0100 Subject: [PATCH 17/82] Allow default values for boolean config settings --- internal/backends/jspf/jspf.go | 5 +---- internal/backends/maloja/maloja.go | 2 +- internal/backends/scrobblerlog/scrobblerlog.go | 7 ++----- internal/config/services.go | 8 ++++++-- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/internal/backends/jspf/jspf.go b/internal/backends/jspf/jspf.go index b87b86a..b73b4b1 100644 --- a/internal/backends/jspf/jspf.go +++ b/internal/backends/jspf/jspf.go @@ -62,10 +62,7 @@ func (b *JSPFBackend) Options() []models.BackendOption { func (b *JSPFBackend) FromConfig(config *config.ServiceConfig) models.Backend { b.filePath = config.GetString("file-path") - b.append = true - if config.IsSet("append") { - b.append = config.GetBool("append") - } + b.append = config.GetBool("append", true) b.playlist = jspf.Playlist{ Title: config.GetString("title"), Creator: config.GetString("username"), diff --git a/internal/backends/maloja/maloja.go b/internal/backends/maloja/maloja.go index 9c91115..135bef3 100644 --- a/internal/backends/maloja/maloja.go +++ b/internal/backends/maloja/maloja.go @@ -56,7 +56,7 @@ func (b *MalojaApiBackend) FromConfig(config *config.ServiceConfig) models.Backe config.GetString("server-url"), config.GetString("token"), ) - b.nofix = config.GetBool("nofix") + b.nofix = config.GetBool("nofix", false) return b } diff --git a/internal/backends/scrobblerlog/scrobblerlog.go b/internal/backends/scrobblerlog/scrobblerlog.go index 09f081f..84cae88 100644 --- a/internal/backends/scrobblerlog/scrobblerlog.go +++ b/internal/backends/scrobblerlog/scrobblerlog.go @@ -56,11 +56,8 @@ func (b *ScrobblerLogBackend) Options() []models.BackendOption { func (b *ScrobblerLogBackend) FromConfig(config *config.ServiceConfig) models.Backend { b.filePath = config.GetString("file-path") - b.includeSkipped = config.GetBool("include-skipped") - b.append = true - if config.IsSet("append") { - b.append = config.GetBool("append") - } + b.includeSkipped = config.GetBool("include-skipped", false) + b.append = config.GetBool("append", true) b.log = ScrobblerLog{ Timezone: "UNKNOWN", Client: "Rockbox unknown $Revision$", diff --git a/internal/config/services.go b/internal/config/services.go index 783adc4..7c8bdb6 100644 --- a/internal/config/services.go +++ b/internal/config/services.go @@ -54,8 +54,12 @@ func (c *ServiceConfig) GetString(key string) string { return cast.ToString(c.ConfigValues[key]) } -func (c *ServiceConfig) GetBool(key string) bool { - return cast.ToBool(c.ConfigValues[key]) +func (c *ServiceConfig) GetBool(key string, defaultValue bool) bool { + if c.IsSet(key) { + return cast.ToBool(c.ConfigValues[key]) + } else { + return defaultValue + } } func (c *ServiceConfig) IsSet(key string) bool { From 8c459f4d2fe1810c3fd600a84a97de1c5907c276 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sat, 13 Jan 2024 13:55:05 +0100 Subject: [PATCH 18/82] Spotify extended streaming history exporter --- internal/backends/backends.go | 22 ++-- internal/backends/spotifyhistory/models.go | 110 ++++++++++++++++ .../backends/spotifyhistory/spotifyhistory.go | 118 ++++++++++++++++++ 3 files changed, 240 insertions(+), 10 deletions(-) create mode 100644 internal/backends/spotifyhistory/models.go create mode 100644 internal/backends/spotifyhistory/spotifyhistory.go diff --git a/internal/backends/backends.go b/internal/backends/backends.go index f257f3d..e4cbbc9 100644 --- a/internal/backends/backends.go +++ b/internal/backends/backends.go @@ -31,6 +31,7 @@ import ( "go.uploadedlobster.com/scotty/internal/backends/maloja" "go.uploadedlobster.com/scotty/internal/backends/scrobblerlog" "go.uploadedlobster.com/scotty/internal/backends/spotify" + "go.uploadedlobster.com/scotty/internal/backends/spotifyhistory" "go.uploadedlobster.com/scotty/internal/backends/subsonic" "go.uploadedlobster.com/scotty/internal/config" "go.uploadedlobster.com/scotty/internal/i18n" @@ -104,16 +105,17 @@ func GetBackends() BackendList { } var knownBackends = map[string]func() models.Backend{ - "deezer": func() models.Backend { return &deezer.DeezerApiBackend{} }, - "dump": func() models.Backend { return &dump.DumpBackend{} }, - "funkwhale": func() models.Backend { return &funkwhale.FunkwhaleApiBackend{} }, - "jspf": func() models.Backend { return &jspf.JSPFBackend{} }, - "lastfm": func() models.Backend { return &lastfm.LastfmApiBackend{} }, - "listenbrainz": func() models.Backend { return &listenbrainz.ListenBrainzApiBackend{} }, - "maloja": func() models.Backend { return &maloja.MalojaApiBackend{} }, - "scrobbler-log": func() models.Backend { return &scrobblerlog.ScrobblerLogBackend{} }, - "spotify": func() models.Backend { return &spotify.SpotifyApiBackend{} }, - "subsonic": func() models.Backend { return &subsonic.SubsonicApiBackend{} }, + "deezer": func() models.Backend { return &deezer.DeezerApiBackend{} }, + "dump": func() models.Backend { return &dump.DumpBackend{} }, + "funkwhale": func() models.Backend { return &funkwhale.FunkwhaleApiBackend{} }, + "jspf": func() models.Backend { return &jspf.JSPFBackend{} }, + "lastfm": func() models.Backend { return &lastfm.LastfmApiBackend{} }, + "listenbrainz": func() models.Backend { return &listenbrainz.ListenBrainzApiBackend{} }, + "maloja": func() models.Backend { return &maloja.MalojaApiBackend{} }, + "scrobbler-log": func() models.Backend { return &scrobblerlog.ScrobblerLogBackend{} }, + "spotify": func() models.Backend { return &spotify.SpotifyApiBackend{} }, + "spotify-history": func() models.Backend { return &spotifyhistory.SpotifyHistoryBackend{} }, + "subsonic": func() models.Backend { return &subsonic.SubsonicApiBackend{} }, } func backendWithConfig(config config.ServiceConfig) (models.Backend, error) { diff --git a/internal/backends/spotifyhistory/models.go b/internal/backends/spotifyhistory/models.go new file mode 100644 index 0000000..89d67ab --- /dev/null +++ b/internal/backends/spotifyhistory/models.go @@ -0,0 +1,110 @@ +/* +Copyright © 2024 Philipp Wolfer + +Scotty is free software: you can redistribute it and/or modify it under the +terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later version. + +Scotty is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +Scotty. If not, see . +*/ + +package spotifyhistory + +import ( + "encoding/json" + "fmt" + "io" + "strings" + "time" + + "go.uploadedlobster.com/scotty/internal/models" +) + +type StreamingHistory []HistoryItem + +type ListenListOptions struct { + IgnoreIncognito bool + IgnoreSkipped bool + SkippedMinDurationMs int +} + +type HistoryItem struct { + Timestamp time.Time `json:"ts"` + UserName string `json:"username"` + Platform string `json:"platform"` + MillisecondsPlayed int `json:"ms_played"` + ConnCountry string `json:"conn_country"` + IpAddrDecrypted string `json:"ip_addr_decrypted"` + UserAgentDecrypted string `json:"user_agent_decrypted"` + MasterMetadataTrackName string `json:"master_metadata_track_name"` + MasterMetadataAlbumArtistName string `json:"master_metadata_album_artist_name"` + MasterMetadataAlbumName string `json:"master_metadata_album_album_name"` + SpotifyTrackUri string `json:"spotify_track_uri"` + EpisodeName string `json:"episode_name"` + EpisodeShowName string `json:"episode_show_name"` + SpotifyEpisodeUri string `json:"spotify_episode_uri"` + ReasonStart string `json:"reason_start"` + ReasonEnd string `json:"reason_end"` + Shuffle bool `json:"shuffle"` + Skipped bool `json:"skipped"` + Offline bool `json:"offline"` + OfflineTimestamp int `json:"offline_timestamp"` + IncognitoMode bool `json:"incognito_mode"` +} + +func (j *StreamingHistory) Read(in io.Reader) error { + bytes, err := io.ReadAll(in) + if err != nil { + return err + } + err = json.Unmarshal(bytes, j) + return err +} + +func (h *StreamingHistory) AsListenList(opt ListenListOptions) models.ListensList { + listens := make(models.ListensList, 0, len(*h)) + for _, item := range *h { + if item.MasterMetadataTrackName == "" || + (opt.IgnoreIncognito && item.IncognitoMode) || + (opt.IgnoreSkipped && item.Skipped) || + (item.Skipped && item.MillisecondsPlayed < opt.SkippedMinDurationMs) { + continue + } + listens = append(listens, item.AsListen()) + } + return listens +} + +func (i HistoryItem) AsListen() models.Listen { + listen := models.Listen{ + Track: models.Track{ + TrackName: i.MasterMetadataTrackName, + ReleaseName: i.MasterMetadataAlbumName, + ArtistNames: []string{i.MasterMetadataAlbumArtistName}, + AdditionalInfo: models.AdditionalInfo{}, + }, + ListenedAt: i.Timestamp, + PlaybackDuration: time.Duration(i.MillisecondsPlayed * int(time.Millisecond)), + UserName: i.UserName, + } + if trackUrl, err := formatSpotifyUri(i.SpotifyTrackUri); err != nil { + listen.AdditionalInfo["spotify_id"] = trackUrl + } + return listen +} + +// Returns a Spotify ID like "spotify:track:5jzma6gCzYtKB1DbEwFZKH" into an +// URL like "https://open.spotify.com/track/5jzma6gCzYtKB1DbEwFZKH" +func formatSpotifyUri(id string) (string, error) { + parts := strings.Split(id, ":") + if len(parts) == 3 && parts[0] == "spotify" { + return fmt.Sprintf("https://opem.spotify.com/%s/%s", parts[1], parts[2]), nil + } else { + return "", fmt.Errorf("Invalid Spotify ID \"%v\"", id) + } +} diff --git a/internal/backends/spotifyhistory/spotifyhistory.go b/internal/backends/spotifyhistory/spotifyhistory.go new file mode 100644 index 0000000..304de0c --- /dev/null +++ b/internal/backends/spotifyhistory/spotifyhistory.go @@ -0,0 +1,118 @@ +/* +Copyright © 2023 Philipp Wolfer + +This file is part of Scotty. + +Scotty is free software: you can redistribute it and/or modify it under the +terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later version. + +Scotty is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +Scotty. If not, see . +*/ + +package spotifyhistory + +import ( + "os" + "path" + "path/filepath" + "slices" + "sort" + "time" + + "go.uploadedlobster.com/scotty/internal/config" + "go.uploadedlobster.com/scotty/internal/i18n" + "go.uploadedlobster.com/scotty/internal/models" +) + +const historyFileGlob = "Streaming_History_Audio_*.json" + +type SpotifyHistoryBackend struct { + dirPath string + ignoreIncognito bool + ignoreSkipped bool + skippedMinDurationMs int +} + +func (b *SpotifyHistoryBackend) Name() string { return "spotify-history" } + +func (b *SpotifyHistoryBackend) Options() []models.BackendOption { + return []models.BackendOption{{ + Name: "dir-path", + Label: i18n.Tr("Directory path"), + Type: models.String, + }, { + Name: "ignore-incognito", + Label: i18n.Tr("Ignore listens in incognito mode"), + Type: models.Bool, + Default: "true", + }, { + Name: "ignore-skipped", + Label: i18n.Tr("Ignore skipped listens"), + Type: models.Bool, + Default: "false", + }} +} + +func (b *SpotifyHistoryBackend) FromConfig(config *config.ServiceConfig) models.Backend { + b.dirPath = config.GetString("dir-path") + b.ignoreIncognito = config.GetBool("ignore-incognito", true) + b.ignoreSkipped = config.GetBool("ignore-skipped", false) + b.skippedMinDurationMs = 30000 + return b +} + +func (b *SpotifyHistoryBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.Progress) { + defer close(results) + + files, err := filepath.Glob(path.Join(b.dirPath, historyFileGlob)) + if err != nil { + progress <- models.Progress{}.Complete() + results <- models.ListensResult{Error: err} + return + } + + slices.Sort(files) + fileCount := int64(len(files)) + p := models.Progress{Total: fileCount} + for i, filePath := range files { + history, err := readHistoryFile(filePath) + if err != nil { + progress <- models.Progress{}.Complete() + results <- models.ListensResult{Error: err} + return + } + listens := history.AsListenList(ListenListOptions{ + IgnoreIncognito: b.ignoreIncognito, + IgnoreSkipped: b.ignoreSkipped, + SkippedMinDurationMs: b.skippedMinDurationMs, + }) + sort.Sort(listens) + results <- models.ListensResult{Items: listens} + p.Elapsed = int64(i) + progress <- p + } + + progress <- p.Complete() +} + +func readHistoryFile(filePath string) (StreamingHistory, error) { + file, err := os.Open(filePath) + if err != nil { + return nil, err + } + + defer file.Close() + history := StreamingHistory{} + err = history.Read(file) + if err != nil { + return nil, err + } + + return history, nil +} From 97e93553a142496fb556239d7f2e3ac728f95db9 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sat, 13 Jan 2024 14:11:58 +0100 Subject: [PATCH 19/82] Support integer config values --- internal/cli/prompt.go | 28 ++++++++++++++++++++++++++++ internal/config/services.go | 8 ++++++++ internal/models/options.go | 1 + 3 files changed, 37 insertions(+) diff --git a/internal/cli/prompt.go b/internal/cli/prompt.go index fd0676d..f2d045b 100644 --- a/internal/cli/prompt.go +++ b/internal/cli/prompt.go @@ -17,6 +17,7 @@ package cli import ( "fmt" + "strconv" "github.com/manifoldco/promptui" "go.uploadedlobster.com/scotty/internal/i18n" @@ -31,6 +32,8 @@ func Prompt(opt models.BackendOption) (any, error) { return PromptSecret(opt) case models.String: return PromptString(opt) + case models.Int: + return PromptInt(opt) default: return nil, fmt.Errorf("unknown prompt type %v", opt.Type) } @@ -78,3 +81,28 @@ func PromptYesNo(label string, defaultValue bool) (bool, error) { _, val, err := sel.Run() return val == yes, err } + +func PromptInt(opt models.BackendOption) (int, error) { + validate := func(s string) error { + if opt.Validate != nil { + if err := opt.Validate(s); err != nil { + return err + } + } + if _, err := strconv.Atoi(s); err != nil { + return err + } + return nil + } + prompt := promptui.Prompt{ + Label: opt.Label, + Validate: validate, + Default: opt.Default, + } + + val, err := prompt.Run() + if err != nil { + return 0, err + } + return strconv.Atoi(val) +} diff --git a/internal/config/services.go b/internal/config/services.go index 7c8bdb6..c04ee12 100644 --- a/internal/config/services.go +++ b/internal/config/services.go @@ -62,6 +62,14 @@ func (c *ServiceConfig) GetBool(key string, defaultValue bool) bool { } } +func (c *ServiceConfig) GetInt(key string, defaultValue int) int { + if c.IsSet(key) { + return cast.ToInt(c.ConfigValues[key]) + } else { + return defaultValue + } +} + func (c *ServiceConfig) IsSet(key string) bool { _, ok := c.ConfigValues[key] return ok diff --git a/internal/models/options.go b/internal/models/options.go index 0763032..ffa3ae6 100644 --- a/internal/models/options.go +++ b/internal/models/options.go @@ -21,6 +21,7 @@ const ( Bool OptionType = "bool" Secret OptionType = "secret" String OptionType = "string" + Int OptionType = "int" ) type BackendOption struct { From 925c21893bff2970775eff0a736b0948730d2c2e Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sat, 13 Jan 2024 14:12:19 +0100 Subject: [PATCH 20/82] spotifyhistory: configurable min duration for skipped tracks --- internal/backends/spotifyhistory/spotifyhistory.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/backends/spotifyhistory/spotifyhistory.go b/internal/backends/spotifyhistory/spotifyhistory.go index 304de0c..2e470e4 100644 --- a/internal/backends/spotifyhistory/spotifyhistory.go +++ b/internal/backends/spotifyhistory/spotifyhistory.go @@ -56,6 +56,11 @@ func (b *SpotifyHistoryBackend) Options() []models.BackendOption { Label: i18n.Tr("Ignore skipped listens"), Type: models.Bool, Default: "false", + }, { + Name: "ignore-min-duration-ms", + Label: i18n.Tr("Minimum playback duration for skipped tracks (milliseconds)"), + Type: models.Int, + Default: "30000", }} } @@ -63,7 +68,7 @@ func (b *SpotifyHistoryBackend) FromConfig(config *config.ServiceConfig) models. b.dirPath = config.GetString("dir-path") b.ignoreIncognito = config.GetBool("ignore-incognito", true) b.ignoreSkipped = config.GetBool("ignore-skipped", false) - b.skippedMinDurationMs = 30000 + b.skippedMinDurationMs = config.GetInt("ignore-min-duration-ms", 30000) return b } From d9d83a4282743c785a6ca15078cea56d19bf5ceb Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 14 Jan 2024 11:54:54 +0100 Subject: [PATCH 21/82] Fixed examples not being run during test --- internal/util/util_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/util/util_test.go b/internal/util/util_test.go index 638e3a6..5aee726 100644 --- a/internal/util/util_test.go +++ b/internal/util/util_test.go @@ -21,13 +21,13 @@ import ( "go.uploadedlobster.com/scotty/internal/util" ) -func MaxExample() { +func ExampleMax() { v := util.Max(2, 5) fmt.Print(v) // Output: 5 } -func MinExample() { +func ExampleMin() { v := util.Min(2, 5) fmt.Print(v) // Output: 2 From bace31471e9402578ad8f9c87715721e114d8cd8 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 14 Jan 2024 13:12:02 +0100 Subject: [PATCH 22/82] New similarity module to help with comparing track titles --- go.mod | 1 + go.sum | 4 ++ internal/similarity/similarity.go | 55 ++++++++++++++++++++++++++ internal/similarity/similarity_test.go | 51 ++++++++++++++++++++++++ 4 files changed, 111 insertions(+) create mode 100644 internal/similarity/similarity.go create mode 100644 internal/similarity/similarity_test.go diff --git a/go.mod b/go.mod index 4c5487f..bbfa37e 100644 --- a/go.mod +++ b/go.mod @@ -28,6 +28,7 @@ require ( require ( github.com/VividCortex/ewma v1.2.0 // indirect github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/agnivade/levenshtein v1.1.1 // indirect github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dustin/go-humanize v1.0.1 // indirect diff --git a/go.sum b/go.sum index fd1332d..4d08441 100644 --- a/go.sum +++ b/go.sum @@ -44,6 +44,9 @@ github.com/Xuanwo/go-locale v1.1.0 h1:51gUxhxl66oXAjI9uPGb2O0qwPECpriKQb2hl35mQk github.com/Xuanwo/go-locale v1.1.0/go.mod h1:UKrHoZB3FPIk9wIG2/tVSobnHgNnceGSH3Y8DY5cASs= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= +github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= +github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -64,6 +67,7 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/delucks/go-subsonic v0.0.0-20220915164742-2744002c4be5 h1:RuuxidatioSKGOiBzL1mTY4X22DQD8weEbS3iRLHnAg= github.com/delucks/go-subsonic v0.0.0-20220915164742-2744002c4be5/go.mod h1:vnbEuj6Z20PLcHB4rrLQAOXGMjtULfMGhRVSFPcSdUo= +github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= diff --git a/internal/similarity/similarity.go b/internal/similarity/similarity.go new file mode 100644 index 0000000..8e8536d --- /dev/null +++ b/internal/similarity/similarity.go @@ -0,0 +1,55 @@ +/* +Copyright © 2024 Philipp Wolfer + +Scotty is free software: you can redistribute it and/or modify it under the +terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later version. + +Scotty is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +Scotty. If not, see . +*/ + +package similarity + +import ( + "regexp" + "strings" + + "github.com/agnivade/levenshtein" + "go.uploadedlobster.com/scotty/internal/util" + "golang.org/x/text/unicode/norm" +) + +// Returns the Levensthein distance between s1 and s2 relative to the length of +// the longer string. +// Unicode normalization on the strings is performed. +func Similarity(s1 string, s2 string) float64 { + s1 = norm.NFKC.String(s1) + s2 = norm.NFKC.String(s2) + l1 := len([]rune(s1)) + l2 := len([]rune(s2)) + maxLen := util.Max(l1, l2) + // Empty strings always compare full equal + if maxLen == 0 { + return 1.0 + } + dist := levenshtein.ComputeDistance(s1, s2) + // fmt.Printf("%v (%v) ~ %v (%v) = %v\n", s1, l1, s2, l2, dist) + return 1.0 - (float64(dist) / float64(maxLen)) +} + +var reExtraTitleInfo = regexp.MustCompile(`\([^)]+\)$`) +var reMultiSpace = regexp.MustCompile(`\s+`) + +// Normalizes a track or release title. +func NormalizeTitle(s string) string { + s = strings.TrimSpace(s) + s = strings.ToLower(s) + s = reExtraTitleInfo.ReplaceAllString(s, "") + s = reMultiSpace.ReplaceAllString(s, " ") + return s +} diff --git a/internal/similarity/similarity_test.go b/internal/similarity/similarity_test.go new file mode 100644 index 0000000..206c6f0 --- /dev/null +++ b/internal/similarity/similarity_test.go @@ -0,0 +1,51 @@ +/* +Copyright © 2024 Philipp Wolfer + +Scotty is free software: you can redistribute it and/or modify it under the +terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later version. + +Scotty is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +Scotty. If not, see . +*/ + +package similarity_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "go.uploadedlobster.com/scotty/internal/similarity" +) + +func ExampleSimilarity() { + s := similarity.Similarity("bar1", "bär1") + fmt.Println(s) + // Output: 0.75 +} + +func TestSimilarity(t *testing.T) { + assert := assert.New(t) + assert.Equal(1.0, similarity.Similarity("", "")) + assert.Equal(0.0, similarity.Similarity("foo", "")) + assert.Equal(0.0, similarity.Similarity("foo", "bar")) + assert.Equal(0.5, similarity.Similarity("foobar", "bar")) + assert.Equal(1.0, similarity.Similarity("foo", "foo")) +} + +func ExampleNormalizeTitle() { + s := similarity.NormalizeTitle(" Forever \tFailure (video edit) ") + fmt.Println(s) + // Output: forever failure +} + +func TestNormalizeTitle(t *testing.T) { + assert := assert.New(t) + assert.Equal("forever failure", similarity.NormalizeTitle("Forever Failure")) + assert.Equal("foo", similarity.NormalizeTitle(" \tfoo\t \t")) +} From b2b5c69278e0c6867084a230ec581bd3204087b3 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 14 Jan 2024 17:14:05 +0100 Subject: [PATCH 23/82] New similarity.CompareTracks function --- internal/similarity/similarity.go | 31 +++++++++++++++++++++-- internal/similarity/similarity_test.go | 35 ++++++++++++++++++++++++++ internal/util/util.go | 16 ++++++++++++ internal/util/util_test.go | 30 ++++++++++++++++++++++ 4 files changed, 110 insertions(+), 2 deletions(-) diff --git a/internal/similarity/similarity.go b/internal/similarity/similarity.go index 8e8536d..4c0b345 100644 --- a/internal/similarity/similarity.go +++ b/internal/similarity/similarity.go @@ -20,6 +20,7 @@ import ( "strings" "github.com/agnivade/levenshtein" + "go.uploadedlobster.com/scotty/internal/models" "go.uploadedlobster.com/scotty/internal/util" "golang.org/x/text/unicode/norm" ) @@ -42,14 +43,40 @@ func Similarity(s1 string, s2 string) float64 { return 1.0 - (float64(dist) / float64(maxLen)) } -var reExtraTitleInfo = regexp.MustCompile(`\([^)]+\)$`) var reMultiSpace = regexp.MustCompile(`\s+`) +var reIgnoredPatterns = []*regexp.Regexp{ + regexp.MustCompile(`\s+\([^)]+\)$`), + regexp.MustCompile(`\s+- (\d{4} )?remaster(ed)?$`), +} // Normalizes a track or release title. func NormalizeTitle(s string) string { s = strings.TrimSpace(s) s = strings.ToLower(s) - s = reExtraTitleInfo.ReplaceAllString(s, "") s = reMultiSpace.ReplaceAllString(s, " ") + for _, re := range reIgnoredPatterns { + s = re.ReplaceAllString(s, "") + } return s } + +// Compare two tracks for similarity. +func CompareTracks(t1 models.Track, t2 models.Track) float64 { + // Identical recording MBID always compares 100% + if t1.RecordingMbid == t2.RecordingMbid && t1.RecordingMbid != "" { + return 1.0 + } + + // Compare track name and artist + sims := []float64{ + Similarity(NormalizeTitle(t1.TrackName), NormalizeTitle(t2.TrackName)), + Similarity(NormalizeTitle(t1.ArtistName()), NormalizeTitle(t2.ArtistName())), + } + + // Compare release names only if they are set for both tracks + if t1.ReleaseName != "" && t2.ReleaseName != "" { + sims = append(sims, Similarity(NormalizeTitle(t1.ReleaseName), NormalizeTitle(t2.ReleaseName))) + } + + return util.Average(sims...) +} diff --git a/internal/similarity/similarity_test.go b/internal/similarity/similarity_test.go index 206c6f0..f1e92a5 100644 --- a/internal/similarity/similarity_test.go +++ b/internal/similarity/similarity_test.go @@ -20,6 +20,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "go.uploadedlobster.com/scotty/internal/models" "go.uploadedlobster.com/scotty/internal/similarity" ) @@ -36,6 +37,7 @@ func TestSimilarity(t *testing.T) { assert.Equal(0.0, similarity.Similarity("foo", "bar")) assert.Equal(0.5, similarity.Similarity("foobar", "bar")) assert.Equal(1.0, similarity.Similarity("foo", "foo")) + assert.Equal(0.6, similarity.Similarity("Forever After", "Forever Failure")) } func ExampleNormalizeTitle() { @@ -48,4 +50,37 @@ func TestNormalizeTitle(t *testing.T) { assert := assert.New(t) assert.Equal("forever failure", similarity.NormalizeTitle("Forever Failure")) assert.Equal("foo", similarity.NormalizeTitle(" \tfoo\t \t")) + assert.Equal("wasted years", similarity.NormalizeTitle("Wasted Years - 2015 Remaster")) + assert.Equal("london calling", similarity.NormalizeTitle("London Calling - Remastered")) + assert.Equal("london calling", similarity.NormalizeTitle("London Calling (Remastered)")) +} + +func ExampleCompareTracks() { + t1 := models.Track{ + ArtistNames: []string{"Paradise Lost"}, + TrackName: "Forever After", + } + t2 := models.Track{ + ArtistNames: []string{"Paradise Lost"}, + TrackName: "Forever Failure (radio edit)", + ReleaseName: "Draconian Times", + } + sim := similarity.CompareTracks(t1, t2) + fmt.Println(sim) + // Output: 0.8333333333333334 +} + +func TestCompareTracksSameMBID(t *testing.T) { + t1 := models.Track{ + ArtistNames: []string{"Paradise Lost"}, + TrackName: "Forever After", + RecordingMbid: models.MBID("2886d15c-09b0-43c6-af56-932f70dde164"), + } + t2 := models.Track{ + ArtistNames: []string{"Paradise Lost"}, + TrackName: "Forever Failure (radio edit)", + ReleaseName: "Draconian Times", + RecordingMbid: models.MBID("2886d15c-09b0-43c6-af56-932f70dde164"), + } + assert.Equal(t, 1.0, similarity.CompareTracks(t1, t2)) } diff --git a/internal/util/util.go b/internal/util/util.go index 99826a1..e8663a7 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -32,3 +32,19 @@ func Min[T constraints.Ordered](m, n T) T { return m } } + +func Sum[T constraints.Integer | constraints.Float](v ...T) T { + var sum T + for _, i := range v { + sum += i + } + return sum +} + +func Average[T constraints.Integer | constraints.Float](v ...T) float64 { + length := len(v) + if length == 0 { + return 0.0 + } + return float64(Sum(v...)) / float64(length) +} diff --git a/internal/util/util_test.go b/internal/util/util_test.go index 5aee726..73ec415 100644 --- a/internal/util/util_test.go +++ b/internal/util/util_test.go @@ -17,7 +17,9 @@ package util_test import ( "fmt" + "testing" + "github.com/stretchr/testify/assert" "go.uploadedlobster.com/scotty/internal/util" ) @@ -32,3 +34,31 @@ func ExampleMin() { fmt.Print(v) // Output: 2 } + +func ExampleSum() { + values := []float64{1.4, 2.2} + sum := util.Sum(values...) + fmt.Print(sum) + // Output: 3.6 +} + +func TestSumEmpty(t *testing.T) { + assert.Equal(t, 0, util.Sum([]int{}...)) +} + +func ExampleAverage() { + values := []float64{1.4, 2.2, 0.9} + sum := util.Average(values...) + fmt.Print(sum) + // Output: 1.5 +} + +func TestAverageEmpty(t *testing.T) { + assert.Equal(t, 0.0, util.Average([]int{}...)) +} + +func TestAverageInt(t *testing.T) { + assert := assert.New(t) + assert.Equal(3.0, util.Average([]int{2, 4, 3}...)) + assert.Equal(1.5, util.Average([]int{2, 1, 1, 2}...)) +} From 0d04b7333876dc613c816e41d64c2636135bd499 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 14 Jan 2024 18:53:52 +0100 Subject: [PATCH 24/82] listenbrainz: implement duplicate listen check on import --- .../backends/listenbrainz/listenbrainz.go | 71 ++++++++++++++++--- 1 file changed, 63 insertions(+), 8 deletions(-) diff --git a/internal/backends/listenbrainz/listenbrainz.go b/internal/backends/listenbrainz/listenbrainz.go index 02b6fb4..4fcfd3a 100644 --- a/internal/backends/listenbrainz/listenbrainz.go +++ b/internal/backends/listenbrainz/listenbrainz.go @@ -24,13 +24,15 @@ import ( "go.uploadedlobster.com/scotty/internal/config" "go.uploadedlobster.com/scotty/internal/i18n" "go.uploadedlobster.com/scotty/internal/models" + "go.uploadedlobster.com/scotty/internal/similarity" "go.uploadedlobster.com/scotty/internal/version" ) type ListenBrainzApiBackend struct { - client Client - username string - existingMbids map[string]bool + client Client + username string + checkDuplicates bool + existingMbids map[string]bool } func (b *ListenBrainzApiBackend) Name() string { return "listenbrainz" } @@ -44,6 +46,10 @@ func (b *ListenBrainzApiBackend) Options() []models.BackendOption { Name: "token", Label: i18n.Tr("Access token"), Type: models.Secret, + }, { + Name: "check-duplicate-listens", + Label: i18n.Tr("Check for duplicate listens on import (slower)"), + Type: models.Bool, }} } @@ -51,6 +57,7 @@ func (b *ListenBrainzApiBackend) FromConfig(config *config.ServiceConfig) models b.client = NewClient(config.GetString("token")) b.client.MaxResults = MaxItemsPerGet b.username = config.GetString("username") + b.checkDuplicates = config.GetBool("check-duplicate-listens", false) return b } @@ -117,6 +124,7 @@ func (b *ListenBrainzApiBackend) ExportListens(oldestTimestamp time.Time, result func (b *ListenBrainzApiBackend) ImportListens(export models.ListensResult, importResult models.ImportResult, progress chan models.Progress) (models.ImportResult, error) { total := len(export.Items) + p := models.Progress{}.FromImportResult(importResult) for i := 0; i < total; i += MaxListensPerRequest { listens := export.Items[i:min(i+MaxListensPerRequest, total)] count := len(listens) @@ -130,6 +138,21 @@ func (b *ListenBrainzApiBackend) ImportListens(export models.ListensResult, impo } for _, l := range listens { + if b.checkDuplicates { + isDupe, err := b.checkDuplicateListen(l) + p.Elapsed += 1 + progress <- p + if err != nil { + return importResult, err + } else if isDupe { + count -= 1 + msg := fmt.Sprintf("Ignored duplicate listen %v: \"%v\" by %v (%v)", + l.ListenedAt, l.TrackName, l.ArtistName(), l.RecordingMbid) + importResult.ImportErrors = append(importResult.ImportErrors, msg) + continue + } + } + l.FillAdditionalInfo() listen := Listen{ ListenedAt: l.ListenedAt.Unix(), @@ -142,17 +165,22 @@ func (b *ListenBrainzApiBackend) ImportListens(export models.ListensResult, impo } listen.TrackMetadata.AdditionalInfo["submission_client"] = version.AppName listen.TrackMetadata.AdditionalInfo["submission_client_version"] = version.AppVersion + submission.Payload = append(submission.Payload, listen) } - _, err := b.client.SubmitListens(submission) - if err != nil { - return importResult, err + if len(submission.Payload) > 0 { + _, err := b.client.SubmitListens(submission) + if err != nil { + return importResult, err + } } - importResult.UpdateTimestamp(listens[count-1].ListenedAt) + if count > 0 { + importResult.UpdateTimestamp(listens[count-1].ListenedAt) + } importResult.ImportCount += count - progress <- models.Progress{}.FromImportResult(importResult) + progress <- p.FromImportResult(importResult) } return importResult, nil @@ -258,6 +286,33 @@ func (b *ListenBrainzApiBackend) ImportLoves(export models.LovesResult, importRe return importResult, nil } +var defaultDuration = time.Duration(3 * time.Minute) + +const trackSimilarityThreshold = 0.9 + +func (b *ListenBrainzApiBackend) checkDuplicateListen(listen models.Listen) (bool, error) { + // Find listens + duration := listen.Duration + if duration == 0 { + duration = defaultDuration + } + minTime := listen.ListenedAt.Add(-duration) + maxTime := listen.ListenedAt.Add(duration) + candidates, err := b.client.GetListens(b.username, maxTime, minTime) + if err != nil { + return false, err + } + + for _, c := range candidates.Payload.Listens { + sim := similarity.CompareTracks(listen.Track, c.TrackMetadata.AsTrack()) + if sim >= trackSimilarityThreshold { + return true, nil + } + } + + return false, nil +} + func (lbListen Listen) AsListen() models.Listen { listen := models.Listen{ ListenedAt: time.Unix(lbListen.ListenedAt, 0), From fa316b3025303b2f6c12ffc0aff0ccdae94dcd80 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 14 Jan 2024 22:04:28 +0100 Subject: [PATCH 25/82] Fixed completed progress bar showing empty --- internal/models/models.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/models/models.go b/internal/models/models.go index 01830cb..46dff9f 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -201,7 +201,7 @@ func (p Progress) FromImportResult(result ImportResult) Progress { } func (p Progress) Complete() Progress { - p.Total = p.Elapsed + p.Elapsed = p.Total p.Completed = true return p } From 01380bd7306ce28b43b84b9a5b442dbc6386a316 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 14 Jan 2024 22:09:12 +0100 Subject: [PATCH 26/82] listenbrainz: localize duplicate listen message --- internal/backends/listenbrainz/listenbrainz.go | 2 +- internal/cli/transfer.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/backends/listenbrainz/listenbrainz.go b/internal/backends/listenbrainz/listenbrainz.go index 4fcfd3a..2c76ecb 100644 --- a/internal/backends/listenbrainz/listenbrainz.go +++ b/internal/backends/listenbrainz/listenbrainz.go @@ -146,7 +146,7 @@ func (b *ListenBrainzApiBackend) ImportListens(export models.ListensResult, impo return importResult, err } else if isDupe { count -= 1 - msg := fmt.Sprintf("Ignored duplicate listen %v: \"%v\" by %v (%v)", + msg := i18n.Tr("Ignored duplicate listen %v: \"%v\" by %v (%v)", l.ListenedAt, l.TrackName, l.ArtistName(), l.RecordingMbid) importResult.ImportErrors = append(importResult.ImportErrors, msg) continue diff --git a/internal/cli/transfer.go b/internal/cli/transfer.go index 1af552e..84bdb2b 100644 --- a/internal/cli/transfer.go +++ b/internal/cli/transfer.go @@ -147,7 +147,7 @@ func (c *TransferCmd[E, I, R]) Transfer(exp backends.ExportProcessor[R], imp bac fmt.Println() fmt.Println(i18n.Tr("During the import the following errors occurred:")) for _, err := range result.ImportErrors { - fmt.Println(i18n.Tr("Error: %v\n", err)) + fmt.Println(i18n.Tr("Error: %v", err)) } } From 60bbbb9f15754b2bb76d2a750c7780795fc60a72 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 14 Jan 2024 22:22:00 +0100 Subject: [PATCH 27/82] spotify-history: min. playback time for skipped tracks is now in seconds --- internal/backends/spotifyhistory/models.go | 8 +++---- .../backends/spotifyhistory/spotifyhistory.go | 22 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/internal/backends/spotifyhistory/models.go b/internal/backends/spotifyhistory/models.go index 89d67ab..fad4fb1 100644 --- a/internal/backends/spotifyhistory/models.go +++ b/internal/backends/spotifyhistory/models.go @@ -28,9 +28,9 @@ import ( type StreamingHistory []HistoryItem type ListenListOptions struct { - IgnoreIncognito bool - IgnoreSkipped bool - SkippedMinDurationMs int + IgnoreIncognito bool + IgnoreSkipped bool + skippedMinSeconds int } type HistoryItem struct { @@ -72,7 +72,7 @@ func (h *StreamingHistory) AsListenList(opt ListenListOptions) models.ListensLis if item.MasterMetadataTrackName == "" || (opt.IgnoreIncognito && item.IncognitoMode) || (opt.IgnoreSkipped && item.Skipped) || - (item.Skipped && item.MillisecondsPlayed < opt.SkippedMinDurationMs) { + (item.Skipped && item.MillisecondsPlayed < opt.skippedMinSeconds*1000) { continue } listens = append(listens, item.AsListen()) diff --git a/internal/backends/spotifyhistory/spotifyhistory.go b/internal/backends/spotifyhistory/spotifyhistory.go index 2e470e4..40323a4 100644 --- a/internal/backends/spotifyhistory/spotifyhistory.go +++ b/internal/backends/spotifyhistory/spotifyhistory.go @@ -33,10 +33,10 @@ import ( const historyFileGlob = "Streaming_History_Audio_*.json" type SpotifyHistoryBackend struct { - dirPath string - ignoreIncognito bool - ignoreSkipped bool - skippedMinDurationMs int + dirPath string + ignoreIncognito bool + ignoreSkipped bool + skippedMinSeconds int } func (b *SpotifyHistoryBackend) Name() string { return "spotify-history" } @@ -57,10 +57,10 @@ func (b *SpotifyHistoryBackend) Options() []models.BackendOption { Type: models.Bool, Default: "false", }, { - Name: "ignore-min-duration-ms", - Label: i18n.Tr("Minimum playback duration for skipped tracks (milliseconds)"), + Name: "ignore-min-duration-seconds", + Label: i18n.Tr("Minimum playback duration for skipped tracks (seconds)"), Type: models.Int, - Default: "30000", + Default: "30", }} } @@ -68,7 +68,7 @@ func (b *SpotifyHistoryBackend) FromConfig(config *config.ServiceConfig) models. b.dirPath = config.GetString("dir-path") b.ignoreIncognito = config.GetBool("ignore-incognito", true) b.ignoreSkipped = config.GetBool("ignore-skipped", false) - b.skippedMinDurationMs = config.GetInt("ignore-min-duration-ms", 30000) + b.skippedMinSeconds = config.GetInt("ignore-min-duration-seconds", 30) return b } @@ -93,9 +93,9 @@ func (b *SpotifyHistoryBackend) ExportListens(oldestTimestamp time.Time, results return } listens := history.AsListenList(ListenListOptions{ - IgnoreIncognito: b.ignoreIncognito, - IgnoreSkipped: b.ignoreSkipped, - SkippedMinDurationMs: b.skippedMinDurationMs, + IgnoreIncognito: b.ignoreIncognito, + IgnoreSkipped: b.ignoreSkipped, + skippedMinSeconds: b.skippedMinSeconds, }) sort.Sort(listens) results <- models.ListensResult{Items: listens} From d704e4d3cb9dda0561e456c3f3303677546cc70d Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 14 Jan 2024 22:22:34 +0100 Subject: [PATCH 28/82] Updated README --- README.md | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index a6de18d..9f9f5c9 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Scotty transfers your listens/scrobbles and favorite tracks between various musi - Submit listens from ListenBrainz to Maloja or Last.fm - Transfer loved tracks from Funkwhale to ListenBrainz - Submit listens stored in a Rockbox `.scrobbler.log` file to ListenBrainz, Last.fm or Maloja +- Submit listens from Spotify extended history files to ListenBrainz, Last.fm or Maloja - Store your favorite tracks from Deezer as a JSPF playlist - Backup your listening history from ListenBrainz or Last.fm @@ -116,17 +117,18 @@ scotty beam listens deezer listenbrainz --timestamp "2023-12-06 14:26:24" ### Supported backends The following table lists the available backends and the currently supported features. -Backend | Listens Export | Listens Import | Loves Export | Loves Import ----------------|----------------|----------------|--------------|------------- -deezer | ✓ | ⨯ | ✓ | - -funkwhale | ✓ | ⨯ | ✓ | - -jspf | - | ✓ | - | ✓ -lastfm | ✓ | ✓ | ✓ | ✓ -listenbrainz | ✓ | ✓ | ✓ | ✓ -maloja | ✓ | ✓ | ⨯ | ⨯ -scrobbler-log | ✓ | ✓ | ⨯ | ⨯ -spotify | ✓ | ⨯ | ✓ | - -subsonic | ⨯ | ⨯ | ✓ | - +Backend | Listens Export | Listens Import | Loves Export | Loves Import +----------------|----------------|----------------|--------------|------------- +deezer | ✓ | ⨯ | ✓ | - +funkwhale | ✓ | ⨯ | ✓ | - +jspf | - | ✓ | - | ✓ +lastfm | ✓ | ✓ | ✓ | ✓ +listenbrainz | ✓ | ✓ | ✓ | ✓ +maloja | ✓ | ✓ | ⨯ | ⨯ +scrobbler-log | ✓ | ✓ | ⨯ | ⨯ +spotify | ✓ | ⨯ | ✓ | - +spotify-history | ✓ | ⨯ | ⨯ | ⨯ +subsonic | ⨯ | ⨯ | ✓ | - **✓** implemented **-** not yet implemented **⨯** unavailable / not planned @@ -143,7 +145,7 @@ You can help translate this project into your language with [Weblate](https://tr ## License -Scotty © 2023 Philipp Wolfer +Scotty © 2023-2024 Philipp Wolfer Scotty is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. From 66242d005728e01f42a3ce5806aeacca8fc275f7 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 14 Jan 2024 22:32:56 +0100 Subject: [PATCH 29/82] Updated changelog --- CHANGES.md | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 615939f..25acd5b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,39 @@ # Scotty Changelog -## 0.3.0 - unreleased -- listenbrainz: fetch listens in reverse listen time order +## 0.4.0 - unreleased +- JSPF: implement append mode +- scrobberlog: default for append mode is enabled +- Subsonic: Only set tags if genre is not empty +- ListenBrainz: Listen import can be configured to check for duplicate listens +- spotify-history: New backend for importing from Spotify extended streaming + history JSON files +- Allow date time string for `--timestamp` parameter +- Fixed completed progress bar showing empty + + +## 0.3.1 - 2023-12-10 +- Prompt user to authenticate after service requiring authentication added +- Commands service auth, edit and delete now all support `--service` flag +- Do not apply locale formatting in UI output to Unix timestamps +- Default for service delete confirmation is now "no" +- Default for Maloja "nofix" option is now "no" +- Fixed last stored timestamp for beam loves not getting loaded +- Fixed crash with invalid target config name in beam commands + + +## 0.3.0 - 2023-12-09 +- Initialize config if it does not exist +- Set database path relative to config file location +- Implemented service configuration commands +- Use positional arguments for source and target in beam commands +- Allow specifying `--timestamp 0` +- Subsonic: fixed filtering songs based on timestamp +- JSPF: add MB playlist extension +- Spotify: fixed loves export count +- Deezer: fixed listen export count +- ListenBrainz: fetch listens in reverse listen time order +- Initial localization of user interface +- Documented general configuration and usage ## 0.2.0 - 2023-11-28 From 6281554248b9351acaea974ac7c49e5b272e7409 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 14 Jan 2024 23:41:15 +0100 Subject: [PATCH 30/82] jspf: fixed creating new file in append mode --- internal/backends/jspf/jspf.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/backends/jspf/jspf.go b/internal/backends/jspf/jspf.go index b73b4b1..17046e7 100644 --- a/internal/backends/jspf/jspf.go +++ b/internal/backends/jspf/jspf.go @@ -1,5 +1,5 @@ /* -Copyright © 2023 Philipp Wolfer +Copyright © 2023-2024 Philipp Wolfer This file is part of Scotty. @@ -180,7 +180,7 @@ func (b *JSPFBackend) readJSPF() error { if b.append { file, err := os.Open(b.filePath) if err != nil { - return err + return nil } defer file.Close() From 210fe928fdabe117337b6eda3992ecd57f4d6fd6 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Mon, 15 Jan 2024 07:34:42 +0100 Subject: [PATCH 31/82] Update config.example.toml Add spotify-history and listenbrainz check-duplicate-listens. Clarify documentation. --- config.example.toml | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/config.example.toml b/config.example.toml index 11930a2..6a5eb88 100644 --- a/config.example.toml +++ b/config.example.toml @@ -11,6 +11,13 @@ backend = "listenbrainz" username = "" # Your ListenBrainz access token from https://listenbrainz.org/profile/ token = "" +# If true, for each listen to submit it is checked whether this listen already +# exists on ListenBrainz. For this similar listens are searched at the listen +# time +/- the duration of the track. The default is false and this check is not +# perfomed. Note that this verification significanly slows down the import and +# is only recommended if you are importing historic listens which might or might +# not already exists in your ListenBrainz profile. +check-duplicate-listens = false [service.maloja] # Maloja is a self hosted listening service (https://github.com/krateng/maloja) @@ -46,8 +53,9 @@ token = "" [service.scrobbler-log] # Read or write listens from a Rockbox .scobbler.log file backend = "scrobbler-log" -# The file path to the .scrobbler.log file -file-path = "data/.scrobbler.log" +# The file path to the .scrobbler.log file. Relative paths are resolved against +# the current working directory when running scotty. +file-path = "./.scrobbler.log" # If true, reading listens from the file also returns listens marked as "skipped" include-skipped = true # If true (default), new listens will be appended to the existing file. Set to @@ -57,16 +65,17 @@ append = true [service.jspf] # Write listens and loves to JSPF playlist files (https://xspf.org/jspf) backend = "jspf" -# The file path to the XSPF file -file-path = "data/playlist.jspf" +# The file path to the JSPF file. Relative paths are resolved against +# the current working directory when running scotty. +file-path = "./playlist.jspf" # If true (default), new listens will be appended to the existing file. Set to # false to overwrite the file and create a new JSPF playlist on every run. append = true -# Title of the playlist +# Title of the playlist. Not used in append mode. title = "My Playlist" -# Creator of the playlist (only informational) +# Creator of the playlist (only informational). Not used in append mode. username = "" -# A unique identifier for your playlist +# A unique identifier for your playlist. Not used in append mode. identifier = "" [service.spotify] @@ -79,6 +88,21 @@ backend = "spotify" client-id = "" client-secret = "" +[service.spotify-history] +# Read listens from a Spotify extended history export +backend = "spotify-history" +# Directory where the extended history JSON files are located. The files must +# follow the naming scheme "Streaming_History_Audio_*.json". +dir-path = "./my_spotify_data_extended/Spotify Extended Streaming History" +# If true (default), ignore listens from a Spotify "private session". +ignore-incognito = true +# If true, ignore listens marked as skipped. Default is false. +ignore-skipped = false +# Only consider skipped listens with a playback duration longer than this number +# of seconds. Default is 30 seconds. If ignore-skipped is set to false this +# setting has no effect. +ignore-min-duration-seconds = 30 + [service.deezer] # Read listens and loves from a Deezer account backend = "deezer" From 91f9b62db3c6610cc64392a3e8e24b47a9fabdf8 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Mon, 15 Jan 2024 07:35:44 +0100 Subject: [PATCH 32/82] Update translations --- internal/translations/catalog.go | 91 ++++++++++-------- .../translations/locales/de/out.gotext.json | 79 ++++++++++++++++ .../translations/locales/en/out.gotext.json | 93 +++++++++++++++++++ 3 files changed, 225 insertions(+), 38 deletions(-) diff --git a/internal/translations/catalog.go b/internal/translations/catalog.go index e91524f..86a3226 100644 --- a/internal/translations/catalog.go +++ b/internal/translations/catalog.go @@ -47,27 +47,33 @@ var messageKeyToIndex = map[string]int{ "Access token received, you can use %v now.\n": 28, "Append to file": 21, "Backend": 36, - "Client ID": 15, - "Client secret": 16, - "Delete the service configuration \"%v\"?": 7, + "Check for duplicate listens on import (slower)": 47, + "Client ID": 15, + "Client secret": 16, + "Delete the service configuration \"%v\"?": 7, + "Directory path": 49, "Disable auto correction of submitted listens": 24, "During the import the following errors occurred:": 41, - "Error: %v\n": 42, - "Error: OAuth state mismatch": 27, - "Failed reading config: %v": 2, - "File path": 20, - "From timestamp: %v (%v)": 38, - "Import failed, last reported timestamp was %v (%s)": 39, - "Imported %v of %v %s into %v.": 40, - "Include skipped listens": 25, - "Latest timestamp: %v (%v)": 43, - "No": 33, - "Playlist title": 22, - "Saved service %v using backend %v": 5, - "Server URL": 17, - "Service": 35, - "Service \"%v\" deleted\n": 9, - "Service name": 3, + "Error: %v": 42, + "Error: OAuth state mismatch": 27, + "Failed reading config: %v": 2, + "File path": 20, + "From timestamp: %v (%v)": 38, + "Ignore listens in incognito mode": 50, + "Ignore skipped listens": 51, + "Ignored duplicate listen %v: \"%v\" by %v (%v)": 48, + "Import failed, last reported timestamp was %v (%s)": 39, + "Imported %v of %v %s into %v.": 40, + "Include skipped listens": 25, + "Latest timestamp: %v (%v)": 43, + "Minimum playback duration for skipped tracks (seconds)": 52, + "No": 33, + "Playlist title": 22, + "Saved service %v using backend %v": 5, + "Server URL": 17, + "Service": 35, + "Service \"%v\" deleted\n": 9, + "Service name": 3, "The backend %v requires authentication. Authenticate now?": 6, "Token received, you can close this window now.": 12, "Transferring %s from %s to %s...": 37, @@ -81,6 +87,7 @@ var messageKeyToIndex = map[string]int{ "done": 31, "exporting": 29, "importing": 30, + "invalid timestamp string \"%v\"": 53, "key must only consist of A-Za-z0-9_-": 45, "no configuration file defined, cannot write config": 44, "no existing service configurations": 34, @@ -88,7 +95,7 @@ var messageKeyToIndex = map[string]int{ "unknown backend \"%s\"": 14, } -var deIndex = []uint32{ // 48 elements +var deIndex = []uint32{ // 55 elements // Entry 0 - 1F 0x00000000, 0x00000013, 0x00000027, 0x00000052, 0x0000005e, 0x0000008d, 0x000000bd, 0x00000104, @@ -101,11 +108,13 @@ var deIndex = []uint32{ // 48 elements // Entry 20 - 3F 0x000003ba, 0x000003bd, 0x000003c2, 0x000003eb, 0x000003f3, 0x000003fb, 0x00000424, 0x00000442, - 0x0000047f, 0x000004aa, 0x000004e1, 0x000004f4, - 0x00000517, 0x00000568, 0x0000059f, 0x000005c6, -} // Size: 216 bytes + 0x0000047f, 0x000004aa, 0x000004e1, 0x000004ef, + 0x00000512, 0x00000563, 0x0000059a, 0x000005c1, + 0x000005c1, 0x000005c1, 0x000005c1, 0x000005c1, + 0x000005c1, 0x000005c1, 0x000005c1, +} // Size: 244 bytes -const deData string = "" + // Size: 1478 bytes +const deData string = "" + // Size: 1473 bytes "\x04\x01\x09\x00\x0e\x02Export: %[1]s\x04\x01\x09\x01\x0a\x0e\x02Import:" + " %[1]s\x02Fehler beim Lesen der Konfiguration: %[1]v\x02Servicename\x02e" + "in Service mit diesem Namen existiert bereits\x02Service %[1]v mit dem B" + @@ -126,12 +135,12 @@ const deData string = "" + // Size: 1478 bytes " von %[2]s nach %[3]s...\x02Ab Zeitstempel: %[1]v (%[2]v)\x02Import fehl" + "geschlagen, letzter Zeitstempel war %[1]v (%[2]s)\x02%[1]v von %[2]v %[3" + "]s in %[4]v importiert.\x02Während des Imports sind folgende Fehler aufg" + - "etreten:\x04\x00\x01\x0a\x0e\x02Fehler: %[1]v\x02Letzter Zeitstempel: %[" + - "1]v (%[2]v)\x02keine Konfigurationsdatei definiert, Konfiguration kann n" + - "icht geschrieben werden\x02Schlüssel darf nur die Zeichen A-Za-z0-9_- be" + - "inhalten\x02keine Servicekonfiguration „%[1]v“" + "etreten:\x02Fehler: %[1]v\x02Letzter Zeitstempel: %[1]v (%[2]v)\x02keine" + + " Konfigurationsdatei definiert, Konfiguration kann nicht geschrieben wer" + + "den\x02Schlüssel darf nur die Zeichen A-Za-z0-9_- beinhalten\x02keine Se" + + "rvicekonfiguration „%[1]v“" -var enIndex = []uint32{ // 48 elements +var enIndex = []uint32{ // 55 elements // Entry 0 - 1F 0x00000000, 0x00000013, 0x00000027, 0x00000044, 0x00000051, 0x00000079, 0x000000a1, 0x000000de, @@ -144,11 +153,13 @@ var enIndex = []uint32{ // 48 elements // Entry 20 - 3F 0x0000032f, 0x00000333, 0x00000336, 0x00000359, 0x00000361, 0x00000369, 0x00000393, 0x000003b1, - 0x000003ea, 0x00000414, 0x00000445, 0x00000457, - 0x00000477, 0x000004aa, 0x000004cf, 0x000004f0, -} // Size: 216 bytes + 0x000003ea, 0x00000414, 0x00000445, 0x00000452, + 0x00000472, 0x000004a5, 0x000004ca, 0x000004eb, + 0x0000051a, 0x00000553, 0x00000562, 0x00000583, + 0x0000059a, 0x000005d1, 0x000005f2, +} // Size: 244 bytes -const enData string = "" + // Size: 1264 bytes +const enData string = "" + // Size: 1522 bytes "\x04\x01\x09\x00\x0e\x02export: %[1]s\x04\x01\x09\x01\x0a\x0e\x02import:" + " %[1]s\x02Failed reading config: %[1]v\x02Service name\x02a service with" + " this name already exists\x02Saved service %[1]v using backend %[2]v\x02" + @@ -167,9 +178,13 @@ const enData string = "" + // Size: 1264 bytes "e\x02Backend\x02Transferring %[1]s from %[2]s to %[3]s...\x02From timest" + "amp: %[1]v (%[2]v)\x02Import failed, last reported timestamp was %[1]v (" + "%[2]s)\x02Imported %[1]v of %[2]v %[3]s into %[4]v.\x02During the import" + - " the following errors occurred:\x04\x00\x01\x0a\x0d\x02Error: %[1]v\x02L" + - "atest timestamp: %[1]v (%[2]v)\x02no configuration file defined, cannot " + - "write config\x02key must only consist of A-Za-z0-9_-\x02no service confi" + - "guration \x22%[1]v\x22" + " the following errors occurred:\x02Error: %[1]v\x02Latest timestamp: %[1" + + "]v (%[2]v)\x02no configuration file defined, cannot write config\x02key " + + "must only consist of A-Za-z0-9_-\x02no service configuration \x22%[1]v" + + "\x22\x02Check for duplicate listens on import (slower)\x02Ignored duplic" + + "ate listen %[1]v: \x22%[2]v\x22 by %[3]v (%[4]v)\x02Directory path\x02Ig" + + "nore listens in incognito mode\x02Ignore skipped listens\x02Minimum play" + + "back duration for skipped tracks (seconds)\x02invalid timestamp string " + + "\x22%[1]v\x22" - // Total table size 3174 bytes (3KiB); checksum: 18BB58D4 + // Total table size 3483 bytes (3KiB); checksum: 43313DBC diff --git a/internal/translations/locales/de/out.gotext.json b/internal/translations/locales/de/out.gotext.json index 94494ee..8d11b1a 100644 --- a/internal/translations/locales/de/out.gotext.json +++ b/internal/translations/locales/de/out.gotext.json @@ -255,6 +255,50 @@ "message": "Unique playlist identifier", "translation": "Eindeutige Playlist-ID" }, + { + "id": "Check for duplicate listens on import (slower)", + "message": "Check for duplicate listens on import (slower)", + "translation": "" + }, + { + "id": "Ignored duplicate listen {ListenedAt}: \"{TrackName}\" by {ArtistName} ({RecordingMbid})", + "message": "Ignored duplicate listen {ListenedAt}: \"{TrackName}\" by {ArtistName} ({RecordingMbid})", + "translation": "", + "placeholders": [ + { + "id": "ListenedAt", + "string": "%[1]v", + "type": "time.Time", + "underlyingType": "struct{wall uint64; ext int64; loc *time.Location}", + "argNum": 1, + "expr": "l.ListenedAt" + }, + { + "id": "TrackName", + "string": "%[2]v", + "type": "string", + "underlyingType": "string", + "argNum": 2, + "expr": "l.TrackName" + }, + { + "id": "ArtistName", + "string": "%[3]v", + "type": "string", + "underlyingType": "string", + "argNum": 3, + "expr": "l.ArtistName()" + }, + { + "id": "RecordingMbid", + "string": "%[4]v", + "type": "go.uploadedlobster.com/scotty/internal/models.MBID", + "underlyingType": "string", + "argNum": 4, + "expr": "l.RecordingMbid" + } + ] + }, { "id": "Disable auto correction of submitted listens", "message": "Disable auto correction of submitted listens", @@ -265,6 +309,26 @@ "message": "Include skipped listens", "translation": "Übersprungene Titel einbeziehen" }, + { + "id": "Directory path", + "message": "Directory path", + "translation": "" + }, + { + "id": "Ignore listens in incognito mode", + "message": "Ignore listens in incognito mode", + "translation": "" + }, + { + "id": "Ignore skipped listens", + "message": "Ignore skipped listens", + "translation": "" + }, + { + "id": "Minimum playback duration for skipped tracks (seconds)", + "message": "Minimum playback duration for skipped tracks (seconds)", + "translation": "" + }, { "id": "Visit the URL for authorization: {Url}", "message": "Visit the URL for authorization: {Url}", @@ -478,6 +542,21 @@ } ] }, + { + "id": "invalid timestamp string \"{FlagValue}\"", + "message": "invalid timestamp string \"{FlagValue}\"", + "translation": "", + "placeholders": [ + { + "id": "FlagValue", + "string": "%[1]v", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "flagValue" + } + ] + }, { "id": "Latest timestamp: {Arg_1} ({Arg_2})", "message": "Latest timestamp: {Arg_1} ({Arg_2})", diff --git a/internal/translations/locales/en/out.gotext.json b/internal/translations/locales/en/out.gotext.json index fb588de..29ed76e 100644 --- a/internal/translations/locales/en/out.gotext.json +++ b/internal/translations/locales/en/out.gotext.json @@ -303,6 +303,54 @@ "translatorComment": "Copied from source.", "fuzzy": true }, + { + "id": "Check for duplicate listens on import (slower)", + "message": "Check for duplicate listens on import (slower)", + "translation": "Check for duplicate listens on import (slower)", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Ignored duplicate listen {ListenedAt}: \"{TrackName}\" by {ArtistName} ({RecordingMbid})", + "message": "Ignored duplicate listen {ListenedAt}: \"{TrackName}\" by {ArtistName} ({RecordingMbid})", + "translation": "Ignored duplicate listen {ListenedAt}: \"{TrackName}\" by {ArtistName} ({RecordingMbid})", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "ListenedAt", + "string": "%[1]v", + "type": "time.Time", + "underlyingType": "struct{wall uint64; ext int64; loc *time.Location}", + "argNum": 1, + "expr": "l.ListenedAt" + }, + { + "id": "TrackName", + "string": "%[2]v", + "type": "string", + "underlyingType": "string", + "argNum": 2, + "expr": "l.TrackName" + }, + { + "id": "ArtistName", + "string": "%[3]v", + "type": "string", + "underlyingType": "string", + "argNum": 3, + "expr": "l.ArtistName()" + }, + { + "id": "RecordingMbid", + "string": "%[4]v", + "type": "go.uploadedlobster.com/scotty/internal/models.MBID", + "underlyingType": "string", + "argNum": 4, + "expr": "l.RecordingMbid" + } + ], + "fuzzy": true + }, { "id": "Disable auto correction of submitted listens", "message": "Disable auto correction of submitted listens", @@ -317,6 +365,34 @@ "translatorComment": "Copied from source.", "fuzzy": true }, + { + "id": "Directory path", + "message": "Directory path", + "translation": "Directory path", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Ignore listens in incognito mode", + "message": "Ignore listens in incognito mode", + "translation": "Ignore listens in incognito mode", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Ignore skipped listens", + "message": "Ignore skipped listens", + "translation": "Ignore skipped listens", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Minimum playback duration for skipped tracks (seconds)", + "message": "Minimum playback duration for skipped tracks (seconds)", + "translation": "Minimum playback duration for skipped tracks (seconds)", + "translatorComment": "Copied from source.", + "fuzzy": true + }, { "id": "Visit the URL for authorization: {Url}", "message": "Visit the URL for authorization: {Url}", @@ -558,6 +634,23 @@ ], "fuzzy": true }, + { + "id": "invalid timestamp string \"{FlagValue}\"", + "message": "invalid timestamp string \"{FlagValue}\"", + "translation": "invalid timestamp string \"{FlagValue}\"", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "FlagValue", + "string": "%[1]v", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "flagValue" + } + ], + "fuzzy": true + }, { "id": "Latest timestamp: {Arg_1} ({Arg_2})", "message": "Latest timestamp: {Arg_1} ({Arg_2})", From 8a2ddb7772b32ea49658c650856b73cf42ef70bf Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Mon, 15 Jan 2024 08:00:17 +0100 Subject: [PATCH 33/82] Replaced ImportResult.ImportErrors with ImportResult.ImportLog --- internal/backends/lastfm/lastfm.go | 4 +-- .../backends/listenbrainz/listenbrainz.go | 4 +-- internal/cli/transfer.go | 8 +++--- internal/models/models.go | 24 ++++++++++++++-- internal/models/models_test.go | 28 +++++++++++++++++-- 5 files changed, 55 insertions(+), 13 deletions(-) diff --git a/internal/backends/lastfm/lastfm.go b/internal/backends/lastfm/lastfm.go index c4c2ec3..d2df067 100644 --- a/internal/backends/lastfm/lastfm.go +++ b/internal/backends/lastfm/lastfm.go @@ -236,7 +236,7 @@ func (b *LastfmApiBackend) ImportListens(export models.ListensResult, importResu for _, s := range result.Scrobbles { ignoreMsg := s.IgnoredMessage.Body if ignoreMsg != "" { - importResult.ImportErrors = append(importResult.ImportErrors, ignoreMsg) + importResult.Log(models.Warning, ignoreMsg) } } err := fmt.Errorf("last.fm import ignored %v scrobbles", count-accepted) @@ -335,7 +335,7 @@ func (b *LastfmApiBackend) ImportLoves(export models.LovesResult, importResult m } else { msg := fmt.Sprintf("Failed import of \"%s\" by %s: %v", love.TrackName, love.ArtistName(), err.Error()) - importResult.ImportErrors = append(importResult.ImportErrors, msg) + importResult.Log(models.Error, msg) } progress <- models.Progress{}.FromImportResult(importResult) diff --git a/internal/backends/listenbrainz/listenbrainz.go b/internal/backends/listenbrainz/listenbrainz.go index 2c76ecb..ebeb64c 100644 --- a/internal/backends/listenbrainz/listenbrainz.go +++ b/internal/backends/listenbrainz/listenbrainz.go @@ -148,7 +148,7 @@ func (b *ListenBrainzApiBackend) ImportListens(export models.ListensResult, impo count -= 1 msg := i18n.Tr("Ignored duplicate listen %v: \"%v\" by %v (%v)", l.ListenedAt, l.TrackName, l.ArtistName(), l.RecordingMbid) - importResult.ImportErrors = append(importResult.ImportErrors, msg) + importResult.Log(models.Info, msg) continue } } @@ -276,7 +276,7 @@ func (b *ListenBrainzApiBackend) ImportLoves(export models.LovesResult, importRe } else { msg := fmt.Sprintf("Failed import of \"%s\" by %s: %v", love.TrackName, love.ArtistName(), errMsg) - importResult.ImportErrors = append(importResult.ImportErrors, msg) + importResult.Log(models.Error, msg) } } diff --git a/internal/cli/transfer.go b/internal/cli/transfer.go index 84bdb2b..e335f1b 100644 --- a/internal/cli/transfer.go +++ b/internal/cli/transfer.go @@ -143,11 +143,11 @@ func (c *TransferCmd[E, I, R]) Transfer(exp backends.ExportProcessor[R], imp bac } // Print errors - if len(result.ImportErrors) > 0 { + if len(result.ImportLog) > 0 { fmt.Println() - fmt.Println(i18n.Tr("During the import the following errors occurred:")) - for _, err := range result.ImportErrors { - fmt.Println(i18n.Tr("Error: %v", err)) + fmt.Println(i18n.Tr("Import log:")) + for _, entry := range result.ImportLog { + fmt.Println(i18n.Tr("%v: %v", entry.Type, entry.Message)) } } diff --git a/internal/models/models.go b/internal/models/models.go index 46dff9f..a225344 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -164,11 +164,24 @@ type ListensResult ExportResult[ListensList] type LovesResult ExportResult[LovesList] +type LogEntryType string + +const ( + Info LogEntryType = "Info" + Warning LogEntryType = "Warning" + Error LogEntryType = "Error" +) + +type LogEntry struct { + Type LogEntryType + Message string +} + type ImportResult struct { TotalCount int ImportCount int LastTimestamp time.Time - ImportErrors []string + ImportLog []LogEntry // Error is only set if an unrecoverable import error occurred Error error @@ -185,7 +198,14 @@ func (i *ImportResult) Update(from ImportResult) { i.TotalCount = from.TotalCount i.ImportCount = from.ImportCount i.UpdateTimestamp(from.LastTimestamp) - i.ImportErrors = append(i.ImportErrors, from.ImportErrors...) + i.ImportLog = append(i.ImportLog, from.ImportLog...) +} + +func (i *ImportResult) Log(t LogEntryType, msg string) { + i.ImportLog = append(i.ImportLog, LogEntry{ + Type: t, + Message: msg, + }) } type Progress struct { diff --git a/internal/models/models_test.go b/internal/models/models_test.go index 59cc4dd..cd1f207 100644 --- a/internal/models/models_test.go +++ b/internal/models/models_test.go @@ -117,23 +117,45 @@ func TestLovesListSort(t *testing.T) { } func TestImportResultUpdate(t *testing.T) { + logEntry1 := models.LogEntry{ + Type: models.Warning, + Message: "foo", + } + logEntry2 := models.LogEntry{ + Type: models.Error, + Message: "bar", + } result := models.ImportResult{ TotalCount: 100, ImportCount: 20, LastTimestamp: time.Now(), - ImportErrors: []string{"foo"}, + ImportLog: []models.LogEntry{logEntry1}, } newResult := models.ImportResult{ TotalCount: 120, ImportCount: 50, LastTimestamp: time.Now().Add(1 * time.Hour), - ImportErrors: []string{"bar"}, + ImportLog: []models.LogEntry{logEntry2}, } result.Update(newResult) assert.Equal(t, 120, result.TotalCount) assert.Equal(t, 50, result.ImportCount) assert.Equal(t, newResult.LastTimestamp, result.LastTimestamp) - assert.Equal(t, []string{"foo", "bar"}, result.ImportErrors) + assert.Equal(t, []models.LogEntry{logEntry1, logEntry2}, result.ImportLog) +} + +func TestImportResultLog(t *testing.T) { + result := models.ImportResult{} + result.Log(models.Warning, "foo") + result.Log(models.Error, "bar") + expected := []models.LogEntry{{ + Type: models.Warning, + Message: "foo", + }, { + Type: models.Error, + Message: "bar", + }} + assert.Equal(t, expected, result.ImportLog) } func TestImportResultUpdateTimestamp(t *testing.T) { From 1cea9bd3016d5a32526b58f3b2e9117e8a1803cd Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Mon, 15 Jan 2024 08:05:05 +0100 Subject: [PATCH 34/82] Use ImportResult log for dump backend --- internal/backends/dump/dump.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/internal/backends/dump/dump.go b/internal/backends/dump/dump.go index 32a957b..eb342f2 100644 --- a/internal/backends/dump/dump.go +++ b/internal/backends/dump/dump.go @@ -17,6 +17,8 @@ Scotty. If not, see . package dump import ( + "fmt" + "go.uploadedlobster.com/scotty/internal/config" "go.uploadedlobster.com/scotty/internal/models" ) @@ -38,9 +40,10 @@ func (b *DumpBackend) ImportListens(export models.ListensResult, importResult mo 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.Progress{}.FromImportResult(importResult) - // fmt.Printf("🎶 %v: \"%v\" by %v (%v)\n", - // listen.ListenedAt, listen.TrackName, listen.ArtistName(), listen.RecordingMbid) } return importResult, nil @@ -50,9 +53,10 @@ func (b *DumpBackend) ImportLoves(export models.LovesResult, importResult models 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.Progress{}.FromImportResult(importResult) - // fmt.Printf("❤️ %v: \"%v\" by %v (%v)\n", - // love.Created, love.TrackName, love.ArtistName(), love.RecordingMbid) } return importResult, nil From 84443d0e69306e1108481121e81f4808249c5bd8 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Mon, 15 Jan 2024 08:21:38 +0100 Subject: [PATCH 35/82] Close export progress in export goroutine Fixes crash in case of importer exiting prematurely --- CHANGES.md | 1 + internal/backends/export.go | 2 ++ internal/cli/transfer.go | 1 - 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 25acd5b..6cbd02b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,6 +9,7 @@ history JSON files - Allow date time string for `--timestamp` parameter - Fixed completed progress bar showing empty +- Fixed crash in case of importer returning an error on import start ## 0.3.1 - 2023-12-10 diff --git a/internal/backends/export.go b/internal/backends/export.go index 9bdb8a1..44b8757 100644 --- a/internal/backends/export.go +++ b/internal/backends/export.go @@ -36,6 +36,7 @@ func (p ListensExportProcessor) ExportBackend() models.Backend { func (p ListensExportProcessor) Process(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.Progress) { p.Backend.ExportListens(oldestTimestamp, results, progress) + close(progress) } type LovesExportProcessor struct { @@ -48,4 +49,5 @@ func (p LovesExportProcessor) ExportBackend() models.Backend { func (p LovesExportProcessor) Process(oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.Progress) { p.Backend.ExportLoves(oldestTimestamp, results, progress) + close(progress) } diff --git a/internal/cli/transfer.go b/internal/cli/transfer.go index e335f1b..427af06 100644 --- a/internal/cli/transfer.go +++ b/internal/cli/transfer.go @@ -126,7 +126,6 @@ func (c *TransferCmd[E, I, R]) Transfer(exp backends.ExportProcessor[R], imp bac if result.LastTimestamp.Unix() < timestamp.Unix() { result.LastTimestamp = timestamp } - close(exportProgress) wg.Wait() progress.Wait() if result.Error != nil { From c69097434cd0b55a1a17e2ee2d5a7cf3f31dd014 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Mon, 15 Jan 2024 08:38:18 +0100 Subject: [PATCH 36/82] Update dependencies --- go.mod | 47 ++-- go.sum | 523 +++++---------------------------------- internal/cli/progress.go | 2 +- 3 files changed, 84 insertions(+), 488 deletions(-) diff --git a/go.mod b/go.mod index bbfa37e..c73cd44 100644 --- a/go.mod +++ b/go.mod @@ -4,22 +4,23 @@ go 1.21.1 require ( github.com/Xuanwo/go-locale v1.1.0 + github.com/agnivade/levenshtein v1.1.1 github.com/cli/browser v1.3.0 github.com/delucks/go-subsonic v0.0.0-20220915164742-2744002c4be5 github.com/fatih/color v1.16.0 github.com/glebarez/sqlite v1.10.0 - github.com/go-resty/resty/v2 v2.10.0 + github.com/go-resty/resty/v2 v2.11.0 github.com/jarcoal/httpmock v1.3.1 github.com/manifoldco/promptui v0.9.0 - github.com/pelletier/go-toml/v2 v2.1.0 + github.com/pelletier/go-toml/v2 v2.1.1 github.com/shkh/lastfm-go v0.0.0-20191215035245-89a801c244e0 - github.com/spf13/cast v1.5.1 + github.com/spf13/cast v1.6.0 github.com/spf13/cobra v1.8.0 - github.com/spf13/viper v1.17.0 + github.com/spf13/viper v1.18.2 github.com/stretchr/testify v1.8.4 - github.com/vbauerster/mpb/v8 v8.6.2 - golang.org/x/exp v0.0.0-20230905200255-921286631fa9 - golang.org/x/oauth2 v0.14.0 + github.com/vbauerster/mpb/v8 v8.7.2 + golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 + golang.org/x/oauth2 v0.16.0 golang.org/x/text v0.14.0 gorm.io/datatypes v1.2.0 gorm.io/gorm v1.25.5 @@ -28,15 +29,14 @@ require ( require ( github.com/VividCortex/ewma v1.2.0 // indirect github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect - github.com/agnivade/levenshtein v1.1.1 // indirect - github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect + github.com/chzyer/readline v1.5.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/fsnotify/fsnotify v1.6.0 // indirect - github.com/glebarez/go-sqlite v1.21.2 // indirect - github.com/go-sql-driver/mysql v1.7.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/glebarez/go-sqlite v1.22.0 // indirect + github.com/go-sql-driver/mysql v1.7.1 // indirect github.com/golang/protobuf v1.5.3 // indirect - github.com/google/uuid v1.4.0 // indirect + github.com/google/uuid v1.5.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect @@ -49,23 +49,22 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.4 // indirect - github.com/sagikazarmark/locafero v0.3.0 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.10.0 // indirect + github.com/spf13/afero v1.11.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.6.0 // indirect - go.uber.org/atomic v1.9.0 // indirect - go.uber.org/multierr v1.9.0 // indirect - golang.org/x/net v0.18.0 // indirect - golang.org/x/sys v0.14.0 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.31.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/net v0.20.0 // indirect + golang.org/x/sys v0.16.0 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/protobuf v1.32.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - gorm.io/driver/mysql v1.4.7 // indirect - modernc.org/libc v1.34.3 // indirect + gorm.io/driver/mysql v1.5.2 // indirect + modernc.org/libc v1.40.2 // indirect modernc.org/mathutil v1.6.0 // indirect modernc.org/memory v1.7.2 // indirect - modernc.org/sqlite v1.27.0 // indirect + modernc.org/sqlite v1.28.0 // indirect ) diff --git a/go.sum b/go.sum index 4d08441..38ff47a 100644 --- a/go.sum +++ b/go.sum @@ -1,43 +1,3 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= github.com/Xuanwo/go-locale v1.1.0 h1:51gUxhxl66oXAjI9uPGb2O0qwPECpriKQb2hl35mQkg= @@ -46,20 +6,19 @@ github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpH github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= +github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= +github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo= github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -67,108 +26,44 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/delucks/go-subsonic v0.0.0-20220915164742-2744002c4be5 h1:RuuxidatioSKGOiBzL1mTY4X22DQD8weEbS3iRLHnAg= github.com/delucks/go-subsonic v0.0.0-20220915164742-2744002c4be5/go.mod h1:vnbEuj6Z20PLcHB4rrLQAOXGMjtULfMGhRVSFPcSdUo= +github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= -github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= -github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= -github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= +github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= github.com/glebarez/sqlite v1.10.0 h1:u4gt8y7OND/cCei/NMHmfbLxF6xP2wgKcT/BJf2pYkc= github.com/glebarez/sqlite v1.10.0/go.mod h1:IJ+lfSOmiekhQsFTJRx/lHtGYmCdtAiTaf5wI9u5uHA= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-resty/resty/v2 v2.10.0 h1:Qla4W/+TMmv0fOeeRqzEpXPLfTUnR5HZ1+lGs+CkiCo= -github.com/go-resty/resty/v2 v2.10.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A= -github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-resty/resty/v2 v2.11.0 h1:i7jMfNOJYMp69lq7qozJP+bjgzfAzeOhuGlyDrqxT/8= +github.com/go-resty/resty/v2 v2.11.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= +github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= -github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= @@ -181,20 +76,12 @@ github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInw github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -216,25 +103,21 @@ github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLg github.com/microsoft/go-mssqldb v0.17.0/go.mod h1:OkoNGhGEs8EZqchVTtochlXruEhEOaO4S0d2sB5aeGQ= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= -github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= +github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.3.0 h1:zT7VEGWC2DTflmccN/5T1etyKvxSxpHsjb9cJvm4SvQ= -github.com/sagikazarmark/locafero v0.3.0/go.mod h1:w+v7UsPNFwzF1cHuOajOOzoq4U7v/ig1mpRjqV+Bu1U= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/shkh/lastfm-go v0.0.0-20191215035245-89a801c244e0 h1:cgqwZtnR+IQfUYDLJ3Kiy4aE+O/wExTzEIg8xwC4Qfs= @@ -245,22 +128,19 @@ github.com/smartystreets/goconvey v1.6.7 h1:I6tZjLXD2Q1kjvNbIzB1wvQBsXmKXiVrhpRE github.com/smartystreets/goconvey v1.6.7/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY= -github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= -github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= -github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.17.0 h1:I5txKw7MJasPL/BrfkbA0Jyo/oELqVmux4pR/UxOMfI= -github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0+yVI= +github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -268,388 +148,105 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/vbauerster/mpb/v8 v8.6.2 h1:9EhnJGQRtvgDVCychJgR96EDCOqgg2NsMuk5JUcX4DA= -github.com/vbauerster/mpb/v8 v8.6.2/go.mod h1:oVJ7T+dib99kZ/VBjoBaC8aPXiSAihnzuKmotuihyFo= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/vbauerster/mpb/v8 v8.7.2 h1:SMJtxhNho1MV3OuFgS1DAzhANN1Ejc5Ct+0iSaIkB14= +github.com/vbauerster/mpb/v8 v8.7.2/go.mod h1:ZFnrjzspgDHoxYLGvxIruiNk73GNTPG4YHgVNpR10VY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= -go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= -golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o= +golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= -golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.14.0 h1:P0Vrf/2538nmC0H+pEQ3MNFRRnVR7RlqyVw+bvm26z0= -golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= +golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211023085530-d6a326fbbf70/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/datatypes v1.2.0 h1:5YT+eokWdIxhJgWHdrb2zYUimyk0+TaFth+7a0ybzco= gorm.io/datatypes v1.2.0/go.mod h1:o1dh0ZvjIjhH/bngTpypG6lVRJ5chTBxE09FH/71k04= -gorm.io/driver/mysql v1.4.7 h1:rY46lkCspzGHn7+IYsNpSfEv9tA+SU4SkkB+GFX125Y= -gorm.io/driver/mysql v1.4.7/go.mod h1:SxzItlnT1cb6e1e4ZRpgJN2VYtcqJgqnHxWr4wsP8oc= +gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs= +gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8= gorm.io/driver/postgres v1.5.0 h1:u2FXTy14l45qc3UeCJ7QaAXZmZfDDv0YrthvmRq1l0U= gorm.io/driver/postgres v1.5.0/go.mod h1:FUZXzO+5Uqg5zzwzv4KK49R8lvGIyscBOqYrtI1Ce9A= gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU= gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI= gorm.io/driver/sqlserver v1.4.1 h1:t4r4r6Jam5E6ejqP7N82qAJIJAht27EGT41HyPfXRw0= gorm.io/driver/sqlserver v1.4.1/go.mod h1:DJ4P+MeZbc5rvY58PnmN1Lnyvb5gw5NPzGshHDnJLig= -gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= +gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -modernc.org/libc v1.34.3 h1:ag+3JIGF0o009YKhKjkqAG3N36X6ctUv2V85hGM45WA= -modernc.org/libc v1.34.3/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE= +modernc.org/libc v1.40.2 h1:pzVHG9jwYZNWANfltHiU3HYfrzYIsX6ysRLJ93adZXA= +modernc.org/libc v1.40.2/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE= modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E= modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= -modernc.org/sqlite v1.27.0 h1:MpKAHoyYB7xqcwnUwkuD+npwEa0fojF0B5QRbN+auJ8= -modernc.org/sqlite v1.27.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ= +modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0= diff --git a/internal/cli/progress.go b/internal/cli/progress.go index 7b4ad17..6d4421d 100644 --- a/internal/cli/progress.go +++ b/internal/cli/progress.go @@ -51,7 +51,7 @@ func setupProgressBar(p *mpb.Progress, name string) *mpb.Bar { mpb.PrependDecorators( decor.Name(" "), decor.OnComplete( - decor.Spinner(nil, decor.WC{W: 2, C: decor.DidentRight}), + decor.Spinner(nil, decor.WC{W: 2, C: decor.DindentRight}), green("✓ "), ), decor.Name(name, decor.WCSyncWidthR), From df423acdeb3412d40b4d9392f01191553435fc19 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 21 Jan 2024 16:19:50 +0100 Subject: [PATCH 37/82] Update translation files --- internal/translations/catalog.go | 74 ++++++----- .../locales/de/messages.gotext.json | 117 ++++++++++++++--- .../translations/locales/de/out.gotext.json | 26 ++-- .../locales/en/messages.gotext.json | 119 ++++++++++++++++-- .../translations/locales/en/out.gotext.json | 26 ++-- 5 files changed, 282 insertions(+), 80 deletions(-) diff --git a/internal/translations/catalog.go b/internal/translations/catalog.go index 86a3226..614b5b6 100644 --- a/internal/translations/catalog.go +++ b/internal/translations/catalog.go @@ -42,31 +42,31 @@ var messageKeyToIndex = map[string]int{ "\tbackend: %v": 11, "\texport: %s": 0, "\timport: %s\n": 1, + "%v: %v": 52, "Aborted": 8, "Access token": 19, "Access token received, you can use %v now.\n": 28, "Append to file": 21, "Backend": 36, - "Check for duplicate listens on import (slower)": 47, + "Check for duplicate listens on import (slower)": 45, "Client ID": 15, "Client secret": 16, "Delete the service configuration \"%v\"?": 7, - "Directory path": 49, - "Disable auto correction of submitted listens": 24, - "During the import the following errors occurred:": 41, - "Error: %v": 42, + "Directory path": 47, + "Disable auto correction of submitted listens": 24, "Error: OAuth state mismatch": 27, "Failed reading config: %v": 2, "File path": 20, "From timestamp: %v (%v)": 38, - "Ignore listens in incognito mode": 50, - "Ignore skipped listens": 51, - "Ignored duplicate listen %v: \"%v\" by %v (%v)": 48, + "Ignore listens in incognito mode": 48, + "Ignore skipped listens": 49, + "Ignored duplicate listen %v: \"%v\" by %v (%v)": 46, "Import failed, last reported timestamp was %v (%s)": 39, + "Import log:": 51, "Imported %v of %v %s into %v.": 40, "Include skipped listens": 25, - "Latest timestamp: %v (%v)": 43, - "Minimum playback duration for skipped tracks (seconds)": 52, + "Latest timestamp: %v (%v)": 41, + "Minimum playback duration for skipped tracks (seconds)": 50, "No": 33, "Playlist title": 22, "Saved service %v using backend %v": 5, @@ -88,10 +88,10 @@ var messageKeyToIndex = map[string]int{ "exporting": 29, "importing": 30, "invalid timestamp string \"%v\"": 53, - "key must only consist of A-Za-z0-9_-": 45, - "no configuration file defined, cannot write config": 44, + "key must only consist of A-Za-z0-9_-": 43, + "no configuration file defined, cannot write config": 42, "no existing service configurations": 34, - "no service configuration \"%v\"": 46, + "no service configuration \"%v\"": 44, "unknown backend \"%s\"": 14, } @@ -108,13 +108,13 @@ var deIndex = []uint32{ // 55 elements // Entry 20 - 3F 0x000003ba, 0x000003bd, 0x000003c2, 0x000003eb, 0x000003f3, 0x000003fb, 0x00000424, 0x00000442, - 0x0000047f, 0x000004aa, 0x000004e1, 0x000004ef, - 0x00000512, 0x00000563, 0x0000059a, 0x000005c1, - 0x000005c1, 0x000005c1, 0x000005c1, 0x000005c1, - 0x000005c1, 0x000005c1, 0x000005c1, + 0x0000047f, 0x000004aa, 0x000004cd, 0x0000051e, + 0x00000555, 0x0000057c, 0x0000057c, 0x0000057c, + 0x0000057c, 0x0000057c, 0x0000057c, 0x0000057c, + 0x0000057c, 0x0000057c, 0x0000057c, } // Size: 244 bytes -const deData string = "" + // Size: 1473 bytes +const deData string = "" + // Size: 1404 bytes "\x04\x01\x09\x00\x0e\x02Export: %[1]s\x04\x01\x09\x01\x0a\x0e\x02Import:" + " %[1]s\x02Fehler beim Lesen der Konfiguration: %[1]v\x02Servicename\x02e" + "in Service mit diesem Namen existiert bereits\x02Service %[1]v mit dem B" + @@ -134,11 +134,10 @@ const deData string = "" + // Size: 1473 bytes "tehenden Servicekonfigurationen\x02Service\x02Backend\x02Übertrage %[1]s" + " von %[2]s nach %[3]s...\x02Ab Zeitstempel: %[1]v (%[2]v)\x02Import fehl" + "geschlagen, letzter Zeitstempel war %[1]v (%[2]s)\x02%[1]v von %[2]v %[3" + - "]s in %[4]v importiert.\x02Während des Imports sind folgende Fehler aufg" + - "etreten:\x02Fehler: %[1]v\x02Letzter Zeitstempel: %[1]v (%[2]v)\x02keine" + - " Konfigurationsdatei definiert, Konfiguration kann nicht geschrieben wer" + - "den\x02Schlüssel darf nur die Zeichen A-Za-z0-9_- beinhalten\x02keine Se" + - "rvicekonfiguration „%[1]v“" + "]s in %[4]v importiert.\x02Letzter Zeitstempel: %[1]v (%[2]v)\x02keine K" + + "onfigurationsdatei definiert, Konfiguration kann nicht geschrieben werde" + + "n\x02Schlüssel darf nur die Zeichen A-Za-z0-9_- beinhalten\x02keine Serv" + + "icekonfiguration „%[1]v“" var enIndex = []uint32{ // 55 elements // Entry 0 - 1F @@ -153,13 +152,13 @@ var enIndex = []uint32{ // 55 elements // Entry 20 - 3F 0x0000032f, 0x00000333, 0x00000336, 0x00000359, 0x00000361, 0x00000369, 0x00000393, 0x000003b1, - 0x000003ea, 0x00000414, 0x00000445, 0x00000452, - 0x00000472, 0x000004a5, 0x000004ca, 0x000004eb, - 0x0000051a, 0x00000553, 0x00000562, 0x00000583, - 0x0000059a, 0x000005d1, 0x000005f2, + 0x000003ea, 0x00000414, 0x00000434, 0x00000467, + 0x0000048c, 0x000004ad, 0x000004dc, 0x00000515, + 0x00000524, 0x00000545, 0x0000055c, 0x00000593, + 0x0000059f, 0x000005ac, 0x000005cd, } // Size: 244 bytes -const enData string = "" + // Size: 1522 bytes +const enData string = "" + // Size: 1485 bytes "\x04\x01\x09\x00\x0e\x02export: %[1]s\x04\x01\x09\x01\x0a\x0e\x02import:" + " %[1]s\x02Failed reading config: %[1]v\x02Service name\x02a service with" + " this name already exists\x02Saved service %[1]v using backend %[2]v\x02" + @@ -177,14 +176,13 @@ const enData string = "" + // Size: 1522 bytes "ing\x02done\x02Yes\x02No\x02no existing service configurations\x02Servic" + "e\x02Backend\x02Transferring %[1]s from %[2]s to %[3]s...\x02From timest" + "amp: %[1]v (%[2]v)\x02Import failed, last reported timestamp was %[1]v (" + - "%[2]s)\x02Imported %[1]v of %[2]v %[3]s into %[4]v.\x02During the import" + - " the following errors occurred:\x02Error: %[1]v\x02Latest timestamp: %[1" + - "]v (%[2]v)\x02no configuration file defined, cannot write config\x02key " + - "must only consist of A-Za-z0-9_-\x02no service configuration \x22%[1]v" + - "\x22\x02Check for duplicate listens on import (slower)\x02Ignored duplic" + - "ate listen %[1]v: \x22%[2]v\x22 by %[3]v (%[4]v)\x02Directory path\x02Ig" + - "nore listens in incognito mode\x02Ignore skipped listens\x02Minimum play" + - "back duration for skipped tracks (seconds)\x02invalid timestamp string " + - "\x22%[1]v\x22" + "%[2]s)\x02Imported %[1]v of %[2]v %[3]s into %[4]v.\x02Latest timestamp:" + + " %[1]v (%[2]v)\x02no configuration file defined, cannot write config\x02" + + "key must only consist of A-Za-z0-9_-\x02no service configuration \x22%[1" + + "]v\x22\x02Check for duplicate listens on import (slower)\x02Ignored dupl" + + "icate listen %[1]v: \x22%[2]v\x22 by %[3]v (%[4]v)\x02Directory path\x02" + + "Ignore listens in incognito mode\x02Ignore skipped listens\x02Minimum pl" + + "ayback duration for skipped tracks (seconds)\x02Import log:\x02%[1]v: %[" + + "2]v\x02invalid timestamp string \x22%[1]v\x22" - // Total table size 3483 bytes (3KiB); checksum: 43313DBC + // Total table size 3377 bytes (3KiB); checksum: 6715024 diff --git a/internal/translations/locales/de/messages.gotext.json b/internal/translations/locales/de/messages.gotext.json index 8d1e08f..3d84fe8 100644 --- a/internal/translations/locales/de/messages.gotext.json +++ b/internal/translations/locales/de/messages.gotext.json @@ -255,6 +255,50 @@ "message": "Unique playlist identifier", "translation": "Eindeutige Playlist-ID" }, + { + "id": "Check for duplicate listens on import (slower)", + "message": "Check for duplicate listens on import (slower)", + "translation": "" + }, + { + "id": "Ignored duplicate listen {ListenedAt}: \"{TrackName}\" by {ArtistName} ({RecordingMbid})", + "message": "Ignored duplicate listen {ListenedAt}: \"{TrackName}\" by {ArtistName} ({RecordingMbid})", + "translation": "", + "placeholders": [ + { + "id": "ListenedAt", + "string": "%[1]v", + "type": "time.Time", + "underlyingType": "struct{wall uint64; ext int64; loc *time.Location}", + "argNum": 1, + "expr": "l.ListenedAt" + }, + { + "id": "TrackName", + "string": "%[2]v", + "type": "string", + "underlyingType": "string", + "argNum": 2, + "expr": "l.TrackName" + }, + { + "id": "ArtistName", + "string": "%[3]v", + "type": "string", + "underlyingType": "string", + "argNum": 3, + "expr": "l.ArtistName()" + }, + { + "id": "RecordingMbid", + "string": "%[4]v", + "type": "go.uploadedlobster.com/scotty/internal/models.MBID", + "underlyingType": "string", + "argNum": 4, + "expr": "l.RecordingMbid" + } + ] + }, { "id": "Disable auto correction of submitted listens", "message": "Disable auto correction of submitted listens", @@ -265,6 +309,26 @@ "message": "Include skipped listens", "translation": "Übersprungene Titel einbeziehen" }, + { + "id": "Directory path", + "message": "Directory path", + "translation": "" + }, + { + "id": "Ignore listens in incognito mode", + "message": "Ignore listens in incognito mode", + "translation": "" + }, + { + "id": "Ignore skipped listens", + "message": "Ignore skipped listens", + "translation": "" + }, + { + "id": "Minimum playback duration for skipped tracks (seconds)", + "message": "Minimum playback duration for skipped tracks (seconds)", + "translation": "" + }, { "id": "Visit the URL for authorization: {Url}", "message": "Visit the URL for authorization: {Url}", @@ -303,23 +367,23 @@ { "id": "exporting", "message": "exporting", + "translation": "exportiere", "translatorComment": "Copied from source.", - "fuzzy": true, - "translation": "exportiere" + "fuzzy": true }, { "id": "importing", "message": "importing", + "translation": "importiere", "translatorComment": "Copied from source.", - "fuzzy": true, - "translation": "importiere" + "fuzzy": true }, { "id": "done", "message": "done", + "translation": "fertig", "translatorComment": "Copied from source.", - "fuzzy": true, - "translation": "fertig" + "fuzzy": true }, { "id": "Yes", @@ -459,22 +523,45 @@ ] }, { - "id": "During the import the following errors occurred:", - "message": "During the import the following errors occurred:", - "translation": "Während des Imports sind folgende Fehler aufgetreten:" + "id": "Import log:", + "message": "Import log:", + "translation": "" }, { - "id": "Error: {Err}", - "message": "Error: {Err}", - "translation": "Fehler: {Err}", + "id": "{Type}: {Message}", + "message": "{Type}: {Message}", + "translation": "", "placeholders": [ { - "id": "Err", + "id": "Type", + "string": "%[1]v", + "type": "go.uploadedlobster.com/scotty/internal/models.LogEntryType", + "underlyingType": "string", + "argNum": 1, + "expr": "entry.Type" + }, + { + "id": "Message", + "string": "%[2]v", + "type": "string", + "underlyingType": "string", + "argNum": 2, + "expr": "entry.Message" + } + ] + }, + { + "id": "invalid timestamp string \"{FlagValue}\"", + "message": "invalid timestamp string \"{FlagValue}\"", + "translation": "", + "placeholders": [ + { + "id": "FlagValue", "string": "%[1]v", "type": "string", "underlyingType": "string", "argNum": 1, - "expr": "err" + "expr": "flagValue" } ] }, @@ -525,4 +612,4 @@ ] } ] -} +} \ No newline at end of file diff --git a/internal/translations/locales/de/out.gotext.json b/internal/translations/locales/de/out.gotext.json index 8d11b1a..3d84fe8 100644 --- a/internal/translations/locales/de/out.gotext.json +++ b/internal/translations/locales/de/out.gotext.json @@ -523,22 +523,30 @@ ] }, { - "id": "During the import the following errors occurred:", - "message": "During the import the following errors occurred:", - "translation": "Während des Imports sind folgende Fehler aufgetreten:" + "id": "Import log:", + "message": "Import log:", + "translation": "" }, { - "id": "Error: {Err}", - "message": "Error: {Err}", - "translation": "Fehler: {Err}", + "id": "{Type}: {Message}", + "message": "{Type}: {Message}", + "translation": "", "placeholders": [ { - "id": "Err", + "id": "Type", "string": "%[1]v", - "type": "string", + "type": "go.uploadedlobster.com/scotty/internal/models.LogEntryType", "underlyingType": "string", "argNum": 1, - "expr": "err" + "expr": "entry.Type" + }, + { + "id": "Message", + "string": "%[2]v", + "type": "string", + "underlyingType": "string", + "argNum": 2, + "expr": "entry.Message" } ] }, diff --git a/internal/translations/locales/en/messages.gotext.json b/internal/translations/locales/en/messages.gotext.json index 03ce260..7687276 100644 --- a/internal/translations/locales/en/messages.gotext.json +++ b/internal/translations/locales/en/messages.gotext.json @@ -303,6 +303,54 @@ "translatorComment": "Copied from source.", "fuzzy": true }, + { + "id": "Check for duplicate listens on import (slower)", + "message": "Check for duplicate listens on import (slower)", + "translation": "Check for duplicate listens on import (slower)", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Ignored duplicate listen {ListenedAt}: \"{TrackName}\" by {ArtistName} ({RecordingMbid})", + "message": "Ignored duplicate listen {ListenedAt}: \"{TrackName}\" by {ArtistName} ({RecordingMbid})", + "translation": "Ignored duplicate listen {ListenedAt}: \"{TrackName}\" by {ArtistName} ({RecordingMbid})", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "ListenedAt", + "string": "%[1]v", + "type": "time.Time", + "underlyingType": "struct{wall uint64; ext int64; loc *time.Location}", + "argNum": 1, + "expr": "l.ListenedAt" + }, + { + "id": "TrackName", + "string": "%[2]v", + "type": "string", + "underlyingType": "string", + "argNum": 2, + "expr": "l.TrackName" + }, + { + "id": "ArtistName", + "string": "%[3]v", + "type": "string", + "underlyingType": "string", + "argNum": 3, + "expr": "l.ArtistName()" + }, + { + "id": "RecordingMbid", + "string": "%[4]v", + "type": "go.uploadedlobster.com/scotty/internal/models.MBID", + "underlyingType": "string", + "argNum": 4, + "expr": "l.RecordingMbid" + } + ], + "fuzzy": true + }, { "id": "Disable auto correction of submitted listens", "message": "Disable auto correction of submitted listens", @@ -317,6 +365,34 @@ "translatorComment": "Copied from source.", "fuzzy": true }, + { + "id": "Directory path", + "message": "Directory path", + "translation": "Directory path", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Ignore listens in incognito mode", + "message": "Ignore listens in incognito mode", + "translation": "Ignore listens in incognito mode", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Ignore skipped listens", + "message": "Ignore skipped listens", + "translation": "Ignore skipped listens", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Minimum playback duration for skipped tracks (seconds)", + "message": "Minimum playback duration for skipped tracks (seconds)", + "translation": "Minimum playback duration for skipped tracks (seconds)", + "translatorComment": "Copied from source.", + "fuzzy": true + }, { "id": "Visit the URL for authorization: {Url}", "message": "Visit the URL for authorization: {Url}", @@ -535,25 +611,50 @@ "fuzzy": true }, { - "id": "During the import the following errors occurred:", - "message": "During the import the following errors occurred:", - "translation": "During the import the following errors occurred:", + "id": "Import log:", + "message": "Import log:", + "translation": "Import log:", "translatorComment": "Copied from source.", "fuzzy": true }, { - "id": "Error: {Err}", - "message": "Error: {Err}", - "translation": "Error: {Err}", + "id": "{Type}: {Message}", + "message": "{Type}: {Message}", + "translation": "{Type}: {Message}", "translatorComment": "Copied from source.", "placeholders": [ { - "id": "Err", + "id": "Type", + "string": "%[1]v", + "type": "go.uploadedlobster.com/scotty/internal/models.LogEntryType", + "underlyingType": "string", + "argNum": 1, + "expr": "entry.Type" + }, + { + "id": "Message", + "string": "%[2]v", + "type": "string", + "underlyingType": "string", + "argNum": 2, + "expr": "entry.Message" + } + ], + "fuzzy": true + }, + { + "id": "invalid timestamp string \"{FlagValue}\"", + "message": "invalid timestamp string \"{FlagValue}\"", + "translation": "invalid timestamp string \"{FlagValue}\"", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "FlagValue", "string": "%[1]v", "type": "string", "underlyingType": "string", "argNum": 1, - "expr": "err" + "expr": "flagValue" } ], "fuzzy": true @@ -613,4 +714,4 @@ "fuzzy": true } ] -} +} \ No newline at end of file diff --git a/internal/translations/locales/en/out.gotext.json b/internal/translations/locales/en/out.gotext.json index 29ed76e..7687276 100644 --- a/internal/translations/locales/en/out.gotext.json +++ b/internal/translations/locales/en/out.gotext.json @@ -611,25 +611,33 @@ "fuzzy": true }, { - "id": "During the import the following errors occurred:", - "message": "During the import the following errors occurred:", - "translation": "During the import the following errors occurred:", + "id": "Import log:", + "message": "Import log:", + "translation": "Import log:", "translatorComment": "Copied from source.", "fuzzy": true }, { - "id": "Error: {Err}", - "message": "Error: {Err}", - "translation": "Error: {Err}", + "id": "{Type}: {Message}", + "message": "{Type}: {Message}", + "translation": "{Type}: {Message}", "translatorComment": "Copied from source.", "placeholders": [ { - "id": "Err", + "id": "Type", "string": "%[1]v", - "type": "string", + "type": "go.uploadedlobster.com/scotty/internal/models.LogEntryType", "underlyingType": "string", "argNum": 1, - "expr": "err" + "expr": "entry.Type" + }, + { + "id": "Message", + "string": "%[2]v", + "type": "string", + "underlyingType": "string", + "argNum": 2, + "expr": "entry.Message" } ], "fuzzy": true From 757aeed7b5075ab536c17039eda5d27eb9c0d017 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 21 Jan 2024 15:26:29 +0000 Subject: [PATCH 38/82] Translated using Weblate (German) Currently translated at 100.0% (54 of 54 strings) Co-authored-by: Philipp Wolfer Translate-URL: https://translate.uploadedlobster.com/projects/scotty/app/de/ Translation: Scotty/app --- .../locales/de/messages.gotext.json | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/internal/translations/locales/de/messages.gotext.json b/internal/translations/locales/de/messages.gotext.json index 3d84fe8..afa4129 100644 --- a/internal/translations/locales/de/messages.gotext.json +++ b/internal/translations/locales/de/messages.gotext.json @@ -258,12 +258,12 @@ { "id": "Check for duplicate listens on import (slower)", "message": "Check for duplicate listens on import (slower)", - "translation": "" + "translation": "Beim Import auf Listen-Duplikate prüfen (langsamer)" }, { "id": "Ignored duplicate listen {ListenedAt}: \"{TrackName}\" by {ArtistName} ({RecordingMbid})", "message": "Ignored duplicate listen {ListenedAt}: \"{TrackName}\" by {ArtistName} ({RecordingMbid})", - "translation": "", + "translation": "Listen-Duplikat ignoriert {ListenedAt}: „{TrackName}“ von {ArtistName} ({RecordingMbid})", "placeholders": [ { "id": "ListenedAt", @@ -312,22 +312,22 @@ { "id": "Directory path", "message": "Directory path", - "translation": "" + "translation": "Verzeichnispfad" }, { "id": "Ignore listens in incognito mode", "message": "Ignore listens in incognito mode", - "translation": "" + "translation": "Listens im Inkognito-Modus ignorieren" }, { "id": "Ignore skipped listens", "message": "Ignore skipped listens", - "translation": "" + "translation": "Übersprungene Listens ignorieren" }, { "id": "Minimum playback duration for skipped tracks (seconds)", "message": "Minimum playback duration for skipped tracks (seconds)", - "translation": "" + "translation": "Minimale Wiedergabedauer für übersprungene Titel (Sekunden)" }, { "id": "Visit the URL for authorization: {Url}", @@ -367,23 +367,23 @@ { "id": "exporting", "message": "exporting", - "translation": "exportiere", "translatorComment": "Copied from source.", - "fuzzy": true + "fuzzy": true, + "translation": "exportiere" }, { "id": "importing", "message": "importing", - "translation": "importiere", "translatorComment": "Copied from source.", - "fuzzy": true + "fuzzy": true, + "translation": "importiere" }, { "id": "done", "message": "done", - "translation": "fertig", "translatorComment": "Copied from source.", - "fuzzy": true + "fuzzy": true, + "translation": "fertig" }, { "id": "Yes", @@ -525,12 +525,12 @@ { "id": "Import log:", "message": "Import log:", - "translation": "" + "translation": "Importlog:" }, { "id": "{Type}: {Message}", "message": "{Type}: {Message}", - "translation": "", + "translation": "{Type}: {Message}", "placeholders": [ { "id": "Type", @@ -553,7 +553,7 @@ { "id": "invalid timestamp string \"{FlagValue}\"", "message": "invalid timestamp string \"{FlagValue}\"", - "translation": "", + "translation": "ungültiger Zeitstempel „{FlagValue}“", "placeholders": [ { "id": "FlagValue", @@ -612,4 +612,4 @@ ] } ] -} \ No newline at end of file +} From fee1eba0800ff5b974930bb7247fa1c4d2b34e88 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 21 Jan 2024 16:28:55 +0100 Subject: [PATCH 39/82] Release 0.4.0 --- CHANGES.md | 5 +++-- internal/version/version.go | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 6cbd02b..ff7bcbd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,13 +1,14 @@ # Scotty Changelog -## 0.4.0 - unreleased +## 0.4.0 - 2024-01-21 - JSPF: implement append mode -- scrobberlog: default for append mode is enabled +- scrobberlog: append mode is enabled by default - Subsonic: Only set tags if genre is not empty - ListenBrainz: Listen import can be configured to check for duplicate listens - spotify-history: New backend for importing from Spotify extended streaming history JSON files - Allow date time string for `--timestamp` parameter +- Enabled output for "dump" backend again - Fixed completed progress bar showing empty - Fixed crash in case of importer returning an error on import start diff --git a/internal/version/version.go b/internal/version/version.go index 82f11ab..4c1b077 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -1,5 +1,5 @@ /* -Copyright © 2023 Philipp Wolfer +Copyright © 2023-2024 Philipp Wolfer Scotty is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -17,7 +17,7 @@ package version const ( AppName = "scotty" - AppVersion = "0.3.1" + AppVersion = "0.4.0" ) func UserAgent() string { From 1aa7b61649452193cc762d29f25d87a94a6f993d Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Fri, 26 Jan 2024 12:19:00 +0100 Subject: [PATCH 40/82] subsonic: include subsonic_id as additional metadata --- internal/backends/subsonic/subsonic.go | 16 +++++++++------- internal/backends/subsonic/subsonic_test.go | 2 ++ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/internal/backends/subsonic/subsonic.go b/internal/backends/subsonic/subsonic.go index 5b192ab..6a59630 100644 --- a/internal/backends/subsonic/subsonic.go +++ b/internal/backends/subsonic/subsonic.go @@ -100,13 +100,15 @@ func SongAsLove(song subsonic.Child, username string) models.Love { UserName: username, Created: song.Starred, Track: models.Track{ - TrackName: song.Title, - ReleaseName: song.Album, - ArtistNames: []string{song.Artist}, - TrackNumber: song.Track, - DiscNumber: song.DiscNumber, - AdditionalInfo: map[string]any{}, - Duration: time.Duration(song.Duration * int(time.Second)), + TrackName: song.Title, + ReleaseName: song.Album, + ArtistNames: []string{song.Artist}, + TrackNumber: song.Track, + DiscNumber: song.DiscNumber, + AdditionalInfo: map[string]any{ + "subsonic_id": song.ID, + }, + Duration: time.Duration(song.Duration * int(time.Second)), }, } diff --git a/internal/backends/subsonic/subsonic_test.go b/internal/backends/subsonic/subsonic_test.go index 68641cd..c5bfe36 100644 --- a/internal/backends/subsonic/subsonic_test.go +++ b/internal/backends/subsonic/subsonic_test.go @@ -39,6 +39,7 @@ func TestFromConfig(t *testing.T) { func TestSongToLove(t *testing.T) { user := "outsidecontext" song := go_subsonic.Child{ + ID: "foo123", Starred: time.Unix(1699574369, 0), Title: "Oweynagat", Album: "Here Now, There Then", @@ -59,4 +60,5 @@ func TestSongToLove(t *testing.T) { assert.Equal(song.Track, love.Track.TrackNumber) assert.Equal(song.DiscNumber, love.Track.DiscNumber) assert.Equal([]string{song.Genre}, love.Track.Tags) + assert.Equal(song.ID, love.AdditionalInfo["subsonic_id"]) } From 3f1bebd8ed9c3e030d8de3617a1e8b2cfb94ebc2 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Mon, 29 Jan 2024 08:27:51 +0100 Subject: [PATCH 41/82] deezer: fix artist and album ID URIs Fixes #7 --- internal/backends/deezer/deezer.go | 4 ++-- internal/backends/deezer/deezer_test.go | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/backends/deezer/deezer.go b/internal/backends/deezer/deezer.go index 896b348..e840c93 100644 --- a/internal/backends/deezer/deezer.go +++ b/internal/backends/deezer/deezer.go @@ -245,8 +245,8 @@ func (t Track) AsTrack() models.Track { info["music_service"] = "deezer.com" info["origin_url"] = t.Link info["deezer_id"] = t.Link - info["deezer_album_id"] = fmt.Sprintf("https://www.deezer.com/track/%v", t.Album.Id) - info["deezer_artist_id"] = fmt.Sprintf("https://www.deezer.com/track/%v", t.Artist.Id) + info["deezer_album_id"] = fmt.Sprintf("https://www.deezer.com/album/%v", t.Album.Id) + info["deezer_artist_id"] = fmt.Sprintf("https://www.deezer.com/artist/%v", t.Artist.Id) return track } diff --git a/internal/backends/deezer/deezer_test.go b/internal/backends/deezer/deezer_test.go index ac81402..9550c0e 100644 --- a/internal/backends/deezer/deezer_test.go +++ b/internal/backends/deezer/deezer_test.go @@ -57,6 +57,8 @@ func TestListenAsListen(t *testing.T) { assert.Equal(t, "deezer.com", listen.AdditionalInfo["music_service"]) assert.Equal(t, "https://www.deezer.com/track/14631511", listen.AdditionalInfo["origin_url"]) assert.Equal(t, "https://www.deezer.com/track/14631511", listen.AdditionalInfo["deezer_id"]) + assert.Equal(t, "https://www.deezer.com/album/1346960", listen.AdditionalInfo["deezer_album_id"]) + assert.Equal(t, "https://www.deezer.com/artist/92", listen.AdditionalInfo["deezer_artist_id"]) } func TestLovedTrackAsLove(t *testing.T) { From 357932f9b077a548806fcd81652147da24e60fec Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 24 Mar 2024 16:36:53 +0100 Subject: [PATCH 42/82] Use resty response.IsSuccess() instead of checking for status code 200 --- internal/backends/deezer/client.go | 2 +- internal/backends/funkwhale/client.go | 4 ++-- internal/backends/listenbrainz/client.go | 10 +++++----- internal/backends/maloja/client.go | 4 ++-- internal/backends/spotify/client.go | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/internal/backends/deezer/client.go b/internal/backends/deezer/client.go index eccd188..3c3b740 100644 --- a/internal/backends/deezer/client.go +++ b/internal/backends/deezer/client.go @@ -85,7 +85,7 @@ func listRequest[T Result](c Client, path string, offset int, limit int) (result } response, err := request.Get(path) - if response.StatusCode() != 200 { + if !response.IsSuccess() { err = errors.New(response.String()) } else if result.Error() != nil { err = errors.New(result.Error().Message) diff --git a/internal/backends/funkwhale/client.go b/internal/backends/funkwhale/client.go index 39071e7..8f2848b 100644 --- a/internal/backends/funkwhale/client.go +++ b/internal/backends/funkwhale/client.go @@ -66,7 +66,7 @@ func (c Client) GetHistoryListenings(user string, page int, perPage int) (result SetResult(&result). Get(path) - if response.StatusCode() != 200 { + if !response.IsSuccess() { err = errors.New(response.String()) return } @@ -84,7 +84,7 @@ func (c Client) GetFavoriteTracks(page int, perPage int) (result FavoriteTracksR SetResult(&result). Get(path) - if response.StatusCode() != 200 { + if !response.IsSuccess() { err = errors.New(response.String()) return } diff --git a/internal/backends/listenbrainz/client.go b/internal/backends/listenbrainz/client.go index 72257bf..1917bd4 100644 --- a/internal/backends/listenbrainz/client.go +++ b/internal/backends/listenbrainz/client.go @@ -74,7 +74,7 @@ func (c Client) GetListens(user string, maxTime time.Time, minTime time.Time) (r SetError(&errorResult). Get(path) - if response.StatusCode() != 200 { + if !response.IsSuccess() { err = errors.New(errorResult.Error) return } @@ -90,7 +90,7 @@ func (c Client) SubmitListens(listens ListenSubmission) (result StatusResult, er SetError(&errorResult). Post(path) - if response.StatusCode() != 200 { + if !response.IsSuccess() { err = errors.New(errorResult.Error) return } @@ -112,7 +112,7 @@ func (c Client) GetFeedback(user string, status int, offset int) (result GetFeed SetError(&errorResult). Get(path) - if response.StatusCode() != 200 { + if !response.IsSuccess() { err = errors.New(errorResult.Error) return } @@ -128,7 +128,7 @@ func (c Client) SendFeedback(feedback Feedback) (result StatusResult, err error) SetError(&errorResult). Post(path) - if response.StatusCode() != 200 { + if !response.IsSuccess() { err = errors.New(errorResult.Error) return } @@ -147,7 +147,7 @@ func (c Client) Lookup(recordingName string, artistName string) (result LookupRe SetError(&errorResult). Get(path) - if response.StatusCode() != 200 { + if !response.IsSuccess() { err = errors.New(errorResult.Error) return } diff --git a/internal/backends/maloja/client.go b/internal/backends/maloja/client.go index 3b79110..f373f93 100644 --- a/internal/backends/maloja/client.go +++ b/internal/backends/maloja/client.go @@ -58,7 +58,7 @@ func (c Client) GetScrobbles(page int, perPage int) (result GetScrobblesResult, SetResult(&result). Get(path) - if response.StatusCode() != 200 { + if !response.IsSuccess() { err = errors.New(response.String()) return } @@ -73,7 +73,7 @@ func (c Client) NewScrobble(scrobble NewScrobble) (result NewScrobbleResult, err SetResult(&result). Post(path) - if response.StatusCode() != 200 { + if !response.IsSuccess() { err = errors.New(response.String()) return } diff --git a/internal/backends/spotify/client.go b/internal/backends/spotify/client.go index 7bbcf48..1c002c0 100644 --- a/internal/backends/spotify/client.go +++ b/internal/backends/spotify/client.go @@ -79,7 +79,7 @@ func (c Client) recentlyPlayed(after *time.Time, before *time.Time, limit int) ( } response, err := request.Get(path) - if response.StatusCode() != 200 { + if !response.IsSuccess() { err = errors.New(response.String()) } return @@ -95,7 +95,7 @@ func (c Client) UserTracks(offset int, limit int) (result TracksResult, err erro SetResult(&result). Get(path) - if response.StatusCode() != 200 { + if !response.IsSuccess() { err = errors.New(response.String()) } return From bcc7bf31671c85e536265b01af96fd03ab939b4e Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Mon, 15 Apr 2024 15:47:16 +0200 Subject: [PATCH 43/82] Replaced util Min/Max functions with builtin --- internal/backends/deezer/deezer.go | 5 ++--- internal/backends/spotify/spotify.go | 3 +-- internal/similarity/similarity.go | 2 +- internal/util/util.go | 16 ---------------- internal/util/util_test.go | 12 ------------ 5 files changed, 4 insertions(+), 34 deletions(-) diff --git a/internal/backends/deezer/deezer.go b/internal/backends/deezer/deezer.go index e840c93..3131c3e 100644 --- a/internal/backends/deezer/deezer.go +++ b/internal/backends/deezer/deezer.go @@ -26,7 +26,6 @@ import ( "go.uploadedlobster.com/scotty/internal/config" "go.uploadedlobster.com/scotty/internal/i18n" "go.uploadedlobster.com/scotty/internal/models" - "go.uploadedlobster.com/scotty/internal/util" "golang.org/x/oauth2" ) @@ -106,7 +105,7 @@ out: // and continue. if offset >= result.Total { p.Total = int64(result.Total) - offset = util.Max(result.Total-perPage, 0) + offset = max(result.Total-perPage, 0) continue } @@ -175,7 +174,7 @@ out: if offset >= result.Total { p.Total = int64(result.Total) totalCount = result.Total - offset = util.Max(result.Total-perPage, 0) + offset = max(result.Total-perPage, 0) continue } diff --git a/internal/backends/spotify/spotify.go b/internal/backends/spotify/spotify.go index b9a51e2..a4e3c87 100644 --- a/internal/backends/spotify/spotify.go +++ b/internal/backends/spotify/spotify.go @@ -28,7 +28,6 @@ import ( "go.uploadedlobster.com/scotty/internal/config" "go.uploadedlobster.com/scotty/internal/i18n" "go.uploadedlobster.com/scotty/internal/models" - "go.uploadedlobster.com/scotty/internal/util" "golang.org/x/oauth2" "golang.org/x/oauth2/spotify" ) @@ -184,7 +183,7 @@ out: if offset >= result.Total { p.Total = int64(result.Total) totalCount = result.Total - offset = util.Max(result.Total-perPage, 0) + offset = max(result.Total-perPage, 0) continue } diff --git a/internal/similarity/similarity.go b/internal/similarity/similarity.go index 4c0b345..358404a 100644 --- a/internal/similarity/similarity.go +++ b/internal/similarity/similarity.go @@ -33,7 +33,7 @@ func Similarity(s1 string, s2 string) float64 { s2 = norm.NFKC.String(s2) l1 := len([]rune(s1)) l2 := len([]rune(s2)) - maxLen := util.Max(l1, l2) + maxLen := max(l1, l2) // Empty strings always compare full equal if maxLen == 0 { return 1.0 diff --git a/internal/util/util.go b/internal/util/util.go index e8663a7..9ef4e14 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -17,22 +17,6 @@ package util import "golang.org/x/exp/constraints" -func Max[T constraints.Ordered](m, n T) T { - if n > m { - return n - } else { - return m - } -} - -func Min[T constraints.Ordered](m, n T) T { - if n < m { - return n - } else { - return m - } -} - func Sum[T constraints.Integer | constraints.Float](v ...T) T { var sum T for _, i := range v { diff --git a/internal/util/util_test.go b/internal/util/util_test.go index 73ec415..077fedd 100644 --- a/internal/util/util_test.go +++ b/internal/util/util_test.go @@ -23,18 +23,6 @@ import ( "go.uploadedlobster.com/scotty/internal/util" ) -func ExampleMax() { - v := util.Max(2, 5) - fmt.Print(v) - // Output: 5 -} - -func ExampleMin() { - v := util.Min(2, 5) - fmt.Print(v) - // Output: 2 -} - func ExampleSum() { values := []float64{1.4, 2.2} sum := util.Sum(values...) From cdf20728aede22157511d73603afba5d8de6dbce Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Mon, 15 Apr 2024 15:47:55 +0200 Subject: [PATCH 44/82] Update and tidy dependencies --- go.mod | 39 +++++++++-------- go.sum | 131 ++++++++++++++++++++++++++++++++------------------------- 2 files changed, 93 insertions(+), 77 deletions(-) diff --git a/go.mod b/go.mod index c73cd44..917d4b0 100644 --- a/go.mod +++ b/go.mod @@ -8,25 +8,26 @@ require ( github.com/cli/browser v1.3.0 github.com/delucks/go-subsonic v0.0.0-20220915164742-2744002c4be5 github.com/fatih/color v1.16.0 - github.com/glebarez/sqlite v1.10.0 - github.com/go-resty/resty/v2 v2.11.0 + github.com/glebarez/sqlite v1.11.0 + github.com/go-resty/resty/v2 v2.12.0 github.com/jarcoal/httpmock v1.3.1 github.com/manifoldco/promptui v0.9.0 - github.com/pelletier/go-toml/v2 v2.1.1 + github.com/pelletier/go-toml/v2 v2.2.1 github.com/shkh/lastfm-go v0.0.0-20191215035245-89a801c244e0 github.com/spf13/cast v1.6.0 github.com/spf13/cobra v1.8.0 github.com/spf13/viper v1.18.2 - github.com/stretchr/testify v1.8.4 - github.com/vbauerster/mpb/v8 v8.7.2 - golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 - golang.org/x/oauth2 v0.16.0 + github.com/stretchr/testify v1.9.0 + github.com/vbauerster/mpb/v8 v8.7.3 + golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 + golang.org/x/oauth2 v0.19.0 golang.org/x/text v0.14.0 gorm.io/datatypes v1.2.0 - gorm.io/gorm v1.25.5 + gorm.io/gorm v1.25.9 ) require ( + filippo.io/edwards25519 v1.1.0 // indirect github.com/VividCortex/ewma v1.2.0 // indirect github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect github.com/chzyer/readline v1.5.1 // indirect @@ -34,9 +35,8 @@ require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/glebarez/go-sqlite v1.22.0 // indirect - github.com/go-sql-driver/mysql v1.7.1 // indirect - github.com/golang/protobuf v1.5.3 // indirect - github.com/google/uuid v1.5.0 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect @@ -46,9 +46,10 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - github.com/rivo/uniseg v0.4.4 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect @@ -56,15 +57,13 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/net v0.20.0 // indirect - golang.org/x/sys v0.16.0 // indirect - google.golang.org/appengine v1.6.8 // indirect - google.golang.org/protobuf v1.32.0 // indirect + golang.org/x/net v0.24.0 // indirect + golang.org/x/sys v0.19.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - gorm.io/driver/mysql v1.5.2 // indirect - modernc.org/libc v1.40.2 // indirect + gorm.io/driver/mysql v1.5.6 // indirect + modernc.org/libc v1.49.3 // indirect modernc.org/mathutil v1.6.0 // indirect - modernc.org/memory v1.7.2 // indirect - modernc.org/sqlite v1.28.0 // indirect + modernc.org/memory v1.8.0 // indirect + modernc.org/sqlite v1.29.6 // indirect ) diff --git a/go.sum b/go.sum index 38ff47a..1e7ee15 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= github.com/Xuanwo/go-locale v1.1.0 h1:51gUxhxl66oXAjI9uPGb2O0qwPECpriKQb2hl35mQkg= @@ -38,28 +40,23 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= -github.com/glebarez/sqlite v1.10.0 h1:u4gt8y7OND/cCei/NMHmfbLxF6xP2wgKcT/BJf2pYkc= -github.com/glebarez/sqlite v1.10.0/go.mod h1:IJ+lfSOmiekhQsFTJRx/lHtGYmCdtAiTaf5wI9u5uHA= -github.com/go-resty/resty/v2 v2.11.0 h1:i7jMfNOJYMp69lq7qozJP+bjgzfAzeOhuGlyDrqxT/8= -github.com/go-resty/resty/v2 v2.11.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A= +github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= +github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= +github.com/go-resty/resty/v2 v2.12.0 h1:rsVL8P90LFvkUYq/V5BTVe203WfRIU4gvcf+yfzJzGA= +github.com/go-resty/resty/v2 v2.12.0/go.mod h1:o0yGPrkS3lOe1+eFajk6kBW8ScXzwU3hD69/gt2yB/0= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= -github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= -github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= -github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= -github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= @@ -95,24 +92,26 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= -github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE= github.com/microsoft/go-mssqldb v0.17.0/go.mod h1:OkoNGhGEs8EZqchVTtochlXruEhEOaO4S0d2sB5aeGQ= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= -github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg= +github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= -github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -141,41 +140,49 @@ github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMV github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/vbauerster/mpb/v8 v8.7.2 h1:SMJtxhNho1MV3OuFgS1DAzhANN1Ejc5Ct+0iSaIkB14= -github.com/vbauerster/mpb/v8 v8.7.2/go.mod h1:ZFnrjzspgDHoxYLGvxIruiNk73GNTPG4YHgVNpR10VY= +github.com/vbauerster/mpb/v8 v8.7.3 h1:n/mKPBav4FFWp5fH4U0lPpXfiOmCEgl5Yx/NM3tKJA0= +github.com/vbauerster/mpb/v8 v8.7.3/go.mod h1:9nFlNpDGVoTmQ4QvNjSLtwLmAFjwmq0XaAF26toHGNM= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 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.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o= -golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 h1:ESSUROHIBHg7USnszlcdmjBEwdMj9VUvU+OPk4yl2mc= +golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= -golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg= +golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -188,24 +195,23 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -213,14 +219,9 @@ golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= +golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= -google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= -google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -231,22 +232,38 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/datatypes v1.2.0 h1:5YT+eokWdIxhJgWHdrb2zYUimyk0+TaFth+7a0ybzco= gorm.io/datatypes v1.2.0/go.mod h1:o1dh0ZvjIjhH/bngTpypG6lVRJ5chTBxE09FH/71k04= -gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs= -gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8= +gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8= +gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= gorm.io/driver/postgres v1.5.0 h1:u2FXTy14l45qc3UeCJ7QaAXZmZfDDv0YrthvmRq1l0U= gorm.io/driver/postgres v1.5.0/go.mod h1:FUZXzO+5Uqg5zzwzv4KK49R8lvGIyscBOqYrtI1Ce9A= gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU= gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI= gorm.io/driver/sqlserver v1.4.1 h1:t4r4r6Jam5E6ejqP7N82qAJIJAht27EGT41HyPfXRw0= gorm.io/driver/sqlserver v1.4.1/go.mod h1:DJ4P+MeZbc5rvY58PnmN1Lnyvb5gw5NPzGshHDnJLig= -gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= -gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= -gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= -modernc.org/libc v1.40.2 h1:pzVHG9jwYZNWANfltHiU3HYfrzYIsX6ysRLJ93adZXA= -modernc.org/libc v1.40.2/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.25.9 h1:wct0gxZIELDk8+ZqF/MVnHLkA1rvYlBWUMv2EdsK1g8= +gorm.io/gorm v1.25.9/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +modernc.org/cc/v4 v4.20.0 h1:45Or8mQfbUqJOG9WaxvlFYOAQO0lQ5RvqBcFCXngjxk= +modernc.org/cc/v4 v4.20.0/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= +modernc.org/ccgo/v4 v4.16.0 h1:ofwORa6vx2FMm0916/CkZjpFPSR70VwTjUCe2Eg5BnA= +modernc.org/ccgo/v4 v4.16.0/go.mod h1:dkNyWIjFrVIZ68DTo36vHK+6/ShBn4ysU61So6PIqCI= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= +modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= +modernc.org/libc v1.49.3 h1:j2MRCRdwJI2ls/sGbeSk0t2bypOG/uvPZUsGQFDulqg= +modernc.org/libc v1.49.3/go.mod h1:yMZuGkn7pXbKfoT/M35gFJOAEdSKdxL0q64sF7KqCDo= modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= -modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E= -modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= -modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ= -modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0= +modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= +modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= +modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= +modernc.org/sqlite v1.29.6 h1:0lOXGrycJPptfHDuohfYgNqoe4hu+gYuN/pKgY5XjS4= +modernc.org/sqlite v1.29.6/go.mod h1:S02dvcmm7TnTRvGhv8IGYyLnIt7AS2KPaB1F/71p75U= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= From 7175d3453d73ae8a1590ad9061e7f1256f3ed642 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Mon, 16 Sep 2024 19:00:24 +0200 Subject: [PATCH 45/82] Fix go version definition in go.mod --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 917d4b0..912f6b1 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module go.uploadedlobster.com/scotty -go 1.21.1 +go 1.21 require ( github.com/Xuanwo/go-locale v1.1.0 From 1c1ce224f7430349a5b386e6429c881bb4e67900 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Mon, 16 Sep 2024 19:02:14 +0200 Subject: [PATCH 46/82] Update dependencies --- go.mod | 46 ++++++++++++++++++++++++---------------------- go.sum | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 22 deletions(-) diff --git a/go.mod b/go.mod index 912f6b1..5a67610 100644 --- a/go.mod +++ b/go.mod @@ -1,29 +1,31 @@ module go.uploadedlobster.com/scotty -go 1.21 +go 1.22.0 + +toolchain go1.22.2 require ( - github.com/Xuanwo/go-locale v1.1.0 + github.com/Xuanwo/go-locale v1.1.2 github.com/agnivade/levenshtein v1.1.1 github.com/cli/browser v1.3.0 - github.com/delucks/go-subsonic v0.0.0-20220915164742-2744002c4be5 - github.com/fatih/color v1.16.0 + github.com/delucks/go-subsonic v0.0.0-20240806025900-2a743ec36238 + github.com/fatih/color v1.17.0 github.com/glebarez/sqlite v1.11.0 - github.com/go-resty/resty/v2 v2.12.0 + github.com/go-resty/resty/v2 v2.15.0 github.com/jarcoal/httpmock v1.3.1 github.com/manifoldco/promptui v0.9.0 - github.com/pelletier/go-toml/v2 v2.2.1 + github.com/pelletier/go-toml/v2 v2.2.3 github.com/shkh/lastfm-go v0.0.0-20191215035245-89a801c244e0 - github.com/spf13/cast v1.6.0 - github.com/spf13/cobra v1.8.0 - github.com/spf13/viper v1.18.2 + github.com/spf13/cast v1.7.0 + github.com/spf13/cobra v1.8.1 + github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.9.0 - github.com/vbauerster/mpb/v8 v8.7.3 - golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 - golang.org/x/oauth2 v0.19.0 - golang.org/x/text v0.14.0 - gorm.io/datatypes v1.2.0 - gorm.io/gorm v1.25.9 + github.com/vbauerster/mpb/v8 v8.8.3 + golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 + golang.org/x/oauth2 v0.23.0 + golang.org/x/text v0.18.0 + gorm.io/datatypes v1.2.2 + gorm.io/gorm v1.25.12 ) require ( @@ -44,26 +46,26 @@ require ( github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/locafero v0.6.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/net v0.24.0 // indirect - golang.org/x/sys v0.19.0 // indirect + golang.org/x/net v0.29.0 // indirect + golang.org/x/sys v0.25.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - gorm.io/driver/mysql v1.5.6 // indirect - modernc.org/libc v1.49.3 // indirect + gorm.io/driver/mysql v1.5.7 // indirect + modernc.org/libc v1.60.1 // indirect modernc.org/mathutil v1.6.0 // indirect modernc.org/memory v1.8.0 // indirect - modernc.org/sqlite v1.29.6 // indirect + modernc.org/sqlite v1.33.1 // indirect ) diff --git a/go.sum b/go.sum index 1e7ee15..2fb0b5a 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1o github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= github.com/Xuanwo/go-locale v1.1.0 h1:51gUxhxl66oXAjI9uPGb2O0qwPECpriKQb2hl35mQkg= github.com/Xuanwo/go-locale v1.1.0/go.mod h1:UKrHoZB3FPIk9wIG2/tVSobnHgNnceGSH3Y8DY5cASs= +github.com/Xuanwo/go-locale v1.1.2 h1:6H+olvrQcyVOZ+GAC2rXu4armacTT4ZrFCA0mB24XVo= +github.com/Xuanwo/go-locale v1.1.2/go.mod h1:1JBER4QV7Ji39GJ4AvVlfvqmTUqopzxQxdg2mXYOw94= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= @@ -22,18 +24,23 @@ github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38 github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo= github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/delucks/go-subsonic v0.0.0-20220915164742-2744002c4be5 h1:RuuxidatioSKGOiBzL1mTY4X22DQD8weEbS3iRLHnAg= github.com/delucks/go-subsonic v0.0.0-20220915164742-2744002c4be5/go.mod h1:vnbEuj6Z20PLcHB4rrLQAOXGMjtULfMGhRVSFPcSdUo= +github.com/delucks/go-subsonic v0.0.0-20240806025900-2a743ec36238 h1:uejyepOdHISrJTw7P84Y7yEC0FMyv1q3KNDRxWsviKw= +github.com/delucks/go-subsonic v0.0.0-20240806025900-2a743ec36238/go.mod h1:vnbEuj6Z20PLcHB4rrLQAOXGMjtULfMGhRVSFPcSdUo= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= @@ -44,6 +51,8 @@ github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GM github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= github.com/go-resty/resty/v2 v2.12.0 h1:rsVL8P90LFvkUYq/V5BTVe203WfRIU4gvcf+yfzJzGA= github.com/go-resty/resty/v2 v2.12.0/go.mod h1:o0yGPrkS3lOe1+eFajk6kBW8ScXzwU3hD69/gt2yB/0= +github.com/go-resty/resty/v2 v2.15.0 h1:clPQLZ2x9h4yGY81IzpMPnty+xoGyFaDg0XMkCsHf90= +github.com/go-resty/resty/v2 v2.15.0/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= @@ -55,6 +64,7 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= @@ -67,8 +77,10 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= github.com/jackc/pgx/v5 v5.3.0 h1:/NQi8KHMpKWHInxXesC8yD4DhkXPrVhmnwYkjp9AmBA= github.com/jackc/pgx/v5 v5.3.0/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8= +github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww= github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= @@ -92,6 +104,9 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= @@ -104,6 +119,8 @@ github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdh github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg= github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -117,6 +134,8 @@ github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/f github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk= +github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/shkh/lastfm-go v0.0.0-20191215035245-89a801c244e0 h1:cgqwZtnR+IQfUYDLJ3Kiy4aE+O/wExTzEIg8xwC4Qfs= @@ -131,12 +150,18 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -151,6 +176,8 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/vbauerster/mpb/v8 v8.7.3 h1:n/mKPBav4FFWp5fH4U0lPpXfiOmCEgl5Yx/NM3tKJA0= github.com/vbauerster/mpb/v8 v8.7.3/go.mod h1:9nFlNpDGVoTmQ4QvNjSLtwLmAFjwmq0XaAF26toHGNM= +github.com/vbauerster/mpb/v8 v8.8.3 h1:dTOByGoqwaTJYPubhVz3lO5O6MK553XVgUo33LdnNsQ= +github.com/vbauerster/mpb/v8 v8.8.3/go.mod h1:JfCCrtcMsJwP6ZwMn9e5LMnNyp3TVNpUWWkN+nd4EWk= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= @@ -160,12 +187,16 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 h1:ESSUROHIBHg7USnszlcdmjBEwdMj9VUvU+OPk4yl2mc= golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= @@ -176,13 +207,18 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg= golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -199,6 +235,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -212,8 +250,11 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -221,6 +262,7 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= +golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= @@ -232,8 +274,12 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/datatypes v1.2.0 h1:5YT+eokWdIxhJgWHdrb2zYUimyk0+TaFth+7a0ybzco= gorm.io/datatypes v1.2.0/go.mod h1:o1dh0ZvjIjhH/bngTpypG6lVRJ5chTBxE09FH/71k04= +gorm.io/datatypes v1.2.2 h1:sdn7ZmG4l7JWtMDUb3L98f2Ym7CO5F8mZLlrQJMfF9g= +gorm.io/datatypes v1.2.2/go.mod h1:f4BsLcFAX67szSv8svwLRjklArSHAvHLeE3pXAS5DZI= gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8= gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= +gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= +gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= gorm.io/driver/postgres v1.5.0 h1:u2FXTy14l45qc3UeCJ7QaAXZmZfDDv0YrthvmRq1l0U= gorm.io/driver/postgres v1.5.0/go.mod h1:FUZXzO+5Uqg5zzwzv4KK49R8lvGIyscBOqYrtI1Ce9A= gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU= @@ -243,16 +289,23 @@ gorm.io/driver/sqlserver v1.4.1/go.mod h1:DJ4P+MeZbc5rvY58PnmN1Lnyvb5gw5NPzGshHD gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/gorm v1.25.9 h1:wct0gxZIELDk8+ZqF/MVnHLkA1rvYlBWUMv2EdsK1g8= gorm.io/gorm v1.25.9/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= modernc.org/cc/v4 v4.20.0 h1:45Or8mQfbUqJOG9WaxvlFYOAQO0lQ5RvqBcFCXngjxk= modernc.org/cc/v4 v4.20.0/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= +modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= modernc.org/ccgo/v4 v4.16.0 h1:ofwORa6vx2FMm0916/CkZjpFPSR70VwTjUCe2Eg5BnA= modernc.org/ccgo/v4 v4.16.0/go.mod h1:dkNyWIjFrVIZ68DTo36vHK+6/ShBn4ysU61So6PIqCI= +modernc.org/ccgo/v4 v4.21.0 h1:kKPI3dF7RIag8YcToh5ZwDcVMIv6VGa0ED5cvh0LMW4= modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= +modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M= modernc.org/libc v1.49.3 h1:j2MRCRdwJI2ls/sGbeSk0t2bypOG/uvPZUsGQFDulqg= modernc.org/libc v1.49.3/go.mod h1:yMZuGkn7pXbKfoT/M35gFJOAEdSKdxL0q64sF7KqCDo= +modernc.org/libc v1.60.1 h1:at373l8IFRTkJIkAU85BIuUoBM4T1b51ds0E1ovPG2s= +modernc.org/libc v1.60.1/go.mod h1:xJuobKuNxKH3RUatS7GjR+suWj+5c2K7bi4m/S5arOY= modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= @@ -263,6 +316,8 @@ modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= modernc.org/sqlite v1.29.6 h1:0lOXGrycJPptfHDuohfYgNqoe4hu+gYuN/pKgY5XjS4= modernc.org/sqlite v1.29.6/go.mod h1:S02dvcmm7TnTRvGhv8IGYyLnIt7AS2KPaB1F/71p75U= +modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM= +modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k= modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= From 04eddfda33cc6f0b87dc0fcea43d5c4f50923ddc Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Mon, 16 Sep 2024 19:07:07 +0200 Subject: [PATCH 47/82] Release 0.4.1 --- CHANGES.md | 6 ++++++ internal/version/version.go | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index ff7bcbd..d3ee1d7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,11 @@ # Scotty Changelog +## 0.4.1 - 2024-09-16 +- Subsonic: include `subsonic_id` as additional metadata +- Deezer: fix artist and album ID URIs (#7) +- Fix installation issues due to wrong go version format in `go.mod` + + ## 0.4.0 - 2024-01-21 - JSPF: implement append mode - scrobberlog: append mode is enabled by default diff --git a/internal/version/version.go b/internal/version/version.go index 4c1b077..3f02fe2 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -17,7 +17,7 @@ package version const ( AppName = "scotty" - AppVersion = "0.4.0" + AppVersion = "0.4.1" ) func UserAgent() string { From 8fff19ceaca8b2298a934bbdaee17b91c6e9adfb Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Thu, 3 Apr 2025 14:56:39 +0200 Subject: [PATCH 48/82] Use MBID type from go.uploadedlobster.com/mbtypes --- go.mod | 5 +- go.sum | 151 +++--------------- internal/backends/funkwhale/funkwhale.go | 7 +- internal/backends/funkwhale/funkwhale_test.go | 16 +- internal/backends/lastfm/lastfm.go | 15 +- .../backends/listenbrainz/listenbrainz.go | 11 +- .../listenbrainz/listenbrainz_test.go | 20 +-- internal/backends/scrobblerlog/parser.go | 3 +- internal/backends/scrobblerlog/parser_test.go | 9 +- internal/models/models.go | 17 +- internal/models/models_test.go | 11 +- internal/similarity/similarity_test.go | 5 +- 12 files changed, 82 insertions(+), 188 deletions(-) diff --git a/go.mod b/go.mod index 5a67610..81d9deb 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module go.uploadedlobster.com/scotty -go 1.22.0 +go 1.23.0 -toolchain go1.22.2 +toolchain go1.23.8 require ( github.com/Xuanwo/go-locale v1.1.2 @@ -21,6 +21,7 @@ require ( github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.9.0 github.com/vbauerster/mpb/v8 v8.8.3 + go.uploadedlobster.com/mbtypes v0.2.0 golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 golang.org/x/oauth2 v0.23.0 golang.org/x/text v0.18.0 diff --git a/go.sum b/go.sum index 2fb0b5a..c2b9efe 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,6 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= -github.com/Xuanwo/go-locale v1.1.0 h1:51gUxhxl66oXAjI9uPGb2O0qwPECpriKQb2hl35mQkg= -github.com/Xuanwo/go-locale v1.1.0/go.mod h1:UKrHoZB3FPIk9wIG2/tVSobnHgNnceGSH3Y8DY5cASs= github.com/Xuanwo/go-locale v1.1.2 h1:6H+olvrQcyVOZ+GAC2rXu4armacTT4ZrFCA0mB24XVo= github.com/Xuanwo/go-locale v1.1.2/go.mod h1:1JBER4QV7Ji39GJ4AvVlfvqmTUqopzxQxdg2mXYOw94= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= @@ -23,22 +21,16 @@ github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo= github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk= -github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/delucks/go-subsonic v0.0.0-20220915164742-2744002c4be5 h1:RuuxidatioSKGOiBzL1mTY4X22DQD8weEbS3iRLHnAg= -github.com/delucks/go-subsonic v0.0.0-20220915164742-2744002c4be5/go.mod h1:vnbEuj6Z20PLcHB4rrLQAOXGMjtULfMGhRVSFPcSdUo= github.com/delucks/go-subsonic v0.0.0-20240806025900-2a743ec36238 h1:uejyepOdHISrJTw7P84Y7yEC0FMyv1q3KNDRxWsviKw= github.com/delucks/go-subsonic v0.0.0-20240806025900-2a743ec36238/go.mod h1:vnbEuj6Z20PLcHB4rrLQAOXGMjtULfMGhRVSFPcSdUo= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= -github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -49,8 +41,6 @@ github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= -github.com/go-resty/resty/v2 v2.12.0 h1:rsVL8P90LFvkUYq/V5BTVe203WfRIU4gvcf+yfzJzGA= -github.com/go-resty/resty/v2 v2.12.0/go.mod h1:o0yGPrkS3lOe1+eFajk6kBW8ScXzwU3hD69/gt2yB/0= github.com/go-resty/resty/v2 v2.15.0 h1:clPQLZ2x9h4yGY81IzpMPnty+xoGyFaDg0XMkCsHf90= github.com/go-resty/resty/v2 v2.15.0/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= @@ -60,35 +50,30 @@ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0kt github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= -github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= -github.com/jackc/pgx/v5 v5.3.0 h1:/NQi8KHMpKWHInxXesC8yD4DhkXPrVhmnwYkjp9AmBA= -github.com/jackc/pgx/v5 v5.3.0/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8= +github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= +github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww= github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -102,13 +87,10 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= -github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= -github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE= @@ -117,11 +99,8 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= -github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg= -github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= @@ -132,152 +111,67 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= -github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk= github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/shkh/lastfm-go v0.0.0-20191215035245-89a801c244e0 h1:cgqwZtnR+IQfUYDLJ3Kiy4aE+O/wExTzEIg8xwC4Qfs= github.com/shkh/lastfm-go v0.0.0-20191215035245-89a801c244e0/go.mod h1:n3nudMl178cEvD44PaopxH9jhJaQzthSxUzLO5iKMy4= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.7 h1:I6tZjLXD2Q1kjvNbIzB1wvQBsXmKXiVrhpRE8ZjP5jY= -github.com/smartystreets/goconvey v1.6.7/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= -github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= -github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/vbauerster/mpb/v8 v8.7.3 h1:n/mKPBav4FFWp5fH4U0lPpXfiOmCEgl5Yx/NM3tKJA0= -github.com/vbauerster/mpb/v8 v8.7.3/go.mod h1:9nFlNpDGVoTmQ4QvNjSLtwLmAFjwmq0XaAF26toHGNM= github.com/vbauerster/mpb/v8 v8.8.3 h1:dTOByGoqwaTJYPubhVz3lO5O6MK553XVgUo33LdnNsQ= github.com/vbauerster/mpb/v8 v8.8.3/go.mod h1:JfCCrtcMsJwP6ZwMn9e5LMnNyp3TVNpUWWkN+nd4EWk= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -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.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +go.uploadedlobster.com/mbtypes v0.2.0 h1:sV9WM5fY8KnnKwKKVRaU3JBEKP4zD7aUX0XQfNZvEdo= +go.uploadedlobster.com/mbtypes v0.2.0/go.mod h1:/ZpwXc8oRpDa7EWeGI9xEY+nGhMIVHhTruikZWD4Krg= golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 h1:ESSUROHIBHg7USnszlcdmjBEwdMj9VUvU+OPk4yl2mc= -golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= -golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg= -golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211023085530-d6a326fbbf70/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= -golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/datatypes v1.2.0 h1:5YT+eokWdIxhJgWHdrb2zYUimyk0+TaFth+7a0ybzco= -gorm.io/datatypes v1.2.0/go.mod h1:o1dh0ZvjIjhH/bngTpypG6lVRJ5chTBxE09FH/71k04= gorm.io/datatypes v1.2.2 h1:sdn7ZmG4l7JWtMDUb3L98f2Ym7CO5F8mZLlrQJMfF9g= gorm.io/datatypes v1.2.2/go.mod h1:f4BsLcFAX67szSv8svwLRjklArSHAvHLeE3pXAS5DZI= -gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8= -gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= gorm.io/driver/postgres v1.5.0 h1:u2FXTy14l45qc3UeCJ7QaAXZmZfDDv0YrthvmRq1l0U= @@ -287,23 +181,16 @@ gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2e gorm.io/driver/sqlserver v1.4.1 h1:t4r4r6Jam5E6ejqP7N82qAJIJAht27EGT41HyPfXRw0= gorm.io/driver/sqlserver v1.4.1/go.mod h1:DJ4P+MeZbc5rvY58PnmN1Lnyvb5gw5NPzGshHDnJLig= gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= -gorm.io/gorm v1.25.9 h1:wct0gxZIELDk8+ZqF/MVnHLkA1rvYlBWUMv2EdsK1g8= -gorm.io/gorm v1.25.9/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= -modernc.org/cc/v4 v4.20.0 h1:45Or8mQfbUqJOG9WaxvlFYOAQO0lQ5RvqBcFCXngjxk= -modernc.org/cc/v4 v4.20.0/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= -modernc.org/ccgo/v4 v4.16.0 h1:ofwORa6vx2FMm0916/CkZjpFPSR70VwTjUCe2Eg5BnA= -modernc.org/ccgo/v4 v4.16.0/go.mod h1:dkNyWIjFrVIZ68DTo36vHK+6/ShBn4ysU61So6PIqCI= +modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= modernc.org/ccgo/v4 v4.21.0 h1:kKPI3dF7RIag8YcToh5ZwDcVMIv6VGa0ED5cvh0LMW4= +modernc.org/ccgo/v4 v4.21.0/go.mod h1:h6kt6H/A2+ew/3MW/p6KEoQmrq/i3pr0J/SiwiaF/g0= modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= -modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= -modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M= -modernc.org/libc v1.49.3 h1:j2MRCRdwJI2ls/sGbeSk0t2bypOG/uvPZUsGQFDulqg= -modernc.org/libc v1.49.3/go.mod h1:yMZuGkn7pXbKfoT/M35gFJOAEdSKdxL0q64sF7KqCDo= +modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= modernc.org/libc v1.60.1 h1:at373l8IFRTkJIkAU85BIuUoBM4T1b51ds0E1ovPG2s= modernc.org/libc v1.60.1/go.mod h1:xJuobKuNxKH3RUatS7GjR+suWj+5c2K7bi4m/S5arOY= modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= @@ -314,8 +201,6 @@ modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= -modernc.org/sqlite v1.29.6 h1:0lOXGrycJPptfHDuohfYgNqoe4hu+gYuN/pKgY5XjS4= -modernc.org/sqlite v1.29.6/go.mod h1:S02dvcmm7TnTRvGhv8IGYyLnIt7AS2KPaB1F/71p75U= modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM= modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k= modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= diff --git a/internal/backends/funkwhale/funkwhale.go b/internal/backends/funkwhale/funkwhale.go index 05017d3..39f8c59 100644 --- a/internal/backends/funkwhale/funkwhale.go +++ b/internal/backends/funkwhale/funkwhale.go @@ -20,6 +20,7 @@ import ( "sort" "time" + "go.uploadedlobster.com/mbtypes" "go.uploadedlobster.com/scotty/internal/config" "go.uploadedlobster.com/scotty/internal/i18n" "go.uploadedlobster.com/scotty/internal/models" @@ -188,7 +189,7 @@ func (f FavoriteTrack) AsLove() models.Love { } func (t Track) AsTrack() models.Track { - recordingMbid := models.MBID(t.RecordingMbid) + recordingMbid := mbtypes.MBID(t.RecordingMbid) track := models.Track{ TrackName: t.Title, ReleaseName: t.Album.Title, @@ -196,8 +197,8 @@ func (t Track) AsTrack() models.Track { TrackNumber: t.Position, DiscNumber: t.DiscNumber, RecordingMbid: recordingMbid, - ReleaseMbid: models.MBID(t.Album.ReleaseMbid), - ArtistMbids: []models.MBID{models.MBID(t.Artist.ArtistMbid)}, + ReleaseMbid: mbtypes.MBID(t.Album.ReleaseMbid), + ArtistMbids: []mbtypes.MBID{mbtypes.MBID(t.Artist.ArtistMbid)}, Tags: t.Tags, AdditionalInfo: map[string]any{ "media_player": FunkwhaleClientName, diff --git a/internal/backends/funkwhale/funkwhale_test.go b/internal/backends/funkwhale/funkwhale_test.go index 12b2b48..915a966 100644 --- a/internal/backends/funkwhale/funkwhale_test.go +++ b/internal/backends/funkwhale/funkwhale_test.go @@ -23,9 +23,9 @@ import ( "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.uploadedlobster.com/mbtypes" "go.uploadedlobster.com/scotty/internal/backends/funkwhale" "go.uploadedlobster.com/scotty/internal/config" - "go.uploadedlobster.com/scotty/internal/models" ) func TestFromConfig(t *testing.T) { @@ -75,9 +75,9 @@ func TestFunkwhaleListeningAsListen(t *testing.T) { assert.Equal(fwListen.Track.DiscNumber, listen.Track.DiscNumber) assert.Equal(fwListen.Track.Tags, listen.Track.Tags) // assert.Equal(backends.FunkwhaleClientName, listen.AdditionalInfo["disc_number"]) - assert.Equal(models.MBID(fwListen.Track.RecordingMbid), listen.RecordingMbid) - assert.Equal(models.MBID(fwListen.Track.Album.ReleaseMbid), listen.ReleaseMbid) - assert.Equal(models.MBID(fwListen.Track.Artist.ArtistMbid), listen.ArtistMbids[0]) + assert.Equal(mbtypes.MBID(fwListen.Track.RecordingMbid), listen.RecordingMbid) + assert.Equal(mbtypes.MBID(fwListen.Track.Album.ReleaseMbid), listen.ReleaseMbid) + assert.Equal(mbtypes.MBID(fwListen.Track.Artist.ArtistMbid), listen.ArtistMbids[0]) assert.Equal(funkwhale.FunkwhaleClientName, listen.AdditionalInfo["media_player"]) } @@ -119,10 +119,10 @@ func TestFunkwhaleFavoriteTrackAsLove(t *testing.T) { assert.Equal(favorite.Track.Position, love.Track.TrackNumber) assert.Equal(favorite.Track.DiscNumber, love.Track.DiscNumber) assert.Equal(favorite.Track.Tags, love.Track.Tags) - assert.Equal(models.MBID(favorite.Track.RecordingMbid), love.RecordingMbid) - assert.Equal(models.MBID(favorite.Track.RecordingMbid), love.Track.RecordingMbid) - assert.Equal(models.MBID(favorite.Track.Album.ReleaseMbid), love.ReleaseMbid) + assert.Equal(mbtypes.MBID(favorite.Track.RecordingMbid), love.RecordingMbid) + assert.Equal(mbtypes.MBID(favorite.Track.RecordingMbid), love.Track.RecordingMbid) + assert.Equal(mbtypes.MBID(favorite.Track.Album.ReleaseMbid), love.ReleaseMbid) require.Len(t, love.Track.ArtistMbids, 1) - assert.Equal(models.MBID(favorite.Track.Artist.ArtistMbid), love.ArtistMbids[0]) + assert.Equal(mbtypes.MBID(favorite.Track.Artist.ArtistMbid), love.ArtistMbids[0]) assert.Equal(funkwhale.FunkwhaleClientName, love.AdditionalInfo["media_player"]) } diff --git a/internal/backends/lastfm/lastfm.go b/internal/backends/lastfm/lastfm.go index d2df067..8063832 100644 --- a/internal/backends/lastfm/lastfm.go +++ b/internal/backends/lastfm/lastfm.go @@ -23,6 +23,7 @@ import ( "time" "github.com/shkh/lastfm-go/lastfm" + "go.uploadedlobster.com/mbtypes" "go.uploadedlobster.com/scotty/internal/auth" "go.uploadedlobster.com/scotty/internal/config" "go.uploadedlobster.com/scotty/internal/i18n" @@ -140,16 +141,16 @@ out: TrackName: scrobble.Name, ArtistNames: []string{}, ReleaseName: scrobble.Album.Name, - RecordingMbid: models.MBID(scrobble.Mbid), - ArtistMbids: []models.MBID{}, - ReleaseMbid: models.MBID(scrobble.Album.Mbid), + RecordingMbid: mbtypes.MBID(scrobble.Mbid), + ArtistMbids: []mbtypes.MBID{}, + ReleaseMbid: mbtypes.MBID(scrobble.Album.Mbid), }, } if scrobble.Artist.Name != "" { listen.Track.ArtistNames = []string{scrobble.Artist.Name} } if scrobble.Artist.Mbid != "" { - listen.Track.ArtistMbids = []models.MBID{models.MBID(scrobble.Artist.Mbid)} + listen.Track.ArtistMbids = []mbtypes.MBID{mbtypes.MBID(scrobble.Artist.Mbid)} } listens = append(listens, listen) } else { @@ -294,12 +295,12 @@ out: love := models.Love{ Created: time.Unix(timestamp, 0), UserName: result.User, - RecordingMbid: models.MBID(track.Mbid), + RecordingMbid: mbtypes.MBID(track.Mbid), Track: models.Track{ TrackName: track.Name, ArtistNames: []string{track.Artist.Name}, - RecordingMbid: models.MBID(track.Mbid), - ArtistMbids: []models.MBID{models.MBID(track.Artist.Mbid)}, + RecordingMbid: mbtypes.MBID(track.Mbid), + ArtistMbids: []mbtypes.MBID{mbtypes.MBID(track.Artist.Mbid)}, AdditionalInfo: models.AdditionalInfo{ "lastfm_url": track.Url, }, diff --git a/internal/backends/listenbrainz/listenbrainz.go b/internal/backends/listenbrainz/listenbrainz.go index ebeb64c..08ffdbc 100644 --- a/internal/backends/listenbrainz/listenbrainz.go +++ b/internal/backends/listenbrainz/listenbrainz.go @@ -21,6 +21,7 @@ import ( "sort" "time" + "go.uploadedlobster.com/mbtypes" "go.uploadedlobster.com/scotty/internal/config" "go.uploadedlobster.com/scotty/internal/i18n" "go.uploadedlobster.com/scotty/internal/models" @@ -323,7 +324,7 @@ func (lbListen Listen) AsListen() models.Listen { } func (f Feedback) AsLove() models.Love { - recordingMbid := models.MBID(f.RecordingMbid) + recordingMbid := mbtypes.MBID(f.RecordingMbid) track := f.TrackMetadata if track == nil { track = &Track{} @@ -350,16 +351,16 @@ func (t Track) AsTrack() models.Track { Duration: t.Duration(), TrackNumber: t.TrackNumber(), DiscNumber: t.DiscNumber(), - RecordingMbid: models.MBID(t.RecordingMbid()), - ReleaseMbid: models.MBID(t.ReleaseMbid()), - ReleaseGroupMbid: models.MBID(t.ReleaseGroupMbid()), + RecordingMbid: mbtypes.MBID(t.RecordingMbid()), + ReleaseMbid: mbtypes.MBID(t.ReleaseMbid()), + ReleaseGroupMbid: mbtypes.MBID(t.ReleaseGroupMbid()), ISRC: t.ISRC(), AdditionalInfo: t.AdditionalInfo, } if t.MbidMapping != nil && len(track.ArtistMbids) == 0 { for _, artistMbid := range t.MbidMapping.ArtistMbids { - track.ArtistMbids = append(track.ArtistMbids, models.MBID(artistMbid)) + track.ArtistMbids = append(track.ArtistMbids, mbtypes.MBID(artistMbid)) } } diff --git a/internal/backends/listenbrainz/listenbrainz_test.go b/internal/backends/listenbrainz/listenbrainz_test.go index f67280e..dc2acdc 100644 --- a/internal/backends/listenbrainz/listenbrainz_test.go +++ b/internal/backends/listenbrainz/listenbrainz_test.go @@ -23,9 +23,9 @@ import ( "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.uploadedlobster.com/mbtypes" "go.uploadedlobster.com/scotty/internal/backends/listenbrainz" "go.uploadedlobster.com/scotty/internal/config" - "go.uploadedlobster.com/scotty/internal/models" ) func TestFromConfig(t *testing.T) { @@ -65,9 +65,9 @@ func TestListenBrainzListenAsListen(t *testing.T) { assert.Equal(t, []string{lbListen.TrackMetadata.ArtistName}, listen.ArtistNames) assert.Equal(t, 5, listen.TrackNumber) assert.Equal(t, 1, listen.DiscNumber) - assert.Equal(t, models.MBID("c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12"), listen.RecordingMbid) - assert.Equal(t, models.MBID("d7f22677-9803-4d21-ba42-081b633a6f68"), listen.ReleaseMbid) - assert.Equal(t, models.MBID("80aca1ee-aa51-41be-9f75-024710d92ff4"), listen.ReleaseGroupMbid) + assert.Equal(t, mbtypes.MBID("c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12"), listen.RecordingMbid) + assert.Equal(t, mbtypes.MBID("d7f22677-9803-4d21-ba42-081b633a6f68"), listen.ReleaseMbid) + assert.Equal(t, mbtypes.MBID("80aca1ee-aa51-41be-9f75-024710d92ff4"), listen.ReleaseGroupMbid) assert.Equal(t, "DES561620801", listen.ISRC) assert.Equal(t, lbListen.TrackMetadata.AdditionalInfo["foo"], listen.AdditionalInfo["foo"]) } @@ -99,11 +99,11 @@ func TestListenBrainzFeedbackAsLove(t *testing.T) { assert.Equal(feedback.TrackMetadata.TrackName, love.TrackName) assert.Equal(feedback.TrackMetadata.ReleaseName, love.ReleaseName) assert.Equal([]string{feedback.TrackMetadata.ArtistName}, love.ArtistNames) - assert.Equal(models.MBID(recordingMbid), love.RecordingMbid) - assert.Equal(models.MBID(recordingMbid), love.Track.RecordingMbid) - assert.Equal(models.MBID(releaseMbid), love.Track.ReleaseMbid) + assert.Equal(mbtypes.MBID(recordingMbid), love.RecordingMbid) + assert.Equal(mbtypes.MBID(recordingMbid), love.Track.RecordingMbid) + assert.Equal(mbtypes.MBID(releaseMbid), love.Track.ReleaseMbid) require.Len(t, love.Track.ArtistMbids, 1) - assert.Equal(models.MBID(artistMbid), love.Track.ArtistMbids[0]) + assert.Equal(mbtypes.MBID(artistMbid), love.Track.ArtistMbids[0]) } func TestListenBrainzPartialFeedbackAsLove(t *testing.T) { @@ -116,7 +116,7 @@ func TestListenBrainzPartialFeedbackAsLove(t *testing.T) { love := feedback.AsLove() assert := assert.New(t) assert.Equal(time.Unix(1699859066, 0).Unix(), love.Created.Unix()) - assert.Equal(models.MBID(recordingMbid), love.RecordingMbid) - assert.Equal(models.MBID(recordingMbid), love.Track.RecordingMbid) + assert.Equal(mbtypes.MBID(recordingMbid), love.RecordingMbid) + assert.Equal(mbtypes.MBID(recordingMbid), love.Track.RecordingMbid) assert.Empty(love.Track.TrackName) } diff --git a/internal/backends/scrobblerlog/parser.go b/internal/backends/scrobblerlog/parser.go index af891ac..ce2d897 100644 --- a/internal/backends/scrobblerlog/parser.go +++ b/internal/backends/scrobblerlog/parser.go @@ -30,6 +30,7 @@ import ( "strings" "time" + "go.uploadedlobster.com/mbtypes" "go.uploadedlobster.com/scotty/internal/models" ) @@ -203,7 +204,7 @@ func rowToListen(row []string, client string) (models.Listen, error) { } if len(row) > 7 { - listen.Track.RecordingMbid = models.MBID(row[7]) + listen.Track.RecordingMbid = mbtypes.MBID(row[7]) } return listen, nil diff --git a/internal/backends/scrobblerlog/parser_test.go b/internal/backends/scrobblerlog/parser_test.go index 51d15c7..583ec49 100644 --- a/internal/backends/scrobblerlog/parser_test.go +++ b/internal/backends/scrobblerlog/parser_test.go @@ -30,6 +30,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.uploadedlobster.com/mbtypes" "go.uploadedlobster.com/scotty/internal/backends/scrobblerlog" "go.uploadedlobster.com/scotty/internal/models" ) @@ -60,10 +61,10 @@ func TestParser(t *testing.T) { assert.Equal(time.Duration(306*time.Second), listen1.Duration) assert.Equal("L", listen1.AdditionalInfo["rockbox_rating"]) assert.Equal(time.Unix(1260342084, 0), listen1.ListenedAt) - assert.Equal(models.MBID(""), listen1.RecordingMbid) + assert.Equal(mbtypes.MBID(""), listen1.RecordingMbid) listen4 := result.Listens[3] assert.Equal("S", listen4.AdditionalInfo["rockbox_rating"]) - assert.Equal(models.MBID("385ba9e9-626d-4750-a607-58e541dca78e"), listen4.RecordingMbid) + assert.Equal(mbtypes.MBID("385ba9e9-626d-4750-a607-58e541dca78e"), listen4.RecordingMbid) } func TestParserExcludeSkipped(t *testing.T) { @@ -74,7 +75,7 @@ func TestParserExcludeSkipped(t *testing.T) { assert.Len(result.Listens, 4) listen4 := result.Listens[3] assert.Equal("L", listen4.AdditionalInfo["rockbox_rating"]) - assert.Equal(models.MBID("1262beaf-19f8-4534-b9ed-7eef9ca8e83f"), listen4.RecordingMbid) + assert.Equal(mbtypes.MBID("1262beaf-19f8-4534-b9ed-7eef9ca8e83f"), listen4.RecordingMbid) } func TestWrite(t *testing.T) { @@ -93,7 +94,7 @@ func TestWrite(t *testing.T) { TrackName: "Reign", TrackNumber: 1, Duration: 271 * time.Second, - RecordingMbid: models.MBID("b59cf4e7-caee-4019-a844-79d2c58d4dff"), + RecordingMbid: mbtypes.MBID("b59cf4e7-caee-4019-a844-79d2c58d4dff"), AdditionalInfo: models.AdditionalInfo{"rockbox_rating": "L"}, }, }, diff --git a/internal/models/models.go b/internal/models/models.go index a225344..a8a6570 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -24,9 +24,10 @@ package models import ( "strings" "time" + + "go.uploadedlobster.com/mbtypes" ) -type MBID string type Entity string const ( @@ -44,11 +45,11 @@ type Track struct { DiscNumber int Duration time.Duration ISRC string - RecordingMbid MBID - ReleaseMbid MBID - ReleaseGroupMbid MBID - ArtistMbids []MBID - WorkMbids []MBID + RecordingMbid mbtypes.MBID + ReleaseMbid mbtypes.MBID + ReleaseGroupMbid mbtypes.MBID + ArtistMbids []mbtypes.MBID + WorkMbids []mbtypes.MBID Tags []string AdditionalInfo AdditionalInfo } @@ -110,8 +111,8 @@ type Love struct { Track Created time.Time UserName string - RecordingMbid MBID - RecordingMsid MBID + RecordingMbid mbtypes.MBID + RecordingMsid mbtypes.MBID } type ListensList []Listen diff --git a/internal/models/models_test.go b/internal/models/models_test.go index cd1f207..5e010bc 100644 --- a/internal/models/models_test.go +++ b/internal/models/models_test.go @@ -28,6 +28,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.uploadedlobster.com/mbtypes" "go.uploadedlobster.com/scotty/internal/models" ) @@ -44,11 +45,11 @@ func TestTrackArtistName(t *testing.T) { func TestTrackFillAdditionalInfo(t *testing.T) { track := models.Track{ - RecordingMbid: models.MBID("c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12"), - ReleaseGroupMbid: models.MBID("80aca1ee-aa51-41be-9f75-024710d92ff4"), - ReleaseMbid: models.MBID("aa1ea1ac-7ec4-4542-a494-105afbfe547d"), - ArtistMbids: []models.MBID{"24412926-c7bd-48e8-afad-8a285b42e131"}, - WorkMbids: []models.MBID{"c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12"}, + RecordingMbid: mbtypes.MBID("c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12"), + ReleaseGroupMbid: mbtypes.MBID("80aca1ee-aa51-41be-9f75-024710d92ff4"), + ReleaseMbid: mbtypes.MBID("aa1ea1ac-7ec4-4542-a494-105afbfe547d"), + ArtistMbids: []mbtypes.MBID{"24412926-c7bd-48e8-afad-8a285b42e131"}, + WorkMbids: []mbtypes.MBID{"c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12"}, TrackNumber: 5, DiscNumber: 1, Duration: time.Duration(413787 * time.Millisecond), diff --git a/internal/similarity/similarity_test.go b/internal/similarity/similarity_test.go index f1e92a5..89fa645 100644 --- a/internal/similarity/similarity_test.go +++ b/internal/similarity/similarity_test.go @@ -20,6 +20,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "go.uploadedlobster.com/mbtypes" "go.uploadedlobster.com/scotty/internal/models" "go.uploadedlobster.com/scotty/internal/similarity" ) @@ -74,13 +75,13 @@ func TestCompareTracksSameMBID(t *testing.T) { t1 := models.Track{ ArtistNames: []string{"Paradise Lost"}, TrackName: "Forever After", - RecordingMbid: models.MBID("2886d15c-09b0-43c6-af56-932f70dde164"), + RecordingMbid: mbtypes.MBID("2886d15c-09b0-43c6-af56-932f70dde164"), } t2 := models.Track{ ArtistNames: []string{"Paradise Lost"}, TrackName: "Forever Failure (radio edit)", ReleaseName: "Draconian Times", - RecordingMbid: models.MBID("2886d15c-09b0-43c6-af56-932f70dde164"), + RecordingMbid: mbtypes.MBID("2886d15c-09b0-43c6-af56-932f70dde164"), } assert.Equal(t, 1.0, similarity.CompareTracks(t1, t2)) } From ad1644672c1d2669e6a7d617e7c3cd45d432af28 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Thu, 3 Apr 2025 15:00:45 +0200 Subject: [PATCH 49/82] Write acronym MBID all uppercase --- internal/backends/dump/dump.go | 4 +- internal/backends/funkwhale/funkwhale.go | 10 ++--- internal/backends/funkwhale/funkwhale_test.go | 28 ++++++------- internal/backends/funkwhale/models.go | 6 +-- internal/backends/jspf/jspf.go | 22 +++++----- internal/backends/lastfm/lastfm.go | 18 ++++---- internal/backends/listenbrainz/client_test.go | 6 +-- .../backends/listenbrainz/listenbrainz.go | 42 +++++++++---------- .../listenbrainz/listenbrainz_test.go | 40 +++++++++--------- internal/backends/listenbrainz/models.go | 34 +++++++-------- internal/backends/listenbrainz/models_test.go | 14 +++---- internal/backends/scrobblerlog/parser.go | 8 ++-- internal/backends/scrobblerlog/parser_test.go | 8 ++-- internal/models/models.go | 32 +++++++------- internal/models/models_test.go | 20 ++++----- internal/similarity/similarity.go | 2 +- internal/similarity/similarity_test.go | 4 +- 17 files changed, 149 insertions(+), 149 deletions(-) diff --git a/internal/backends/dump/dump.go b/internal/backends/dump/dump.go index eb342f2..728a774 100644 --- a/internal/backends/dump/dump.go +++ b/internal/backends/dump/dump.go @@ -41,7 +41,7 @@ func (b *DumpBackend) ImportListens(export models.ListensResult, importResult mo importResult.UpdateTimestamp(listen.ListenedAt) importResult.ImportCount += 1 msg := fmt.Sprintf("🎶 %v: \"%v\" by %v (%v)", - listen.ListenedAt, listen.TrackName, listen.ArtistName(), listen.RecordingMbid) + listen.ListenedAt, listen.TrackName, listen.ArtistName(), listen.RecordingMBID) importResult.Log(models.Info, msg) progress <- models.Progress{}.FromImportResult(importResult) } @@ -54,7 +54,7 @@ func (b *DumpBackend) ImportLoves(export models.LovesResult, importResult models importResult.UpdateTimestamp(love.Created) importResult.ImportCount += 1 msg := fmt.Sprintf("❤️ %v: \"%v\" by %v (%v)", - love.Created, love.TrackName, love.ArtistName(), love.RecordingMbid) + love.Created, love.TrackName, love.ArtistName(), love.RecordingMBID) importResult.Log(models.Info, msg) progress <- models.Progress{}.FromImportResult(importResult) } diff --git a/internal/backends/funkwhale/funkwhale.go b/internal/backends/funkwhale/funkwhale.go index 39f8c59..88bb72c 100644 --- a/internal/backends/funkwhale/funkwhale.go +++ b/internal/backends/funkwhale/funkwhale.go @@ -176,7 +176,7 @@ func (f FavoriteTrack) AsLove() models.Love { track := f.Track.AsTrack() love := models.Love{ UserName: f.User.UserName, - RecordingMbid: track.RecordingMbid, + RecordingMBID: track.RecordingMBID, Track: track, } @@ -189,16 +189,16 @@ func (f FavoriteTrack) AsLove() models.Love { } func (t Track) AsTrack() models.Track { - recordingMbid := mbtypes.MBID(t.RecordingMbid) + recordingMBID := mbtypes.MBID(t.RecordingMBID) track := models.Track{ TrackName: t.Title, ReleaseName: t.Album.Title, ArtistNames: []string{t.Artist.Name}, TrackNumber: t.Position, DiscNumber: t.DiscNumber, - RecordingMbid: recordingMbid, - ReleaseMbid: mbtypes.MBID(t.Album.ReleaseMbid), - ArtistMbids: []mbtypes.MBID{mbtypes.MBID(t.Artist.ArtistMbid)}, + RecordingMBID: recordingMBID, + ReleaseMBID: mbtypes.MBID(t.Album.ReleaseMBID), + ArtistMBIDs: []mbtypes.MBID{mbtypes.MBID(t.Artist.ArtistMBID)}, Tags: t.Tags, AdditionalInfo: map[string]any{ "media_player": FunkwhaleClientName, diff --git a/internal/backends/funkwhale/funkwhale_test.go b/internal/backends/funkwhale/funkwhale_test.go index 915a966..6961c8e 100644 --- a/internal/backends/funkwhale/funkwhale_test.go +++ b/internal/backends/funkwhale/funkwhale_test.go @@ -44,17 +44,17 @@ func TestFunkwhaleListeningAsListen(t *testing.T) { }, Track: funkwhale.Track{ Title: "Oweynagat", - RecordingMbid: "c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12", + RecordingMBID: "c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12", Position: 5, DiscNumber: 1, Tags: []string{"foo", "bar"}, Artist: funkwhale.Artist{ Name: "Dool", - ArtistMbid: "24412926-c7bd-48e8-afad-8a285b42e131", + ArtistMBID: "24412926-c7bd-48e8-afad-8a285b42e131", }, Album: funkwhale.Album{ Title: "Here Now, There Then", - ReleaseMbid: "d7f22677-9803-4d21-ba42-081b633a6f68", + ReleaseMBID: "d7f22677-9803-4d21-ba42-081b633a6f68", }, Uploads: []funkwhale.Upload{ { @@ -75,9 +75,9 @@ func TestFunkwhaleListeningAsListen(t *testing.T) { assert.Equal(fwListen.Track.DiscNumber, listen.Track.DiscNumber) assert.Equal(fwListen.Track.Tags, listen.Track.Tags) // assert.Equal(backends.FunkwhaleClientName, listen.AdditionalInfo["disc_number"]) - assert.Equal(mbtypes.MBID(fwListen.Track.RecordingMbid), listen.RecordingMbid) - assert.Equal(mbtypes.MBID(fwListen.Track.Album.ReleaseMbid), listen.ReleaseMbid) - assert.Equal(mbtypes.MBID(fwListen.Track.Artist.ArtistMbid), listen.ArtistMbids[0]) + assert.Equal(mbtypes.MBID(fwListen.Track.RecordingMBID), listen.RecordingMBID) + assert.Equal(mbtypes.MBID(fwListen.Track.Album.ReleaseMBID), listen.ReleaseMBID) + assert.Equal(mbtypes.MBID(fwListen.Track.Artist.ArtistMBID), listen.ArtistMBIDs[0]) assert.Equal(funkwhale.FunkwhaleClientName, listen.AdditionalInfo["media_player"]) } @@ -89,17 +89,17 @@ func TestFunkwhaleFavoriteTrackAsLove(t *testing.T) { }, Track: funkwhale.Track{ Title: "Oweynagat", - RecordingMbid: "c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12", + RecordingMBID: "c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12", Position: 5, DiscNumber: 1, Tags: []string{"foo", "bar"}, Artist: funkwhale.Artist{ Name: "Dool", - ArtistMbid: "24412926-c7bd-48e8-afad-8a285b42e131", + ArtistMBID: "24412926-c7bd-48e8-afad-8a285b42e131", }, Album: funkwhale.Album{ Title: "Here Now, There Then", - ReleaseMbid: "d7f22677-9803-4d21-ba42-081b633a6f68", + ReleaseMBID: "d7f22677-9803-4d21-ba42-081b633a6f68", }, Uploads: []funkwhale.Upload{ { @@ -119,10 +119,10 @@ func TestFunkwhaleFavoriteTrackAsLove(t *testing.T) { assert.Equal(favorite.Track.Position, love.Track.TrackNumber) assert.Equal(favorite.Track.DiscNumber, love.Track.DiscNumber) assert.Equal(favorite.Track.Tags, love.Track.Tags) - assert.Equal(mbtypes.MBID(favorite.Track.RecordingMbid), love.RecordingMbid) - assert.Equal(mbtypes.MBID(favorite.Track.RecordingMbid), love.Track.RecordingMbid) - assert.Equal(mbtypes.MBID(favorite.Track.Album.ReleaseMbid), love.ReleaseMbid) - require.Len(t, love.Track.ArtistMbids, 1) - assert.Equal(mbtypes.MBID(favorite.Track.Artist.ArtistMbid), love.ArtistMbids[0]) + assert.Equal(mbtypes.MBID(favorite.Track.RecordingMBID), love.RecordingMBID) + assert.Equal(mbtypes.MBID(favorite.Track.RecordingMBID), love.Track.RecordingMBID) + assert.Equal(mbtypes.MBID(favorite.Track.Album.ReleaseMBID), love.ReleaseMBID) + require.Len(t, love.Track.ArtistMBIDs, 1) + assert.Equal(mbtypes.MBID(favorite.Track.Artist.ArtistMBID), love.ArtistMBIDs[0]) assert.Equal(funkwhale.FunkwhaleClientName, love.AdditionalInfo["media_player"]) } diff --git a/internal/backends/funkwhale/models.go b/internal/backends/funkwhale/models.go index 6e0349e..86b66bc 100644 --- a/internal/backends/funkwhale/models.go +++ b/internal/backends/funkwhale/models.go @@ -56,7 +56,7 @@ type Track struct { Title string `json:"title"` Position int `json:"position"` DiscNumber int `json:"disc_number"` - RecordingMbid string `json:"mbid"` + RecordingMBID string `json:"mbid"` Tags []string `json:"tags"` Uploads []Upload `json:"uploads"` } @@ -64,7 +64,7 @@ type Track struct { type Artist struct { Id int `json:"int"` Name string `json:"name"` - ArtistMbid string `json:"mbid"` + ArtistMBID string `json:"mbid"` } type Album struct { @@ -73,7 +73,7 @@ type Album struct { AlbumArtist Artist `json:"artist"` ReleaseDate string `json:"release_date"` TrackCount int `json:"track_count"` - ReleaseMbid string `json:"mbid"` + ReleaseMBID string `json:"mbid"` } type User struct { diff --git a/internal/backends/jspf/jspf.go b/internal/backends/jspf/jspf.go index 17046e7..bfa3892 100644 --- a/internal/backends/jspf/jspf.go +++ b/internal/backends/jspf/jspf.go @@ -118,8 +118,8 @@ func listenAsTrack(l models.Listen) jspf.Track { extension.AddedBy = l.UserName track.Extension[jspf.MusicBrainzTrackExtensionId] = extension - if l.RecordingMbid != "" { - track.Identifier = append(track.Identifier, "https://musicbrainz.org/recording/"+string(l.RecordingMbid)) + if l.RecordingMBID != "" { + track.Identifier = append(track.Identifier, "https://musicbrainz.org/recording/"+string(l.RecordingMBID)) } return track @@ -133,12 +133,12 @@ func loveAsTrack(l models.Love) jspf.Track { extension.AddedBy = l.UserName track.Extension[jspf.MusicBrainzTrackExtensionId] = extension - recordingMbid := l.Track.RecordingMbid - if l.RecordingMbid != "" { - recordingMbid = l.RecordingMbid + recordingMBID := l.Track.RecordingMBID + if l.RecordingMBID != "" { + recordingMBID = l.RecordingMBID } - if recordingMbid != "" { - track.Identifier = append(track.Identifier, "https://musicbrainz.org/recording/"+string(recordingMbid)) + if recordingMBID != "" { + track.Identifier = append(track.Identifier, "https://musicbrainz.org/recording/"+string(recordingMBID)) } return track @@ -159,15 +159,15 @@ func trackAsTrack(t models.Track) jspf.Track { func makeMusicBrainzExtension(t models.Track) jspf.MusicBrainzTrackExtension { extension := jspf.MusicBrainzTrackExtension{ AdditionalMetadata: t.AdditionalInfo, - ArtistIdentifiers: make([]string, len(t.ArtistMbids)), + ArtistIdentifiers: make([]string, len(t.ArtistMBIDs)), } - for i, mbid := range t.ArtistMbids { + for i, mbid := range t.ArtistMBIDs { extension.ArtistIdentifiers[i] = "https://musicbrainz.org/artist/" + string(mbid) } - if t.ReleaseMbid != "" { - extension.ReleaseIdentifier = "https://musicbrainz.org/release/" + string(t.ReleaseMbid) + if t.ReleaseMBID != "" { + extension.ReleaseIdentifier = "https://musicbrainz.org/release/" + string(t.ReleaseMBID) } // The tracknumber tag would be redundant diff --git a/internal/backends/lastfm/lastfm.go b/internal/backends/lastfm/lastfm.go index 8063832..ba660de 100644 --- a/internal/backends/lastfm/lastfm.go +++ b/internal/backends/lastfm/lastfm.go @@ -141,16 +141,16 @@ out: TrackName: scrobble.Name, ArtistNames: []string{}, ReleaseName: scrobble.Album.Name, - RecordingMbid: mbtypes.MBID(scrobble.Mbid), - ArtistMbids: []mbtypes.MBID{}, - ReleaseMbid: mbtypes.MBID(scrobble.Album.Mbid), + RecordingMBID: mbtypes.MBID(scrobble.Mbid), + ArtistMBIDs: []mbtypes.MBID{}, + ReleaseMBID: mbtypes.MBID(scrobble.Album.Mbid), }, } if scrobble.Artist.Name != "" { listen.Track.ArtistNames = []string{scrobble.Artist.Name} } if scrobble.Artist.Mbid != "" { - listen.Track.ArtistMbids = []mbtypes.MBID{mbtypes.MBID(scrobble.Artist.Mbid)} + listen.Track.ArtistMBIDs = []mbtypes.MBID{mbtypes.MBID(scrobble.Artist.Mbid)} } listens = append(listens, listen) } else { @@ -204,8 +204,8 @@ func (b *LastfmApiBackend) ImportListens(export models.ListensResult, importResu if l.TrackNumber > 0 { trackNumbers = append(trackNumbers, strconv.Itoa(l.TrackNumber)) } - if l.RecordingMbid != "" { - mbids = append(mbids, string(l.RecordingMbid)) + if l.RecordingMBID != "" { + mbids = append(mbids, string(l.RecordingMBID)) } // if l.ReleaseArtist != "" { // albumArtists = append(albums, l.ReleaseArtist) @@ -295,12 +295,12 @@ out: love := models.Love{ Created: time.Unix(timestamp, 0), UserName: result.User, - RecordingMbid: mbtypes.MBID(track.Mbid), + RecordingMBID: mbtypes.MBID(track.Mbid), Track: models.Track{ TrackName: track.Name, ArtistNames: []string{track.Artist.Name}, - RecordingMbid: mbtypes.MBID(track.Mbid), - ArtistMbids: []mbtypes.MBID{mbtypes.MBID(track.Artist.Mbid)}, + RecordingMBID: mbtypes.MBID(track.Mbid), + ArtistMBIDs: []mbtypes.MBID{mbtypes.MBID(track.Artist.Mbid)}, AdditionalInfo: models.AdditionalInfo{ "lastfm_url": track.Url, }, diff --git a/internal/backends/listenbrainz/client_test.go b/internal/backends/listenbrainz/client_test.go index cc36f1d..0fbbdfd 100644 --- a/internal/backends/listenbrainz/client_test.go +++ b/internal/backends/listenbrainz/client_test.go @@ -114,7 +114,7 @@ func TestGetFeedback(t *testing.T) { assert.Equal(302, result.TotalCount) assert.Equal(3, result.Offset) require.Len(t, result.Feedback, 2) - assert.Equal("c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12", result.Feedback[0].RecordingMbid) + assert.Equal("c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12", result.Feedback[0].RecordingMBID) } func TestSendFeedback(t *testing.T) { @@ -131,7 +131,7 @@ func TestSendFeedback(t *testing.T) { httpmock.RegisterResponder("POST", url, responder) feedback := listenbrainz.Feedback{ - RecordingMbid: "c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12", + RecordingMBID: "c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12", Score: 1, } result, err := client.SendFeedback(feedback) @@ -154,7 +154,7 @@ func TestLookup(t *testing.T) { assert := assert.New(t) assert.Equal("Say Just Words", result.RecordingName) assert.Equal("Paradise Lost", result.ArtistCreditName) - assert.Equal("569436a1-234a-44bc-a370-8f4d252bef21", result.RecordingMbid) + assert.Equal("569436a1-234a-44bc-a370-8f4d252bef21", result.RecordingMBID) } func setupHttpMock(t *testing.T, client *http.Client, url string, testDataPath string) { diff --git a/internal/backends/listenbrainz/listenbrainz.go b/internal/backends/listenbrainz/listenbrainz.go index 08ffdbc..f18a29d 100644 --- a/internal/backends/listenbrainz/listenbrainz.go +++ b/internal/backends/listenbrainz/listenbrainz.go @@ -33,7 +33,7 @@ type ListenBrainzApiBackend struct { client Client username string checkDuplicates bool - existingMbids map[string]bool + existingMBIDs map[string]bool } func (b *ListenBrainzApiBackend) Name() string { return "listenbrainz" } @@ -148,7 +148,7 @@ func (b *ListenBrainzApiBackend) ImportListens(export models.ListensResult, impo } else if isDupe { count -= 1 msg := i18n.Tr("Ignored duplicate listen %v: \"%v\" by %v (%v)", - l.ListenedAt, l.TrackName, l.ArtistName(), l.RecordingMbid) + l.ListenedAt, l.TrackName, l.ArtistName(), l.RecordingMBID) importResult.Log(models.Info, msg) continue } @@ -230,7 +230,7 @@ out: } func (b *ListenBrainzApiBackend) ImportLoves(export models.LovesResult, importResult models.ImportResult, progress chan models.Progress) (models.ImportResult, error) { - if len(b.existingMbids) == 0 { + if len(b.existingMBIDs) == 0 { existingLovesChan := make(chan models.LovesResult) go b.ExportLoves(time.Unix(0, 0), existingLovesChan, progress) existingLoves := <-existingLovesChan @@ -239,30 +239,30 @@ func (b *ListenBrainzApiBackend) ImportLoves(export models.LovesResult, importRe } // TODO: Store MBIDs directly - b.existingMbids = make(map[string]bool, len(existingLoves.Items)) + b.existingMBIDs = make(map[string]bool, len(existingLoves.Items)) for _, love := range existingLoves.Items { - b.existingMbids[string(love.RecordingMbid)] = true + b.existingMBIDs[string(love.RecordingMBID)] = true } } for _, love := range export.Items { - recordingMbid := string(love.RecordingMbid) + recordingMBID := string(love.RecordingMBID) - if recordingMbid == "" { + if recordingMBID == "" { lookup, err := b.client.Lookup(love.TrackName, love.ArtistName()) if err == nil { - recordingMbid = lookup.RecordingMbid + recordingMBID = lookup.RecordingMBID } } - if recordingMbid != "" { + if recordingMBID != "" { ok := false errMsg := "" - if b.existingMbids[recordingMbid] { + if b.existingMBIDs[recordingMBID] { ok = true } else { resp, err := b.client.SendFeedback(Feedback{ - RecordingMbid: recordingMbid, + RecordingMBID: recordingMBID, Score: 1, }) ok = err == nil && resp.Status == "ok" @@ -324,20 +324,20 @@ func (lbListen Listen) AsListen() models.Listen { } func (f Feedback) AsLove() models.Love { - recordingMbid := mbtypes.MBID(f.RecordingMbid) + recordingMBID := mbtypes.MBID(f.RecordingMBID) track := f.TrackMetadata if track == nil { track = &Track{} } love := models.Love{ UserName: f.UserName, - RecordingMbid: recordingMbid, + RecordingMBID: recordingMBID, Created: time.Unix(f.Created, 0), Track: track.AsTrack(), } - if love.Track.RecordingMbid == "" { - love.Track.RecordingMbid = love.RecordingMbid + if love.Track.RecordingMBID == "" { + love.Track.RecordingMBID = love.RecordingMBID } return love @@ -351,16 +351,16 @@ func (t Track) AsTrack() models.Track { Duration: t.Duration(), TrackNumber: t.TrackNumber(), DiscNumber: t.DiscNumber(), - RecordingMbid: mbtypes.MBID(t.RecordingMbid()), - ReleaseMbid: mbtypes.MBID(t.ReleaseMbid()), - ReleaseGroupMbid: mbtypes.MBID(t.ReleaseGroupMbid()), + RecordingMBID: mbtypes.MBID(t.RecordingMBID()), + ReleaseMBID: mbtypes.MBID(t.ReleaseMBID()), + ReleaseGroupMBID: mbtypes.MBID(t.ReleaseGroupMBID()), ISRC: t.ISRC(), AdditionalInfo: t.AdditionalInfo, } - if t.MbidMapping != nil && len(track.ArtistMbids) == 0 { - for _, artistMbid := range t.MbidMapping.ArtistMbids { - track.ArtistMbids = append(track.ArtistMbids, mbtypes.MBID(artistMbid)) + if t.MBIDMapping != nil && len(track.ArtistMBIDs) == 0 { + for _, artistMBID := range t.MBIDMapping.ArtistMBIDs { + track.ArtistMBIDs = append(track.ArtistMBIDs, mbtypes.MBID(artistMBID)) } } diff --git a/internal/backends/listenbrainz/listenbrainz_test.go b/internal/backends/listenbrainz/listenbrainz_test.go index dc2acdc..d6d577f 100644 --- a/internal/backends/listenbrainz/listenbrainz_test.go +++ b/internal/backends/listenbrainz/listenbrainz_test.go @@ -65,30 +65,30 @@ func TestListenBrainzListenAsListen(t *testing.T) { assert.Equal(t, []string{lbListen.TrackMetadata.ArtistName}, listen.ArtistNames) assert.Equal(t, 5, listen.TrackNumber) assert.Equal(t, 1, listen.DiscNumber) - assert.Equal(t, mbtypes.MBID("c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12"), listen.RecordingMbid) - assert.Equal(t, mbtypes.MBID("d7f22677-9803-4d21-ba42-081b633a6f68"), listen.ReleaseMbid) - assert.Equal(t, mbtypes.MBID("80aca1ee-aa51-41be-9f75-024710d92ff4"), listen.ReleaseGroupMbid) + assert.Equal(t, mbtypes.MBID("c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12"), listen.RecordingMBID) + assert.Equal(t, mbtypes.MBID("d7f22677-9803-4d21-ba42-081b633a6f68"), listen.ReleaseMBID) + assert.Equal(t, mbtypes.MBID("80aca1ee-aa51-41be-9f75-024710d92ff4"), listen.ReleaseGroupMBID) assert.Equal(t, "DES561620801", listen.ISRC) assert.Equal(t, lbListen.TrackMetadata.AdditionalInfo["foo"], listen.AdditionalInfo["foo"]) } func TestListenBrainzFeedbackAsLove(t *testing.T) { - recordingMbid := "c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12" - releaseMbid := "d7f22677-9803-4d21-ba42-081b633a6f68" - artistMbid := "d7f22677-9803-4d21-ba42-081b633a6f68" + recordingMBID := "c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12" + releaseMBID := "d7f22677-9803-4d21-ba42-081b633a6f68" + artistMBID := "d7f22677-9803-4d21-ba42-081b633a6f68" feedback := listenbrainz.Feedback{ Created: 1699859066, - RecordingMbid: recordingMbid, + RecordingMBID: recordingMBID, Score: 1, UserName: "ousidecontext", TrackMetadata: &listenbrainz.Track{ TrackName: "Oweynagat", ArtistName: "Dool", ReleaseName: "Here Now, There Then", - MbidMapping: &listenbrainz.MbidMapping{ - RecordingMbid: recordingMbid, - ReleaseMbid: releaseMbid, - ArtistMbids: []string{artistMbid}, + MBIDMapping: &listenbrainz.MBIDMapping{ + RecordingMBID: recordingMBID, + ReleaseMBID: releaseMBID, + ArtistMBIDs: []string{artistMBID}, }, }, } @@ -99,24 +99,24 @@ func TestListenBrainzFeedbackAsLove(t *testing.T) { assert.Equal(feedback.TrackMetadata.TrackName, love.TrackName) assert.Equal(feedback.TrackMetadata.ReleaseName, love.ReleaseName) assert.Equal([]string{feedback.TrackMetadata.ArtistName}, love.ArtistNames) - assert.Equal(mbtypes.MBID(recordingMbid), love.RecordingMbid) - assert.Equal(mbtypes.MBID(recordingMbid), love.Track.RecordingMbid) - assert.Equal(mbtypes.MBID(releaseMbid), love.Track.ReleaseMbid) - require.Len(t, love.Track.ArtistMbids, 1) - assert.Equal(mbtypes.MBID(artistMbid), love.Track.ArtistMbids[0]) + assert.Equal(mbtypes.MBID(recordingMBID), love.RecordingMBID) + assert.Equal(mbtypes.MBID(recordingMBID), love.Track.RecordingMBID) + assert.Equal(mbtypes.MBID(releaseMBID), love.Track.ReleaseMBID) + require.Len(t, love.Track.ArtistMBIDs, 1) + assert.Equal(mbtypes.MBID(artistMBID), love.Track.ArtistMBIDs[0]) } func TestListenBrainzPartialFeedbackAsLove(t *testing.T) { - recordingMbid := "c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12" + recordingMBID := "c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12" feedback := listenbrainz.Feedback{ Created: 1699859066, - RecordingMbid: recordingMbid, + RecordingMBID: recordingMBID, Score: 1, } love := feedback.AsLove() assert := assert.New(t) assert.Equal(time.Unix(1699859066, 0).Unix(), love.Created.Unix()) - assert.Equal(mbtypes.MBID(recordingMbid), love.RecordingMbid) - assert.Equal(mbtypes.MBID(recordingMbid), love.Track.RecordingMbid) + assert.Equal(mbtypes.MBID(recordingMBID), love.RecordingMBID) + assert.Equal(mbtypes.MBID(recordingMBID), love.Track.RecordingMBID) assert.Empty(love.Track.TrackName) } diff --git a/internal/backends/listenbrainz/models.go b/internal/backends/listenbrainz/models.go index c1552c7..e86ce9d 100644 --- a/internal/backends/listenbrainz/models.go +++ b/internal/backends/listenbrainz/models.go @@ -66,20 +66,20 @@ type Track struct { ArtistName string `json:"artist_name,omitempty"` ReleaseName string `json:"release_name,omitempty"` AdditionalInfo map[string]any `json:"additional_info,omitempty"` - MbidMapping *MbidMapping `json:"mbid_mapping,omitempty"` + MBIDMapping *MBIDMapping `json:"mbid_mapping,omitempty"` } -type MbidMapping struct { +type MBIDMapping struct { RecordingName string `json:"recording_name,omitempty"` - RecordingMbid string `json:"recording_mbid,omitempty"` - ReleaseMbid string `json:"release_mbid,omitempty"` - ArtistMbids []string `json:"artist_mbids,omitempty"` + RecordingMBID string `json:"recording_mbid,omitempty"` + ReleaseMBID string `json:"release_mbid,omitempty"` + ArtistMBIDs []string `json:"artist_mbids,omitempty"` Artists []Artist `json:"artists,omitempty"` } type Artist struct { ArtistCreditName string `json:"artist_credit_name,omitempty"` - ArtistMbid string `json:"artist_mbid,omitempty"` + ArtistMBID string `json:"artist_mbid,omitempty"` JoinPhrase string `json:"join_phrase,omitempty"` } @@ -92,7 +92,7 @@ type GetFeedbackResult struct { type Feedback struct { Created int64 `json:"created,omitempty"` - RecordingMbid string `json:"recording_mbid,omitempty"` + RecordingMBID string `json:"recording_mbid,omitempty"` RecordingMsid string `json:"recording_msid,omitempty"` Score int `json:"score,omitempty"` TrackMetadata *Track `json:"track_metadata,omitempty"` @@ -103,9 +103,9 @@ type LookupResult struct { ArtistCreditName string `json:"artist_credit_name"` ReleaseName string `json:"release_name"` RecordingName string `json:"recording_name"` - RecordingMbid string `json:"recording_mbid"` - ReleaseMbid string `json:"release_mbid"` - ArtistMbids []string `json:"artist_mbids"` + RecordingMBID string `json:"recording_mbid"` + ReleaseMBID string `json:"release_mbid"` + ArtistMBIDs []string `json:"artist_mbids"` } type StatusResult struct { @@ -162,25 +162,25 @@ func (t Track) ISRC() string { return tryGetValueOrEmpty[string](t.AdditionalInfo, "isrc") } -func (t Track) RecordingMbid() string { +func (t Track) RecordingMBID() string { mbid := tryGetValueOrEmpty[string](t.AdditionalInfo, "recording_mbid") - if mbid == "" && t.MbidMapping != nil { - return t.MbidMapping.RecordingMbid + if mbid == "" && t.MBIDMapping != nil { + return t.MBIDMapping.RecordingMBID } else { return mbid } } -func (t Track) ReleaseMbid() string { +func (t Track) ReleaseMBID() string { mbid := tryGetValueOrEmpty[string](t.AdditionalInfo, "release_mbid") - if mbid == "" && t.MbidMapping != nil { - return t.MbidMapping.ReleaseMbid + if mbid == "" && t.MBIDMapping != nil { + return t.MBIDMapping.ReleaseMBID } else { return mbid } } -func (t Track) ReleaseGroupMbid() string { +func (t Track) ReleaseGroupMBID() string { return tryGetValueOrEmpty[string](t.AdditionalInfo, "release_group_mbid") } diff --git a/internal/backends/listenbrainz/models_test.go b/internal/backends/listenbrainz/models_test.go index 845690d..55e5267 100644 --- a/internal/backends/listenbrainz/models_test.go +++ b/internal/backends/listenbrainz/models_test.go @@ -140,40 +140,40 @@ func TestTrackIsrc(t *testing.T) { assert.Equal(t, expected, track.ISRC()) } -func TestTrackRecordingMbid(t *testing.T) { +func TestTrackRecordingMBID(t *testing.T) { expected := "e02cc1c3-93fd-4e24-8b77-325060de920b" track := listenbrainz.Track{ AdditionalInfo: map[string]any{ "recording_mbid": expected, }, } - assert.Equal(t, expected, track.RecordingMbid()) + assert.Equal(t, expected, track.RecordingMBID()) } -func TestTrackReleaseMbid(t *testing.T) { +func TestTrackReleaseMBID(t *testing.T) { expected := "e02cc1c3-93fd-4e24-8b77-325060de920b" track := listenbrainz.Track{ AdditionalInfo: map[string]any{ "release_mbid": expected, }, } - assert.Equal(t, expected, track.ReleaseMbid()) + assert.Equal(t, expected, track.ReleaseMBID()) } -func TestReleaseGroupMbid(t *testing.T) { +func TestReleaseGroupMBID(t *testing.T) { expected := "e02cc1c3-93fd-4e24-8b77-325060de920b" track := listenbrainz.Track{ AdditionalInfo: map[string]any{ "release_group_mbid": expected, }, } - assert.Equal(t, expected, track.ReleaseGroupMbid()) + assert.Equal(t, expected, track.ReleaseGroupMBID()) } func TestMarshalPartialFeedback(t *testing.T) { feedback := listenbrainz.Feedback{ Created: 1699859066, - RecordingMbid: "c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12", + RecordingMBID: "c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12", } b, err := json.Marshal(feedback) require.NoError(t, err) diff --git a/internal/backends/scrobblerlog/parser.go b/internal/backends/scrobblerlog/parser.go index ce2d897..1ef08f7 100644 --- a/internal/backends/scrobblerlog/parser.go +++ b/internal/backends/scrobblerlog/parser.go @@ -58,7 +58,7 @@ func Parse(data io.Reader, includeSkipped bool) (ScrobblerLog, error) { for { // A row is: - // artistName releaseName trackName trackNumber duration rating timestamp recordingMbid + // artistName releaseName trackName trackNumber duration rating timestamp recordingMBID row, err := tsvReader.Read() if err == io.EOF { break @@ -101,7 +101,7 @@ func Write(data io.Writer, listens models.ListensList) (lastTimestamp time.Time, } // A row is: - // artistName releaseName trackName trackNumber duration rating timestamp recordingMbid + // artistName releaseName trackName trackNumber duration rating timestamp recordingMBID rating, ok := listen.AdditionalInfo["rockbox_rating"].(string) if !ok || rating == "" { rating = "L" @@ -114,7 +114,7 @@ func Write(data io.Writer, listens models.ListensList) (lastTimestamp time.Time, strconv.Itoa(int(listen.Duration.Seconds())), rating, strconv.Itoa(int(listen.ListenedAt.Unix())), - string(listen.RecordingMbid), + string(listen.RecordingMBID), }) } @@ -204,7 +204,7 @@ func rowToListen(row []string, client string) (models.Listen, error) { } if len(row) > 7 { - listen.Track.RecordingMbid = mbtypes.MBID(row[7]) + listen.Track.RecordingMBID = mbtypes.MBID(row[7]) } return listen, nil diff --git a/internal/backends/scrobblerlog/parser_test.go b/internal/backends/scrobblerlog/parser_test.go index 583ec49..480481f 100644 --- a/internal/backends/scrobblerlog/parser_test.go +++ b/internal/backends/scrobblerlog/parser_test.go @@ -61,10 +61,10 @@ func TestParser(t *testing.T) { assert.Equal(time.Duration(306*time.Second), listen1.Duration) assert.Equal("L", listen1.AdditionalInfo["rockbox_rating"]) assert.Equal(time.Unix(1260342084, 0), listen1.ListenedAt) - assert.Equal(mbtypes.MBID(""), listen1.RecordingMbid) + assert.Equal(mbtypes.MBID(""), listen1.RecordingMBID) listen4 := result.Listens[3] assert.Equal("S", listen4.AdditionalInfo["rockbox_rating"]) - assert.Equal(mbtypes.MBID("385ba9e9-626d-4750-a607-58e541dca78e"), listen4.RecordingMbid) + assert.Equal(mbtypes.MBID("385ba9e9-626d-4750-a607-58e541dca78e"), listen4.RecordingMBID) } func TestParserExcludeSkipped(t *testing.T) { @@ -75,7 +75,7 @@ func TestParserExcludeSkipped(t *testing.T) { assert.Len(result.Listens, 4) listen4 := result.Listens[3] assert.Equal("L", listen4.AdditionalInfo["rockbox_rating"]) - assert.Equal(mbtypes.MBID("1262beaf-19f8-4534-b9ed-7eef9ca8e83f"), listen4.RecordingMbid) + assert.Equal(mbtypes.MBID("1262beaf-19f8-4534-b9ed-7eef9ca8e83f"), listen4.RecordingMBID) } func TestWrite(t *testing.T) { @@ -94,7 +94,7 @@ func TestWrite(t *testing.T) { TrackName: "Reign", TrackNumber: 1, Duration: 271 * time.Second, - RecordingMbid: mbtypes.MBID("b59cf4e7-caee-4019-a844-79d2c58d4dff"), + RecordingMBID: mbtypes.MBID("b59cf4e7-caee-4019-a844-79d2c58d4dff"), AdditionalInfo: models.AdditionalInfo{"rockbox_rating": "L"}, }, }, diff --git a/internal/models/models.go b/internal/models/models.go index a8a6570..39ae236 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -45,11 +45,11 @@ type Track struct { DiscNumber int Duration time.Duration ISRC string - RecordingMbid mbtypes.MBID - ReleaseMbid mbtypes.MBID - ReleaseGroupMbid mbtypes.MBID - ArtistMbids []mbtypes.MBID - WorkMbids []mbtypes.MBID + RecordingMBID mbtypes.MBID + ReleaseMBID mbtypes.MBID + ReleaseGroupMBID mbtypes.MBID + ArtistMBIDs []mbtypes.MBID + WorkMBIDs []mbtypes.MBID Tags []string AdditionalInfo AdditionalInfo } @@ -63,20 +63,20 @@ func (t *Track) FillAdditionalInfo() { if t.AdditionalInfo == nil { t.AdditionalInfo = make(AdditionalInfo, 5) } - if t.RecordingMbid != "" { - t.AdditionalInfo["recording_mbid"] = t.RecordingMbid + if t.RecordingMBID != "" { + t.AdditionalInfo["recording_mbid"] = t.RecordingMBID } - if t.ReleaseGroupMbid != "" { - t.AdditionalInfo["release_group_mbid"] = t.ReleaseGroupMbid + if t.ReleaseGroupMBID != "" { + t.AdditionalInfo["release_group_mbid"] = t.ReleaseGroupMBID } - if t.ReleaseMbid != "" { - t.AdditionalInfo["release_mbid"] = t.ReleaseMbid + if t.ReleaseMBID != "" { + t.AdditionalInfo["release_mbid"] = t.ReleaseMBID } - if len(t.ArtistMbids) > 0 { - t.AdditionalInfo["artist_mbids"] = t.ArtistMbids + if len(t.ArtistMBIDs) > 0 { + t.AdditionalInfo["artist_mbids"] = t.ArtistMBIDs } - if len(t.WorkMbids) > 0 { - t.AdditionalInfo["work_mbids"] = t.WorkMbids + if len(t.WorkMBIDs) > 0 { + t.AdditionalInfo["work_mbids"] = t.WorkMBIDs } if t.ISRC != "" { t.AdditionalInfo["isrc"] = t.ISRC @@ -111,7 +111,7 @@ type Love struct { Track Created time.Time UserName string - RecordingMbid mbtypes.MBID + RecordingMBID mbtypes.MBID RecordingMsid mbtypes.MBID } diff --git a/internal/models/models_test.go b/internal/models/models_test.go index 5e010bc..0deaefa 100644 --- a/internal/models/models_test.go +++ b/internal/models/models_test.go @@ -45,11 +45,11 @@ func TestTrackArtistName(t *testing.T) { func TestTrackFillAdditionalInfo(t *testing.T) { track := models.Track{ - RecordingMbid: mbtypes.MBID("c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12"), - ReleaseGroupMbid: mbtypes.MBID("80aca1ee-aa51-41be-9f75-024710d92ff4"), - ReleaseMbid: mbtypes.MBID("aa1ea1ac-7ec4-4542-a494-105afbfe547d"), - ArtistMbids: []mbtypes.MBID{"24412926-c7bd-48e8-afad-8a285b42e131"}, - WorkMbids: []mbtypes.MBID{"c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12"}, + RecordingMBID: mbtypes.MBID("c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12"), + ReleaseGroupMBID: mbtypes.MBID("80aca1ee-aa51-41be-9f75-024710d92ff4"), + ReleaseMBID: mbtypes.MBID("aa1ea1ac-7ec4-4542-a494-105afbfe547d"), + ArtistMBIDs: []mbtypes.MBID{"24412926-c7bd-48e8-afad-8a285b42e131"}, + WorkMBIDs: []mbtypes.MBID{"c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12"}, TrackNumber: 5, DiscNumber: 1, Duration: time.Duration(413787 * time.Millisecond), @@ -59,11 +59,11 @@ func TestTrackFillAdditionalInfo(t *testing.T) { track.FillAdditionalInfo() i := track.AdditionalInfo assert := assert.New(t) - assert.Equal(track.RecordingMbid, i["recording_mbid"]) - assert.Equal(track.ReleaseGroupMbid, i["release_group_mbid"]) - assert.Equal(track.ReleaseMbid, i["release_mbid"]) - assert.Equal(track.ArtistMbids, i["artist_mbids"]) - assert.Equal(track.WorkMbids, i["work_mbids"]) + assert.Equal(track.RecordingMBID, i["recording_mbid"]) + assert.Equal(track.ReleaseGroupMBID, i["release_group_mbid"]) + assert.Equal(track.ReleaseMBID, i["release_mbid"]) + assert.Equal(track.ArtistMBIDs, i["artist_mbids"]) + assert.Equal(track.WorkMBIDs, i["work_mbids"]) assert.Equal(track.TrackNumber, i["tracknumber"]) assert.Equal(track.DiscNumber, i["discnumber"]) assert.Equal(track.Duration.Milliseconds(), i["duration_ms"]) diff --git a/internal/similarity/similarity.go b/internal/similarity/similarity.go index 358404a..3fb27c4 100644 --- a/internal/similarity/similarity.go +++ b/internal/similarity/similarity.go @@ -63,7 +63,7 @@ func NormalizeTitle(s string) string { // Compare two tracks for similarity. func CompareTracks(t1 models.Track, t2 models.Track) float64 { // Identical recording MBID always compares 100% - if t1.RecordingMbid == t2.RecordingMbid && t1.RecordingMbid != "" { + if t1.RecordingMBID == t2.RecordingMBID && t1.RecordingMBID != "" { return 1.0 } diff --git a/internal/similarity/similarity_test.go b/internal/similarity/similarity_test.go index 89fa645..c43e1d7 100644 --- a/internal/similarity/similarity_test.go +++ b/internal/similarity/similarity_test.go @@ -75,13 +75,13 @@ func TestCompareTracksSameMBID(t *testing.T) { t1 := models.Track{ ArtistNames: []string{"Paradise Lost"}, TrackName: "Forever After", - RecordingMbid: mbtypes.MBID("2886d15c-09b0-43c6-af56-932f70dde164"), + RecordingMBID: mbtypes.MBID("2886d15c-09b0-43c6-af56-932f70dde164"), } t2 := models.Track{ ArtistNames: []string{"Paradise Lost"}, TrackName: "Forever Failure (radio edit)", ReleaseName: "Draconian Times", - RecordingMbid: mbtypes.MBID("2886d15c-09b0-43c6-af56-932f70dde164"), + RecordingMBID: mbtypes.MBID("2886d15c-09b0-43c6-af56-932f70dde164"), } assert.Equal(t, 1.0, similarity.CompareTracks(t1, t2)) } From 13eb8342abfa2ed02953d3a929ee30425b6ec6de Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Thu, 3 Apr 2025 15:08:02 +0200 Subject: [PATCH 50/82] Use mbtypes.ISRC type --- internal/backends/listenbrainz/listenbrainz_test.go | 2 +- internal/backends/listenbrainz/models.go | 5 +++-- internal/backends/listenbrainz/models_test.go | 5 +++-- internal/backends/spotify/models.go | 8 +++++--- internal/backends/spotify/spotify_test.go | 3 ++- internal/models/models.go | 2 +- internal/models/models_test.go | 2 +- 7 files changed, 16 insertions(+), 11 deletions(-) diff --git a/internal/backends/listenbrainz/listenbrainz_test.go b/internal/backends/listenbrainz/listenbrainz_test.go index d6d577f..ddeac01 100644 --- a/internal/backends/listenbrainz/listenbrainz_test.go +++ b/internal/backends/listenbrainz/listenbrainz_test.go @@ -68,7 +68,7 @@ func TestListenBrainzListenAsListen(t *testing.T) { assert.Equal(t, mbtypes.MBID("c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12"), listen.RecordingMBID) assert.Equal(t, mbtypes.MBID("d7f22677-9803-4d21-ba42-081b633a6f68"), listen.ReleaseMBID) assert.Equal(t, mbtypes.MBID("80aca1ee-aa51-41be-9f75-024710d92ff4"), listen.ReleaseGroupMBID) - assert.Equal(t, "DES561620801", listen.ISRC) + assert.Equal(t, mbtypes.ISRC("DES561620801"), listen.ISRC) assert.Equal(t, lbListen.TrackMetadata.AdditionalInfo["foo"], listen.AdditionalInfo["foo"]) } diff --git a/internal/backends/listenbrainz/models.go b/internal/backends/listenbrainz/models.go index e86ce9d..833acd5 100644 --- a/internal/backends/listenbrainz/models.go +++ b/internal/backends/listenbrainz/models.go @@ -25,6 +25,7 @@ import ( "strconv" "time" + "go.uploadedlobster.com/mbtypes" "golang.org/x/exp/constraints" ) @@ -158,8 +159,8 @@ func (t Track) DiscNumber() int { return 0 } -func (t Track) ISRC() string { - return tryGetValueOrEmpty[string](t.AdditionalInfo, "isrc") +func (t Track) ISRC() mbtypes.ISRC { + return mbtypes.ISRC(tryGetValueOrEmpty[string](t.AdditionalInfo, "isrc")) } func (t Track) RecordingMBID() string { diff --git a/internal/backends/listenbrainz/models_test.go b/internal/backends/listenbrainz/models_test.go index 55e5267..f10f635 100644 --- a/internal/backends/listenbrainz/models_test.go +++ b/internal/backends/listenbrainz/models_test.go @@ -28,6 +28,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.uploadedlobster.com/mbtypes" "go.uploadedlobster.com/scotty/internal/backends/listenbrainz" ) @@ -131,10 +132,10 @@ func TestTrackTrackNumberString(t *testing.T) { } func TestTrackIsrc(t *testing.T) { - expected := "TCAEJ1934417" + expected := mbtypes.ISRC("TCAEJ1934417") track := listenbrainz.Track{ AdditionalInfo: map[string]any{ - "isrc": expected, + "isrc": string(expected), }, } assert.Equal(t, expected, track.ISRC()) diff --git a/internal/backends/spotify/models.go b/internal/backends/spotify/models.go index e80eccc..a22de21 100644 --- a/internal/backends/spotify/models.go +++ b/internal/backends/spotify/models.go @@ -22,6 +22,8 @@ THE SOFTWARE. package spotify +import "go.uploadedlobster.com/mbtypes" + type TracksResult struct { Href string `json:"href"` Limit int `json:"limit"` @@ -98,9 +100,9 @@ type Artist struct { } type ExternalIds struct { - ISRC string `json:"isrc"` - EAN string `json:"ean"` - UPC string `json:"upc"` + ISRC mbtypes.ISRC `json:"isrc"` + EAN string `json:"ean"` + UPC string `json:"upc"` } type ExternalUrls struct { diff --git a/internal/backends/spotify/spotify_test.go b/internal/backends/spotify/spotify_test.go index bd7ff58..1aa7e87 100644 --- a/internal/backends/spotify/spotify_test.go +++ b/internal/backends/spotify/spotify_test.go @@ -26,6 +26,7 @@ import ( "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.uploadedlobster.com/mbtypes" "go.uploadedlobster.com/scotty/internal/backends/spotify" "go.uploadedlobster.com/scotty/internal/config" ) @@ -59,7 +60,7 @@ func TestSpotifyListenAsListen(t *testing.T) { assert.Equal(t, []string{"Dool"}, listen.ArtistNames) assert.Equal(t, 5, listen.TrackNumber) assert.Equal(t, 1, listen.DiscNumber) - assert.Equal(t, "DES561620801", listen.ISRC) + assert.Equal(t, mbtypes.ISRC("DES561620801"), listen.ISRC) info := listen.AdditionalInfo assert.Equal(t, "spotify.com", info["music_service"]) assert.Equal(t, "https://open.spotify.com/track/2JKUgGuXK3dEvyuIJ4Yj2V", info["origin_url"]) diff --git a/internal/models/models.go b/internal/models/models.go index 39ae236..0d5abf2 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -44,7 +44,7 @@ type Track struct { TrackNumber int DiscNumber int Duration time.Duration - ISRC string + ISRC mbtypes.ISRC RecordingMBID mbtypes.MBID ReleaseMBID mbtypes.MBID ReleaseGroupMBID mbtypes.MBID diff --git a/internal/models/models_test.go b/internal/models/models_test.go index 0deaefa..5395610 100644 --- a/internal/models/models_test.go +++ b/internal/models/models_test.go @@ -53,7 +53,7 @@ func TestTrackFillAdditionalInfo(t *testing.T) { TrackNumber: 5, DiscNumber: 1, Duration: time.Duration(413787 * time.Millisecond), - ISRC: "DES561620801", + ISRC: mbtypes.ISRC("DES561620801"), Tags: []string{"rock", "psychedelic rock"}, } track.FillAdditionalInfo() From 0d9bc74bc0ba4821480b12dbff882dd34f5c08a0 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Thu, 3 Apr 2025 15:19:26 +0200 Subject: [PATCH 51/82] More conversion to mbtypes.MBID --- internal/backends/funkwhale/funkwhale.go | 7 ++- internal/backends/funkwhale/funkwhale_test.go | 15 +++--- internal/backends/funkwhale/models.go | 38 +++++++-------- internal/backends/listenbrainz/client_test.go | 5 +- .../backends/listenbrainz/listenbrainz.go | 18 ++++---- .../listenbrainz/listenbrainz_test.go | 22 ++++----- internal/backends/listenbrainz/models.go | 46 +++++++++---------- internal/backends/listenbrainz/models_test.go | 12 ++--- 8 files changed, 82 insertions(+), 81 deletions(-) diff --git a/internal/backends/funkwhale/funkwhale.go b/internal/backends/funkwhale/funkwhale.go index 88bb72c..48c3d8f 100644 --- a/internal/backends/funkwhale/funkwhale.go +++ b/internal/backends/funkwhale/funkwhale.go @@ -189,16 +189,15 @@ func (f FavoriteTrack) AsLove() models.Love { } func (t Track) AsTrack() models.Track { - recordingMBID := mbtypes.MBID(t.RecordingMBID) track := models.Track{ TrackName: t.Title, ReleaseName: t.Album.Title, ArtistNames: []string{t.Artist.Name}, TrackNumber: t.Position, DiscNumber: t.DiscNumber, - RecordingMBID: recordingMBID, - ReleaseMBID: mbtypes.MBID(t.Album.ReleaseMBID), - ArtistMBIDs: []mbtypes.MBID{mbtypes.MBID(t.Artist.ArtistMBID)}, + RecordingMBID: t.RecordingMBID, + ReleaseMBID: t.Album.ReleaseMBID, + ArtistMBIDs: []mbtypes.MBID{t.Artist.ArtistMBID}, Tags: t.Tags, AdditionalInfo: map[string]any{ "media_player": FunkwhaleClientName, diff --git a/internal/backends/funkwhale/funkwhale_test.go b/internal/backends/funkwhale/funkwhale_test.go index 6961c8e..d8654d8 100644 --- a/internal/backends/funkwhale/funkwhale_test.go +++ b/internal/backends/funkwhale/funkwhale_test.go @@ -23,7 +23,6 @@ import ( "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "go.uploadedlobster.com/mbtypes" "go.uploadedlobster.com/scotty/internal/backends/funkwhale" "go.uploadedlobster.com/scotty/internal/config" ) @@ -75,9 +74,9 @@ func TestFunkwhaleListeningAsListen(t *testing.T) { assert.Equal(fwListen.Track.DiscNumber, listen.Track.DiscNumber) assert.Equal(fwListen.Track.Tags, listen.Track.Tags) // assert.Equal(backends.FunkwhaleClientName, listen.AdditionalInfo["disc_number"]) - assert.Equal(mbtypes.MBID(fwListen.Track.RecordingMBID), listen.RecordingMBID) - assert.Equal(mbtypes.MBID(fwListen.Track.Album.ReleaseMBID), listen.ReleaseMBID) - assert.Equal(mbtypes.MBID(fwListen.Track.Artist.ArtistMBID), listen.ArtistMBIDs[0]) + assert.Equal(fwListen.Track.RecordingMBID, listen.RecordingMBID) + assert.Equal(fwListen.Track.Album.ReleaseMBID, listen.ReleaseMBID) + assert.Equal(fwListen.Track.Artist.ArtistMBID, listen.ArtistMBIDs[0]) assert.Equal(funkwhale.FunkwhaleClientName, listen.AdditionalInfo["media_player"]) } @@ -119,10 +118,10 @@ func TestFunkwhaleFavoriteTrackAsLove(t *testing.T) { assert.Equal(favorite.Track.Position, love.Track.TrackNumber) assert.Equal(favorite.Track.DiscNumber, love.Track.DiscNumber) assert.Equal(favorite.Track.Tags, love.Track.Tags) - assert.Equal(mbtypes.MBID(favorite.Track.RecordingMBID), love.RecordingMBID) - assert.Equal(mbtypes.MBID(favorite.Track.RecordingMBID), love.Track.RecordingMBID) - assert.Equal(mbtypes.MBID(favorite.Track.Album.ReleaseMBID), love.ReleaseMBID) + assert.Equal(favorite.Track.RecordingMBID, love.RecordingMBID) + assert.Equal(favorite.Track.RecordingMBID, love.Track.RecordingMBID) + assert.Equal(favorite.Track.Album.ReleaseMBID, love.ReleaseMBID) require.Len(t, love.Track.ArtistMBIDs, 1) - assert.Equal(mbtypes.MBID(favorite.Track.Artist.ArtistMBID), love.ArtistMBIDs[0]) + assert.Equal(favorite.Track.Artist.ArtistMBID, love.ArtistMBIDs[0]) assert.Equal(funkwhale.FunkwhaleClientName, love.AdditionalInfo["media_player"]) } diff --git a/internal/backends/funkwhale/models.go b/internal/backends/funkwhale/models.go index 86b66bc..10d57d0 100644 --- a/internal/backends/funkwhale/models.go +++ b/internal/backends/funkwhale/models.go @@ -21,6 +21,8 @@ THE SOFTWARE. */ package funkwhale +import "go.uploadedlobster.com/mbtypes" + type ListeningsResult struct { Count int `json:"count"` Previous string `json:"previous"` @@ -50,30 +52,30 @@ type FavoriteTrack struct { } type Track struct { - Id int `json:"int"` - Artist Artist `json:"artist"` - Album Album `json:"album"` - Title string `json:"title"` - Position int `json:"position"` - DiscNumber int `json:"disc_number"` - RecordingMBID string `json:"mbid"` - Tags []string `json:"tags"` - Uploads []Upload `json:"uploads"` + Id int `json:"int"` + Artist Artist `json:"artist"` + Album Album `json:"album"` + Title string `json:"title"` + Position int `json:"position"` + DiscNumber int `json:"disc_number"` + RecordingMBID mbtypes.MBID `json:"mbid"` + Tags []string `json:"tags"` + Uploads []Upload `json:"uploads"` } type Artist struct { - Id int `json:"int"` - Name string `json:"name"` - ArtistMBID string `json:"mbid"` + Id int `json:"int"` + Name string `json:"name"` + ArtistMBID mbtypes.MBID `json:"mbid"` } type Album struct { - Id int `json:"int"` - Title string `json:"title"` - AlbumArtist Artist `json:"artist"` - ReleaseDate string `json:"release_date"` - TrackCount int `json:"track_count"` - ReleaseMBID string `json:"mbid"` + Id int `json:"int"` + Title string `json:"title"` + AlbumArtist Artist `json:"artist"` + ReleaseDate string `json:"release_date"` + TrackCount int `json:"track_count"` + ReleaseMBID mbtypes.MBID `json:"mbid"` } type User struct { diff --git a/internal/backends/listenbrainz/client_test.go b/internal/backends/listenbrainz/client_test.go index 0fbbdfd..4e72756 100644 --- a/internal/backends/listenbrainz/client_test.go +++ b/internal/backends/listenbrainz/client_test.go @@ -29,6 +29,7 @@ import ( "github.com/jarcoal/httpmock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.uploadedlobster.com/mbtypes" "go.uploadedlobster.com/scotty/internal/backends/listenbrainz" ) @@ -114,7 +115,7 @@ func TestGetFeedback(t *testing.T) { assert.Equal(302, result.TotalCount) assert.Equal(3, result.Offset) require.Len(t, result.Feedback, 2) - assert.Equal("c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12", result.Feedback[0].RecordingMBID) + assert.Equal(mbtypes.MBID("c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12"), result.Feedback[0].RecordingMBID) } func TestSendFeedback(t *testing.T) { @@ -154,7 +155,7 @@ func TestLookup(t *testing.T) { assert := assert.New(t) assert.Equal("Say Just Words", result.RecordingName) assert.Equal("Paradise Lost", result.ArtistCreditName) - assert.Equal("569436a1-234a-44bc-a370-8f4d252bef21", result.RecordingMBID) + assert.Equal(mbtypes.MBID("569436a1-234a-44bc-a370-8f4d252bef21"), result.RecordingMBID) } func setupHttpMock(t *testing.T, client *http.Client, url string, testDataPath string) { diff --git a/internal/backends/listenbrainz/listenbrainz.go b/internal/backends/listenbrainz/listenbrainz.go index f18a29d..97f721d 100644 --- a/internal/backends/listenbrainz/listenbrainz.go +++ b/internal/backends/listenbrainz/listenbrainz.go @@ -33,7 +33,7 @@ type ListenBrainzApiBackend struct { client Client username string checkDuplicates bool - existingMBIDs map[string]bool + existingMBIDs map[mbtypes.MBID]bool } func (b *ListenBrainzApiBackend) Name() string { return "listenbrainz" } @@ -239,14 +239,14 @@ func (b *ListenBrainzApiBackend) ImportLoves(export models.LovesResult, importRe } // TODO: Store MBIDs directly - b.existingMBIDs = make(map[string]bool, len(existingLoves.Items)) + b.existingMBIDs = make(map[mbtypes.MBID]bool, len(existingLoves.Items)) for _, love := range existingLoves.Items { - b.existingMBIDs[string(love.RecordingMBID)] = true + b.existingMBIDs[love.RecordingMBID] = true } } for _, love := range export.Items { - recordingMBID := string(love.RecordingMBID) + recordingMBID := love.RecordingMBID if recordingMBID == "" { lookup, err := b.client.Lookup(love.TrackName, love.ArtistName()) @@ -324,7 +324,7 @@ func (lbListen Listen) AsListen() models.Listen { } func (f Feedback) AsLove() models.Love { - recordingMBID := mbtypes.MBID(f.RecordingMBID) + recordingMBID := f.RecordingMBID track := f.TrackMetadata if track == nil { track = &Track{} @@ -351,16 +351,16 @@ func (t Track) AsTrack() models.Track { Duration: t.Duration(), TrackNumber: t.TrackNumber(), DiscNumber: t.DiscNumber(), - RecordingMBID: mbtypes.MBID(t.RecordingMBID()), - ReleaseMBID: mbtypes.MBID(t.ReleaseMBID()), - ReleaseGroupMBID: mbtypes.MBID(t.ReleaseGroupMBID()), + RecordingMBID: t.RecordingMBID(), + ReleaseMBID: t.ReleaseMBID(), + ReleaseGroupMBID: t.ReleaseGroupMBID(), ISRC: t.ISRC(), AdditionalInfo: t.AdditionalInfo, } if t.MBIDMapping != nil && len(track.ArtistMBIDs) == 0 { for _, artistMBID := range t.MBIDMapping.ArtistMBIDs { - track.ArtistMBIDs = append(track.ArtistMBIDs, mbtypes.MBID(artistMBID)) + track.ArtistMBIDs = append(track.ArtistMBIDs, artistMBID) } } diff --git a/internal/backends/listenbrainz/listenbrainz_test.go b/internal/backends/listenbrainz/listenbrainz_test.go index ddeac01..93428d7 100644 --- a/internal/backends/listenbrainz/listenbrainz_test.go +++ b/internal/backends/listenbrainz/listenbrainz_test.go @@ -73,9 +73,9 @@ func TestListenBrainzListenAsListen(t *testing.T) { } func TestListenBrainzFeedbackAsLove(t *testing.T) { - recordingMBID := "c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12" - releaseMBID := "d7f22677-9803-4d21-ba42-081b633a6f68" - artistMBID := "d7f22677-9803-4d21-ba42-081b633a6f68" + recordingMBID := mbtypes.MBID("c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12") + releaseMBID := mbtypes.MBID("d7f22677-9803-4d21-ba42-081b633a6f68") + artistMBID := mbtypes.MBID("d7f22677-9803-4d21-ba42-081b633a6f68") feedback := listenbrainz.Feedback{ Created: 1699859066, RecordingMBID: recordingMBID, @@ -88,7 +88,7 @@ func TestListenBrainzFeedbackAsLove(t *testing.T) { MBIDMapping: &listenbrainz.MBIDMapping{ RecordingMBID: recordingMBID, ReleaseMBID: releaseMBID, - ArtistMBIDs: []string{artistMBID}, + ArtistMBIDs: []mbtypes.MBID{artistMBID}, }, }, } @@ -99,15 +99,15 @@ func TestListenBrainzFeedbackAsLove(t *testing.T) { assert.Equal(feedback.TrackMetadata.TrackName, love.TrackName) assert.Equal(feedback.TrackMetadata.ReleaseName, love.ReleaseName) assert.Equal([]string{feedback.TrackMetadata.ArtistName}, love.ArtistNames) - assert.Equal(mbtypes.MBID(recordingMBID), love.RecordingMBID) - assert.Equal(mbtypes.MBID(recordingMBID), love.Track.RecordingMBID) - assert.Equal(mbtypes.MBID(releaseMBID), love.Track.ReleaseMBID) + assert.Equal(recordingMBID, love.RecordingMBID) + assert.Equal(recordingMBID, love.Track.RecordingMBID) + assert.Equal(releaseMBID, love.Track.ReleaseMBID) require.Len(t, love.Track.ArtistMBIDs, 1) - assert.Equal(mbtypes.MBID(artistMBID), love.Track.ArtistMBIDs[0]) + assert.Equal(artistMBID, love.Track.ArtistMBIDs[0]) } func TestListenBrainzPartialFeedbackAsLove(t *testing.T) { - recordingMBID := "c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12" + recordingMBID := mbtypes.MBID("c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12") feedback := listenbrainz.Feedback{ Created: 1699859066, RecordingMBID: recordingMBID, @@ -116,7 +116,7 @@ func TestListenBrainzPartialFeedbackAsLove(t *testing.T) { love := feedback.AsLove() assert := assert.New(t) assert.Equal(time.Unix(1699859066, 0).Unix(), love.Created.Unix()) - assert.Equal(mbtypes.MBID(recordingMBID), love.RecordingMBID) - assert.Equal(mbtypes.MBID(recordingMBID), love.Track.RecordingMBID) + assert.Equal(recordingMBID, love.RecordingMBID) + assert.Equal(recordingMBID, love.Track.RecordingMBID) assert.Empty(love.Track.TrackName) } diff --git a/internal/backends/listenbrainz/models.go b/internal/backends/listenbrainz/models.go index 833acd5..4102ee5 100644 --- a/internal/backends/listenbrainz/models.go +++ b/internal/backends/listenbrainz/models.go @@ -71,11 +71,11 @@ type Track struct { } type MBIDMapping struct { - RecordingName string `json:"recording_name,omitempty"` - RecordingMBID string `json:"recording_mbid,omitempty"` - ReleaseMBID string `json:"release_mbid,omitempty"` - ArtistMBIDs []string `json:"artist_mbids,omitempty"` - Artists []Artist `json:"artists,omitempty"` + RecordingName string `json:"recording_name,omitempty"` + RecordingMBID mbtypes.MBID `json:"recording_mbid,omitempty"` + ReleaseMBID mbtypes.MBID `json:"release_mbid,omitempty"` + ArtistMBIDs []mbtypes.MBID `json:"artist_mbids,omitempty"` + Artists []Artist `json:"artists,omitempty"` } type Artist struct { @@ -92,21 +92,21 @@ type GetFeedbackResult struct { } type Feedback struct { - Created int64 `json:"created,omitempty"` - RecordingMBID string `json:"recording_mbid,omitempty"` - RecordingMsid string `json:"recording_msid,omitempty"` - Score int `json:"score,omitempty"` - TrackMetadata *Track `json:"track_metadata,omitempty"` - UserName string `json:"user_id,omitempty"` + Created int64 `json:"created,omitempty"` + RecordingMBID mbtypes.MBID `json:"recording_mbid,omitempty"` + RecordingMsid mbtypes.MBID `json:"recording_msid,omitempty"` + Score int `json:"score,omitempty"` + TrackMetadata *Track `json:"track_metadata,omitempty"` + UserName string `json:"user_id,omitempty"` } type LookupResult struct { - ArtistCreditName string `json:"artist_credit_name"` - ReleaseName string `json:"release_name"` - RecordingName string `json:"recording_name"` - RecordingMBID string `json:"recording_mbid"` - ReleaseMBID string `json:"release_mbid"` - ArtistMBIDs []string `json:"artist_mbids"` + ArtistCreditName string `json:"artist_credit_name"` + ReleaseName string `json:"release_name"` + RecordingName string `json:"recording_name"` + RecordingMBID mbtypes.MBID `json:"recording_mbid"` + ReleaseMBID mbtypes.MBID `json:"release_mbid"` + ArtistMBIDs []mbtypes.MBID `json:"artist_mbids"` } type StatusResult struct { @@ -163,8 +163,8 @@ func (t Track) ISRC() mbtypes.ISRC { return mbtypes.ISRC(tryGetValueOrEmpty[string](t.AdditionalInfo, "isrc")) } -func (t Track) RecordingMBID() string { - mbid := tryGetValueOrEmpty[string](t.AdditionalInfo, "recording_mbid") +func (t Track) RecordingMBID() mbtypes.MBID { + mbid := mbtypes.MBID(tryGetValueOrEmpty[string](t.AdditionalInfo, "recording_mbid")) if mbid == "" && t.MBIDMapping != nil { return t.MBIDMapping.RecordingMBID } else { @@ -172,8 +172,8 @@ func (t Track) RecordingMBID() string { } } -func (t Track) ReleaseMBID() string { - mbid := tryGetValueOrEmpty[string](t.AdditionalInfo, "release_mbid") +func (t Track) ReleaseMBID() mbtypes.MBID { + mbid := mbtypes.MBID(tryGetValueOrEmpty[string](t.AdditionalInfo, "release_mbid")) if mbid == "" && t.MBIDMapping != nil { return t.MBIDMapping.ReleaseMBID } else { @@ -181,8 +181,8 @@ func (t Track) ReleaseMBID() string { } } -func (t Track) ReleaseGroupMBID() string { - return tryGetValueOrEmpty[string](t.AdditionalInfo, "release_group_mbid") +func (t Track) ReleaseGroupMBID() mbtypes.MBID { + return mbtypes.MBID(tryGetValueOrEmpty[string](t.AdditionalInfo, "release_group_mbid")) } func tryGetValueOrEmpty[T any](dict map[string]any, key string) T { diff --git a/internal/backends/listenbrainz/models_test.go b/internal/backends/listenbrainz/models_test.go index f10f635..9f5b14a 100644 --- a/internal/backends/listenbrainz/models_test.go +++ b/internal/backends/listenbrainz/models_test.go @@ -142,30 +142,30 @@ func TestTrackIsrc(t *testing.T) { } func TestTrackRecordingMBID(t *testing.T) { - expected := "e02cc1c3-93fd-4e24-8b77-325060de920b" + expected := mbtypes.MBID("e02cc1c3-93fd-4e24-8b77-325060de920b") track := listenbrainz.Track{ AdditionalInfo: map[string]any{ - "recording_mbid": expected, + "recording_mbid": string(expected), }, } assert.Equal(t, expected, track.RecordingMBID()) } func TestTrackReleaseMBID(t *testing.T) { - expected := "e02cc1c3-93fd-4e24-8b77-325060de920b" + expected := mbtypes.MBID("e02cc1c3-93fd-4e24-8b77-325060de920b") track := listenbrainz.Track{ AdditionalInfo: map[string]any{ - "release_mbid": expected, + "release_mbid": string(expected), }, } assert.Equal(t, expected, track.ReleaseMBID()) } func TestReleaseGroupMBID(t *testing.T) { - expected := "e02cc1c3-93fd-4e24-8b77-325060de920b" + expected := mbtypes.MBID("e02cc1c3-93fd-4e24-8b77-325060de920b") track := listenbrainz.Track{ AdditionalInfo: map[string]any{ - "release_group_mbid": expected, + "release_group_mbid": string(expected), }, } assert.Equal(t, expected, track.ReleaseGroupMBID()) From dc834e9b6fa4b455214626bf81783d3128b13e54 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Mon, 7 Apr 2025 08:46:46 +0200 Subject: [PATCH 52/82] update dependencies --- go.mod | 56 +++++++++---------- go.sum | 173 +++++++++++++++++++++++++++------------------------------ 2 files changed, 107 insertions(+), 122 deletions(-) diff --git a/go.mod b/go.mod index 81d9deb..051277f 100644 --- a/go.mod +++ b/go.mod @@ -5,27 +5,27 @@ go 1.23.0 toolchain go1.23.8 require ( - github.com/Xuanwo/go-locale v1.1.2 - github.com/agnivade/levenshtein v1.1.1 + github.com/Xuanwo/go-locale v1.1.3 + github.com/agnivade/levenshtein v1.2.1 github.com/cli/browser v1.3.0 github.com/delucks/go-subsonic v0.0.0-20240806025900-2a743ec36238 - github.com/fatih/color v1.17.0 + github.com/fatih/color v1.18.0 github.com/glebarez/sqlite v1.11.0 - github.com/go-resty/resty/v2 v2.15.0 + github.com/go-resty/resty/v2 v2.16.5 github.com/jarcoal/httpmock v1.3.1 github.com/manifoldco/promptui v0.9.0 github.com/pelletier/go-toml/v2 v2.2.3 github.com/shkh/lastfm-go v0.0.0-20191215035245-89a801c244e0 - github.com/spf13/cast v1.7.0 - github.com/spf13/cobra v1.8.1 - github.com/spf13/viper v1.19.0 - github.com/stretchr/testify v1.9.0 - github.com/vbauerster/mpb/v8 v8.8.3 + github.com/spf13/cast v1.7.1 + github.com/spf13/cobra v1.9.1 + github.com/spf13/viper v1.20.1 + github.com/stretchr/testify v1.10.0 + github.com/vbauerster/mpb/v8 v8.9.3 go.uploadedlobster.com/mbtypes v0.2.0 - golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 - golang.org/x/oauth2 v0.23.0 - golang.org/x/text v0.18.0 - gorm.io/datatypes v1.2.2 + golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 + golang.org/x/oauth2 v0.29.0 + golang.org/x/text v0.24.0 + gorm.io/datatypes v1.2.5 gorm.io/gorm v1.25.12 ) @@ -36,37 +36,33 @@ require ( github.com/chzyer/readline v1.5.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/glebarez/go-sqlite v1.22.0 // indirect - github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/go-sql-driver/mysql v1.9.1 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect - github.com/magiconair/properties v1.8.7 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/sagikazarmark/locafero v0.6.0 // indirect - github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sagikazarmark/locafero v0.9.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.11.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/afero v1.14.0 // indirect + github.com/spf13/pflag v1.0.6 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/net v0.29.0 // indirect - golang.org/x/sys v0.25.0 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect + golang.org/x/net v0.38.0 // indirect + golang.org/x/sys v0.32.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gorm.io/driver/mysql v1.5.7 // indirect - modernc.org/libc v1.60.1 // indirect - modernc.org/mathutil v1.6.0 // indirect - modernc.org/memory v1.8.0 // indirect - modernc.org/sqlite v1.33.1 // indirect + modernc.org/libc v1.62.1 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.9.1 // indirect + modernc.org/sqlite v1.37.0 // indirect ) diff --git a/go.sum b/go.sum index c2b9efe..c1692a4 100644 --- a/go.sum +++ b/go.sum @@ -2,12 +2,12 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= -github.com/Xuanwo/go-locale v1.1.2 h1:6H+olvrQcyVOZ+GAC2rXu4armacTT4ZrFCA0mB24XVo= -github.com/Xuanwo/go-locale v1.1.2/go.mod h1:1JBER4QV7Ji39GJ4AvVlfvqmTUqopzxQxdg2mXYOw94= +github.com/Xuanwo/go-locale v1.1.3 h1:EWZZJJt5rqPHHbqPRH1zFCn5D7xHjjebODctA4aUO3A= +github.com/Xuanwo/go-locale v1.1.3/go.mod h1:REn+F/c+AtGSWYACBSYZgl23AP+0lfQC+SEFPN+hj30= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= -github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= -github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= +github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= +github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -21,43 +21,42 @@ github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo= github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/delucks/go-subsonic v0.0.0-20240806025900-2a743ec36238 h1:uejyepOdHISrJTw7P84Y7yEC0FMyv1q3KNDRxWsviKw= github.com/delucks/go-subsonic v0.0.0-20240806025900-2a743ec36238/go.mod h1:vnbEuj6Z20PLcHB4rrLQAOXGMjtULfMGhRVSFPcSdUo= -github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= -github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= +github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= -github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= -github.com/go-resty/resty/v2 v2.15.0 h1:clPQLZ2x9h4yGY81IzpMPnty+xoGyFaDg0XMkCsHf90= -github.com/go-resty/resty/v2 v2.15.0/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU= +github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM= +github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= -github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= -github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-sql-driver/mysql v1.9.1 h1:FrjNGn/BsJQjVRuSa8CBrM5BWA9BWoXXat3KrtSb/iI= +github.com/go-sql-driver/mysql v1.9.1/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= -github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= @@ -78,13 +77,10 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= @@ -93,10 +89,8 @@ github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOj github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= -github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE= -github.com/microsoft/go-mssqldb v0.17.0/go.mod h1:OkoNGhGEs8EZqchVTtochlXruEhEOaO4S0d2sB5aeGQ= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA= +github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= @@ -111,99 +105,94 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk= -github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= -github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k= +github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk= github.com/shkh/lastfm-go v0.0.0-20191215035245-89a801c244e0 h1:cgqwZtnR+IQfUYDLJ3Kiy4aE+O/wExTzEIg8xwC4Qfs= github.com/shkh/lastfm-go v0.0.0-20191215035245-89a801c244e0/go.mod h1:n3nudMl178cEvD44PaopxH9jhJaQzthSxUzLO5iKMy4= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= -github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= -github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= +github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= +github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= +github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= +github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/vbauerster/mpb/v8 v8.8.3 h1:dTOByGoqwaTJYPubhVz3lO5O6MK553XVgUo33LdnNsQ= -github.com/vbauerster/mpb/v8 v8.8.3/go.mod h1:JfCCrtcMsJwP6ZwMn9e5LMnNyp3TVNpUWWkN+nd4EWk= +github.com/vbauerster/mpb/v8 v8.9.3 h1:PnMeF+sMvYv9u23l6DO6Q3+Mdj408mjLRXIzmUmU2Z8= +github.com/vbauerster/mpb/v8 v8.9.3/go.mod h1:hxS8Hz4C6ijnppDSIX6LjG8FYJSoPo9iIOcE53Zik0c= 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.2.0 h1:sV9WM5fY8KnnKwKKVRaU3JBEKP4zD7aUX0XQfNZvEdo= go.uploadedlobster.com/mbtypes v0.2.0/go.mod h1:/ZpwXc8oRpDa7EWeGI9xEY+nGhMIVHhTruikZWD4Krg= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= -golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= -golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= -golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= -golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= -golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= -golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= +golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= -golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= +golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= +golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/datatypes v1.2.2 h1:sdn7ZmG4l7JWtMDUb3L98f2Ym7CO5F8mZLlrQJMfF9g= -gorm.io/datatypes v1.2.2/go.mod h1:f4BsLcFAX67szSv8svwLRjklArSHAvHLeE3pXAS5DZI= +gorm.io/datatypes v1.2.5 h1:9UogU3jkydFVW1bIVVeoYsTpLRgwDVW3rHfJG6/Ek9I= +gorm.io/datatypes v1.2.5/go.mod h1:I5FUdlKpLb5PMqeMQhm30CQ6jXP8Rj89xkTeCSAaAD4= gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= gorm.io/driver/postgres v1.5.0 h1:u2FXTy14l45qc3UeCJ7QaAXZmZfDDv0YrthvmRq1l0U= gorm.io/driver/postgres v1.5.0/go.mod h1:FUZXzO+5Uqg5zzwzv4KK49R8lvGIyscBOqYrtI1Ce9A= gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU= gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI= -gorm.io/driver/sqlserver v1.4.1 h1:t4r4r6Jam5E6ejqP7N82qAJIJAht27EGT41HyPfXRw0= -gorm.io/driver/sqlserver v1.4.1/go.mod h1:DJ4P+MeZbc5rvY58PnmN1Lnyvb5gw5NPzGshHDnJLig= +gorm.io/driver/sqlserver v1.5.4 h1:xA+Y1KDNspv79q43bPyjDMUgHoYHLhXYmdFcYPobg8g= +gorm.io/driver/sqlserver v1.5.4/go.mod h1:+frZ/qYmuna11zHPlh5oc2O6ZA/lS88Keb0XSH1Zh/g= gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= -modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= -modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= -modernc.org/ccgo/v4 v4.21.0 h1:kKPI3dF7RIag8YcToh5ZwDcVMIv6VGa0ED5cvh0LMW4= -modernc.org/ccgo/v4 v4.21.0/go.mod h1:h6kt6H/A2+ew/3MW/p6KEoQmrq/i3pr0J/SiwiaF/g0= +modernc.org/cc/v4 v4.25.2 h1:T2oH7sZdGvTaie0BRNFbIYsabzCxUQg8nLqCdQ2i0ic= +modernc.org/cc/v4 v4.25.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.25.1 h1:TFSzPrAGmDsdnhT9X2UrcPMI3N/mJ9/X9ykKXwLhDsU= +modernc.org/ccgo/v4 v4.25.1/go.mod h1:njjuAYiPflywOOrm3B7kCB444ONP5pAVr8PIEoE0uDw= modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= -modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M= -modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= -modernc.org/libc v1.60.1 h1:at373l8IFRTkJIkAU85BIuUoBM4T1b51ds0E1ovPG2s= -modernc.org/libc v1.60.1/go.mod h1:xJuobKuNxKH3RUatS7GjR+suWj+5c2K7bi4m/S5arOY= -modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= -modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= -modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= -modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= -modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= -modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= -modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= -modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= -modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM= -modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k= -modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= -modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/libc v1.62.1 h1:s0+fv5E3FymN8eJVmnk0llBe6rOxCu/DEU+XygRbS8s= +modernc.org/libc v1.62.1/go.mod h1:iXhATfJQLjG3NWy56a6WVU73lWOcdYVxsvwCgoPljuo= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.9.1 h1:V/Z1solwAVmMW1yttq3nDdZPJqV1rM05Ccq6KMSZ34g= +modernc.org/memory v1.9.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI= +modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= From 5f9c0f24ab63a4ad089983e3a5563ecc7e4ae0e0 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Wed, 9 Apr 2025 22:11:59 +0200 Subject: [PATCH 53/82] Updated dependencies --- go.mod | 10 +++++----- go.sum | 28 ++++++++++++++-------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/go.mod b/go.mod index 051277f..51a40fa 100644 --- a/go.mod +++ b/go.mod @@ -14,15 +14,15 @@ require ( github.com/go-resty/resty/v2 v2.16.5 github.com/jarcoal/httpmock v1.3.1 github.com/manifoldco/promptui v0.9.0 - github.com/pelletier/go-toml/v2 v2.2.3 + github.com/pelletier/go-toml/v2 v2.2.4 github.com/shkh/lastfm-go v0.0.0-20191215035245-89a801c244e0 github.com/spf13/cast v1.7.1 github.com/spf13/cobra v1.9.1 github.com/spf13/viper v1.20.1 github.com/stretchr/testify v1.10.0 github.com/vbauerster/mpb/v8 v8.9.3 - go.uploadedlobster.com/mbtypes v0.2.0 - golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 + go.uploadedlobster.com/mbtypes v0.4.0 + golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 golang.org/x/oauth2 v0.29.0 golang.org/x/text v0.24.0 gorm.io/datatypes v1.2.5 @@ -38,7 +38,7 @@ require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/glebarez/go-sqlite v1.22.0 // indirect - github.com/go-sql-driver/mysql v1.9.1 // indirect + github.com/go-sql-driver/mysql v1.9.2 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -57,7 +57,7 @@ require ( github.com/spf13/pflag v1.0.6 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/net v0.38.0 // indirect + golang.org/x/net v0.39.0 // indirect golang.org/x/sys v0.32.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gorm.io/driver/mysql v1.5.7 // indirect diff --git a/go.sum b/go.sum index c1692a4..511ace4 100644 --- a/go.sum +++ b/go.sum @@ -43,8 +43,8 @@ github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7b github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM= github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= -github.com/go-sql-driver/mysql v1.9.1 h1:FrjNGn/BsJQjVRuSa8CBrM5BWA9BWoXXat3KrtSb/iI= -github.com/go-sql-driver/mysql v1.9.1/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU= +github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= @@ -93,8 +93,8 @@ github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= -github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= -github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= @@ -129,16 +129,16 @@ github.com/vbauerster/mpb/v8 v8.9.3 h1:PnMeF+sMvYv9u23l6DO6Q3+Mdj408mjLRXIzmUmU2 github.com/vbauerster/mpb/v8 v8.9.3/go.mod h1:hxS8Hz4C6ijnppDSIX6LjG8FYJSoPo9iIOcE53Zik0c= 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.2.0 h1:sV9WM5fY8KnnKwKKVRaU3JBEKP4zD7aUX0XQfNZvEdo= -go.uploadedlobster.com/mbtypes v0.2.0/go.mod h1:/ZpwXc8oRpDa7EWeGI9xEY+nGhMIVHhTruikZWD4Krg= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= -golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= -golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= +go.uploadedlobster.com/mbtypes v0.4.0 h1:D5asCgHsRWufj4Yn5u0IuH2J9z1UuYImYkYIp1Z1Q7s= +go.uploadedlobster.com/mbtypes v0.4.0/go.mod h1:Bu1K1Hl77QTAE2Z7QKiW/JAp9KqYWQebkRRfG02dlZM= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= @@ -152,8 +152,8 @@ golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= -golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= +golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 329f696b553fea2ab3b14c7faaace086bdf6b03d Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Wed, 9 Apr 2025 22:30:06 +0200 Subject: [PATCH 54/82] Manage gotext as a tool with go.mod --- go.mod | 7 ++++++- internal/translations/translations.go | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 51a40fa..ee4e6f0 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module go.uploadedlobster.com/scotty go 1.23.0 -toolchain go1.23.8 +toolchain go1.24.2 require ( github.com/Xuanwo/go-locale v1.1.3 @@ -57,8 +57,11 @@ require ( github.com/spf13/pflag v1.0.6 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect + golang.org/x/mod v0.24.0 // indirect golang.org/x/net v0.39.0 // indirect + golang.org/x/sync v0.13.0 // indirect golang.org/x/sys v0.32.0 // indirect + golang.org/x/tools v0.32.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gorm.io/driver/mysql v1.5.7 // indirect modernc.org/libc v1.62.1 // indirect @@ -66,3 +69,5 @@ require ( modernc.org/memory v1.9.1 // indirect modernc.org/sqlite v1.37.0 // indirect ) + +tool golang.org/x/text/cmd/gotext diff --git a/internal/translations/translations.go b/internal/translations/translations.go index c555d32..9961c41 100644 --- a/internal/translations/translations.go +++ b/internal/translations/translations.go @@ -6,4 +6,4 @@ package are published under the conditions of CC0 1.0 Universal (CC0 1.0) package translations -//go:generate gotext -srclang=en update -out=catalog.go -lang=en,de go.uploadedlobster.com/scotty +//go:generate go tool gotext -srclang=en update -out=catalog.go -lang=en,de go.uploadedlobster.com/scotty From 1ea90d2d2b0af02f66ea22902f775999fe1cf25d Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Wed, 9 Apr 2025 22:31:34 +0200 Subject: [PATCH 55/82] Update translation files --- internal/translations/catalog.go | 154 +++++++++--------- .../translations/locales/de/out.gotext.json | 26 +-- .../translations/locales/en/out.gotext.json | 12 +- 3 files changed, 98 insertions(+), 94 deletions(-) diff --git a/internal/translations/catalog.go b/internal/translations/catalog.go index 614b5b6..987612a 100644 --- a/internal/translations/catalog.go +++ b/internal/translations/catalog.go @@ -42,56 +42,56 @@ var messageKeyToIndex = map[string]int{ "\tbackend: %v": 11, "\texport: %s": 0, "\timport: %s\n": 1, - "%v: %v": 52, + "%v: %v": 47, "Aborted": 8, "Access token": 19, - "Access token received, you can use %v now.\n": 28, + "Access token received, you can use %v now.\n": 33, "Append to file": 21, - "Backend": 36, - "Check for duplicate listens on import (slower)": 45, + "Backend": 41, + "Check for duplicate listens on import (slower)": 24, "Client ID": 15, "Client secret": 16, "Delete the service configuration \"%v\"?": 7, - "Directory path": 47, - "Disable auto correction of submitted listens": 24, - "Error: OAuth state mismatch": 27, + "Directory path": 27, + "Disable auto correction of submitted listens": 25, + "Error: OAuth state mismatch": 32, "Failed reading config: %v": 2, "File path": 20, - "From timestamp: %v (%v)": 38, - "Ignore listens in incognito mode": 48, - "Ignore skipped listens": 49, - "Ignored duplicate listen %v: \"%v\" by %v (%v)": 46, - "Import failed, last reported timestamp was %v (%s)": 39, - "Import log:": 51, - "Imported %v of %v %s into %v.": 40, - "Include skipped listens": 25, - "Latest timestamp: %v (%v)": 41, - "Minimum playback duration for skipped tracks (seconds)": 50, - "No": 33, + "From timestamp: %v (%v)": 43, + "Ignore listens in incognito mode": 28, + "Ignore skipped listens": 29, + "Ignored duplicate listen %v: \"%v\" by %v (%v)": 53, + "Import failed, last reported timestamp was %v (%s)": 44, + "Import log:": 46, + "Imported %v of %v %s into %v.": 45, + "Include skipped listens": 26, + "Latest timestamp: %v (%v)": 49, + "Minimum playback duration for skipped tracks (seconds)": 30, + "No": 38, "Playlist title": 22, "Saved service %v using backend %v": 5, "Server URL": 17, - "Service": 35, + "Service": 40, "Service \"%v\" deleted\n": 9, "Service name": 3, "The backend %v requires authentication. Authenticate now?": 6, "Token received, you can close this window now.": 12, - "Transferring %s from %s to %s...": 37, + "Transferring %s from %s to %s...": 42, "Unique playlist identifier": 23, "Updated service %v using backend %v\n": 10, "User name": 18, - "Visit the URL for authorization: %v": 26, - "Yes": 32, + "Visit the URL for authorization: %v": 31, + "Yes": 37, "a service with this name already exists": 4, "backend %s does not implement %s": 13, - "done": 31, - "exporting": 29, - "importing": 30, - "invalid timestamp string \"%v\"": 53, - "key must only consist of A-Za-z0-9_-": 43, - "no configuration file defined, cannot write config": 42, - "no existing service configurations": 34, - "no service configuration \"%v\"": 44, + "done": 36, + "exporting": 34, + "importing": 35, + "invalid timestamp string \"%v\"": 48, + "key must only consist of A-Za-z0-9_-": 51, + "no configuration file defined, cannot write config": 50, + "no existing service configurations": 39, + "no service configuration \"%v\"": 52, "unknown backend \"%s\"": 14, } @@ -103,18 +103,18 @@ var deIndex = []uint32{ // 55 elements 0x000001ac, 0x000001e7, 0x00000213, 0x00000233, 0x0000023d, 0x0000024b, 0x00000256, 0x00000263, 0x00000271, 0x0000027b, 0x0000028e, 0x000002a1, - 0x000002b8, 0x000002ec, 0x0000030d, 0x00000333, - 0x0000035d, 0x0000039d, 0x000003a8, 0x000003b3, + 0x000002b8, 0x000002ed, 0x00000321, 0x00000342, + 0x00000352, 0x00000378, 0x0000039a, 0x000003d8, // Entry 20 - 3F - 0x000003ba, 0x000003bd, 0x000003c2, 0x000003eb, - 0x000003f3, 0x000003fb, 0x00000424, 0x00000442, - 0x0000047f, 0x000004aa, 0x000004cd, 0x0000051e, - 0x00000555, 0x0000057c, 0x0000057c, 0x0000057c, - 0x0000057c, 0x0000057c, 0x0000057c, 0x0000057c, - 0x0000057c, 0x0000057c, 0x0000057c, + 0x000003fe, 0x00000428, 0x00000468, 0x00000473, + 0x0000047e, 0x00000485, 0x00000488, 0x0000048d, + 0x000004b6, 0x000004be, 0x000004c6, 0x000004ef, + 0x0000050d, 0x0000054a, 0x00000575, 0x00000580, + 0x0000058d, 0x000005b1, 0x000005d4, 0x00000625, + 0x0000065c, 0x00000683, 0x00000683, } // Size: 244 bytes -const deData string = "" + // Size: 1404 bytes +const deData string = "" + // Size: 1667 bytes "\x04\x01\x09\x00\x0e\x02Export: %[1]s\x04\x01\x09\x01\x0a\x0e\x02Import:" + " %[1]s\x02Fehler beim Lesen der Konfiguration: %[1]v\x02Servicename\x02e" + "in Service mit diesem Namen existiert bereits\x02Service %[1]v mit dem B" + @@ -126,18 +126,22 @@ const deData string = "" + // Size: 1404 bytes " geschlossen werden.\x02das backend %[1]s implementiert %[2]s nicht\x02u" + "nbekanntes Backend „%[1]s“\x02Client-ID\x02Client-Secret\x02Server-URL" + "\x02Benutzername\x02Zugriffstoken\x02Dateipfad\x02An Datei anhängen\x02T" + - "itel der Playlist\x02Eindeutige Playlist-ID\x02Autokorrektur für übermit" + - "telte Titel deaktivieren\x02Übersprungene Titel einbeziehen\x02URL für A" + - "utorisierung öffnen: %[1]v\x02Fehler: OAuth-State stimmt nicht überein" + - "\x04\x00\x01\x0a;\x02Zugriffstoken erhalten, %[1]v kann jetzt verwendet " + - "werden.\x02exportiere\x02importiere\x02fertig\x02Ja\x02Nein\x02keine bes" + - "tehenden Servicekonfigurationen\x02Service\x02Backend\x02Übertrage %[1]s" + - " von %[2]s nach %[3]s...\x02Ab Zeitstempel: %[1]v (%[2]v)\x02Import fehl" + - "geschlagen, letzter Zeitstempel war %[1]v (%[2]s)\x02%[1]v von %[2]v %[3" + - "]s in %[4]v importiert.\x02Letzter Zeitstempel: %[1]v (%[2]v)\x02keine K" + - "onfigurationsdatei definiert, Konfiguration kann nicht geschrieben werde" + - "n\x02Schlüssel darf nur die Zeichen A-Za-z0-9_- beinhalten\x02keine Serv" + - "icekonfiguration „%[1]v“" + "itel der Playlist\x02Eindeutige Playlist-ID\x02Beim Import auf Listen-Du" + + "plikate prüfen (langsamer)\x02Autokorrektur für übermittelte Titel deakt" + + "ivieren\x02Übersprungene Titel einbeziehen\x02Verzeichnispfad\x02Listens" + + " im Inkognito-Modus ignorieren\x02Übersprungene Listens ignorieren\x02Mi" + + "nimale Wiedergabedauer für übersprungene Titel (Sekunden)\x02URL für Aut" + + "orisierung öffnen: %[1]v\x02Fehler: OAuth-State stimmt nicht überein\x04" + + "\x00\x01\x0a;\x02Zugriffstoken erhalten, %[1]v kann jetzt verwendet werd" + + "en.\x02exportiere\x02importiere\x02fertig\x02Ja\x02Nein\x02keine bestehe" + + "nden Servicekonfigurationen\x02Service\x02Backend\x02Übertrage %[1]s von" + + " %[2]s nach %[3]s...\x02Ab Zeitstempel: %[1]v (%[2]v)\x02Import fehlgesc" + + "hlagen, letzter Zeitstempel war %[1]v (%[2]s)\x02%[1]v von %[2]v %[3]s i" + + "n %[4]v importiert.\x02Importlog:\x02%[1]v: %[2]v\x02ungültiger Zeitstem" + + "pel „%[1]v“\x02Letzter Zeitstempel: %[1]v (%[2]v)\x02keine Konfiguration" + + "sdatei definiert, Konfiguration kann nicht geschrieben werden\x02Schlüss" + + "el darf nur die Zeichen A-Za-z0-9_- beinhalten\x02keine Servicekonfigura" + + "tion „%[1]v“" var enIndex = []uint32{ // 55 elements // Entry 0 - 1F @@ -147,15 +151,15 @@ var enIndex = []uint32{ // 55 elements 0x00000170, 0x0000019f, 0x000001c6, 0x000001de, 0x000001e8, 0x000001f6, 0x00000201, 0x0000020b, 0x00000218, 0x00000222, 0x00000231, 0x00000240, - 0x0000025b, 0x00000288, 0x000002a0, 0x000002c7, - 0x000002e3, 0x00000316, 0x00000320, 0x0000032a, + 0x0000025b, 0x0000028a, 0x000002b7, 0x000002cf, + 0x000002de, 0x000002ff, 0x00000316, 0x0000034d, // Entry 20 - 3F - 0x0000032f, 0x00000333, 0x00000336, 0x00000359, - 0x00000361, 0x00000369, 0x00000393, 0x000003b1, - 0x000003ea, 0x00000414, 0x00000434, 0x00000467, - 0x0000048c, 0x000004ad, 0x000004dc, 0x00000515, - 0x00000524, 0x00000545, 0x0000055c, 0x00000593, - 0x0000059f, 0x000005ac, 0x000005cd, + 0x00000374, 0x00000390, 0x000003c3, 0x000003cd, + 0x000003d7, 0x000003dc, 0x000003e0, 0x000003e3, + 0x00000406, 0x0000040e, 0x00000416, 0x00000440, + 0x0000045e, 0x00000497, 0x000004c1, 0x000004cd, + 0x000004da, 0x000004fb, 0x0000051b, 0x0000054e, + 0x00000573, 0x00000594, 0x000005cd, } // Size: 244 bytes const enData string = "" + // Size: 1485 bytes @@ -169,20 +173,20 @@ const enData string = "" + // Size: 1485 bytes "eceived, you can close this window now.\x02backend %[1]s does not implem" + "ent %[2]s\x02unknown backend \x22%[1]s\x22\x02Client ID\x02Client secret" + "\x02Server URL\x02User name\x02Access token\x02File path\x02Append to fi" + - "le\x02Playlist title\x02Unique playlist identifier\x02Disable auto corre" + - "ction of submitted listens\x02Include skipped listens\x02Visit the URL f" + - "or authorization: %[1]v\x02Error: OAuth state mismatch\x04\x00\x01\x0a." + - "\x02Access token received, you can use %[1]v now.\x02exporting\x02import" + - "ing\x02done\x02Yes\x02No\x02no existing service configurations\x02Servic" + - "e\x02Backend\x02Transferring %[1]s from %[2]s to %[3]s...\x02From timest" + - "amp: %[1]v (%[2]v)\x02Import failed, last reported timestamp was %[1]v (" + - "%[2]s)\x02Imported %[1]v of %[2]v %[3]s into %[4]v.\x02Latest timestamp:" + - " %[1]v (%[2]v)\x02no configuration file defined, cannot write config\x02" + - "key must only consist of A-Za-z0-9_-\x02no service configuration \x22%[1" + - "]v\x22\x02Check for duplicate listens on import (slower)\x02Ignored dupl" + - "icate listen %[1]v: \x22%[2]v\x22 by %[3]v (%[4]v)\x02Directory path\x02" + - "Ignore listens in incognito mode\x02Ignore skipped listens\x02Minimum pl" + - "ayback duration for skipped tracks (seconds)\x02Import log:\x02%[1]v: %[" + - "2]v\x02invalid timestamp string \x22%[1]v\x22" + "le\x02Playlist title\x02Unique playlist identifier\x02Check for duplicat" + + "e listens on import (slower)\x02Disable auto correction of submitted lis" + + "tens\x02Include skipped listens\x02Directory path\x02Ignore listens in i" + + "ncognito mode\x02Ignore skipped listens\x02Minimum playback duration for" + + " skipped tracks (seconds)\x02Visit the URL for authorization: %[1]v\x02E" + + "rror: OAuth state mismatch\x04\x00\x01\x0a.\x02Access token received, yo" + + "u can use %[1]v now.\x02exporting\x02importing\x02done\x02Yes\x02No\x02n" + + "o existing service configurations\x02Service\x02Backend\x02Transferring " + + "%[1]s from %[2]s to %[3]s...\x02From timestamp: %[1]v (%[2]v)\x02Import " + + "failed, last reported timestamp was %[1]v (%[2]s)\x02Imported %[1]v of %" + + "[2]v %[3]s into %[4]v.\x02Import log:\x02%[1]v: %[2]v\x02invalid timesta" + + "mp string \x22%[1]v\x22\x02Latest timestamp: %[1]v (%[2]v)\x02no configu" + + "ration file defined, cannot write config\x02key must only consist of A-Z" + + "a-z0-9_-\x02no service configuration \x22%[1]v\x22\x02Ignored duplicate " + + "listen %[1]v: \x22%[2]v\x22 by %[3]v (%[4]v)" - // Total table size 3377 bytes (3KiB); checksum: 6715024 + // Total table size 3640 bytes (3KiB); checksum: 719A868A diff --git a/internal/translations/locales/de/out.gotext.json b/internal/translations/locales/de/out.gotext.json index 3d84fe8..18333c6 100644 --- a/internal/translations/locales/de/out.gotext.json +++ b/internal/translations/locales/de/out.gotext.json @@ -258,11 +258,11 @@ { "id": "Check for duplicate listens on import (slower)", "message": "Check for duplicate listens on import (slower)", - "translation": "" + "translation": "Beim Import auf Listen-Duplikate prüfen (langsamer)" }, { - "id": "Ignored duplicate listen {ListenedAt}: \"{TrackName}\" by {ArtistName} ({RecordingMbid})", - "message": "Ignored duplicate listen {ListenedAt}: \"{TrackName}\" by {ArtistName} ({RecordingMbid})", + "id": "Ignored duplicate listen {ListenedAt}: \"{TrackName}\" by {ArtistName} ({RecordingMBID})", + "message": "Ignored duplicate listen {ListenedAt}: \"{TrackName}\" by {ArtistName} ({RecordingMBID})", "translation": "", "placeholders": [ { @@ -290,12 +290,12 @@ "expr": "l.ArtistName()" }, { - "id": "RecordingMbid", + "id": "RecordingMBID", "string": "%[4]v", - "type": "go.uploadedlobster.com/scotty/internal/models.MBID", + "type": "go.uploadedlobster.com/mbtypes.MBID", "underlyingType": "string", "argNum": 4, - "expr": "l.RecordingMbid" + "expr": "l.RecordingMBID" } ] }, @@ -312,22 +312,22 @@ { "id": "Directory path", "message": "Directory path", - "translation": "" + "translation": "Verzeichnispfad" }, { "id": "Ignore listens in incognito mode", "message": "Ignore listens in incognito mode", - "translation": "" + "translation": "Listens im Inkognito-Modus ignorieren" }, { "id": "Ignore skipped listens", "message": "Ignore skipped listens", - "translation": "" + "translation": "Übersprungene Listens ignorieren" }, { "id": "Minimum playback duration for skipped tracks (seconds)", "message": "Minimum playback duration for skipped tracks (seconds)", - "translation": "" + "translation": "Minimale Wiedergabedauer für übersprungene Titel (Sekunden)" }, { "id": "Visit the URL for authorization: {Url}", @@ -525,12 +525,12 @@ { "id": "Import log:", "message": "Import log:", - "translation": "" + "translation": "Importlog:" }, { "id": "{Type}: {Message}", "message": "{Type}: {Message}", - "translation": "", + "translation": "{Type}: {Message}", "placeholders": [ { "id": "Type", @@ -553,7 +553,7 @@ { "id": "invalid timestamp string \"{FlagValue}\"", "message": "invalid timestamp string \"{FlagValue}\"", - "translation": "", + "translation": "ungültiger Zeitstempel „{FlagValue}“", "placeholders": [ { "id": "FlagValue", diff --git a/internal/translations/locales/en/out.gotext.json b/internal/translations/locales/en/out.gotext.json index 7687276..54c6719 100644 --- a/internal/translations/locales/en/out.gotext.json +++ b/internal/translations/locales/en/out.gotext.json @@ -311,9 +311,9 @@ "fuzzy": true }, { - "id": "Ignored duplicate listen {ListenedAt}: \"{TrackName}\" by {ArtistName} ({RecordingMbid})", - "message": "Ignored duplicate listen {ListenedAt}: \"{TrackName}\" by {ArtistName} ({RecordingMbid})", - "translation": "Ignored duplicate listen {ListenedAt}: \"{TrackName}\" by {ArtistName} ({RecordingMbid})", + "id": "Ignored duplicate listen {ListenedAt}: \"{TrackName}\" by {ArtistName} ({RecordingMBID})", + "message": "Ignored duplicate listen {ListenedAt}: \"{TrackName}\" by {ArtistName} ({RecordingMBID})", + "translation": "Ignored duplicate listen {ListenedAt}: \"{TrackName}\" by {ArtistName} ({RecordingMBID})", "translatorComment": "Copied from source.", "placeholders": [ { @@ -341,12 +341,12 @@ "expr": "l.ArtistName()" }, { - "id": "RecordingMbid", + "id": "RecordingMBID", "string": "%[4]v", - "type": "go.uploadedlobster.com/scotty/internal/models.MBID", + "type": "go.uploadedlobster.com/mbtypes.MBID", "underlyingType": "string", "argNum": 4, - "expr": "l.RecordingMbid" + "expr": "l.RecordingMBID" } ], "fuzzy": true From 01e7569051f867b7fcf3fc3c19deb4992edb9f71 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 27 Apr 2025 13:26:47 +0200 Subject: [PATCH 56/82] Fixed progress for subsonic loves export --- internal/backends/subsonic/subsonic.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/internal/backends/subsonic/subsonic.go b/internal/backends/subsonic/subsonic.go index 6a59630..ed33214 100644 --- a/internal/backends/subsonic/subsonic.go +++ b/internal/backends/subsonic/subsonic.go @@ -78,8 +78,11 @@ func (b *SubsonicApiBackend) ExportLoves(oldestTimestamp time.Time, results chan return } - progress <- models.Progress{Elapsed: int64(len(starred.Song))}.Complete() - results <- models.LovesResult{Items: b.filterSongs(starred.Song, oldestTimestamp)} + loves := b.filterSongs(starred.Song, oldestTimestamp) + progress <- models.Progress{ + Total: int64(loves.Len()), + }.Complete() + results <- models.LovesResult{Items: loves} } func (b *SubsonicApiBackend) filterSongs(songs []*subsonic.Child, oldestTimestamp time.Time) models.LovesList { From da6c920789639c95f9111f70d6809894b2d4f2f0 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 27 Apr 2025 16:55:14 +0200 Subject: [PATCH 57/82] ListenBrainz: Fix loves import loading all existing loves Fixes import if the user had more than 1000 loves already --- .../backends/listenbrainz/listenbrainz.go | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/internal/backends/listenbrainz/listenbrainz.go b/internal/backends/listenbrainz/listenbrainz.go index 97f721d..e9c407b 100644 --- a/internal/backends/listenbrainz/listenbrainz.go +++ b/internal/backends/listenbrainz/listenbrainz.go @@ -233,15 +233,18 @@ func (b *ListenBrainzApiBackend) ImportLoves(export models.LovesResult, importRe if len(b.existingMBIDs) == 0 { existingLovesChan := make(chan models.LovesResult) go b.ExportLoves(time.Unix(0, 0), existingLovesChan, progress) - existingLoves := <-existingLovesChan - if existingLoves.Error != nil { - return importResult, existingLoves.Error - } // TODO: Store MBIDs directly - b.existingMBIDs = make(map[mbtypes.MBID]bool, len(existingLoves.Items)) - for _, love := range existingLoves.Items { - b.existingMBIDs[love.RecordingMBID] = true + b.existingMBIDs = make(map[mbtypes.MBID]bool, MaxItemsPerGet) + + for existingLoves := range existingLovesChan { + if existingLoves.Error != nil { + return importResult, existingLoves.Error + } + + for _, love := range existingLoves.Items { + b.existingMBIDs[love.RecordingMBID] = true + } } } @@ -268,6 +271,8 @@ func (b *ListenBrainzApiBackend) ImportLoves(export models.LovesResult, importRe ok = err == nil && resp.Status == "ok" if err != nil { errMsg = err.Error() + } else { + b.existingMBIDs[recordingMBID] = true } } From 2d66d41873fd4852718d545b876d5a09379abb7a Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 27 Apr 2025 16:57:30 +0200 Subject: [PATCH 58/82] ListenBrainz: Fix love import progress Exporting existing loves must not mark the progress as completed. --- internal/backends/listenbrainz/listenbrainz.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/internal/backends/listenbrainz/listenbrainz.go b/internal/backends/listenbrainz/listenbrainz.go index e9c407b..e3fe7cc 100644 --- a/internal/backends/listenbrainz/listenbrainz.go +++ b/internal/backends/listenbrainz/listenbrainz.go @@ -188,6 +188,10 @@ func (b *ListenBrainzApiBackend) ImportListens(export models.ListensResult, impo } func (b *ListenBrainzApiBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.Progress) { + b.exportLoves(oldestTimestamp, results, progress, false) +} + +func (b *ListenBrainzApiBackend) exportLoves(oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.Progress, completeProgress bool) { offset := 0 defer close(results) loves := make(models.LovesList, 0, 2*MaxItemsPerGet) @@ -225,14 +229,16 @@ out: } sort.Sort(loves) - progress <- p.Complete() + if completeProgress { + progress <- p.Complete() + } results <- models.LovesResult{Items: loves} } func (b *ListenBrainzApiBackend) ImportLoves(export models.LovesResult, importResult models.ImportResult, progress chan models.Progress) (models.ImportResult, error) { if len(b.existingMBIDs) == 0 { existingLovesChan := make(chan models.LovesResult) - go b.ExportLoves(time.Unix(0, 0), existingLovesChan, progress) + go b.exportLoves(time.Unix(0, 0), existingLovesChan, progress, false) // TODO: Store MBIDs directly b.existingMBIDs = make(map[mbtypes.MBID]bool, MaxItemsPerGet) From bed60c7cdf1b059788d41177541192f1502aa56c Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 27 Apr 2025 17:22:29 +0200 Subject: [PATCH 59/82] Update dependencies --- go.mod | 6 +++--- go.sum | 24 ++++++++++++------------ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index ee4e6f0..cab478a 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,7 @@ require ( golang.org/x/oauth2 v0.29.0 golang.org/x/text v0.24.0 gorm.io/datatypes v1.2.5 - gorm.io/gorm v1.25.12 + gorm.io/gorm v1.26.0 ) require ( @@ -64,9 +64,9 @@ require ( golang.org/x/tools v0.32.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gorm.io/driver/mysql v1.5.7 // indirect - modernc.org/libc v1.62.1 // indirect + modernc.org/libc v1.64.0 // indirect modernc.org/mathutil v1.7.1 // indirect - modernc.org/memory v1.9.1 // indirect + modernc.org/memory v1.10.0 // indirect modernc.org/sqlite v1.37.0 // indirect ) diff --git a/go.sum b/go.sum index 511ace4..40290fc 100644 --- a/go.sum +++ b/go.sum @@ -170,22 +170,22 @@ gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2e gorm.io/driver/sqlserver v1.5.4 h1:xA+Y1KDNspv79q43bPyjDMUgHoYHLhXYmdFcYPobg8g= gorm.io/driver/sqlserver v1.5.4/go.mod h1:+frZ/qYmuna11zHPlh5oc2O6ZA/lS88Keb0XSH1Zh/g= gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= -gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= -gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= -modernc.org/cc/v4 v4.25.2 h1:T2oH7sZdGvTaie0BRNFbIYsabzCxUQg8nLqCdQ2i0ic= -modernc.org/cc/v4 v4.25.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= -modernc.org/ccgo/v4 v4.25.1 h1:TFSzPrAGmDsdnhT9X2UrcPMI3N/mJ9/X9ykKXwLhDsU= -modernc.org/ccgo/v4 v4.25.1/go.mod h1:njjuAYiPflywOOrm3B7kCB444ONP5pAVr8PIEoE0uDw= -modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= -modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +gorm.io/gorm v1.26.0 h1:9lqQVPG5aNNS6AyHdRiwScAVnXHg/L/Srzx55G5fOgs= +gorm.io/gorm v1.26.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= +modernc.org/cc/v4 v4.26.0 h1:QMYvbVduUGH0rrO+5mqF/PSPPRZNpRtg2CLELy7vUpA= +modernc.org/cc/v4 v4.26.0/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.26.0 h1:gVzXaDzGeBYJ2uXTOpR8FR7OlksDOe9jxnjhIKCsiTc= +modernc.org/ccgo/v4 v4.26.0/go.mod h1:Sem8f7TFUtVXkG2fiaChQtyyfkqhJBg/zjEJBkmuAVY= +modernc.org/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8= +modernc.org/fileutil v1.3.1/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= -modernc.org/libc v1.62.1 h1:s0+fv5E3FymN8eJVmnk0llBe6rOxCu/DEU+XygRbS8s= -modernc.org/libc v1.62.1/go.mod h1:iXhATfJQLjG3NWy56a6WVU73lWOcdYVxsvwCgoPljuo= +modernc.org/libc v1.64.0 h1:U0k8BD2d3cD3e9I8RLcZgJBHAcsJzbXx5mKGSb5pyJA= +modernc.org/libc v1.64.0/go.mod h1:7m9VzGq7APssBTydds2zBcxGREwvIGpuUBaKTXdm2Qs= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= -modernc.org/memory v1.9.1 h1:V/Z1solwAVmMW1yttq3nDdZPJqV1rM05Ccq6KMSZ34g= -modernc.org/memory v1.9.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/memory v1.10.0 h1:fzumd51yQ1DxcOxSO+S6X7+QTuVU+n8/Aj7swYjFfC4= +modernc.org/memory v1.10.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= From 910056b0a67f07a422078ebb860958d45bdf16db Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Thu, 23 Nov 2023 09:57:35 +0100 Subject: [PATCH 60/82] Subsonic: Support for some OpenSubsonic tags Mainly this makes the MusicBrainz recording ID available --- go.mod | 2 ++ go.sum | 36 ++++++++++++++++++++++++++ internal/backends/subsonic/subsonic.go | 29 ++++++++++++++------- 3 files changed, 58 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index cab478a..79382fd 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/spf13/cobra v1.9.1 github.com/spf13/viper v1.20.1 github.com/stretchr/testify v1.10.0 + github.com/supersonic-app/go-subsonic v0.0.0-20241224013245-9b2841f3711d github.com/vbauerster/mpb/v8 v8.9.3 go.uploadedlobster.com/mbtypes v0.4.0 golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 @@ -57,6 +58,7 @@ require ( github.com/spf13/pflag v1.0.6 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect + golang.org/x/image v0.26.0 // indirect golang.org/x/mod v0.24.0 // indirect golang.org/x/net v0.39.0 // indirect golang.org/x/sync v0.13.0 // indirect diff --git a/go.sum b/go.sum index 40290fc..715e6c7 100644 --- a/go.sum +++ b/go.sum @@ -125,35 +125,71 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/supersonic-app/go-subsonic v0.0.0-20241224013245-9b2841f3711d h1:70+Nn7yh+cfeKqqXVTdpneFqXuvrBLyP7U6GVUsjTU4= +github.com/supersonic-app/go-subsonic v0.0.0-20241224013245-9b2841f3711d/go.mod h1:D+OWPXeD9owcdcoXATv5YPBGWxxVvn5k98rt5B4wMc4= github.com/vbauerster/mpb/v8 v8.9.3 h1:PnMeF+sMvYv9u23l6DO6Q3+Mdj408mjLRXIzmUmU2Z8= github.com/vbauerster/mpb/v8 v8.9.3/go.mod h1:hxS8Hz4C6ijnppDSIX6LjG8FYJSoPo9iIOcE53Zik0c= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 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= +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.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= +golang.org/x/image v0.13.0/go.mod h1:6mmbMOeV28HuMTgA6OSRkdXKYw/t5W9Uwn2Yv1r3Yxk= +golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY= +golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/backends/subsonic/subsonic.go b/internal/backends/subsonic/subsonic.go index ed33214..59d4719 100644 --- a/internal/backends/subsonic/subsonic.go +++ b/internal/backends/subsonic/subsonic.go @@ -21,7 +21,8 @@ import ( "sort" "time" - "github.com/delucks/go-subsonic" + "github.com/supersonic-app/go-subsonic/subsonic" + "go.uploadedlobster.com/mbtypes" "go.uploadedlobster.com/scotty/internal/config" "go.uploadedlobster.com/scotty/internal/i18n" "go.uploadedlobster.com/scotty/internal/models" @@ -99,15 +100,19 @@ func (b *SubsonicApiBackend) filterSongs(songs []*subsonic.Child, oldestTimestam } func SongAsLove(song subsonic.Child, username string) models.Love { + recordingMBID := mbtypes.MBID(song.MusicBrainzID) love := models.Love{ - UserName: username, - Created: song.Starred, + UserName: username, + Created: song.Starred, + RecordingMBID: recordingMBID, Track: models.Track{ - TrackName: song.Title, - ReleaseName: song.Album, - ArtistNames: []string{song.Artist}, - TrackNumber: song.Track, - DiscNumber: song.DiscNumber, + TrackName: song.Title, + ReleaseName: song.Album, + ArtistNames: []string{song.Artist}, + TrackNumber: song.Track, + DiscNumber: song.DiscNumber, + RecordingMBID: recordingMBID, + Tags: []string{}, AdditionalInfo: map[string]any{ "subsonic_id": song.ID, }, @@ -115,7 +120,13 @@ func SongAsLove(song subsonic.Child, username string) models.Love { }, } - if song.Genre != "" { + if len(song.Genres) > 0 { + genres := make([]string, 0, len(song.Genres)) + for _, genre := range song.Genres { + genres = append(genres, genre.Name) + } + love.Track.Tags = genres + } else if song.Genre != "" { love.Track.Tags = []string{song.Genre} } From 90bf51a00bee93bc988770c1a592d313b21235ca Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 27 Apr 2025 17:54:29 +0200 Subject: [PATCH 61/82] ListenBrainz: Log missing recording MBID on love import --- internal/backends/listenbrainz/listenbrainz.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/internal/backends/listenbrainz/listenbrainz.go b/internal/backends/listenbrainz/listenbrainz.go index e3fe7cc..bd09f85 100644 --- a/internal/backends/listenbrainz/listenbrainz.go +++ b/internal/backends/listenbrainz/listenbrainz.go @@ -256,6 +256,9 @@ func (b *ListenBrainzApiBackend) ImportLoves(export models.LovesResult, importRe for _, love := range export.Items { recordingMBID := love.RecordingMBID + if recordingMBID == "" { + recordingMBID = love.Track.RecordingMBID + } if recordingMBID == "" { lookup, err := b.client.Lookup(love.TrackName, love.ArtistName()) @@ -290,6 +293,10 @@ func (b *ListenBrainzApiBackend) ImportLoves(export models.LovesResult, importRe love.TrackName, love.ArtistName(), errMsg) importResult.Log(models.Error, msg) } + } else { + msg := fmt.Sprintf("Failed import of \"%s\" by %s: no recording MBID", + love.TrackName, love.ArtistName()) + importResult.Log(models.Error, msg) } progress <- models.Progress{}.FromImportResult(importResult) From 7c0774fb8d219a11f9053d9a9fd05b90fe31c7b2 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 27 Apr 2025 18:11:58 +0200 Subject: [PATCH 62/82] ListenBrainz: Fixed loves export --- .../backends/listenbrainz/listenbrainz.go | 31 ++++++++++++++----- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/internal/backends/listenbrainz/listenbrainz.go b/internal/backends/listenbrainz/listenbrainz.go index bd09f85..c05943f 100644 --- a/internal/backends/listenbrainz/listenbrainz.go +++ b/internal/backends/listenbrainz/listenbrainz.go @@ -188,10 +188,27 @@ func (b *ListenBrainzApiBackend) ImportListens(export models.ListensResult, impo } func (b *ListenBrainzApiBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.Progress) { - b.exportLoves(oldestTimestamp, results, progress, false) + defer close(results) + exportChan := make(chan models.LovesResult) + p := models.Progress{} + + go b.exportLoves(time.Unix(0, 0), exportChan) + for existingLoves := range exportChan { + if existingLoves.Error != nil { + progress <- p.Complete() + results <- models.LovesResult{Error: existingLoves.Error} + } + + p.Total = int64(existingLoves.Total) + p.Elapsed += int64(existingLoves.Items.Len()) + progress <- p + results <- existingLoves + } + + progress <- p.Complete() } -func (b *ListenBrainzApiBackend) exportLoves(oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.Progress, completeProgress bool) { +func (b *ListenBrainzApiBackend) exportLoves(oldestTimestamp time.Time, results chan models.LovesResult) { offset := 0 defer close(results) loves := make(models.LovesList, 0, 2*MaxItemsPerGet) @@ -201,7 +218,6 @@ out: for { result, err := b.client.GetFeedback(b.username, 1, offset) if err != nil { - progress <- p.Complete() results <- models.LovesResult{Error: err} return } @@ -216,7 +232,6 @@ out: if love.Created.Unix() > oldestTimestamp.Unix() { loves = append(loves, love) p.Elapsed += 1 - progress <- p } else { break out } @@ -229,16 +244,16 @@ out: } sort.Sort(loves) - if completeProgress { - progress <- p.Complete() + results <- models.LovesResult{ + Total: len(loves), + Items: loves, } - results <- models.LovesResult{Items: loves} } func (b *ListenBrainzApiBackend) ImportLoves(export models.LovesResult, importResult models.ImportResult, progress chan models.Progress) (models.ImportResult, error) { if len(b.existingMBIDs) == 0 { existingLovesChan := make(chan models.LovesResult) - go b.exportLoves(time.Unix(0, 0), existingLovesChan, progress, false) + go b.exportLoves(time.Unix(0, 0), existingLovesChan) // TODO: Store MBIDs directly b.existingMBIDs = make(map[mbtypes.MBID]bool, MaxItemsPerGet) From 20c9ada6ec9fa2562884a5bbaf2b4bb8f66b0544 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 27 Apr 2025 18:14:48 +0200 Subject: [PATCH 63/82] RecordingMsid -> RecordingMSID --- internal/backends/listenbrainz/models.go | 4 ++-- internal/models/models.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/backends/listenbrainz/models.go b/internal/backends/listenbrainz/models.go index 4102ee5..ada75d3 100644 --- a/internal/backends/listenbrainz/models.go +++ b/internal/backends/listenbrainz/models.go @@ -57,7 +57,7 @@ type ListenSubmission struct { type Listen struct { InsertedAt int64 `json:"inserted_at,omitempty"` ListenedAt int64 `json:"listened_at"` - RecordingMsid string `json:"recording_msid,omitempty"` + RecordingMSID string `json:"recording_msid,omitempty"` UserName string `json:"user_name,omitempty"` TrackMetadata Track `json:"track_metadata"` } @@ -94,7 +94,7 @@ type GetFeedbackResult struct { type Feedback struct { Created int64 `json:"created,omitempty"` RecordingMBID mbtypes.MBID `json:"recording_mbid,omitempty"` - RecordingMsid mbtypes.MBID `json:"recording_msid,omitempty"` + RecordingMSID mbtypes.MBID `json:"recording_msid,omitempty"` Score int `json:"score,omitempty"` TrackMetadata *Track `json:"track_metadata,omitempty"` UserName string `json:"user_id,omitempty"` diff --git a/internal/models/models.go b/internal/models/models.go index 0d5abf2..18e3b44 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -112,7 +112,7 @@ type Love struct { Created time.Time UserName string RecordingMBID mbtypes.MBID - RecordingMsid mbtypes.MBID + RecordingMSID mbtypes.MBID } type ListensList []Listen From db78bfe457afd7ca737dddf60c383d401d2548e0 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 27 Apr 2025 18:16:27 +0200 Subject: [PATCH 64/82] Fixed subsonic test --- internal/backends/subsonic/subsonic_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/backends/subsonic/subsonic_test.go b/internal/backends/subsonic/subsonic_test.go index c5bfe36..f6508c5 100644 --- a/internal/backends/subsonic/subsonic_test.go +++ b/internal/backends/subsonic/subsonic_test.go @@ -20,9 +20,9 @@ import ( "testing" "time" - go_subsonic "github.com/delucks/go-subsonic" "github.com/spf13/viper" "github.com/stretchr/testify/assert" + go_subsonic "github.com/supersonic-app/go-subsonic/subsonic" "go.uploadedlobster.com/scotty/internal/backends/subsonic" "go.uploadedlobster.com/scotty/internal/config" ) From 9e1c2d84359a5f584b4cb2888a915f5a3124fe74 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 27 Apr 2025 18:17:19 +0200 Subject: [PATCH 65/82] Remove github.com/delucks/go-subsonic from go.mod --- go.mod | 1 - go.sum | 2 -- 2 files changed, 3 deletions(-) diff --git a/go.mod b/go.mod index 79382fd..9871720 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,6 @@ require ( github.com/Xuanwo/go-locale v1.1.3 github.com/agnivade/levenshtein v1.2.1 github.com/cli/browser v1.3.0 - github.com/delucks/go-subsonic v0.0.0-20240806025900-2a743ec36238 github.com/fatih/color v1.18.0 github.com/glebarez/sqlite v1.11.0 github.com/go-resty/resty/v2 v2.16.5 diff --git a/go.sum b/go.sum index 715e6c7..a1aa433 100644 --- a/go.sum +++ b/go.sum @@ -24,8 +24,6 @@ github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEf github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/delucks/go-subsonic v0.0.0-20240806025900-2a743ec36238 h1:uejyepOdHISrJTw7P84Y7yEC0FMyv1q3KNDRxWsviKw= -github.com/delucks/go-subsonic v0.0.0-20240806025900-2a743ec36238/go.mod h1:vnbEuj6Z20PLcHB4rrLQAOXGMjtULfMGhRVSFPcSdUo= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= From 91f78d04dd9b6873eb19e554237f6772314f7a85 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 27 Apr 2025 18:56:09 +0200 Subject: [PATCH 66/82] ListenBrainz: Handle missing loves metadata for merged recordings If a loved recording MBID got merged into another recording, the love on ListenBrainz has no metadata. Lookup the metadata directly from MusicBrainz. --- go.mod | 1 + go.sum | 2 + .../backends/listenbrainz/listenbrainz.go | 44 +++++++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/go.mod b/go.mod index 9871720..c13d7ae 100644 --- a/go.mod +++ b/go.mod @@ -57,6 +57,7 @@ require ( github.com/spf13/pflag v1.0.6 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect + go.uploadedlobster.com/musicbrainzws2 v0.13.1 // indirect golang.org/x/image v0.26.0 // indirect golang.org/x/mod v0.24.0 // indirect golang.org/x/net v0.39.0 // indirect diff --git a/go.sum b/go.sum index a1aa433..1ee05c8 100644 --- a/go.sum +++ b/go.sum @@ -132,6 +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.13.1 h1:34GKI7l9eTCyh9ozNOHmlwAAUTDK9WVRsFZK5trxcwQ= +go.uploadedlobster.com/musicbrainzws2 v0.13.1/go.mod h1:TVln70Fzp/++fw0/jCP1xXwgilVwDkzTwRbV8GwUYLA= 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.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= diff --git a/internal/backends/listenbrainz/listenbrainz.go b/internal/backends/listenbrainz/listenbrainz.go index c05943f..d13c869 100644 --- a/internal/backends/listenbrainz/listenbrainz.go +++ b/internal/backends/listenbrainz/listenbrainz.go @@ -22,6 +22,7 @@ import ( "time" "go.uploadedlobster.com/mbtypes" + "go.uploadedlobster.com/musicbrainzws2" "go.uploadedlobster.com/scotty/internal/config" "go.uploadedlobster.com/scotty/internal/i18n" "go.uploadedlobster.com/scotty/internal/models" @@ -31,6 +32,7 @@ import ( type ListenBrainzApiBackend struct { client Client + mbClient musicbrainzws2.Client username string checkDuplicates bool existingMBIDs map[mbtypes.MBID]bool @@ -56,6 +58,7 @@ func (b *ListenBrainzApiBackend) Options() []models.BackendOption { func (b *ListenBrainzApiBackend) FromConfig(config *config.ServiceConfig) models.Backend { b.client = NewClient(config.GetString("token")) + b.mbClient = *musicbrainzws2.NewClient(version.AppName, version.AppVersion) b.client.MaxResults = MaxItemsPerGet b.username = config.GetString("username") b.checkDuplicates = config.GetBool("check-duplicate-listens", false) @@ -228,6 +231,16 @@ out: } for _, feedback := range result.Feedback { + // Missing track metadata indicates that the recording MBID is no + // longer available and might have been merged. Try fetching details + // from MusicBrainz. + if feedback.TrackMetadata == nil { + track, err := b.lookupRecording(feedback.RecordingMBID) + if err == nil { + feedback.TrackMetadata = track + } + } + love := feedback.AsLove() if love.Created.Unix() > oldestTimestamp.Unix() { loves = append(loves, love) @@ -265,6 +278,12 @@ func (b *ListenBrainzApiBackend) ImportLoves(export models.LovesResult, importRe for _, love := range existingLoves.Items { b.existingMBIDs[love.RecordingMBID] = true + // In case the loved MBID got merged the track MBID represents the + // actual recording MBID. + if love.Track.RecordingMBID != "" && + love.Track.RecordingMBID != love.RecordingMBID { + b.existingMBIDs[love.Track.RecordingMBID] = true + } } } } @@ -347,6 +366,31 @@ func (b *ListenBrainzApiBackend) checkDuplicateListen(listen models.Listen) (boo return false, nil } +func (b *ListenBrainzApiBackend) lookupRecording(mbid mbtypes.MBID) (*Track, error) { + filter := musicbrainzws2.IncludesFilter{ + Includes: []string{"artist-credits"}, + } + recording, err := b.mbClient.LookupRecording(mbid, filter) + if err != nil { + return nil, err + } + + artistMBIDs := make([]mbtypes.MBID, 0, len(recording.ArtistCredit)) + for _, artist := range recording.ArtistCredit { + artistMBIDs = append(artistMBIDs, artist.Artist.ID) + } + track := Track{ + TrackName: recording.Title, + ArtistName: recording.ArtistCredit.String(), + MBIDMapping: &MBIDMapping{ + // In case of redirects this MBID differs from the looked up MBID + RecordingMBID: recording.ID, + ArtistMBIDs: artistMBIDs, + }, + } + return &track, nil +} + func (lbListen Listen) AsListen() models.Listen { listen := models.Listen{ ListenedAt: time.Unix(lbListen.ListenedAt, 0), From 4a30bdf9d9aa22de5ca378dd31a41d4ceecdbe2a Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Mon, 28 Apr 2025 08:03:33 +0200 Subject: [PATCH 67/82] Update go.mod --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index c13d7ae..22a3154 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/supersonic-app/go-subsonic v0.0.0-20241224013245-9b2841f3711d github.com/vbauerster/mpb/v8 v8.9.3 go.uploadedlobster.com/mbtypes v0.4.0 + go.uploadedlobster.com/musicbrainzws2 v0.13.1 golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 golang.org/x/oauth2 v0.29.0 golang.org/x/text v0.24.0 @@ -57,7 +58,6 @@ require ( github.com/spf13/pflag v1.0.6 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uploadedlobster.com/musicbrainzws2 v0.13.1 // indirect golang.org/x/image v0.26.0 // indirect golang.org/x/mod v0.24.0 // indirect golang.org/x/net v0.39.0 // indirect From 9184d2c3cfcdc4c9ff68cea8cd64698cfd3da242 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Mon, 28 Apr 2025 08:08:01 +0200 Subject: [PATCH 68/82] Update changelog for next version --- CHANGES.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index d3ee1d7..11251cd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,14 @@ # Scotty Changelog +## 0.5.0 - (not yet released) +- ListenBrainz: handle missing loves metadata in case of merged recordings +- ListenBrainz: fix loves import loading all existing loves +- ListenBrainz: fixed progress for loves import +- ListenBrainz: log missing recording MBID on love import +- Subsonic: support OpenSubsonic fields for recording MBID and genres (#5) +- Subsonic: fixed progress for loves export + + ## 0.4.1 - 2024-09-16 - Subsonic: include `subsonic_id` as additional metadata - Deezer: fix artist and album ID URIs (#7) From 69665bc28680b09601ee7db10af211452d842415 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Mon, 28 Apr 2025 08:54:17 +0200 Subject: [PATCH 69/82] scrobblerlog: consider timezone from parsed file --- internal/backends/scrobblerlog/parser.go | 42 ++++++++++++++++++++---- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/internal/backends/scrobblerlog/parser.go b/internal/backends/scrobblerlog/parser.go index 1ef08f7..eeb603b 100644 --- a/internal/backends/scrobblerlog/parser.go +++ b/internal/backends/scrobblerlog/parser.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 @@ -38,6 +38,7 @@ type ScrobblerLog struct { Timezone string Client string Listens models.ListensList + location *time.Location } func Parse(data io.Reader, includeSkipped bool) (ScrobblerLog, error) { @@ -79,8 +80,7 @@ func Parse(data io.Reader, includeSkipped bool) (ScrobblerLog, error) { continue } - client := strings.Split(result.Client, " ")[0] - listen, err := rowToListen(row, client) + listen, err := result.rowToListen(row) if err != nil { return result, err } @@ -138,14 +138,19 @@ func ReadHeader(reader *bufio.Reader, log *ScrobblerLog) error { err = fmt.Errorf("not a scrobbler log file") } + // The timezone can be set to "UTC" or "UNKNOWN", if the device writing + // the log knows the time, but not the timezone. timezone, found := strings.CutPrefix(text, "#TZ/") if found { log.Timezone = timezone + log.location = locationFromTimezone(log.Timezone) + continue } client, found := strings.CutPrefix(text, "#CLIENT/") if found { log.Client = client + continue } } @@ -171,7 +176,7 @@ func WriteHeader(writer io.Writer, log *ScrobblerLog) error { return nil } -func rowToListen(row []string, client string) (models.Listen, error) { +func (l ScrobblerLog) rowToListen(row []string) (models.Listen, error) { var listen models.Listen trackNumber, err := strconv.Atoi(row[3]) if err != nil { @@ -183,11 +188,12 @@ func rowToListen(row []string, client string) (models.Listen, error) { return listen, err } - timestamp, err := strconv.Atoi(row[6]) + timestamp, err := strconv.ParseInt(row[6], 10, 64) if err != nil { return listen, err } + client := strings.Split(l.Client, " ")[0] listen = models.Listen{ Track: models.Track{ ArtistNames: []string{row[0]}, @@ -200,7 +206,7 @@ func rowToListen(row []string, client string) (models.Listen, error) { "media_player": client, }, }, - ListenedAt: time.Unix(int64(timestamp), 0), + ListenedAt: timeFromLocalTimestamp(timestamp, l.location), } if len(row) > 7 { @@ -209,3 +215,27 @@ func rowToListen(row []string, client string) (models.Listen, error) { return listen, nil } + +// Convert the timezone string from the header to a time.Location. +// Often this is set to "UNKNOWN" in the log file, in which case it defaults +// to UTC. +func locationFromTimezone(timezone string) *time.Location { + location, err := time.LoadLocation(timezone) + if err != nil { + return time.UTC + } + return location +} + +// Convert a Unix timestamp to a time.Time object, but treat the timestamp +// as being in the given location's timezone instead of UTC. +func timeFromLocalTimestamp(timestamp int64, location *time.Location) time.Time { + t := time.Unix(timestamp, 0) + + // The time is now in UTC. Get the offset to the requested timezone. + _, offset := t.In(location).Zone() + if offset != 0 { + t = t.Add(time.Duration(offset) * time.Second) + } + return t +} From aeb3a56982d5d2f9d46c3654c880ca829df0d322 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 29 Apr 2025 08:36:34 +0200 Subject: [PATCH 70/82] Moved scrobblerlog parsing to separate package --- .../backends/scrobblerlog/scrobblerlog.go | 19 +++--- .../backends => pkg}/scrobblerlog/parser.go | 61 ++++++++++++------- .../scrobblerlog/parser_test.go | 50 +++++++-------- 3 files changed, 74 insertions(+), 56 deletions(-) rename {internal/backends => pkg}/scrobblerlog/parser.go (78%) rename {internal/backends => pkg}/scrobblerlog/parser_test.go (80%) diff --git a/internal/backends/scrobblerlog/scrobblerlog.go b/internal/backends/scrobblerlog/scrobblerlog.go index 84cae88..bb05086 100644 --- a/internal/backends/scrobblerlog/scrobblerlog.go +++ b/internal/backends/scrobblerlog/scrobblerlog.go @@ -25,6 +25,7 @@ import ( "go.uploadedlobster.com/scotty/internal/config" "go.uploadedlobster.com/scotty/internal/i18n" "go.uploadedlobster.com/scotty/internal/models" + "go.uploadedlobster.com/scotty/pkg/scrobblerlog" ) type ScrobblerLogBackend struct { @@ -32,7 +33,7 @@ type ScrobblerLogBackend struct { includeSkipped bool append bool file *os.File - log ScrobblerLog + log scrobblerlog.ScrobblerLog } func (b *ScrobblerLogBackend) Name() string { return "scrobbler-log" } @@ -58,9 +59,9 @@ func (b *ScrobblerLogBackend) FromConfig(config *config.ServiceConfig) models.Ba b.filePath = config.GetString("file-path") b.includeSkipped = config.GetBool("include-skipped", false) b.append = config.GetBool("append", true) - b.log = ScrobblerLog{ - Timezone: "UNKNOWN", - Client: "Rockbox unknown $Revision$", + b.log = scrobblerlog.ScrobblerLog{ + TZ: scrobblerlog.TZ_UTC, + Client: "Rockbox unknown $Revision$", } return b } @@ -88,7 +89,7 @@ func (b *ScrobblerLogBackend) StartImport() error { } else { // Verify existing file is a scrobbler log reader := bufio.NewReader(file) - if err = ReadHeader(reader, &b.log); err != nil { + if err = b.log.ReadHeader(reader); err != nil { file.Close() return err } @@ -99,7 +100,7 @@ func (b *ScrobblerLogBackend) StartImport() error { } if !b.append { - if err = WriteHeader(file, &b.log); err != nil { + if err = b.log.WriteHeader(file); err != nil { file.Close() return err } @@ -124,21 +125,21 @@ func (b *ScrobblerLogBackend) ExportListens(oldestTimestamp time.Time, results c defer file.Close() - log, err := Parse(file, b.includeSkipped) + err = b.log.Parse(file, b.includeSkipped) if err != nil { progress <- models.Progress{}.Complete() results <- models.ListensResult{Error: err} return } - listens := log.Listens.NewerThan(oldestTimestamp) + listens := b.log.Listens.NewerThan(oldestTimestamp) sort.Sort(listens) progress <- models.Progress{Elapsed: int64(len(listens))}.Complete() results <- models.ListensResult{Items: listens} } func (b *ScrobblerLogBackend) ImportListens(export models.ListensResult, importResult models.ImportResult, progress chan models.Progress) (models.ImportResult, error) { - lastTimestamp, err := Write(b.file, export.Items) + lastTimestamp, err := b.log.Append(b.file, export.Items) if err != nil { return importResult, err } diff --git a/internal/backends/scrobblerlog/parser.go b/pkg/scrobblerlog/parser.go similarity index 78% rename from internal/backends/scrobblerlog/parser.go rename to pkg/scrobblerlog/parser.go index eeb603b..a200d05 100644 --- a/internal/backends/scrobblerlog/parser.go +++ b/pkg/scrobblerlog/parser.go @@ -19,6 +19,12 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + +// Package to parse and writer .scrobbler.log files as written by Rockbox. +// +// See +// - https://www.rockbox.org/wiki/LastFMLog +// - https://git.rockbox.org/cgit/rockbox.git/tree/apps/plugins/lastfm_scrobbler.c package scrobblerlog import ( @@ -34,22 +40,31 @@ import ( "go.uploadedlobster.com/scotty/internal/models" ) +// TZInfo is the timezone information in the header of the scrobbler log file. +// It can be "UTC" or "UNKNOWN", if the device writing the scrobbler log file +// knows the time, but not the timezone. +type TZInfo string + +const ( + TZ_UNKNOWN TZInfo = "UNKNOWN" + TZ_UTC TZInfo = "UTC" +) + +// Represents a scrobbler log file. type ScrobblerLog struct { - Timezone string + TZ TZInfo Client string Listens models.ListensList location *time.Location } -func Parse(data io.Reader, includeSkipped bool) (ScrobblerLog, error) { - result := ScrobblerLog{ - Listens: make(models.ListensList, 0), - } +func (l *ScrobblerLog) Parse(data io.Reader, includeSkipped bool) error { + l.Listens = make(models.ListensList, 0) reader := bufio.NewReader(data) - err := ReadHeader(reader, &result) + err := l.ReadHeader(reader) if err != nil { - return result, err + return err } tsvReader := csv.NewReader(reader) @@ -64,7 +79,7 @@ func Parse(data io.Reader, includeSkipped bool) (ScrobblerLog, error) { if err == io.EOF { break } else if err != nil { - return result, err + return err } // fmt.Printf("row: %v\n", row) @@ -72,7 +87,7 @@ func Parse(data io.Reader, includeSkipped bool) (ScrobblerLog, error) { // We consider only the last field (recording MBID) optional if len(row) < 7 { line, _ := tsvReader.FieldPos(0) - return result, fmt.Errorf("invalid record in scrobblerlog line %v", line) + return fmt.Errorf("invalid record in scrobblerlog line %v", line) } rating := row[5] @@ -80,18 +95,18 @@ func Parse(data io.Reader, includeSkipped bool) (ScrobblerLog, error) { continue } - listen, err := result.rowToListen(row) + listen, err := l.rowToListen(row) if err != nil { - return result, err + return err } - result.Listens = append(result.Listens, listen) + l.Listens = append(l.Listens, listen) } - return result, nil + return nil } -func Write(data io.Writer, listens models.ListensList) (lastTimestamp time.Time, err error) { +func (l *ScrobblerLog) Append(data io.Writer, listens models.ListensList) (lastTimestamp time.Time, err error) { tsvWriter := csv.NewWriter(data) tsvWriter.Comma = '\t' @@ -122,7 +137,7 @@ func Write(data io.Writer, listens models.ListensList) (lastTimestamp time.Time, return } -func ReadHeader(reader *bufio.Reader, log *ScrobblerLog) error { +func (l *ScrobblerLog) ReadHeader(reader *bufio.Reader) error { // Skip header for i := 0; i < 3; i++ { line, _, err := reader.ReadLine() @@ -142,14 +157,14 @@ func ReadHeader(reader *bufio.Reader, log *ScrobblerLog) error { // the log knows the time, but not the timezone. timezone, found := strings.CutPrefix(text, "#TZ/") if found { - log.Timezone = timezone - log.location = locationFromTimezone(log.Timezone) + l.TZ = TZInfo(timezone) + l.location = locationFromTimezone(l.TZ) continue } client, found := strings.CutPrefix(text, "#CLIENT/") if found { - log.Client = client + l.Client = client continue } } @@ -161,11 +176,11 @@ func ReadHeader(reader *bufio.Reader, log *ScrobblerLog) error { return nil } -func WriteHeader(writer io.Writer, log *ScrobblerLog) error { +func (l *ScrobblerLog) WriteHeader(writer io.Writer) error { headers := []string{ "#AUDIOSCROBBLER/1.1\n", - "#TZ/" + log.Timezone + "\n", - "#CLIENT/" + log.Client + "\n", + "#TZ/" + string(l.TZ) + "\n", + "#CLIENT/" + l.Client + "\n", } for _, line := range headers { _, err := writer.Write([]byte(line)) @@ -219,8 +234,8 @@ func (l ScrobblerLog) rowToListen(row []string) (models.Listen, error) { // Convert the timezone string from the header to a time.Location. // Often this is set to "UNKNOWN" in the log file, in which case it defaults // to UTC. -func locationFromTimezone(timezone string) *time.Location { - location, err := time.LoadLocation(timezone) +func locationFromTimezone(timezone TZInfo) *time.Location { + location, err := time.LoadLocation(string(timezone)) if err != nil { return time.UTC } diff --git a/internal/backends/scrobblerlog/parser_test.go b/pkg/scrobblerlog/parser_test.go similarity index 80% rename from internal/backends/scrobblerlog/parser_test.go rename to pkg/scrobblerlog/parser_test.go index 480481f..b70f408 100644 --- a/internal/backends/scrobblerlog/parser_test.go +++ b/pkg/scrobblerlog/parser_test.go @@ -31,8 +31,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uploadedlobster.com/mbtypes" - "go.uploadedlobster.com/scotty/internal/backends/scrobblerlog" "go.uploadedlobster.com/scotty/internal/models" + "go.uploadedlobster.com/scotty/pkg/scrobblerlog" ) var testScrobblerLog = `#AUDIOSCROBBLER/1.1 @@ -48,9 +48,10 @@ Teeth Agency You Don't Have To Live In Pain Wolfs Jam 2 107 L 1260359404 1262bea func TestParser(t *testing.T) { assert := assert.New(t) data := bytes.NewBufferString(testScrobblerLog) - result, err := scrobblerlog.Parse(data, true) + result := scrobblerlog.ScrobblerLog{} + err := result.Parse(data, true) require.NoError(t, err) - assert.Equal("UNKNOWN", result.Timezone) + assert.Equal(scrobblerlog.TZ_UNKNOWN, result.TZ) assert.Equal("Rockbox sansaclipplus $Revision$", result.Client) assert.Len(result.Listens, 5) listen1 := result.Listens[0] @@ -70,7 +71,8 @@ func TestParser(t *testing.T) { func TestParserExcludeSkipped(t *testing.T) { assert := assert.New(t) data := bytes.NewBufferString(testScrobblerLog) - result, err := scrobblerlog.Parse(data, false) + result := scrobblerlog.ScrobblerLog{} + err := result.Parse(data, false) require.NoError(t, err) assert.Len(result.Listens, 4) listen4 := result.Listens[3] @@ -78,37 +80,37 @@ func TestParserExcludeSkipped(t *testing.T) { assert.Equal(mbtypes.MBID("1262beaf-19f8-4534-b9ed-7eef9ca8e83f"), listen4.RecordingMBID) } -func TestWrite(t *testing.T) { +func TestAppend(t *testing.T) { assert := assert.New(t) data := make([]byte, 0, 10) buffer := bytes.NewBuffer(data) log := scrobblerlog.ScrobblerLog{ - Timezone: "Unknown", - Client: "Rockbox foo $Revision$", - Listens: []models.Listen{ - { - ListenedAt: time.Unix(1699572072, 0), - Track: models.Track{ - ArtistNames: []string{"Prinzhorn Dance School"}, - ReleaseName: "Home Economics", - TrackName: "Reign", - TrackNumber: 1, - Duration: 271 * time.Second, - RecordingMBID: mbtypes.MBID("b59cf4e7-caee-4019-a844-79d2c58d4dff"), - AdditionalInfo: models.AdditionalInfo{"rockbox_rating": "L"}, - }, + TZ: scrobblerlog.TZ_UNKNOWN, + Client: "Rockbox foo $Revision$", + } + listens := []models.Listen{ + { + ListenedAt: time.Unix(1699572072, 0), + Track: models.Track{ + ArtistNames: []string{"Prinzhorn Dance School"}, + ReleaseName: "Home Economics", + TrackName: "Reign", + TrackNumber: 1, + Duration: 271 * time.Second, + RecordingMBID: mbtypes.MBID("b59cf4e7-caee-4019-a844-79d2c58d4dff"), + AdditionalInfo: models.AdditionalInfo{"rockbox_rating": "L"}, }, }, } - err := scrobblerlog.WriteHeader(buffer, &log) + err := log.WriteHeader(buffer) require.NoError(t, err) - lastTimestamp, err := scrobblerlog.Write(buffer, log.Listens) + lastTimestamp, err := log.Append(buffer, listens) require.NoError(t, err) result := buffer.String() lines := strings.Split(result, "\n") assert.Equal(5, len(lines)) assert.Equal("#AUDIOSCROBBLER/1.1", lines[0]) - assert.Equal("#TZ/Unknown", lines[1]) + assert.Equal("#TZ/UNKNOWN", lines[1]) assert.Equal("#CLIENT/Rockbox foo $Revision$", lines[2]) assert.Equal( "Prinzhorn Dance School\tHome Economics\tReign\t1\t271\tL\t1699572072\tb59cf4e7-caee-4019-a844-79d2c58d4dff", @@ -121,9 +123,9 @@ func TestReadHeader(t *testing.T) { data := bytes.NewBufferString(testScrobblerLog) reader := bufio.NewReader(data) log := scrobblerlog.ScrobblerLog{} - err := scrobblerlog.ReadHeader(reader, &log) + err := log.ReadHeader(reader) assert.NoError(t, err) - assert.Equal(t, log.Timezone, "UNKNOWN") + assert.Equal(t, log.TZ, scrobblerlog.TZ_UNKNOWN) assert.Equal(t, log.Client, "Rockbox sansaclipplus $Revision$") assert.Empty(t, log.Listens) } From aad542850a139177a8e451d1f32a976ed59db9a7 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 29 Apr 2025 09:18:57 +0200 Subject: [PATCH 71/82] scrobblerlog: Use specific Record type This makes the interface more generic and easier to reuse in other projects. --- .../backends/scrobblerlog/scrobblerlog.go | 54 ++++++++++- pkg/scrobblerlog/parser.go | 93 ++++++++++--------- pkg/scrobblerlog/parser_test.go | 61 ++++++------ 3 files changed, 132 insertions(+), 76 deletions(-) diff --git a/internal/backends/scrobblerlog/scrobblerlog.go b/internal/backends/scrobblerlog/scrobblerlog.go index bb05086..bf5afac 100644 --- a/internal/backends/scrobblerlog/scrobblerlog.go +++ b/internal/backends/scrobblerlog/scrobblerlog.go @@ -20,6 +20,7 @@ import ( "bufio" "os" "sort" + "strings" "time" "go.uploadedlobster.com/scotty/internal/config" @@ -132,14 +133,22 @@ func (b *ScrobblerLogBackend) ExportListens(oldestTimestamp time.Time, results c return } - listens := b.log.Listens.NewerThan(oldestTimestamp) - sort.Sort(listens) + listens := make(models.ListensList, 0, len(b.log.Records)) + client := strings.Split(b.log.Client, " ")[0] + for _, record := range b.log.Records { + listens = append(listens, recordToListen(record, client)) + } + sort.Sort(listens.NewerThan(oldestTimestamp)) progress <- models.Progress{Elapsed: int64(len(listens))}.Complete() results <- models.ListensResult{Items: listens} } func (b *ScrobblerLogBackend) ImportListens(export models.ListensResult, importResult models.ImportResult, progress chan models.Progress) (models.ImportResult, error) { - lastTimestamp, err := b.log.Append(b.file, export.Items) + records := make([]scrobblerlog.Record, len(export.Items)) + for i, listen := range export.Items { + records[i] = listenToRecord(listen) + } + lastTimestamp, err := b.log.Append(b.file, records) if err != nil { return importResult, err } @@ -150,3 +159,42 @@ func (b *ScrobblerLogBackend) ImportListens(export models.ListensResult, importR return importResult, nil } + +func recordToListen(record scrobblerlog.Record, client string) models.Listen { + return models.Listen{ + ListenedAt: record.Timestamp, + Track: models.Track{ + ArtistNames: []string{record.ArtistName}, + ReleaseName: record.AlbumName, + TrackName: record.TrackName, + TrackNumber: record.TrackNumber, + Duration: record.Duration, + RecordingMBID: record.MusicBrainzRecordingID, + AdditionalInfo: models.AdditionalInfo{ + "rockbox_rating": record.Rating, + "media_player": client, + }, + }, + } +} + +func listenToRecord(listen models.Listen) scrobblerlog.Record { + var rating scrobblerlog.Rating + rockboxRating, ok := listen.AdditionalInfo["rockbox_rating"].(string) + if !ok || rockboxRating == "" { + rating = scrobblerlog.RATING_LISTENED + } else { + rating = scrobblerlog.Rating(rating) + } + + return scrobblerlog.Record{ + ArtistName: listen.ArtistName(), + AlbumName: listen.ReleaseName, + TrackName: listen.TrackName, + TrackNumber: listen.TrackNumber, + Duration: listen.Duration, + Rating: rating, + Timestamp: listen.ListenedAt, + MusicBrainzRecordingID: listen.RecordingMBID, + } +} diff --git a/pkg/scrobblerlog/parser.go b/pkg/scrobblerlog/parser.go index a200d05..8f9b88a 100644 --- a/pkg/scrobblerlog/parser.go +++ b/pkg/scrobblerlog/parser.go @@ -37,7 +37,6 @@ import ( "time" "go.uploadedlobster.com/mbtypes" - "go.uploadedlobster.com/scotty/internal/models" ) // TZInfo is the timezone information in the header of the scrobbler log file. @@ -50,16 +49,36 @@ const ( TZ_UTC TZInfo = "UTC" ) +// L if listened at least 50% or S if skipped +type Rating string + +const ( + RATING_LISTENED Rating = "L" + RATING_SKIPPED Rating = "S" +) + +// A single entry of a track in the scrobbler log file. +type Record struct { + ArtistName string + AlbumName string + TrackName string + TrackNumber int + Duration time.Duration + Rating Rating + Timestamp time.Time + MusicBrainzRecordingID mbtypes.MBID +} + // Represents a scrobbler log file. type ScrobblerLog struct { TZ TZInfo Client string - Listens models.ListensList + Records []Record location *time.Location } func (l *ScrobblerLog) Parse(data io.Reader, includeSkipped bool) error { - l.Listens = make(models.ListensList, 0) + l.Records = make([]Record, 0) reader := bufio.NewReader(data) err := l.ReadHeader(reader) @@ -95,41 +114,37 @@ func (l *ScrobblerLog) Parse(data io.Reader, includeSkipped bool) error { continue } - listen, err := l.rowToListen(row) + record, err := l.rowToRecord(row) if err != nil { return err } - l.Listens = append(l.Listens, listen) + l.Records = append(l.Records, record) } return nil } -func (l *ScrobblerLog) Append(data io.Writer, listens models.ListensList) (lastTimestamp time.Time, err error) { +func (l *ScrobblerLog) Append(data io.Writer, records []Record) (lastTimestamp time.Time, err error) { tsvWriter := csv.NewWriter(data) tsvWriter.Comma = '\t' - for _, listen := range listens { - if listen.ListenedAt.Unix() > lastTimestamp.Unix() { - lastTimestamp = listen.ListenedAt + for _, record := range records { + if record.Timestamp.After(lastTimestamp) { + lastTimestamp = record.Timestamp } // A row is: // artistName releaseName trackName trackNumber duration rating timestamp recordingMBID - rating, ok := listen.AdditionalInfo["rockbox_rating"].(string) - if !ok || rating == "" { - rating = "L" - } err = tsvWriter.Write([]string{ - listen.ArtistName(), - listen.ReleaseName, - listen.TrackName, - strconv.Itoa(listen.TrackNumber), - strconv.Itoa(int(listen.Duration.Seconds())), - rating, - strconv.Itoa(int(listen.ListenedAt.Unix())), - string(listen.RecordingMBID), + record.ArtistName, + record.AlbumName, + record.TrackName, + strconv.Itoa(record.TrackNumber), + strconv.Itoa(int(record.Duration.Seconds())), + string(record.Rating), + strconv.FormatInt(record.Timestamp.Unix(), 10), + string(record.MusicBrainzRecordingID), }) } @@ -191,44 +206,38 @@ func (l *ScrobblerLog) WriteHeader(writer io.Writer) error { return nil } -func (l ScrobblerLog) rowToListen(row []string) (models.Listen, error) { - var listen models.Listen +func (l ScrobblerLog) rowToRecord(row []string) (Record, error) { + var record Record trackNumber, err := strconv.Atoi(row[3]) if err != nil { - return listen, err + return record, err } duration, err := strconv.Atoi(row[4]) if err != nil { - return listen, err + return record, err } timestamp, err := strconv.ParseInt(row[6], 10, 64) if err != nil { - return listen, err + return record, err } - client := strings.Split(l.Client, " ")[0] - listen = models.Listen{ - Track: models.Track{ - ArtistNames: []string{row[0]}, - ReleaseName: row[1], - TrackName: row[2], - TrackNumber: trackNumber, - Duration: time.Duration(duration * int(time.Second)), - AdditionalInfo: models.AdditionalInfo{ - "rockbox_rating": row[5], - "media_player": client, - }, - }, - ListenedAt: timeFromLocalTimestamp(timestamp, l.location), + record = Record{ + ArtistName: row[0], + AlbumName: row[1], + TrackName: row[2], + TrackNumber: trackNumber, + Duration: time.Duration(duration) * time.Second, + Rating: Rating(row[5]), + Timestamp: timeFromLocalTimestamp(timestamp, l.location), } if len(row) > 7 { - listen.Track.RecordingMBID = mbtypes.MBID(row[7]) + record.MusicBrainzRecordingID = mbtypes.MBID(row[7]) } - return listen, nil + return record, nil } // Convert the timezone string from the header to a time.Location. diff --git a/pkg/scrobblerlog/parser_test.go b/pkg/scrobblerlog/parser_test.go index b70f408..9b4513f 100644 --- a/pkg/scrobblerlog/parser_test.go +++ b/pkg/scrobblerlog/parser_test.go @@ -31,7 +31,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uploadedlobster.com/mbtypes" - "go.uploadedlobster.com/scotty/internal/models" "go.uploadedlobster.com/scotty/pkg/scrobblerlog" ) @@ -53,19 +52,20 @@ func TestParser(t *testing.T) { require.NoError(t, err) assert.Equal(scrobblerlog.TZ_UNKNOWN, result.TZ) assert.Equal("Rockbox sansaclipplus $Revision$", result.Client) - assert.Len(result.Listens, 5) - listen1 := result.Listens[0] - assert.Equal("Özcan Deniz", listen1.ArtistName()) - assert.Equal("Ses ve Ayrilik", listen1.ReleaseName) - assert.Equal("Sevdanin rengi (sipacik) byMrTurkey", listen1.TrackName) - assert.Equal(5, listen1.TrackNumber) - assert.Equal(time.Duration(306*time.Second), listen1.Duration) - assert.Equal("L", listen1.AdditionalInfo["rockbox_rating"]) - assert.Equal(time.Unix(1260342084, 0), listen1.ListenedAt) - assert.Equal(mbtypes.MBID(""), listen1.RecordingMBID) - listen4 := result.Listens[3] - assert.Equal("S", listen4.AdditionalInfo["rockbox_rating"]) - assert.Equal(mbtypes.MBID("385ba9e9-626d-4750-a607-58e541dca78e"), listen4.RecordingMBID) + assert.Len(result.Records, 5) + record1 := result.Records[0] + assert.Equal("Özcan Deniz", record1.ArtistName) + assert.Equal("Ses ve Ayrilik", record1.AlbumName) + assert.Equal("Sevdanin rengi (sipacik) byMrTurkey", record1.TrackName) + assert.Equal(5, record1.TrackNumber) + assert.Equal(time.Duration(306*time.Second), record1.Duration) + assert.Equal(scrobblerlog.RATING_LISTENED, record1.Rating) + assert.Equal(time.Unix(1260342084, 0), record1.Timestamp) + assert.Equal(mbtypes.MBID(""), record1.MusicBrainzRecordingID) + record4 := result.Records[3] + assert.Equal(scrobblerlog.RATING_SKIPPED, record4.Rating) + assert.Equal(mbtypes.MBID("385ba9e9-626d-4750-a607-58e541dca78e"), + record4.MusicBrainzRecordingID) } func TestParserExcludeSkipped(t *testing.T) { @@ -74,10 +74,11 @@ func TestParserExcludeSkipped(t *testing.T) { result := scrobblerlog.ScrobblerLog{} err := result.Parse(data, false) require.NoError(t, err) - assert.Len(result.Listens, 4) - listen4 := result.Listens[3] - assert.Equal("L", listen4.AdditionalInfo["rockbox_rating"]) - assert.Equal(mbtypes.MBID("1262beaf-19f8-4534-b9ed-7eef9ca8e83f"), listen4.RecordingMBID) + assert.Len(result.Records, 4) + record4 := result.Records[3] + assert.Equal(scrobblerlog.RATING_LISTENED, record4.Rating) + assert.Equal(mbtypes.MBID("1262beaf-19f8-4534-b9ed-7eef9ca8e83f"), + record4.MusicBrainzRecordingID) } func TestAppend(t *testing.T) { @@ -88,23 +89,21 @@ func TestAppend(t *testing.T) { TZ: scrobblerlog.TZ_UNKNOWN, Client: "Rockbox foo $Revision$", } - listens := []models.Listen{ + records := []scrobblerlog.Record{ { - ListenedAt: time.Unix(1699572072, 0), - Track: models.Track{ - ArtistNames: []string{"Prinzhorn Dance School"}, - ReleaseName: "Home Economics", - TrackName: "Reign", - TrackNumber: 1, - Duration: 271 * time.Second, - RecordingMBID: mbtypes.MBID("b59cf4e7-caee-4019-a844-79d2c58d4dff"), - AdditionalInfo: models.AdditionalInfo{"rockbox_rating": "L"}, - }, + ArtistName: "Prinzhorn Dance School", + AlbumName: "Home Economics", + TrackName: "Reign", + TrackNumber: 1, + Duration: 271 * time.Second, + Rating: scrobblerlog.RATING_LISTENED, + Timestamp: time.Unix(1699572072, 0), + MusicBrainzRecordingID: mbtypes.MBID("b59cf4e7-caee-4019-a844-79d2c58d4dff"), }, } err := log.WriteHeader(buffer) require.NoError(t, err) - lastTimestamp, err := log.Append(buffer, listens) + lastTimestamp, err := log.Append(buffer, records) require.NoError(t, err) result := buffer.String() lines := strings.Split(result, "\n") @@ -127,5 +126,5 @@ func TestReadHeader(t *testing.T) { assert.NoError(t, err) assert.Equal(t, log.TZ, scrobblerlog.TZ_UNKNOWN) assert.Equal(t, log.Client, "Rockbox sansaclipplus $Revision$") - assert.Empty(t, log.Listens) + assert.Empty(t, log.Records) } From 0f4b04c641c531cd0d9a2f42e30b2a32701b0bf5 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 29 Apr 2025 10:03:28 +0200 Subject: [PATCH 72/82] Renamed Backend.FromConfig to Backend.InitConfig and added error handling --- internal/backends/backends.go | 6 +++++- internal/backends/deezer/deezer.go | 4 ++-- internal/backends/deezer/deezer_test.go | 7 ++++--- internal/backends/dump/dump.go | 4 ++-- internal/backends/funkwhale/funkwhale.go | 4 ++-- internal/backends/funkwhale/funkwhale_test.go | 7 ++++--- internal/backends/jspf/jspf.go | 4 ++-- internal/backends/jspf/jspf_test.go | 7 ++++--- internal/backends/lastfm/lastfm.go | 4 ++-- internal/backends/listenbrainz/listenbrainz.go | 4 ++-- internal/backends/listenbrainz/listenbrainz_test.go | 7 ++++--- internal/backends/maloja/maloja.go | 4 ++-- internal/backends/maloja/maloja_test.go | 7 ++++--- internal/backends/scrobblerlog/scrobblerlog.go | 4 ++-- internal/backends/scrobblerlog/scrobblerlog_test.go | 7 ++++--- internal/backends/spotify/spotify.go | 4 ++-- internal/backends/spotify/spotify_test.go | 7 ++++--- internal/backends/spotifyhistory/spotifyhistory.go | 4 ++-- internal/backends/subsonic/subsonic.go | 4 ++-- internal/backends/subsonic/subsonic_test.go | 7 ++++--- internal/models/interfaces.go | 2 +- 21 files changed, 60 insertions(+), 48 deletions(-) diff --git a/internal/backends/backends.go b/internal/backends/backends.go index e4cbbc9..a9c3292 100644 --- a/internal/backends/backends.go +++ b/internal/backends/backends.go @@ -123,7 +123,11 @@ func backendWithConfig(config config.ServiceConfig) (models.Backend, error) { if err != nil { return nil, err } - return backend.FromConfig(&config), nil + err = backend.InitConfig(&config) + if err != nil { + return nil, err + } + return backend, nil } func ImplementsInterface[T interface{}](backend *models.Backend) (bool, string) { diff --git a/internal/backends/deezer/deezer.go b/internal/backends/deezer/deezer.go index 3131c3e..e7d9762 100644 --- a/internal/backends/deezer/deezer.go +++ b/internal/backends/deezer/deezer.go @@ -49,10 +49,10 @@ func (b *DeezerApiBackend) Options() []models.BackendOption { }} } -func (b *DeezerApiBackend) FromConfig(config *config.ServiceConfig) models.Backend { +func (b *DeezerApiBackend) InitConfig(config *config.ServiceConfig) error { b.clientId = config.GetString("client-id") b.clientSecret = config.GetString("client-secret") - return b + return nil } func (b *DeezerApiBackend) OAuth2Strategy(redirectUrl *url.URL) auth.OAuth2Strategy { diff --git a/internal/backends/deezer/deezer_test.go b/internal/backends/deezer/deezer_test.go index 9550c0e..19776f4 100644 --- a/internal/backends/deezer/deezer_test.go +++ b/internal/backends/deezer/deezer_test.go @@ -35,13 +35,14 @@ var ( testTrack []byte ) -func TestFromConfig(t *testing.T) { +func TestInitConfig(t *testing.T) { c := viper.New() c.Set("client-id", "someclientid") c.Set("client-secret", "someclientsecret") service := config.NewServiceConfig("test", c) - backend := (&deezer.DeezerApiBackend{}).FromConfig(&service) - assert.IsType(t, &deezer.DeezerApiBackend{}, backend) + backend := deezer.DeezerApiBackend{} + err := backend.InitConfig(&service) + assert.NoError(t, err) } func TestListenAsListen(t *testing.T) { diff --git a/internal/backends/dump/dump.go b/internal/backends/dump/dump.go index 728a774..70be12d 100644 --- a/internal/backends/dump/dump.go +++ b/internal/backends/dump/dump.go @@ -29,8 +29,8 @@ func (b *DumpBackend) Name() string { return "dump" } func (b *DumpBackend) Options() []models.BackendOption { return nil } -func (b *DumpBackend) FromConfig(config *config.ServiceConfig) models.Backend { - return b +func (b *DumpBackend) InitConfig(config *config.ServiceConfig) error { + return nil } func (b *DumpBackend) StartImport() error { return nil } diff --git a/internal/backends/funkwhale/funkwhale.go b/internal/backends/funkwhale/funkwhale.go index 48c3d8f..99bf43d 100644 --- a/internal/backends/funkwhale/funkwhale.go +++ b/internal/backends/funkwhale/funkwhale.go @@ -51,13 +51,13 @@ func (b *FunkwhaleApiBackend) Options() []models.BackendOption { }} } -func (b *FunkwhaleApiBackend) FromConfig(config *config.ServiceConfig) models.Backend { +func (b *FunkwhaleApiBackend) InitConfig(config *config.ServiceConfig) error { b.client = NewClient( config.GetString("server-url"), config.GetString("token"), ) b.username = config.GetString("username") - return b + return nil } func (b *FunkwhaleApiBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.Progress) { diff --git a/internal/backends/funkwhale/funkwhale_test.go b/internal/backends/funkwhale/funkwhale_test.go index d8654d8..93ab97b 100644 --- a/internal/backends/funkwhale/funkwhale_test.go +++ b/internal/backends/funkwhale/funkwhale_test.go @@ -27,12 +27,13 @@ import ( "go.uploadedlobster.com/scotty/internal/config" ) -func TestFromConfig(t *testing.T) { +func TestInitConfig(t *testing.T) { c := viper.New() c.Set("token", "thetoken") service := config.NewServiceConfig("test", c) - backend := (&funkwhale.FunkwhaleApiBackend{}).FromConfig(&service) - assert.IsType(t, &funkwhale.FunkwhaleApiBackend{}, backend) + backend := funkwhale.FunkwhaleApiBackend{} + err := backend.InitConfig(&service) + assert.NoError(t, err) } func TestFunkwhaleListeningAsListen(t *testing.T) { diff --git a/internal/backends/jspf/jspf.go b/internal/backends/jspf/jspf.go index bfa3892..152c810 100644 --- a/internal/backends/jspf/jspf.go +++ b/internal/backends/jspf/jspf.go @@ -60,7 +60,7 @@ func (b *JSPFBackend) Options() []models.BackendOption { }} } -func (b *JSPFBackend) FromConfig(config *config.ServiceConfig) models.Backend { +func (b *JSPFBackend) InitConfig(config *config.ServiceConfig) error { b.filePath = config.GetString("file-path") b.append = config.GetBool("append", true) b.playlist = jspf.Playlist{ @@ -75,7 +75,7 @@ func (b *JSPFBackend) FromConfig(config *config.ServiceConfig) models.Backend { }, }, } - return b + return nil } func (b *JSPFBackend) StartImport() error { diff --git a/internal/backends/jspf/jspf_test.go b/internal/backends/jspf/jspf_test.go index 31b5370..bf4f99d 100644 --- a/internal/backends/jspf/jspf_test.go +++ b/internal/backends/jspf/jspf_test.go @@ -26,13 +26,14 @@ import ( "go.uploadedlobster.com/scotty/internal/config" ) -func TestFromConfig(t *testing.T) { +func TestInitConfig(t *testing.T) { c := viper.New() c.Set("file-path", "/foo/bar.jspf") c.Set("title", "My Playlist") c.Set("username", "outsidecontext") c.Set("identifier", "http://example.com/playlist1") service := config.NewServiceConfig("test", c) - backend := (&jspf.JSPFBackend{}).FromConfig(&service) - assert.IsType(t, &jspf.JSPFBackend{}, backend) + backend := jspf.JSPFBackend{} + err := backend.InitConfig(&service) + assert.NoError(t, err) } diff --git a/internal/backends/lastfm/lastfm.go b/internal/backends/lastfm/lastfm.go index ba660de..2d4a9d5 100644 --- a/internal/backends/lastfm/lastfm.go +++ b/internal/backends/lastfm/lastfm.go @@ -61,12 +61,12 @@ func (b *LastfmApiBackend) Options() []models.BackendOption { }} } -func (b *LastfmApiBackend) FromConfig(config *config.ServiceConfig) models.Backend { +func (b *LastfmApiBackend) InitConfig(config *config.ServiceConfig) error { clientId := config.GetString("client-id") clientSecret := config.GetString("client-secret") b.client = lastfm.New(clientId, clientSecret) b.username = config.GetString("username") - return b + return nil } func (b *LastfmApiBackend) StartImport() error { return nil } diff --git a/internal/backends/listenbrainz/listenbrainz.go b/internal/backends/listenbrainz/listenbrainz.go index d13c869..49755c6 100644 --- a/internal/backends/listenbrainz/listenbrainz.go +++ b/internal/backends/listenbrainz/listenbrainz.go @@ -56,13 +56,13 @@ func (b *ListenBrainzApiBackend) Options() []models.BackendOption { }} } -func (b *ListenBrainzApiBackend) FromConfig(config *config.ServiceConfig) models.Backend { +func (b *ListenBrainzApiBackend) InitConfig(config *config.ServiceConfig) error { b.client = NewClient(config.GetString("token")) b.mbClient = *musicbrainzws2.NewClient(version.AppName, version.AppVersion) b.client.MaxResults = MaxItemsPerGet b.username = config.GetString("username") b.checkDuplicates = config.GetBool("check-duplicate-listens", false) - return b + return nil } func (b *ListenBrainzApiBackend) StartImport() error { return nil } diff --git a/internal/backends/listenbrainz/listenbrainz_test.go b/internal/backends/listenbrainz/listenbrainz_test.go index 93428d7..bf2e4d3 100644 --- a/internal/backends/listenbrainz/listenbrainz_test.go +++ b/internal/backends/listenbrainz/listenbrainz_test.go @@ -28,12 +28,13 @@ import ( "go.uploadedlobster.com/scotty/internal/config" ) -func TestFromConfig(t *testing.T) { +func TestInitConfig(t *testing.T) { c := viper.New() c.Set("token", "thetoken") service := config.NewServiceConfig("test", c) - backend := (&listenbrainz.ListenBrainzApiBackend{}).FromConfig(&service) - assert.IsType(t, &listenbrainz.ListenBrainzApiBackend{}, backend) + backend := listenbrainz.ListenBrainzApiBackend{} + err := backend.InitConfig(&service) + assert.NoError(t, err) } func TestListenBrainzListenAsListen(t *testing.T) { diff --git a/internal/backends/maloja/maloja.go b/internal/backends/maloja/maloja.go index 135bef3..e9e3348 100644 --- a/internal/backends/maloja/maloja.go +++ b/internal/backends/maloja/maloja.go @@ -51,13 +51,13 @@ func (b *MalojaApiBackend) Options() []models.BackendOption { }} } -func (b *MalojaApiBackend) FromConfig(config *config.ServiceConfig) models.Backend { +func (b *MalojaApiBackend) InitConfig(config *config.ServiceConfig) error { b.client = NewClient( config.GetString("server-url"), config.GetString("token"), ) b.nofix = config.GetBool("nofix", false) - return b + return nil } func (b *MalojaApiBackend) StartImport() error { return nil } diff --git a/internal/backends/maloja/maloja_test.go b/internal/backends/maloja/maloja_test.go index 52be58c..4a1f318 100644 --- a/internal/backends/maloja/maloja_test.go +++ b/internal/backends/maloja/maloja_test.go @@ -26,12 +26,13 @@ import ( "go.uploadedlobster.com/scotty/internal/config" ) -func TestFromConfig(t *testing.T) { +func TestInitConfig(t *testing.T) { c := viper.New() c.Set("token", "thetoken") service := config.NewServiceConfig("test", c) - backend := (&maloja.MalojaApiBackend{}).FromConfig(&service) - assert.IsType(t, &maloja.MalojaApiBackend{}, backend) + backend := maloja.MalojaApiBackend{} + err := backend.InitConfig(&service) + assert.NoError(t, err) } func TestScrobbleAsListen(t *testing.T) { diff --git a/internal/backends/scrobblerlog/scrobblerlog.go b/internal/backends/scrobblerlog/scrobblerlog.go index bf5afac..1fdfaff 100644 --- a/internal/backends/scrobblerlog/scrobblerlog.go +++ b/internal/backends/scrobblerlog/scrobblerlog.go @@ -56,7 +56,7 @@ func (b *ScrobblerLogBackend) Options() []models.BackendOption { }} } -func (b *ScrobblerLogBackend) FromConfig(config *config.ServiceConfig) models.Backend { +func (b *ScrobblerLogBackend) InitConfig(config *config.ServiceConfig) error { b.filePath = config.GetString("file-path") b.includeSkipped = config.GetBool("include-skipped", false) b.append = config.GetBool("append", true) @@ -64,7 +64,7 @@ func (b *ScrobblerLogBackend) FromConfig(config *config.ServiceConfig) models.Ba TZ: scrobblerlog.TZ_UTC, Client: "Rockbox unknown $Revision$", } - return b + return nil } func (b *ScrobblerLogBackend) StartImport() error { diff --git a/internal/backends/scrobblerlog/scrobblerlog_test.go b/internal/backends/scrobblerlog/scrobblerlog_test.go index 04e76c1..7a8ab14 100644 --- a/internal/backends/scrobblerlog/scrobblerlog_test.go +++ b/internal/backends/scrobblerlog/scrobblerlog_test.go @@ -25,10 +25,11 @@ import ( "go.uploadedlobster.com/scotty/internal/config" ) -func TestFromConfig(t *testing.T) { +func TestInitConfig(t *testing.T) { c := viper.New() c.Set("token", "thetoken") service := config.NewServiceConfig("test", c) - backend := (&scrobblerlog.ScrobblerLogBackend{}).FromConfig(&service) - assert.IsType(t, &scrobblerlog.ScrobblerLogBackend{}, backend) + backend := scrobblerlog.ScrobblerLogBackend{} + err := backend.InitConfig(&service) + assert.NoError(t, err) } diff --git a/internal/backends/spotify/spotify.go b/internal/backends/spotify/spotify.go index a4e3c87..ae2fc25 100644 --- a/internal/backends/spotify/spotify.go +++ b/internal/backends/spotify/spotify.go @@ -52,10 +52,10 @@ func (b *SpotifyApiBackend) Options() []models.BackendOption { }} } -func (b *SpotifyApiBackend) FromConfig(config *config.ServiceConfig) models.Backend { +func (b *SpotifyApiBackend) InitConfig(config *config.ServiceConfig) error { b.clientId = config.GetString("client-id") b.clientSecret = config.GetString("client-secret") - return b + return nil } func (b *SpotifyApiBackend) OAuth2Strategy(redirectUrl *url.URL) auth.OAuth2Strategy { diff --git a/internal/backends/spotify/spotify_test.go b/internal/backends/spotify/spotify_test.go index 1aa7e87..8949128 100644 --- a/internal/backends/spotify/spotify_test.go +++ b/internal/backends/spotify/spotify_test.go @@ -38,13 +38,14 @@ var ( testTrack []byte ) -func TestFromConfig(t *testing.T) { +func TestInitConfig(t *testing.T) { c := viper.New() c.Set("client-id", "someclientid") c.Set("client-secret", "someclientsecret") service := config.NewServiceConfig("test", c) - backend := (&spotify.SpotifyApiBackend{}).FromConfig(&service) - assert.IsType(t, &spotify.SpotifyApiBackend{}, backend) + backend := spotify.SpotifyApiBackend{} + err := backend.InitConfig(&service) + assert.NoError(t, err) } func TestSpotifyListenAsListen(t *testing.T) { diff --git a/internal/backends/spotifyhistory/spotifyhistory.go b/internal/backends/spotifyhistory/spotifyhistory.go index 40323a4..1c986be 100644 --- a/internal/backends/spotifyhistory/spotifyhistory.go +++ b/internal/backends/spotifyhistory/spotifyhistory.go @@ -64,12 +64,12 @@ func (b *SpotifyHistoryBackend) Options() []models.BackendOption { }} } -func (b *SpotifyHistoryBackend) FromConfig(config *config.ServiceConfig) models.Backend { +func (b *SpotifyHistoryBackend) InitConfig(config *config.ServiceConfig) error { b.dirPath = config.GetString("dir-path") b.ignoreIncognito = config.GetBool("ignore-incognito", true) b.ignoreSkipped = config.GetBool("ignore-skipped", false) b.skippedMinSeconds = config.GetInt("ignore-min-duration-seconds", 30) - return b + return nil } func (b *SpotifyHistoryBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.Progress) { diff --git a/internal/backends/subsonic/subsonic.go b/internal/backends/subsonic/subsonic.go index 59d4719..1c26bfd 100644 --- a/internal/backends/subsonic/subsonic.go +++ b/internal/backends/subsonic/subsonic.go @@ -52,7 +52,7 @@ func (b *SubsonicApiBackend) Options() []models.BackendOption { }} } -func (b *SubsonicApiBackend) FromConfig(config *config.ServiceConfig) models.Backend { +func (b *SubsonicApiBackend) InitConfig(config *config.ServiceConfig) error { b.client = subsonic.Client{ Client: &http.Client{}, BaseUrl: config.GetString("server-url"), @@ -60,7 +60,7 @@ func (b *SubsonicApiBackend) FromConfig(config *config.ServiceConfig) models.Bac ClientName: version.AppName, } b.password = config.GetString("token") - return b + return nil } func (b *SubsonicApiBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.Progress) { diff --git a/internal/backends/subsonic/subsonic_test.go b/internal/backends/subsonic/subsonic_test.go index f6508c5..638c116 100644 --- a/internal/backends/subsonic/subsonic_test.go +++ b/internal/backends/subsonic/subsonic_test.go @@ -27,13 +27,14 @@ import ( "go.uploadedlobster.com/scotty/internal/config" ) -func TestFromConfig(t *testing.T) { +func TestInitConfig(t *testing.T) { c := viper.New() c.Set("server-url", "https://subsonic.example.com") c.Set("token", "thetoken") service := config.NewServiceConfig("test", c) - backend := (&subsonic.SubsonicApiBackend{}).FromConfig(&service) - assert.IsType(t, &subsonic.SubsonicApiBackend{}, backend) + backend := subsonic.SubsonicApiBackend{} + err := backend.InitConfig(&service) + assert.NoError(t, err) } func TestSongToLove(t *testing.T) { diff --git a/internal/models/interfaces.go b/internal/models/interfaces.go index cc19d8d..1c593d0 100644 --- a/internal/models/interfaces.go +++ b/internal/models/interfaces.go @@ -30,7 +30,7 @@ type Backend interface { Name() string // Initialize the backend from a config. - FromConfig(config *config.ServiceConfig) Backend + InitConfig(config *config.ServiceConfig) error // Return configuration options Options() []BackendOption From ed191d2f15131dea901964227b9c485d85497a3b Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 29 Apr 2025 10:05:40 +0200 Subject: [PATCH 73/82] scrobblerlog: Allow configuring fallback time zone Fixes #6 --- config.example.toml | 7 +++ .../backends/scrobblerlog/scrobblerlog.go | 14 ++++++ .../scrobblerlog/scrobblerlog_test.go | 10 +++++ pkg/scrobblerlog/parser.go | 44 +++++++++---------- pkg/scrobblerlog/parser_test.go | 15 +++++++ 5 files changed, 68 insertions(+), 22 deletions(-) diff --git a/config.example.toml b/config.example.toml index 6a5eb88..bdccb16 100644 --- a/config.example.toml +++ b/config.example.toml @@ -61,6 +61,13 @@ include-skipped = true # If true (default), new listens will be appended to the existing file. Set to # false to overwrite the file and create a new scrobbler log on every run. append = true +# Specify the time zone of the listens in the scrobbler log. While the log files +# are supposed to contain Unix timestamps, which are always in UTC, the player +# writing the log might not be time zone aware. This can cause the timestamps +# to be in a different time zone. Use the time-zone setting to specify a +# different time zone, e.g. "Europe/Berlin" or "America/New_York". +# The default is UTC. +time-zone = "UTC" [service.jspf] # Write listens and loves to JSPF playlist files (https://xspf.org/jspf) diff --git a/internal/backends/scrobblerlog/scrobblerlog.go b/internal/backends/scrobblerlog/scrobblerlog.go index 1fdfaff..22c8577 100644 --- a/internal/backends/scrobblerlog/scrobblerlog.go +++ b/internal/backends/scrobblerlog/scrobblerlog.go @@ -18,6 +18,7 @@ package scrobblerlog import ( "bufio" + "fmt" "os" "sort" "strings" @@ -34,6 +35,7 @@ type ScrobblerLogBackend struct { includeSkipped bool append bool file *os.File + timezone *time.Location log scrobblerlog.ScrobblerLog } @@ -53,6 +55,10 @@ func (b *ScrobblerLogBackend) Options() []models.BackendOption { Label: i18n.Tr("Append to file"), Type: models.Bool, Default: "true", + }, { + Name: "time-zone", + Label: i18n.Tr("Specify a time zone for the listen timestamps"), + Type: models.String, }} } @@ -60,6 +66,14 @@ func (b *ScrobblerLogBackend) InitConfig(config *config.ServiceConfig) error { b.filePath = config.GetString("file-path") b.includeSkipped = config.GetBool("include-skipped", false) b.append = config.GetBool("append", true) + timezone := config.GetString("time-zone") + if timezone != "" { + location, err := time.LoadLocation(timezone) + if err != nil { + return fmt.Errorf("Invalid time-zone %q: %w", timezone, err) + } + b.log.FallbackTimezone = location + } b.log = scrobblerlog.ScrobblerLog{ TZ: scrobblerlog.TZ_UTC, Client: "Rockbox unknown $Revision$", diff --git a/internal/backends/scrobblerlog/scrobblerlog_test.go b/internal/backends/scrobblerlog/scrobblerlog_test.go index 7a8ab14..962aebf 100644 --- a/internal/backends/scrobblerlog/scrobblerlog_test.go +++ b/internal/backends/scrobblerlog/scrobblerlog_test.go @@ -33,3 +33,13 @@ func TestInitConfig(t *testing.T) { err := backend.InitConfig(&service) assert.NoError(t, err) } + +func TestInitConfigInvalidTimezone(t *testing.T) { + c := viper.New() + configuredTimezone := "Invalid/Timezone" + c.Set("time-zone", configuredTimezone) + service := config.NewServiceConfig("test", c) + backend := scrobblerlog.ScrobblerLogBackend{} + err := backend.InitConfig(&service) + assert.ErrorContains(t, err, `Invalid time-zone "Invalid/Timezone"`) +} diff --git a/pkg/scrobblerlog/parser.go b/pkg/scrobblerlog/parser.go index 8f9b88a..9e33754 100644 --- a/pkg/scrobblerlog/parser.go +++ b/pkg/scrobblerlog/parser.go @@ -71,10 +71,12 @@ type Record struct { // Represents a scrobbler log file. type ScrobblerLog struct { - TZ TZInfo - Client string - Records []Record - location *time.Location + TZ TZInfo + Client string + Records []Record + // Timezone to be used for timestamps in the log file, + // if TZ is set to [TZ_UNKNOWN]. + FallbackTimezone *time.Location } func (l *ScrobblerLog) Parse(data io.Reader, includeSkipped bool) error { @@ -173,7 +175,6 @@ func (l *ScrobblerLog) ReadHeader(reader *bufio.Reader) error { timezone, found := strings.CutPrefix(text, "#TZ/") if found { l.TZ = TZInfo(timezone) - l.location = locationFromTimezone(l.TZ) continue } @@ -223,6 +224,11 @@ func (l ScrobblerLog) rowToRecord(row []string) (Record, error) { return record, err } + var timezone *time.Location = nil + if l.TZ == TZ_UNKNOWN { + timezone = l.FallbackTimezone + } + record = Record{ ArtistName: row[0], AlbumName: row[1], @@ -230,7 +236,7 @@ func (l ScrobblerLog) rowToRecord(row []string) (Record, error) { TrackNumber: trackNumber, Duration: time.Duration(duration) * time.Second, Rating: Rating(row[5]), - Timestamp: timeFromLocalTimestamp(timestamp, l.location), + Timestamp: timeFromLocalTimestamp(timestamp, timezone), } if len(row) > 7 { @@ -240,26 +246,20 @@ func (l ScrobblerLog) rowToRecord(row []string) (Record, error) { return record, nil } -// Convert the timezone string from the header to a time.Location. -// Often this is set to "UNKNOWN" in the log file, in which case it defaults -// to UTC. -func locationFromTimezone(timezone TZInfo) *time.Location { - location, err := time.LoadLocation(string(timezone)) - if err != nil { - return time.UTC - } - return location -} - -// Convert a Unix timestamp to a time.Time object, but treat the timestamp +// Convert a Unix timestamp to a [time.Time] object, but treat the timestamp // as being in the given location's timezone instead of UTC. +// If location is nil, the timestamp is returned as UTC. func timeFromLocalTimestamp(timestamp int64, location *time.Location) time.Time { t := time.Unix(timestamp, 0) - // The time is now in UTC. Get the offset to the requested timezone. - _, offset := t.In(location).Zone() - if offset != 0 { - t = t.Add(time.Duration(offset) * time.Second) + // The time is now in UTC. Get the offset to the requested timezone + // and shift the time accordingly. + if location != nil { + _, offset := t.In(location).Zone() + if offset != 0 { + t = t.Add(time.Duration(offset) * time.Second) + } } + return t } diff --git a/pkg/scrobblerlog/parser_test.go b/pkg/scrobblerlog/parser_test.go index 9b4513f..fe2f3ec 100644 --- a/pkg/scrobblerlog/parser_test.go +++ b/pkg/scrobblerlog/parser_test.go @@ -81,6 +81,21 @@ func TestParserExcludeSkipped(t *testing.T) { record4.MusicBrainzRecordingID) } +func TestParserFallbackTimezone(t *testing.T) { + assert := assert.New(t) + data := bytes.NewBufferString(testScrobblerLog) + result := scrobblerlog.ScrobblerLog{ + FallbackTimezone: time.FixedZone("UTC+2", 7200), + } + err := result.Parse(data, false) + require.NoError(t, err) + record1 := result.Records[0] + assert.Equal( + time.Unix(1260342084, 0).Add(2*time.Hour), + record1.Timestamp, + ) +} + func TestAppend(t *testing.T) { assert := assert.New(t) data := make([]byte, 0, 10) From b104c2bc428a9b1d74fc30f85e0fde212f228b72 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 29 Apr 2025 10:10:32 +0200 Subject: [PATCH 74/82] scrobblerlog: fixed listen export progress --- internal/backends/scrobblerlog/scrobblerlog.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/backends/scrobblerlog/scrobblerlog.go b/internal/backends/scrobblerlog/scrobblerlog.go index 22c8577..55c3517 100644 --- a/internal/backends/scrobblerlog/scrobblerlog.go +++ b/internal/backends/scrobblerlog/scrobblerlog.go @@ -153,7 +153,7 @@ func (b *ScrobblerLogBackend) ExportListens(oldestTimestamp time.Time, results c listens = append(listens, recordToListen(record, client)) } sort.Sort(listens.NewerThan(oldestTimestamp)) - progress <- models.Progress{Elapsed: int64(len(listens))}.Complete() + progress <- models.Progress{Total: int64(len(listens))}.Complete() results <- models.ListensResult{Items: listens} } From 159f486cdca54ff98f7e5be4ea0c7152c20854ac Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 29 Apr 2025 10:32:59 +0200 Subject: [PATCH 75/82] Upgrade musicbrainzws2 --- go.mod | 2 +- go.sum | 4 ++-- internal/backends/listenbrainz/listenbrainz.go | 6 +++++- internal/version/version.go | 1 + 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 22a3154..ef1286c 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.9.3 go.uploadedlobster.com/mbtypes v0.4.0 - go.uploadedlobster.com/musicbrainzws2 v0.13.1 + go.uploadedlobster.com/musicbrainzws2 v0.14.0 golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 golang.org/x/oauth2 v0.29.0 golang.org/x/text v0.24.0 diff --git a/go.sum b/go.sum index 1ee05c8..8ade87a 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.13.1 h1:34GKI7l9eTCyh9ozNOHmlwAAUTDK9WVRsFZK5trxcwQ= -go.uploadedlobster.com/musicbrainzws2 v0.13.1/go.mod h1:TVln70Fzp/++fw0/jCP1xXwgilVwDkzTwRbV8GwUYLA= +go.uploadedlobster.com/musicbrainzws2 v0.14.0 h1:YaEtxNwLSNT1gzFipQ4XlaThNfXjBpzzb4I6WhIeUwg= +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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= diff --git a/internal/backends/listenbrainz/listenbrainz.go b/internal/backends/listenbrainz/listenbrainz.go index 49755c6..d0074b1 100644 --- a/internal/backends/listenbrainz/listenbrainz.go +++ b/internal/backends/listenbrainz/listenbrainz.go @@ -58,7 +58,11 @@ func (b *ListenBrainzApiBackend) Options() []models.BackendOption { func (b *ListenBrainzApiBackend) InitConfig(config *config.ServiceConfig) error { b.client = NewClient(config.GetString("token")) - b.mbClient = *musicbrainzws2.NewClient(version.AppName, version.AppVersion) + b.mbClient = *musicbrainzws2.NewClient(musicbrainzws2.AppInfo{ + Name: version.AppName, + Version: version.AppVersion, + URL: version.AppURL, + }) b.client.MaxResults = MaxItemsPerGet b.username = config.GetString("username") b.checkDuplicates = config.GetBool("check-duplicate-listens", false) diff --git a/internal/version/version.go b/internal/version/version.go index 3f02fe2..818bec1 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -18,6 +18,7 @@ package version const ( AppName = "scotty" AppVersion = "0.4.1" + AppURL = "https://git.sr.ht/~phw/scotty/" ) func UserAgent() string { From 47486ff659bdf084cc7211c25e7f2633c9bcfc91 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 29 Apr 2025 11:05:37 +0200 Subject: [PATCH 76/82] Update weblate configuration --- .build.yml | 2 +- .weblate | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 .weblate diff --git a/.build.yml b/.build.yml index 8be4e81..32f13ec 100644 --- a/.build.yml +++ b/.build.yml @@ -5,7 +5,7 @@ packages: - hut - weblate-wlc secrets: - - 2a17e258-3e99-4093-9527-832c350d9c53 + - eafb7dc6-c02d-4b26-a960-61b968a4f454 oauth: pages.sr.ht/PAGES:RW tasks: - weblate-update: | diff --git a/.weblate b/.weblate new file mode 100644 index 0000000..9c9511e --- /dev/null +++ b/.weblate @@ -0,0 +1,3 @@ +[weblate] +url = https://translate.uploadedlobster.com/api/ +translation = scotty/app From c817480809516756aa7709d8a1221f546926cea3 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 29 Apr 2025 11:12:28 +0200 Subject: [PATCH 77/82] Updated Weblate CI secret and fixed build --- .build.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.build.yml b/.build.yml index 32f13ec..d1064df 100644 --- a/.build.yml +++ b/.build.yml @@ -5,10 +5,11 @@ packages: - hut - weblate-wlc secrets: - - eafb7dc6-c02d-4b26-a960-61b968a4f454 + - 0e2ad815-6c46-4cea-878e-70fc33f71e77 oauth: pages.sr.ht/PAGES:RW tasks: - weblate-update: | + cd scotty wlc --format text pull scotty - test: | cd scotty From 597914e6db39ea6e63ad48d1bd76d3ed30f09ffd Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 29 Apr 2025 11:15:49 +0200 Subject: [PATCH 78/82] Announce new releases to Go Module Index --- .build.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.build.yml b/.build.yml index d1064df..a5d2238 100644 --- a/.build.yml +++ b/.build.yml @@ -29,5 +29,15 @@ tasks: - publish-redirect: | # Update redirect on https://go.uploadedlobster.com/scotty ./scotty/pages/publish.sh + # Skip releasing if this is not a tagged release + - only-tags: | + cd scotty + GIT_REF=$(git describe --always) + [[ "$GIT_REF" =~ ^v[0-9]+\.[0-9]+(\.[0-9]+)?$ ]] || complete-build + - announce-release: | + # Announce new release to Go Module Index + cd scotty + VERSION=$(git describe --exact-match) + curl "https://proxy.golang.org/go.uploadedlobster.com/scotty/@v/${VERSION}.info" artifacts: - scotty/dist/artifacts.tar From e135ea5fa90f46d3d17394b274870baee7510c0c Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 29 Apr 2025 11:43:42 +0200 Subject: [PATCH 79/82] Update goreleaser config file format --- .goreleaser.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 06b612a..48c88c8 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -6,7 +6,7 @@ # yaml-language-server: $schema=https://goreleaser.com/static/schema.json # vim: set ts=2 sw=2 tw=0 fo=cnqoj -version: 1 +version: 2 before: hooks: @@ -28,7 +28,7 @@ universal_binaries: - replace: true archives: - - format: tar.gz + - formats: ['tar.gz'] # this name template makes the OS and Arch compatible with the results of `uname`. name_template: >- {{ .ProjectName }}-{{ .Version }}_ @@ -42,7 +42,7 @@ archives: # use zip for windows archives format_overrides: - goos: windows - format: zip + formats: ['zip'] files: - COPYING - README.md From 82858315fa9598aff00136ab088cdc14c1e74aab Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 29 Apr 2025 11:44:04 +0200 Subject: [PATCH 80/82] Disable Linux 386 builds Compilaton fails with latest gorm --- .goreleaser.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 48c88c8..1a1e0ba 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -21,6 +21,8 @@ builds: - windows - darwin ignore: + - goos: linux + goarch: "386" - goos: windows goarch: "386" From 1516a3a9d6eb5d5c834c73c9d72fdfecdc03c379 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 29 Apr 2025 12:57:28 +0200 Subject: [PATCH 81/82] scrobblerlog: renamed setting include-skipped to ignore-skipped This makes the setting consistent with the similar setting for spotify --- config.example.toml | 10 ++++---- .../backends/scrobblerlog/scrobblerlog.go | 23 ++++++++++--------- pkg/scrobblerlog/parser.go | 11 ++++----- pkg/scrobblerlog/parser_test.go | 6 ++--- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/config.example.toml b/config.example.toml index bdccb16..6b81bac 100644 --- a/config.example.toml +++ b/config.example.toml @@ -56,8 +56,8 @@ backend = "scrobbler-log" # The file path to the .scrobbler.log file. Relative paths are resolved against # the current working directory when running scotty. file-path = "./.scrobbler.log" -# If true, reading listens from the file also returns listens marked as "skipped" -include-skipped = true +# If true (default), ignore listens marked as skipped. +ignore-skipped = true # If true (default), new listens will be appended to the existing file. Set to # false to overwrite the file and create a new scrobbler log on every run. append = true @@ -105,9 +105,9 @@ dir-path = "./my_spotify_data_extended/Spotify Extended Streaming Histor ignore-incognito = true # If true, ignore listens marked as skipped. Default is false. ignore-skipped = false -# Only consider skipped listens with a playback duration longer than this number -# of seconds. Default is 30 seconds. If ignore-skipped is set to false this -# setting has no effect. +# Only consider skipped listens with a playback duration longer than or equal to +# this number of seconds. Default is 30 seconds. If ignore-skipped is enabled +# this setting has no effect. ignore-min-duration-seconds = 30 [service.deezer] diff --git a/internal/backends/scrobblerlog/scrobblerlog.go b/internal/backends/scrobblerlog/scrobblerlog.go index 55c3517..26d417a 100644 --- a/internal/backends/scrobblerlog/scrobblerlog.go +++ b/internal/backends/scrobblerlog/scrobblerlog.go @@ -31,12 +31,12 @@ import ( ) type ScrobblerLogBackend struct { - filePath string - includeSkipped bool - append bool - file *os.File - timezone *time.Location - log scrobblerlog.ScrobblerLog + filePath string + ignoreSkipped bool + append bool + file *os.File + timezone *time.Location + log scrobblerlog.ScrobblerLog } func (b *ScrobblerLogBackend) Name() string { return "scrobbler-log" } @@ -47,9 +47,10 @@ func (b *ScrobblerLogBackend) Options() []models.BackendOption { Label: i18n.Tr("File path"), Type: models.String, }, { - Name: "include-skipped", - Label: i18n.Tr("Include skipped listens"), - Type: models.Bool, + Name: "ignore-skipped", + Label: i18n.Tr("Ignore skipped listens"), + Type: models.Bool, + Default: "true", }, { Name: "append", Label: i18n.Tr("Append to file"), @@ -64,7 +65,7 @@ func (b *ScrobblerLogBackend) Options() []models.BackendOption { func (b *ScrobblerLogBackend) InitConfig(config *config.ServiceConfig) error { b.filePath = config.GetString("file-path") - b.includeSkipped = config.GetBool("include-skipped", false) + b.ignoreSkipped = config.GetBool("ignore-skipped", true) b.append = config.GetBool("append", true) timezone := config.GetString("time-zone") if timezone != "" { @@ -140,7 +141,7 @@ func (b *ScrobblerLogBackend) ExportListens(oldestTimestamp time.Time, results c defer file.Close() - err = b.log.Parse(file, b.includeSkipped) + err = b.log.Parse(file, b.ignoreSkipped) if err != nil { progress <- models.Progress{}.Complete() results <- models.ListensResult{Error: err} diff --git a/pkg/scrobblerlog/parser.go b/pkg/scrobblerlog/parser.go index 9e33754..892f6e8 100644 --- a/pkg/scrobblerlog/parser.go +++ b/pkg/scrobblerlog/parser.go @@ -79,7 +79,7 @@ type ScrobblerLog struct { FallbackTimezone *time.Location } -func (l *ScrobblerLog) Parse(data io.Reader, includeSkipped bool) error { +func (l *ScrobblerLog) Parse(data io.Reader, ignoreSkipped bool) error { l.Records = make([]Record, 0) reader := bufio.NewReader(data) @@ -111,16 +111,15 @@ func (l *ScrobblerLog) Parse(data io.Reader, includeSkipped bool) error { return fmt.Errorf("invalid record in scrobblerlog line %v", line) } - rating := row[5] - if !includeSkipped && rating == "S" { - continue - } - record, err := l.rowToRecord(row) if err != nil { return err } + if ignoreSkipped && record.Rating == RATING_SKIPPED { + continue + } + l.Records = append(l.Records, record) } diff --git a/pkg/scrobblerlog/parser_test.go b/pkg/scrobblerlog/parser_test.go index fe2f3ec..7fd57c3 100644 --- a/pkg/scrobblerlog/parser_test.go +++ b/pkg/scrobblerlog/parser_test.go @@ -48,7 +48,7 @@ func TestParser(t *testing.T) { assert := assert.New(t) data := bytes.NewBufferString(testScrobblerLog) result := scrobblerlog.ScrobblerLog{} - err := result.Parse(data, true) + err := result.Parse(data, false) require.NoError(t, err) assert.Equal(scrobblerlog.TZ_UNKNOWN, result.TZ) assert.Equal("Rockbox sansaclipplus $Revision$", result.Client) @@ -68,11 +68,11 @@ func TestParser(t *testing.T) { record4.MusicBrainzRecordingID) } -func TestParserExcludeSkipped(t *testing.T) { +func TestParserIgnoreSkipped(t *testing.T) { assert := assert.New(t) data := bytes.NewBufferString(testScrobblerLog) result := scrobblerlog.ScrobblerLog{} - err := result.Parse(data, false) + err := result.Parse(data, true) require.NoError(t, err) assert.Len(result.Records, 4) record4 := result.Records[3] From 39b31fc664dab411a033d2908930cb5eaa390d8b Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 29 Apr 2025 13:01:54 +0200 Subject: [PATCH 82/82] Update changelog --- CHANGES.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 11251cd..cda0d79 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,12 @@ - ListenBrainz: log missing recording MBID on love import - Subsonic: support OpenSubsonic fields for recording MBID and genres (#5) - Subsonic: fixed progress for loves export +- scrobblerlog: add "time-zone" config option (#6). +- scrobblerlog: fixed progress for listen export +- scrobblerlog: renamed setting `include-skipped` to `ignore-skipped`. + +Note: 386 builds for Linux are not available with this release due to an +incompatibility with latest version of gorm. ## 0.4.1 - 2024-09-16