From 46e6a667c8037e01846fef4ddad7caf5d023ddb0 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Fri, 24 Nov 2023 08:58:31 +0100 Subject: [PATCH 001/150] Added constants for app name and version, use custom user-agent --- cmd/root.go | 9 +++--- internal/backends/deezer/client.go | 2 ++ internal/backends/funkwhale/client.go | 2 ++ internal/backends/listenbrainz/client.go | 2 ++ .../backends/listenbrainz/listenbrainz.go | 4 ++- internal/backends/maloja/client.go | 2 ++ internal/backends/spotify/client.go | 2 ++ internal/backends/subsonic/subsonic.go | 3 +- internal/version/version.go | 25 ++++++++++++++++ internal/version/version_test.go | 30 +++++++++++++++++++ 10 files changed, 75 insertions(+), 6 deletions(-) create mode 100644 internal/version/version.go create mode 100644 internal/version/version_test.go diff --git a/cmd/root.go b/cmd/root.go index f1cc298..10dc528 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -23,17 +23,18 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" + "go.uploadedlobster.com/scotty/internal/version" ) var cfgFile string // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ - Use: "scotty", + Use: version.AppName, Short: "Beam data between music listening services", Long: `Scotty transfers your listens/scrobbles between ListenBrainz and various other listening and streaming services.`, - Version: "0.1.0", + Version: version.AppVersion, // Uncomment the following line if your bare application // has an action associated with it: // Run: func(cmd *cobra.Command, args []string) { }, @@ -67,7 +68,7 @@ func init() { func defaultConfigDir() string { configDir, err := os.UserConfigDir() cobra.CheckErr(err) - return path.Join(configDir, "scotty") + return path.Join(configDir, version.AppName) } // initConfig reads in config file and ENV variables if set. @@ -78,7 +79,7 @@ func initConfig() { } else { viper.AddConfigPath(defaultConfigDir()) viper.SetConfigType("toml") - viper.SetConfigName("scotty") + viper.SetConfigName(version.AppName) } viper.AutomaticEnv() // read in environment variables that match diff --git a/internal/backends/deezer/client.go b/internal/backends/deezer/client.go index ef48a4c..0d9cbb0 100644 --- a/internal/backends/deezer/client.go +++ b/internal/backends/deezer/client.go @@ -27,6 +27,7 @@ import ( "strconv" "github.com/go-resty/resty/v2" + "go.uploadedlobster.com/scotty/internal/version" "golang.org/x/oauth2" ) @@ -43,6 +44,7 @@ func NewClient(token oauth2.TokenSource) Client { client := resty.New() client.SetBaseURL(baseURL) client.SetHeader("Accept", "application/json") + client.SetHeader("User-Agent", version.UserAgent()) client.SetRetryCount(5) return Client{ HttpClient: client, diff --git a/internal/backends/funkwhale/client.go b/internal/backends/funkwhale/client.go index ebd049c..b757d6f 100644 --- a/internal/backends/funkwhale/client.go +++ b/internal/backends/funkwhale/client.go @@ -27,6 +27,7 @@ import ( "github.com/go-resty/resty/v2" "go.uploadedlobster.com/scotty/internal/ratelimit" + "go.uploadedlobster.com/scotty/internal/version" ) const MaxItemsPerGet = 50 @@ -42,6 +43,7 @@ func NewClient(serverUrl string, token string) Client { client.SetAuthScheme("Bearer") client.SetAuthToken(token) client.SetHeader("Accept", "application/json") + client.SetHeader("User-Agent", version.UserAgent()) // Handle rate limiting (see https://docs.funkwhale.audio/developer/api/rate-limit.html) ratelimit.EnableHTTPHeaderRateLimit(client, "Retry-After") diff --git a/internal/backends/listenbrainz/client.go b/internal/backends/listenbrainz/client.go index da59144..aa30b78 100644 --- a/internal/backends/listenbrainz/client.go +++ b/internal/backends/listenbrainz/client.go @@ -28,6 +28,7 @@ import ( "github.com/go-resty/resty/v2" "go.uploadedlobster.com/scotty/internal/ratelimit" + "go.uploadedlobster.com/scotty/internal/version" ) const ( @@ -48,6 +49,7 @@ func NewClient(token string) Client { client.SetAuthScheme("Token") client.SetAuthToken(token) client.SetHeader("Accept", "application/json") + client.SetHeader("User-Agent", version.UserAgent()) // Handle rate limiting (see https://listenbrainz.readthedocs.io/en/latest/users/api/index.html#rate-limiting) ratelimit.EnableHTTPHeaderRateLimit(client, "X-RateLimit-Reset-In") diff --git a/internal/backends/listenbrainz/listenbrainz.go b/internal/backends/listenbrainz/listenbrainz.go index 35cd37a..e972eca 100644 --- a/internal/backends/listenbrainz/listenbrainz.go +++ b/internal/backends/listenbrainz/listenbrainz.go @@ -23,6 +23,7 @@ import ( "github.com/spf13/viper" "go.uploadedlobster.com/scotty/internal/models" + "go.uploadedlobster.com/scotty/internal/version" ) type ListenBrainzApiBackend struct { @@ -119,7 +120,8 @@ func (b *ListenBrainzApiBackend) ImportListens(export models.ListensResult, impo AdditionalInfo: l.AdditionalInfo, }, } - listen.TrackMetadata.AdditionalInfo["submission_client"] = "Scotty" + listen.TrackMetadata.AdditionalInfo["submission_client"] = version.AppName + listen.TrackMetadata.AdditionalInfo["submission_client_version"] = version.AppVersion submission.Payload = append(submission.Payload, listen) } diff --git a/internal/backends/maloja/client.go b/internal/backends/maloja/client.go index 5e3ea31..3b79110 100644 --- a/internal/backends/maloja/client.go +++ b/internal/backends/maloja/client.go @@ -26,6 +26,7 @@ import ( "strconv" "github.com/go-resty/resty/v2" + "go.uploadedlobster.com/scotty/internal/version" ) const MaxItemsPerGet = 1000 @@ -39,6 +40,7 @@ func NewClient(serverUrl string, token string) Client { client := resty.New() client.SetBaseURL(serverUrl) client.SetHeader("Accept", "application/json") + client.SetHeader("User-Agent", version.UserAgent()) client.SetRetryCount(5) return Client{ HttpClient: client, diff --git a/internal/backends/spotify/client.go b/internal/backends/spotify/client.go index 2ec12b0..08c00f3 100644 --- a/internal/backends/spotify/client.go +++ b/internal/backends/spotify/client.go @@ -30,6 +30,7 @@ import ( "github.com/go-resty/resty/v2" "go.uploadedlobster.com/scotty/internal/ratelimit" + "go.uploadedlobster.com/scotty/internal/version" "golang.org/x/oauth2" ) @@ -48,6 +49,7 @@ func NewClient(token oauth2.TokenSource) Client { client := resty.NewWithClient(httpClient) client.SetBaseURL(baseURL) client.SetHeader("Accept", "application/json") + client.SetHeader("User-Agent", version.UserAgent()) // Handle rate limiting (see https://developer.spotify.com/documentation/web-api/concepts/rate-limits) ratelimit.EnableHTTPHeaderRateLimit(client, "Retry-After") diff --git a/internal/backends/subsonic/subsonic.go b/internal/backends/subsonic/subsonic.go index 348a249..732b9ab 100644 --- a/internal/backends/subsonic/subsonic.go +++ b/internal/backends/subsonic/subsonic.go @@ -24,6 +24,7 @@ import ( "github.com/delucks/go-subsonic" "github.com/spf13/viper" "go.uploadedlobster.com/scotty/internal/models" + "go.uploadedlobster.com/scotty/internal/version" ) type SubsonicApiBackend struct { @@ -38,7 +39,7 @@ func (b *SubsonicApiBackend) FromConfig(config *viper.Viper) models.Backend { Client: &http.Client{}, BaseUrl: config.GetString("server-url"), User: config.GetString("username"), - ClientName: "Scotty", + ClientName: version.AppName, } b.password = config.GetString("token") return b diff --git a/internal/version/version.go b/internal/version/version.go new file mode 100644 index 0000000..43a6205 --- /dev/null +++ b/internal/version/version.go @@ -0,0 +1,25 @@ +/* +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 version + +const ( + AppName = "scotty" + AppVersion = "0.1.0" +) + +func UserAgent() string { + return AppName + "/" + AppVersion +} diff --git a/internal/version/version_test.go b/internal/version/version_test.go new file mode 100644 index 0000000..4472b91 --- /dev/null +++ b/internal/version/version_test.go @@ -0,0 +1,30 @@ +/* +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 version_test + +import ( + "testing" + + "go.uploadedlobster.com/scotty/internal/version" +) + +func TestUserAgent(t *testing.T) { + expected := "scotty/" + version.AppVersion + ua := version.UserAgent() + if ua != expected { + t.Errorf("Expected UserAgent to be '%v', got '%v'", expected, ua) + } +} From 5aed55285485a24f92b7996f80f87daccb454c04 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Fri, 24 Nov 2023 10:30:11 +0100 Subject: [PATCH 002/150] Moved tokensource to auth package --- internal/{backends => auth}/tokensource.go | 2 +- internal/backends/auth.go | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) rename internal/{backends => auth}/tokensource.go (99%) diff --git a/internal/backends/tokensource.go b/internal/auth/tokensource.go similarity index 99% rename from internal/backends/tokensource.go rename to internal/auth/tokensource.go index 6dfc184..626f3c4 100644 --- a/internal/backends/tokensource.go +++ b/internal/auth/tokensource.go @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with Scotty. If not, see . */ -package backends +package auth import ( "context" diff --git a/internal/backends/auth.go b/internal/backends/auth.go index 5bca84c..49584ec 100644 --- a/internal/backends/auth.go +++ b/internal/backends/auth.go @@ -22,6 +22,7 @@ import ( "strings" "github.com/spf13/viper" + "go.uploadedlobster.com/scotty/internal/auth" "go.uploadedlobster.com/scotty/internal/models" "go.uploadedlobster.com/scotty/internal/storage" ) @@ -36,19 +37,19 @@ 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, auth := backend.(models.OAuth2Authenticator) - if auth { + authenticator, needAuth := backend.(models.OAuth2Authenticator) + if needAuth { redirectURL, err := BuildRedirectURL(config, backend.Name()) if err != nil { - return auth, err + return needAuth, err } token, err := db.GetOAuth2Token(service) if err != nil { - return auth, err + return needAuth, err } conf := authenticator.OAuth2Strategy(redirectURL).Config() - tokenSource := NewDatabaseTokenSource(db, service, &conf, token) + tokenSource := auth.NewDatabaseTokenSource(db, service, &conf, token) authenticator.OAuth2Setup(tokenSource) } - return auth, nil + return needAuth, nil } From 0828fa00fbbfb7bb73ca6c252ea032ef6080809d Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Fri, 24 Nov 2023 14:49:02 +0100 Subject: [PATCH 003/150] Use goreleaser for packaging --- .build.yml | 18 ++++++++-------- .gitignore | 1 + .goreleaser.yaml | 53 ++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 33 +++++++++++++++--------------- go.sum | 48 +++++++++++++++++++++++++++++++++++-------- 5 files changed, 120 insertions(+), 33 deletions(-) create mode 100644 .goreleaser.yaml diff --git a/.build.yml b/.build.yml index cc71df2..f21034b 100644 --- a/.build.yml +++ b/.build.yml @@ -1,6 +1,7 @@ -image: alpine/edge +image: archlinux packages: - go + - goreleaser-bin - hut oauth: pages.sr.ht/PAGES:RW tasks: @@ -10,15 +11,14 @@ tasks: go test -v ./... - build: | cd scotty - GOOS=windows GOARCH=amd64 go build -o bin/scotty.exe . - GOOS=linux GOARCH=amd64 go build -o bin/scotty-linux . - GOOS=darwin GOARCH=amd64 go build -o bin/scotty-mac-intel . - GOOS=darwin GOARCH=arm64 go build -o bin/scotty-mac-arm . + goreleaser release --snapshot --clean - publish-redirect: | # Update redirect on https://go.uploadedlobster.com/scotty ./scotty/pages/publish.sh artifacts: - - scotty/bin/scotty.exe - - scotty/bin/scotty-linux - - scotty/bin/scotty-mac-intel - - scotty/bin/scotty-mac-arm + - scotty/dist/scotty_Darwin_all.tar.gz + - scotty/dist/scotty_Linux_arm64.tar.gz + - scotty/dist/scotty_Linux_i386.tar.gz + - scotty/dist/scotty_Linux_x86_64.tar.gz + - scotty/dist/scotty_Windows_arm64.zip + - scotty/dist/scotty_Windows_x86_64.zip diff --git a/.gitignore b/.gitignore index 3ca8ad7..13d4620 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ /scotty.toml # Binaries for programs and plugins +dist/ *.exe *.dll *.so diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..69adc4c --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,53 @@ +# This is an example .goreleaser.yml file with some sensible defaults. +# Make sure to check the documentation at https://goreleaser.com + +# The lines below are called `modelines`. See `:help modeline` +# Feel free to remove those if you don't want/need to use them. +# yaml-language-server: $schema=https://goreleaser.com/static/schema.json +# vim: set ts=2 sw=2 tw=0 fo=cnqoj + +version: 1 + +before: + hooks: + # You may remove this if you don't use go modules. + - go mod tidy + +builds: + - env: + - CGO_ENABLED=0 + goos: + - linux + - windows + - darwin + ignore: + - goos: windows + goarch: "386" + +universal_binaries: + - replace: true + +archives: + - format: tar.gz + # this name template makes the OS and Arch compatible with the results of `uname`. + name_template: >- + {{ .ProjectName }}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + # use zip for windows archives + format_overrides: + - goos: windows + format: zip + files: + - COPYING + - README.md + +# changelog: +# sort: asc +# filters: +# exclude: +# - "^docs:" +# - "^test:" diff --git a/go.mod b/go.mod index 41da809..f6e61af 100644 --- a/go.mod +++ b/go.mod @@ -2,24 +2,35 @@ module go.uploadedlobster.com/scotty go 1.21.1 +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.10.0 + github.com/jarcoal/httpmock v1.3.1 + github.com/spf13/cobra v1.8.0 + github.com/spf13/viper v1.17.0 + 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 + gorm.io/datatypes v1.2.0 + gorm.io/gorm v1.25.5 +) + require ( github.com/VividCortex/ewma v1.2.0 // indirect github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect - github.com/cli/browser v1.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/delucks/go-subsonic v0.0.0-20220915164742-2744002c4be5 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/fatih/color v1.16.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/glebarez/go-sqlite v1.21.2 // indirect - github.com/glebarez/sqlite v1.10.0 // indirect - github.com/go-resty/resty/v2 v2.10.0 // indirect github.com/go-sql-driver/mysql v1.7.0 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/uuid v1.4.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jarcoal/httpmock v1.3.1 // 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 @@ -36,28 +47,18 @@ require ( github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.10.0 // indirect github.com/spf13/cast v1.5.1 // indirect - github.com/spf13/cobra v1.8.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/spf13/viper v1.17.0 // indirect - github.com/stretchr/objx v0.5.0 // indirect - github.com/stretchr/testify v1.8.4 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/vbauerster/mpb v3.4.0+incompatible // indirect - github.com/vbauerster/mpb/v8 v8.6.2 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect - golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/net v0.18.0 // indirect - golang.org/x/oauth2 v0.14.0 // indirect golang.org/x/sys v0.14.0 // indirect golang.org/x/text v0.14.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - gorm.io/datatypes v1.2.0 // indirect gorm.io/driver/mysql v1.4.7 // indirect - gorm.io/gorm v1.25.5 // indirect modernc.org/libc v1.34.3 // indirect modernc.org/mathutil v1.6.0 // indirect modernc.org/memory v1.7.2 // indirect diff --git a/go.sum b/go.sum index 2dcb0ae..84e28c7 100644 --- a/go.sum +++ b/go.sum @@ -69,6 +69,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m 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= @@ -82,6 +84,10 @@ github.com/go-resty/resty/v2 v2.10.0 h1:Qla4W/+TMmv0fOeeRqzEpXPLfTUnR5HZ1+lGs+Ck 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-sql-driver/mysql v1.7.0/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= @@ -122,6 +128,8 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ 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= @@ -135,6 +143,8 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf 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= @@ -150,6 +160,12 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1: 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= +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/pgx/v5 v5.3.0 h1:/NQi8KHMpKWHInxXesC8yD4DhkXPrVhmnwYkjp9AmBA= +github.com/jackc/pgx/v5 v5.3.0/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8= 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= @@ -162,8 +178,12 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X 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= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -173,6 +193,12 @@ 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/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.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= @@ -189,6 +215,8 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ 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= @@ -208,7 +236,6 @@ 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/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 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= 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= @@ -220,8 +247,6 @@ 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 v3.4.0+incompatible h1:mfiiYw87ARaeRW6x5gWwYRUawxaW1tLAD8IceomUCNw= -github.com/vbauerster/mpb v3.4.0+incompatible/go.mod h1:zAHG26FUhVKETRu+MWqYXcI70POlC6N8up9p1dID7SU= 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= @@ -248,6 +273,8 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm 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= @@ -320,7 +347,6 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx 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 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= 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= @@ -389,9 +415,6 @@ golang.org/x/sys v0.0.0-20220908164124-27713097b956/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.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= 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= @@ -410,12 +433,13 @@ 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.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 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= 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/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= @@ -565,6 +589,8 @@ google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 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= @@ -576,6 +602,12 @@ 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/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.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= From ed784643f78a37a1477563a7123f3eb0fe7b4719 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Fri, 24 Nov 2023 16:04:47 +0100 Subject: [PATCH 004/150] Removed workspace files These should not be needed --- go.work | 3 --- go.work.sum | 24 ------------------------ 2 files changed, 27 deletions(-) delete mode 100644 go.work delete mode 100644 go.work.sum diff --git a/go.work b/go.work deleted file mode 100644 index 5e9a32c..0000000 --- a/go.work +++ /dev/null @@ -1,3 +0,0 @@ -go 1.21.1 - -use . diff --git a/go.work.sum b/go.work.sum deleted file mode 100644 index f53dd90..0000000 --- a/go.work.sum +++ /dev/null @@ -1,24 +0,0 @@ -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= -github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= -github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From ca99a3d5fb71112147d01922d1eb8d341a3eb666 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 26 Nov 2023 12:22:25 +0100 Subject: [PATCH 005/150] jspf: consider love track MBID --- internal/backends/jspf/jspf.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/internal/backends/jspf/jspf.go b/internal/backends/jspf/jspf.go index f08a225..26bc57f 100644 --- a/internal/backends/jspf/jspf.go +++ b/internal/backends/jspf/jspf.go @@ -98,8 +98,12 @@ func loveAsTrack(l models.Love) jspf.Track { extension.AddedBy = l.UserName track.Extension[jspf.MusicBrainzTrackExtensionId] = extension + recordingMbid := l.Track.RecordingMbid if l.RecordingMbid != "" { - track.Identifier = append(track.Identifier, "https://musicbrainz.org/recording/"+string(l.RecordingMbid)) + recordingMbid = l.RecordingMbid + } + if recordingMbid != "" { + track.Identifier = append(track.Identifier, "https://musicbrainz.org/recording/"+string(recordingMbid)) } return track From 0045a5096ee44cbfea13bb2a889d1c0b1aca434d Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 26 Nov 2023 15:25:56 +0100 Subject: [PATCH 006/150] listenbrainz: fixed listens pagination --- internal/backends/listenbrainz/listenbrainz.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/backends/listenbrainz/listenbrainz.go b/internal/backends/listenbrainz/listenbrainz.go index e972eca..cafcc0f 100644 --- a/internal/backends/listenbrainz/listenbrainz.go +++ b/internal/backends/listenbrainz/listenbrainz.go @@ -98,7 +98,7 @@ out: func (b *ListenBrainzApiBackend) ImportListens(export models.ListensResult, importResult models.ImportResult, progress chan models.Progress) (models.ImportResult, error) { total := len(export.Listens) for i := 0; i < total; i += MaxListensPerRequest { - listens := export.Listens[i:min(i+MaxItemsPerGet, total)] + listens := export.Listens[i:min(i+MaxListensPerRequest, total)] count := len(listens) if count == 0 { break From 3ccbb20a9e2d6591961722224a36764c3ce15eb5 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 26 Nov 2023 12:44:48 +0100 Subject: [PATCH 007/150] Fixed tracking import errors --- internal/models/models.go | 1 + internal/models/models_test.go | 3 +++ 2 files changed, 4 insertions(+) diff --git a/internal/models/models.go b/internal/models/models.go index 7500759..2175ea1 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -181,6 +181,7 @@ 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...) } type Progress struct { diff --git a/internal/models/models_test.go b/internal/models/models_test.go index da01c36..59cc4dd 100644 --- a/internal/models/models_test.go +++ b/internal/models/models_test.go @@ -121,16 +121,19 @@ func TestImportResultUpdate(t *testing.T) { TotalCount: 100, ImportCount: 20, LastTimestamp: time.Now(), + ImportErrors: []string{"foo"}, } newResult := models.ImportResult{ TotalCount: 120, ImportCount: 50, LastTimestamp: time.Now().Add(1 * time.Hour), + ImportErrors: []string{"bar"}, } 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) } func TestImportResultUpdateTimestamp(t *testing.T) { From 5b8f4788f952d995279502263164ca1cde9fb678 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Thu, 23 Nov 2023 23:14:47 +0100 Subject: [PATCH 008/150] lastfm: authentication --- cmd/auth.go | 18 +++++----- go.mod | 1 + go.sum | 2 ++ internal/auth/callback.go | 4 +-- internal/auth/strategy.go | 21 ++++++++++-- internal/backends/backends.go | 2 ++ internal/backends/deezer/auth.go | 9 +++-- internal/backends/lastfm/auth.go | 51 ++++++++++++++++++++++++++++ internal/backends/lastfm/lastfm.go | 54 ++++++++++++++++++++++++++++++ scotty.example.toml | 11 ++++++ 10 files changed, 158 insertions(+), 15 deletions(-) create mode 100644 internal/backends/lastfm/auth.go create mode 100644 internal/backends/lastfm/lastfm.go diff --git a/cmd/auth.go b/cmd/auth.go index 810c41a..67b7411 100644 --- a/cmd/auth.go +++ b/cmd/auth.go @@ -43,10 +43,6 @@ var authCmd = &cobra.Command{ redirectURL, err := backends.BuildRedirectURL(viper.GetViper(), backend.Name()) cobra.CheckErr(err) - // Start an HTTP server to listen for the response - responseChan := make(chan auth.CodeResponse) - auth.RunOauth2CallbackServer(*redirectURL, responseChan) - // The backend must provide an authentication strategy strategy := backend.OAuth2Strategy(redirectURL) @@ -56,14 +52,20 @@ var authCmd = &cobra.Command{ state := "somestate" // FIXME: Should be a random string // Redirect user to consent page to ask for permission specified scopes. - url := strategy.AuthCodeURL(verifier, state) - fmt.Printf("Visit the URL for the auth dialog: %v\n", url) - err = browser.OpenURL(url) + authUrl := strategy.AuthCodeURL(verifier, state) + + // Start an HTTP server to listen for the response + responseChan := make(chan auth.CodeResponse) + auth.RunOauth2CallbackServer(*redirectURL, authUrl.Param, responseChan) + + // Open the URL + fmt.Printf("Visit the URL for the auth dialog: %v\n", authUrl.Url) + err = browser.OpenURL(authUrl.Url) cobra.CheckErr(err) // Retrieve the code from the authentication callback code := <-responseChan - if code.State != state { + if code.State != authUrl.State { cobra.CompErrorln("Error: oauth state mismatch") os.Exit(1) } diff --git a/go.mod b/go.mod index f6e61af..0d85553 100644 --- a/go.mod +++ b/go.mod @@ -44,6 +44,7 @@ require ( github.com/rivo/uniseg v0.4.4 // indirect github.com/sagikazarmark/locafero v0.3.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/shkh/lastfm-go v0.0.0-20191215035245-89a801c244e0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.10.0 // indirect github.com/spf13/cast v1.5.1 // indirect diff --git a/go.sum b/go.sum index 84e28c7..51d651e 100644 --- a/go.sum +++ b/go.sum @@ -222,6 +222,8 @@ github.com/sagikazarmark/locafero v0.3.0 h1:zT7VEGWC2DTflmccN/5T1etyKvxSxpHsjb9c github.com/sagikazarmark/locafero v0.3.0/go.mod h1:w+v7UsPNFwzF1cHuOajOOzoq4U7v/ig1mpRjqV+Bu1U= 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/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= diff --git a/internal/auth/callback.go b/internal/auth/callback.go index 4fa41f6..0ad9c9d 100644 --- a/internal/auth/callback.go +++ b/internal/auth/callback.go @@ -21,9 +21,9 @@ import ( "net/url" ) -func RunOauth2CallbackServer(redirectURL url.URL, responseChan chan CodeResponse) { +func RunOauth2CallbackServer(redirectURL url.URL, param string, responseChan chan CodeResponse) { http.HandleFunc(redirectURL.Path, func(w http.ResponseWriter, r *http.Request) { - code := r.URL.Query().Get("code") + code := r.URL.Query().Get(param) state := r.URL.Query().Get("state") fmt.Fprint(w, "Token received, you can close this window now.") responseChan <- CodeResponse{ diff --git a/internal/auth/strategy.go b/internal/auth/strategy.go index 403c429..3d03fa4 100644 --- a/internal/auth/strategy.go +++ b/internal/auth/strategy.go @@ -24,11 +24,21 @@ import ( type OAuth2Strategy interface { Config() oauth2.Config - AuthCodeURL(verifier string, state string) string + AuthCodeURL(verifier string, state string) AuthUrl ExchangeToken(code CodeResponse, verifier string) (*oauth2.Token, error) } +type AuthUrl struct { + // The URL the user must visit to approve access + Url string + // Random state string passed on to the callback. + // Leave empty if the service does not support state. + State string + // Parameter name of the code passed on to the callback (usually "code") + Param string +} + type CodeResponse struct { Code string State string @@ -46,8 +56,13 @@ func (s StandardStrategy) Config() oauth2.Config { return s.conf } -func (s StandardStrategy) AuthCodeURL(verifier string, state string) string { - return s.conf.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(verifier)) +func (s StandardStrategy) AuthCodeURL(verifier string, state string) AuthUrl { + url := s.conf.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(verifier)) + return AuthUrl{ + Url: url, + State: state, + Param: "code", + } } func (s StandardStrategy) ExchangeToken(code CodeResponse, verifier string) (*oauth2.Token, error) { diff --git a/internal/backends/backends.go b/internal/backends/backends.go index cf78dad..029cca7 100644 --- a/internal/backends/backends.go +++ b/internal/backends/backends.go @@ -27,6 +27,7 @@ import ( "go.uploadedlobster.com/scotty/internal/backends/dump" "go.uploadedlobster.com/scotty/internal/backends/funkwhale" "go.uploadedlobster.com/scotty/internal/backends/jspf" + "go.uploadedlobster.com/scotty/internal/backends/lastfm" "go.uploadedlobster.com/scotty/internal/backends/listenbrainz" "go.uploadedlobster.com/scotty/internal/backends/maloja" "go.uploadedlobster.com/scotty/internal/backends/scrobblerlog" @@ -80,6 +81,7 @@ var knownBackends = map[string]func() models.Backend{ "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{} }, diff --git a/internal/backends/deezer/auth.go b/internal/backends/deezer/auth.go index 53bfd98..aa30b04 100644 --- a/internal/backends/deezer/auth.go +++ b/internal/backends/deezer/auth.go @@ -33,8 +33,13 @@ func (s deezerStrategy) Config() oauth2.Config { return s.conf } -func (s deezerStrategy) AuthCodeURL(verifier string, state string) string { - return s.conf.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(verifier)) +func (s deezerStrategy) AuthCodeURL(verifier string, state string) auth.AuthUrl { + url := s.conf.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(verifier)) + return auth.AuthUrl{ + Url: url, + State: state, + Param: "code", + } } func (s deezerStrategy) ExchangeToken(code auth.CodeResponse, verifier string) (*oauth2.Token, error) { diff --git a/internal/backends/lastfm/auth.go b/internal/backends/lastfm/auth.go new file mode 100644 index 0000000..c9718d5 --- /dev/null +++ b/internal/backends/lastfm/auth.go @@ -0,0 +1,51 @@ +/* +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 lastfm + +import ( + "net/url" + + "github.com/shkh/lastfm-go/lastfm" + "go.uploadedlobster.com/scotty/internal/auth" + "golang.org/x/oauth2" +) + +type lastfmStrategy struct { + client *lastfm.Api + redirectUrl *url.URL +} + +func (s lastfmStrategy) Config() oauth2.Config { + return oauth2.Config{} +} + +func (s lastfmStrategy) AuthCodeURL(verifier string, state string) auth.AuthUrl { + // Last.fm does not use OAuth2, but the provided authorization flow with + // callback URL is close enough we can shoehorn it into the existing + // authentication strategy. + // TODO: Investigate and use callback-less flow with api.GetAuthTokenUrl(token) + url := s.client.GetAuthRequestUrl(s.redirectUrl.String()) + return auth.AuthUrl{ + Url: url, + State: "", // last.fm does not use state + Param: "token", + } +} + +func (s lastfmStrategy) ExchangeToken(code auth.CodeResponse, verifier string) (*oauth2.Token, error) { + // The token is directly valid + return &oauth2.Token{AccessToken: code.Code}, nil +} diff --git a/internal/backends/lastfm/lastfm.go b/internal/backends/lastfm/lastfm.go new file mode 100644 index 0000000..0b8c957 --- /dev/null +++ b/internal/backends/lastfm/lastfm.go @@ -0,0 +1,54 @@ +/* +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 lastfm + +import ( + "net/url" + + "github.com/shkh/lastfm-go/lastfm" + "github.com/spf13/viper" + "go.uploadedlobster.com/scotty/internal/auth" + "go.uploadedlobster.com/scotty/internal/models" + "golang.org/x/oauth2" +) + +type LastfmApiBackend struct { + client *lastfm.Api +} + +func (b *LastfmApiBackend) Name() string { return "lastfm" } + +func (b *LastfmApiBackend) FromConfig(config *viper.Viper) models.Backend { + clientId := config.GetString("client-id") + clientSecret := config.GetString("client-secret") + b.client = lastfm.New(clientId, clientSecret) + return b +} + +func (b *LastfmApiBackend) OAuth2Strategy(redirectUrl *url.URL) auth.OAuth2Strategy { + return lastfmStrategy{ + client: b.client, + redirectUrl: redirectUrl, + } +} + +func (b *LastfmApiBackend) OAuth2Setup(token oauth2.TokenSource) error { + t, err := token.Token() + if err != nil { + return err + } + return b.client.LoginWithToken(t.AccessToken) +} diff --git a/scotty.example.toml b/scotty.example.toml index 0e09b70..391fda5 100644 --- a/scotty.example.toml +++ b/scotty.example.toml @@ -86,6 +86,17 @@ backend = "deezer" client-id = "" client-secret = "" +[service.lastfm] +backend = "lastfm" +# Your Last.fm username +username = "" +# You need to register an application on https://www.last.fm/api/account/create +# and set the API ID and shared secret below. +# When registering use "http://127.0.0.1:2222/callback/lastfm" as the +# callback URI. +client-id = "" +client-secret = "" + [service.dump] # This backend allows writing listens and loves as console output. Useful for # debugging the export from other services. From 1249238d3abd35c7aef3183b3a59403db1a65fa4 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 26 Nov 2023 12:22:11 +0100 Subject: [PATCH 009/150] lastfm: loves export --- internal/backends/lastfm/lastfm.go | 80 +++++++++++++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/internal/backends/lastfm/lastfm.go b/internal/backends/lastfm/lastfm.go index 0b8c957..f215654 100644 --- a/internal/backends/lastfm/lastfm.go +++ b/internal/backends/lastfm/lastfm.go @@ -17,6 +17,9 @@ package lastfm import ( "net/url" + "sort" + "strconv" + "time" "github.com/shkh/lastfm-go/lastfm" "github.com/spf13/viper" @@ -25,8 +28,11 @@ import ( "golang.org/x/oauth2" ) +const MaxItemsPerGet = 50 + type LastfmApiBackend struct { - client *lastfm.Api + client *lastfm.Api + username string } func (b *LastfmApiBackend) Name() string { return "lastfm" } @@ -35,6 +41,7 @@ func (b *LastfmApiBackend) FromConfig(config *viper.Viper) models.Backend { clientId := config.GetString("client-id") clientSecret := config.GetString("client-secret") b.client = lastfm.New(clientId, clientSecret) + b.username = config.GetString("username") return b } @@ -52,3 +59,74 @@ func (b *LastfmApiBackend) OAuth2Setup(token oauth2.TokenSource) error { } return b.client.LoginWithToken(t.AccessToken) } + +func (b *LastfmApiBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.Progress) { + // Choose a high offset, we attempt to search the loves backwards starting + // at the oldest one. + page := 1 + perPage := MaxItemsPerGet + + defer close(results) + + loves := make(models.LovesList, 0, 2*MaxItemsPerGet) + p := models.Progress{Total: int64(perPage)} + var totalCount int + +out: + for { + result, err := b.client.User.GetLovedTracks(lastfm.P{ + "user": b.username, + "limit": MaxItemsPerGet, + "page": page, + }) + if err != nil { + progress <- p.Complete() + results <- models.LovesResult{Error: err} + return + } + + p.Total = int64(result.Total) + count := len(result.Tracks) + if count == 0 { + break out + } + + for _, track := range result.Tracks { + timestamp, err := strconv.ParseInt(track.Date.Uts, 10, 64) + if err != nil { + progress <- p.Complete() + results <- models.LovesResult{Error: err} + return + } + if timestamp > oldestTimestamp.Unix() { + totalCount += 1 + love := models.Love{ + Created: time.Unix(timestamp, 0), + UserName: result.User, + RecordingMbid: models.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)}, + AdditionalInfo: models.AdditionalInfo{ + "lastfm_url": track.Url, + }, + }, + } + loves = append(loves, love) + } else { + break out + } + } + + p.Elapsed += int64(count) + progress <- p + + page += 1 + } + + sort.Sort(loves) + results <- models.LovesResult{Loves: loves, Total: totalCount} + progress <- p.Complete() +} From 406e150987e3a3b7f0b4406fc343dd2768a1d4cf Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 26 Nov 2023 12:56:27 +0100 Subject: [PATCH 010/150] lastfm: loves import --- README.md | 2 +- internal/backends/auth.go | 5 ++++- internal/backends/backends_test.go | 20 ++++++++++++++++++-- internal/backends/lastfm/auth.go | 7 ++++++- internal/backends/lastfm/lastfm.go | 29 ++++++++++++++++++++++++++++- 5 files changed, 57 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 862d388..d7d2be9 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ deezer | ✓ | ⨯ | ✓ | - dump | ⨯ | ✓ | ⨯ | ✓ funkwhale | ✓ | ⨯ | ✓ | - jspf | - | ✓ | - | ✓ -lastfm | - | - | - | - +lastfm | - | - | ✓ | ✓ listenbrainz | ✓ | ✓ | ✓ | ✓ maloja | ✓ | ✓ | ⨯ | ⨯ scrobbler-log | ✓ | ✓ | ⨯ | ⨯ diff --git a/internal/backends/auth.go b/internal/backends/auth.go index 49584ec..d27efd6 100644 --- a/internal/backends/auth.go +++ b/internal/backends/auth.go @@ -49,7 +49,10 @@ func Authenticate(service string, backend models.Backend, db storage.Database, c } conf := authenticator.OAuth2Strategy(redirectURL).Config() tokenSource := auth.NewDatabaseTokenSource(db, service, &conf, token) - authenticator.OAuth2Setup(tokenSource) + err = authenticator.OAuth2Setup(tokenSource) + if err != nil { + return needAuth, err + } } return needAuth, nil } diff --git a/internal/backends/backends_test.go b/internal/backends/backends_test.go index 89407a3..1af09a4 100644 --- a/internal/backends/backends_test.go +++ b/internal/backends/backends_test.go @@ -24,12 +24,15 @@ import ( "github.com/spf13/viper" "github.com/stretchr/testify/assert" "go.uploadedlobster.com/scotty/internal/backends" + "go.uploadedlobster.com/scotty/internal/backends/deezer" "go.uploadedlobster.com/scotty/internal/backends/dump" "go.uploadedlobster.com/scotty/internal/backends/funkwhale" "go.uploadedlobster.com/scotty/internal/backends/jspf" + "go.uploadedlobster.com/scotty/internal/backends/lastfm" "go.uploadedlobster.com/scotty/internal/backends/listenbrainz" "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/subsonic" "go.uploadedlobster.com/scotty/internal/models" ) @@ -73,6 +76,10 @@ func TestGetBackends(t *testing.T) { } func TestImplementsInterfaces(t *testing.T) { + expectInterface[models.ListensExport](t, &deezer.DeezerApiBackend{}) + expectInterface[models.LovesExport](t, &deezer.DeezerApiBackend{}) + // expectInterface[models.LovesImport](t, &deezer.DeezerApiBackend{}) + expectInterface[models.ListensImport](t, &dump.DumpBackend{}) expectInterface[models.LovesImport](t, &dump.DumpBackend{}) @@ -82,18 +89,27 @@ func TestImplementsInterfaces(t *testing.T) { // expectInterface[models.LovesImport](t, &funkwhale.FunkwhaleApiBackend{}) // expectInterface[models.ListensExport](t, &jspf.JSPFBackend{}) - // expectInterface[models.ListensImport](t, &jspf.JSPFBackend{}) + expectInterface[models.ListensImport](t, &jspf.JSPFBackend{}) // expectInterface[models.LovesExport](t, &jspf.JSPFBackend{}) expectInterface[models.LovesImport](t, &jspf.JSPFBackend{}) + // expectInterface[models.ListensExport](t, &lastfm.LastfmApiBackend{}) + // expectInterface[models.ListensImport](t, &lastfm.LastfmApiBackend{}) + expectInterface[models.LovesExport](t, &lastfm.LastfmApiBackend{}) + expectInterface[models.LovesImport](t, &lastfm.LastfmApiBackend{}) + expectInterface[models.ListensExport](t, &listenbrainz.ListenBrainzApiBackend{}) - // expectInterface[models.ListensImport](t, &listenbrainz.ListenBrainzApiBackend{}) + expectInterface[models.ListensImport](t, &listenbrainz.ListenBrainzApiBackend{}) expectInterface[models.LovesExport](t, &listenbrainz.ListenBrainzApiBackend{}) expectInterface[models.LovesImport](t, &listenbrainz.ListenBrainzApiBackend{}) expectInterface[models.ListensExport](t, &maloja.MalojaApiBackend{}) expectInterface[models.ListensImport](t, &maloja.MalojaApiBackend{}) + expectInterface[models.ListensExport](t, &spotify.SpotifyApiBackend{}) + expectInterface[models.LovesExport](t, &spotify.SpotifyApiBackend{}) + // expectInterface[models.LovesImport](t, &spotify.SpotifyApiBackend{}) + expectInterface[models.ListensExport](t, &scrobblerlog.ScrobblerLogBackend{}) expectInterface[models.ListensImport](t, &scrobblerlog.ScrobblerLogBackend{}) diff --git a/internal/backends/lastfm/auth.go b/internal/backends/lastfm/auth.go index c9718d5..bbc65bc 100644 --- a/internal/backends/lastfm/auth.go +++ b/internal/backends/lastfm/auth.go @@ -47,5 +47,10 @@ func (s lastfmStrategy) AuthCodeURL(verifier string, state string) auth.AuthUrl func (s lastfmStrategy) ExchangeToken(code auth.CodeResponse, verifier string) (*oauth2.Token, error) { // The token is directly valid - return &oauth2.Token{AccessToken: code.Code}, nil + err := s.client.LoginWithToken(code.Code) + if err != nil { + return nil, err + } + sk := s.client.GetSessionKey() + return &oauth2.Token{AccessToken: sk}, nil } diff --git a/internal/backends/lastfm/lastfm.go b/internal/backends/lastfm/lastfm.go index f215654..c65a0b4 100644 --- a/internal/backends/lastfm/lastfm.go +++ b/internal/backends/lastfm/lastfm.go @@ -16,6 +16,7 @@ Scotty. If not, see . package lastfm import ( + "fmt" "net/url" "sort" "strconv" @@ -45,6 +46,9 @@ func (b *LastfmApiBackend) FromConfig(config *viper.Viper) models.Backend { return b } +func (b *LastfmApiBackend) StartImport() error { return nil } +func (b *LastfmApiBackend) FinishImport() error { return nil } + func (b *LastfmApiBackend) OAuth2Strategy(redirectUrl *url.URL) auth.OAuth2Strategy { return lastfmStrategy{ client: b.client, @@ -57,7 +61,8 @@ func (b *LastfmApiBackend) OAuth2Setup(token oauth2.TokenSource) error { if err != nil { return err } - return b.client.LoginWithToken(t.AccessToken) + b.client.SetSession(t.AccessToken) + return nil } func (b *LastfmApiBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.Progress) { @@ -130,3 +135,25 @@ out: results <- models.LovesResult{Loves: loves, Total: totalCount} progress <- p.Complete() } + +func (b *LastfmApiBackend) ImportLoves(export models.LovesResult, importResult models.ImportResult, progress chan models.Progress) (models.ImportResult, error) { + for _, love := range export.Loves { + err := b.client.Track.Love(lastfm.P{ + "track": love.TrackName, + "artist": love.ArtistName(), + }) + + if err == nil { + importResult.UpdateTimestamp(love.Created) + importResult.ImportCount += 1 + } else { + msg := fmt.Sprintf("Failed import of \"%s\" by %s: %v", + love.TrackName, love.ArtistName(), err.Error()) + importResult.ImportErrors = append(importResult.ImportErrors, msg) + } + + progress <- models.Progress{}.FromImportResult(importResult) + } + + return importResult, nil +} From 267018901bd4cfcecdc11a3b36431f5af4d61366 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 26 Nov 2023 15:25:22 +0100 Subject: [PATCH 011/150] lastfm: listens import --- internal/backends/lastfm/lastfm.go | 82 +++++++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/internal/backends/lastfm/lastfm.go b/internal/backends/lastfm/lastfm.go index c65a0b4..e9cdf2d 100644 --- a/internal/backends/lastfm/lastfm.go +++ b/internal/backends/lastfm/lastfm.go @@ -16,6 +16,7 @@ Scotty. If not, see . package lastfm import ( + "errors" "fmt" "net/url" "sort" @@ -29,7 +30,10 @@ import ( "golang.org/x/oauth2" ) -const MaxItemsPerGet = 50 +const ( + MaxItemsPerGet = 1000 + MaxListensPerRequest = 50 +) type LastfmApiBackend struct { client *lastfm.Api @@ -65,6 +69,82 @@ func (b *LastfmApiBackend) OAuth2Setup(token oauth2.TokenSource) error { return nil } +func (b *LastfmApiBackend) ImportListens(export models.ListensResult, importResult models.ImportResult, progress chan models.Progress) (models.ImportResult, error) { + total := len(export.Listens) + for i := 0; i < total; i += MaxListensPerRequest { + listens := export.Listens[i:min(i+MaxListensPerRequest, total)] + count := len(listens) + if count == 0 { + break + } + + artists := make([]string, count) + tracks := make([]string, count) + timestamps := make([]string, count) + albums := make([]string, count) + trackNumbers := make([]string, count) + mbids := make([]string, count) + // albumArtists := make([]string, count) + durations := make([]int64, count) + + for _, l := range listens { + artists = append(artists, l.ArtistName()) + tracks = append(tracks, l.TrackName) + timestamps = append(timestamps, strconv.FormatInt(l.ListenedAt.Unix(), 10)) + if l.ReleaseName != "" { + albums = append(albums, l.ReleaseName) + } + if l.TrackNumber > 0 { + trackNumbers = append(trackNumbers, strconv.Itoa(l.TrackNumber)) + } + if l.RecordingMbid != "" { + mbids = append(mbids, string(l.RecordingMbid)) + } + // if l.ReleaseArtist != "" { + // albumArtists = append(albums, l.ReleaseArtist) + // } + if l.Duration > 0 { + durations = append(durations, int64(l.Duration.Seconds())) + } + } + + result, err := b.client.Track.Scrobble(lastfm.P{ + "artist": artists, + "track": tracks, + "timestamp": timestamps, + "album": albums, + "trackNumber": trackNumbers, + "mbid": mbids, + "duration": durations, + }) + if err != nil { + return importResult, err + } + + accepted, err := strconv.Atoi(result.Accepted) + if err != nil { + return importResult, err + } + + if accepted < count { + for _, s := range result.Scrobbles { + ignoreMsg := s.IgnoredMessage.Body + if ignoreMsg != "" { + importResult.ImportErrors = append(importResult.ImportErrors, ignoreMsg) + } + } + errMsg := fmt.Sprintf("Last.fm import ignored %v scrobbles", count-accepted) + return importResult, errors.New(errMsg) + } + + importResult.UpdateTimestamp(listens[count-1].ListenedAt) + importResult.ImportCount += accepted + progress <- models.Progress{}.FromImportResult(importResult) + } + + return importResult, nil +} + func (b *LastfmApiBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.Progress) { // Choose a high offset, we attempt to search the loves backwards starting // at the oldest one. From f9d25e3b6f64d01a7079596e3729bb9c2f1ad641 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 28 Nov 2023 08:55:53 +0100 Subject: [PATCH 012/150] lastfm: listens export --- README.md | 3 +- internal/backends/lastfm/lastfm.go | 98 ++++++++++++++++++++++++++++-- 2 files changed, 96 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d7d2be9..8f85065 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Scotty transfers your listens/scrobbles and favorite tracks between various musi - Transfer loved tracks from Funkwhale to ListenBrainz - Submit listens stored in a Rockbox `.scrobbler.log` file 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 ## Installation @@ -44,7 +45,7 @@ deezer | ✓ | ⨯ | ✓ | - dump | ⨯ | ✓ | ⨯ | ✓ funkwhale | ✓ | ⨯ | ✓ | - jspf | - | ✓ | - | ✓ -lastfm | - | - | ✓ | ✓ +lastfm | ✓ | ✓ | ✓ | ✓ listenbrainz | ✓ | ✓ | ✓ | ✓ maloja | ✓ | ✓ | ⨯ | ⨯ scrobbler-log | ✓ | ✓ | ⨯ | ⨯ diff --git a/internal/backends/lastfm/lastfm.go b/internal/backends/lastfm/lastfm.go index e9cdf2d..e79c82c 100644 --- a/internal/backends/lastfm/lastfm.go +++ b/internal/backends/lastfm/lastfm.go @@ -31,8 +31,10 @@ import ( ) const ( - MaxItemsPerGet = 1000 - MaxListensPerRequest = 50 + MaxItemsPerGet = 1000 + MaxListensPerGet = 200 + MaxListensPerSubmission = 50 + MaxPage = 1000000 ) type LastfmApiBackend struct { @@ -69,10 +71,98 @@ func (b *LastfmApiBackend) OAuth2Setup(token oauth2.TokenSource) error { return nil } +func (b *LastfmApiBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.Progress) { + page := MaxPage + minTime := oldestTimestamp + perPage := MaxItemsPerGet + + defer close(results) + + // We need to gather the full list of listens in order to sort them + p := models.Progress{Total: int64(page)} + +out: + for page > 0 { + args := lastfm.P{ + "user": b.username, + "limit": MaxListensPerGet, + // last.fm includes the listen with the exact timestamp in the result + "from": oldestTimestamp.Add(time.Second).Unix(), + "page": page, + } + result, err := b.client.User.GetRecentTracks(args) + if err != nil { + results <- models.ListensResult{Error: err} + progress <- p.Complete() + return + } + + count := len(result.Tracks) + if count == 0 { + // The page was outside of the result range, adjust and request again + if page > result.TotalPages { + page = result.TotalPages + continue + } + break + } + + listens := make(models.ListensList, 0, 2*perPage) + for _, scrobble := range result.Tracks { + timestamp, err := strconv.ParseInt(scrobble.Date.Uts, 10, 64) + if err != nil { + results <- models.ListensResult{Error: err} + progress <- p.Complete() + break out + } + if timestamp > oldestTimestamp.Unix() { + p.Elapsed += 1 + listen := models.Listen{ + ListenedAt: time.Unix(timestamp, 0), + UserName: b.username, + Track: models.Track{ + TrackName: scrobble.Name, + ArtistNames: []string{}, + ReleaseName: scrobble.Album.Name, + RecordingMbid: models.MBID(scrobble.Mbid), + ArtistMbids: []models.MBID{}, + ReleaseMbid: models.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)} + } + listens = append(listens, listen) + } else { + break out + } + } + + sort.Sort(listens) + minTime = listens[len(listens)-1].ListenedAt + page -= 1 + + results <- models.ListensResult{ + Listens: listens, + Total: result.Total, + OldestTimestamp: minTime, + } + p.Total = int64(result.TotalPages) + p.Elapsed = int64(result.TotalPages - page) + progress <- p + } + + results <- models.ListensResult{OldestTimestamp: minTime} + progress <- p.Complete() +} + func (b *LastfmApiBackend) ImportListens(export models.ListensResult, importResult models.ImportResult, progress chan models.Progress) (models.ImportResult, error) { total := len(export.Listens) - for i := 0; i < total; i += MaxListensPerRequest { - listens := export.Listens[i:min(i+MaxListensPerRequest, total)] + for i := 0; i < total; i += MaxListensPerSubmission { + listens := export.Listens[i:min(i+MaxListensPerSubmission, total)] count := len(listens) if count == 0 { break From d96df4367d78205d69baa9e0133775c061114331 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 28 Nov 2023 09:09:53 +0100 Subject: [PATCH 013/150] Release 0.2.0 --- CHANGES.md | 11 +++++++++++ README.md | 9 +++++---- internal/version/version.go | 2 +- 3 files changed, 17 insertions(+), 5 deletions(-) create mode 100644 CHANGES.md diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000..c763e1d --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,11 @@ +# Scotty Changelog + +## 0.2.0 - 2023-11-28 +- lastfm: support for scrobble and love export/import +- jspf: consider loved track MBID +- Fixed tracking of import errors + + +## 0.1.0 - 2023-11-24 +- Initial development release with support for ListenBrainz, Maloja, Funkwhale, + Subsonic, Spotify, Deezer, JSPF and scrobblerlog files. diff --git a/README.md b/README.md index 8f85065..771a66c 100644 --- a/README.md +++ b/README.md @@ -16,13 +16,14 @@ Scotty transfers your listens/scrobbles and favorite tracks between various musi ## Installation -Scotty is a command line utility written in Go. Currently the easiest way to get Scotty is by installing it from source with the following command: +Scotty is a command line utility written in Go. Binary packages for various +operating systems are available on the [release page](https://git.sr.ht/~phw/scotty/refs). - go install go.uploadedlobster.com/scotty@latest +You can also install Scotty from source with the following command: -This requires `go` to be installed on your systems. You can get it from https://go.dev/dl/ . + go install go.uploadedlobster.com/scotty@latest -In the future pre-built binary releases of Scotty will also be made available. +This requires `go` to be installed on your system. You can get it from https://go.dev/dl/ . ## Configuration diff --git a/internal/version/version.go b/internal/version/version.go index 43a6205..9a4f9fc 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -17,7 +17,7 @@ package version const ( AppName = "scotty" - AppVersion = "0.1.0" + AppVersion = "0.2.0" ) func UserAgent() string { From cde9b28c28de8e1ce43b1d4ac91c189d52466624 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 28 Nov 2023 10:01:35 +0100 Subject: [PATCH 014/150] improved packaging - use version in archive name - "macOS" instead of "Darwin" - include example config in archives - use subdir in archives --- .build.yml | 8 +++++++- .goreleaser.yaml | 10 ++++++++-- README.md | 2 +- scotty.example.toml => config.example.toml | 0 go.mod | 2 +- 5 files changed, 17 insertions(+), 5 deletions(-) rename scotty.example.toml => config.example.toml (100%) diff --git a/.build.yml b/.build.yml index f21034b..e79ae95 100644 --- a/.build.yml +++ b/.build.yml @@ -11,7 +11,13 @@ tasks: go test -v ./... - build: | cd scotty - goreleaser release --snapshot --clean + GIT_REF=$(git describe --always) + if [[ "$GIT_REF" =~ ^v[0-9]+\.[0-9]+(\.[0-9]+)?$ ]] + then + goreleaser release --clean + else + goreleaser release --snapshot --clean + fi - publish-redirect: | # Update redirect on https://go.uploadedlobster.com/scotty ./scotty/pages/publish.sh diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 69adc4c..06b612a 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -31,12 +31,14 @@ archives: - format: tar.gz # this name template makes the OS and Arch compatible with the results of `uname`. name_template: >- - {{ .ProjectName }}_ - {{- title .Os }}_ + {{ .ProjectName }}-{{ .Version }}_ + {{- if eq .Os "darwin" }}macos + {{- else }}{{ .Os }}{{ end }}_ {{- if eq .Arch "amd64" }}x86_64 {{- else if eq .Arch "386" }}i386 {{- else }}{{ .Arch }}{{ end }} {{- if .Arm }}v{{ .Arm }}{{ end }} + wrap_in_directory: true # use zip for windows archives format_overrides: - goos: windows @@ -44,6 +46,10 @@ archives: files: - COPYING - README.md + - config.example.toml + +release: + disable: true # changelog: # sort: asc diff --git a/README.md b/README.md index 771a66c..3432851 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ This requires `go` to be installed on your system. You can get it from https://g ## Configuration -Scotty requires the configuration of the services in a configuration file in TOML format. See [scotty.example.toml](./scotty.example.toml) for details. +Scotty requires the configuration of the services in a configuration file in TOML format. See [config.example.toml](./config.example.toml) for details. ## Usage diff --git a/scotty.example.toml b/config.example.toml similarity index 100% rename from scotty.example.toml rename to config.example.toml diff --git a/go.mod b/go.mod index 0d85553..e8feb7c 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/glebarez/sqlite v1.10.0 github.com/go-resty/resty/v2 v2.10.0 github.com/jarcoal/httpmock v1.3.1 + github.com/shkh/lastfm-go v0.0.0-20191215035245-89a801c244e0 github.com/spf13/cobra v1.8.0 github.com/spf13/viper v1.17.0 github.com/stretchr/testify v1.8.4 @@ -44,7 +45,6 @@ require ( github.com/rivo/uniseg v0.4.4 // indirect github.com/sagikazarmark/locafero v0.3.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect - github.com/shkh/lastfm-go v0.0.0-20191215035245-89a801c244e0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.10.0 // indirect github.com/spf13/cast v1.5.1 // indirect From 4bf0f2c81d5c2c69b7410b5d96c09c7ba7876b83 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 28 Nov 2023 17:45:53 +0100 Subject: [PATCH 015/150] listenbrainz: fetch listens in reverse listen time order This allows parallel import --- CHANGES.md | 4 ++ internal/backends/listenbrainz/client_test.go | 2 + .../backends/listenbrainz/listenbrainz.go | 41 +++++++++++-------- internal/backends/listenbrainz/models.go | 1 + .../listenbrainz/testdata/listens.json | 1 + internal/backends/spotify/spotify.go | 5 +-- 6 files changed, 34 insertions(+), 20 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index c763e1d..615939f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,9 @@ # Scotty Changelog +## 0.3.0 - unreleased +- listenbrainz: fetch listens in reverse listen time order + + ## 0.2.0 - 2023-11-28 - lastfm: support for scrobble and love export/import - jspf: consider loved track MBID diff --git a/internal/backends/listenbrainz/client_test.go b/internal/backends/listenbrainz/client_test.go index faabbe1..cc36f1d 100644 --- a/internal/backends/listenbrainz/client_test.go +++ b/internal/backends/listenbrainz/client_test.go @@ -53,6 +53,8 @@ func TestGetListens(t *testing.T) { assert := assert.New(t) assert.Equal(2, result.Payload.Count) + assert.Equal(int64(1699718723), result.Payload.LatestListenTimestamp) + assert.Equal(int64(1152911863), result.Payload.OldestListenTimestamp) require.Len(t, result.Payload.Listens, 2) assert.Equal("Shadowplay", result.Payload.Listens[0].TrackMetadata.TrackName) } diff --git a/internal/backends/listenbrainz/listenbrainz.go b/internal/backends/listenbrainz/listenbrainz.go index cafcc0f..0b29767 100644 --- a/internal/backends/listenbrainz/listenbrainz.go +++ b/internal/backends/listenbrainz/listenbrainz.go @@ -46,20 +46,19 @@ func (b *ListenBrainzApiBackend) FinishImport() error { return nil } func (b *ListenBrainzApiBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.Progress) { startTime := time.Now() - maxTime := startTime - minTime := time.Unix(0, 0) + minTime := oldestTimestamp + if minTime.Unix() < 1 { + minTime = time.Unix(1, 0) + } - totalDuration := startTime.Sub(oldestTimestamp) + totalDuration := startTime.Sub(minTime) defer close(results) - // FIXME: Optimize by fetching the listens in reverse listen time order - listens := make(models.ListensList, 0, 2*MaxItemsPerGet) p := models.Progress{Total: int64(totalDuration.Seconds())} -out: for { - result, err := b.client.GetListens(b.username, maxTime, minTime) + result, err := b.client.GetListens(b.username, time.Now(), minTime) if err != nil { progress <- p.Complete() results <- models.ListensResult{Error: err} @@ -68,31 +67,39 @@ out: count := len(result.Payload.Listens) if count == 0 { - break + if minTime.Unix() < result.Payload.OldestListenTimestamp { + minTime = time.Unix(result.Payload.OldestListenTimestamp, 0) + totalDuration = startTime.Sub(minTime) + p.Total = int64(totalDuration.Seconds()) + continue + } else { + break + } } - // Set maxTime to the oldest returned listen - maxTime = time.Unix(result.Payload.Listens[count-1].ListenedAt, 0) - remainingTime := maxTime.Sub(oldestTimestamp) + // Set minTime to the newest returned listen + minTime = time.Unix(result.Payload.Listens[0].ListenedAt, 0) + remainingTime := startTime.Sub(minTime) + + listens := make(models.ListensList, 0, count) for _, listen := range result.Payload.Listens { if listen.ListenedAt > oldestTimestamp.Unix() { listens = append(listens, listen.AsListen()) } else { - // result contains listens older then oldestTimestamp, - // we can stop requesting more - p.Total = int64(startTime.Sub(time.Unix(listen.ListenedAt, 0)).Seconds()) - break out + // result contains listens older then oldestTimestamp + break } } + sort.Sort(listens) p.Elapsed = int64(totalDuration.Seconds() - remainingTime.Seconds()) progress <- p + results <- models.ListensResult{Listens: listens, OldestTimestamp: minTime} } - sort.Sort(listens) + results <- models.ListensResult{OldestTimestamp: minTime} progress <- p.Complete() - results <- models.ListensResult{Listens: listens, OldestTimestamp: oldestTimestamp} } func (b *ListenBrainzApiBackend) ImportListens(export models.ListensResult, importResult models.ImportResult, progress chan models.Progress) (models.ImportResult, error) { diff --git a/internal/backends/listenbrainz/models.go b/internal/backends/listenbrainz/models.go index a7cd32b..c1552c7 100644 --- a/internal/backends/listenbrainz/models.go +++ b/internal/backends/listenbrainz/models.go @@ -36,6 +36,7 @@ type GetListenPayload struct { Count int `json:"count"` UserName string `json:"user_id"` LatestListenTimestamp int64 `json:"latest_listen_ts"` + OldestListenTimestamp int64 `json:"oldest_listen_ts"` Listens []Listen `json:"listens"` } diff --git a/internal/backends/listenbrainz/testdata/listens.json b/internal/backends/listenbrainz/testdata/listens.json index 5d799b8..41119ba 100644 --- a/internal/backends/listenbrainz/testdata/listens.json +++ b/internal/backends/listenbrainz/testdata/listens.json @@ -2,6 +2,7 @@ "payload": { "count": 2, "latest_listen_ts": 1699718723, + "oldest_listen_ts": 1152911863, "listens": [ { "inserted_at": 1699719320, diff --git a/internal/backends/spotify/spotify.go b/internal/backends/spotify/spotify.go index 44db61c..1c797c8 100644 --- a/internal/backends/spotify/spotify.go +++ b/internal/backends/spotify/spotify.go @@ -122,15 +122,14 @@ func (b *SpotifyApiBackend) ExportListens(oldestTimestamp time.Time, results cha break } - listens := make(models.ListensList, 0, len(result.Items)) + listens := make(models.ListensList, 0, count) for _, listen := range result.Items { l := listen.AsListen() if l.ListenedAt.Unix() > oldestTimestamp.Unix() { listens = append(listens, l) } else { - // result contains listens older then oldestTimestamp, - // we can stop requesting more + // result contains listens older then oldestTimestamp break } } From b169dd2cc41600b5b7c5a20b5aecae5336760fda Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 28 Nov 2023 17:58:52 +0100 Subject: [PATCH 016/150] auth: generate oauth2 state randomly --- cmd/auth.go | 2 +- internal/auth/util.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 internal/auth/util.go diff --git a/cmd/auth.go b/cmd/auth.go index 67b7411..f7876fc 100644 --- a/cmd/auth.go +++ b/cmd/auth.go @@ -50,7 +50,7 @@ var authCmd = &cobra.Command{ // https://www.ietf.org/archive/id/draft-ietf-oauth-security-topics-22.html#name-countermeasures-6 verifier := oauth2.GenerateVerifier() - state := "somestate" // FIXME: Should be a random string + state := auth.RandomState() // Redirect user to consent page to ask for permission specified scopes. authUrl := strategy.AuthCodeURL(verifier, state) diff --git a/internal/auth/util.go b/internal/auth/util.go new file mode 100644 index 0000000..76b55b0 --- /dev/null +++ b/internal/auth/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 auth + +import "math/rand" + +const stateLength = 10 + +func RandomState() string { + return randString(stateLength) +} + +const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + +func randString(n int) string { + b := make([]byte, n) + for i := range b { + b[i] = letters[rand.Intn(len(letters))] + } + return string(b) +} From 8a1cd8ded74830457b999976221f288dc92ab0fa Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 28 Nov 2023 18:11:45 +0100 Subject: [PATCH 017/150] Combine all build artifacts in single archive --- .build.yml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.build.yml b/.build.yml index e79ae95..1286795 100644 --- a/.build.yml +++ b/.build.yml @@ -18,13 +18,10 @@ tasks: else goreleaser release --snapshot --clean fi + cd dist/ + tar cvf artifacts.tar scotty-*.{gz,zip} scotty_*_checksums.txt - publish-redirect: | # Update redirect on https://go.uploadedlobster.com/scotty ./scotty/pages/publish.sh artifacts: - - scotty/dist/scotty_Darwin_all.tar.gz - - scotty/dist/scotty_Linux_arm64.tar.gz - - scotty/dist/scotty_Linux_i386.tar.gz - - scotty/dist/scotty_Linux_x86_64.tar.gz - - scotty/dist/scotty_Windows_arm64.zip - - scotty/dist/scotty_Windows_x86_64.zip + - scotty/dist/artifacts.tar From a4a05ea047a4ac3e018b536de8be10cc00f9bf70 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Fri, 1 Dec 2023 11:48:23 +0100 Subject: [PATCH 018/150] subsonic: fixed filtering songs based on timestamp --- internal/backends/subsonic/subsonic.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/backends/subsonic/subsonic.go b/internal/backends/subsonic/subsonic.go index 732b9ab..2d42fff 100644 --- a/internal/backends/subsonic/subsonic.go +++ b/internal/backends/subsonic/subsonic.go @@ -66,11 +66,11 @@ func (b *SubsonicApiBackend) ExportLoves(oldestTimestamp time.Time, results chan } func (b *SubsonicApiBackend) filterSongs(songs []*subsonic.Child, oldestTimestamp time.Time) models.LovesList { - loves := make(models.LovesList, len(songs)) - for i, song := range songs { + loves := make(models.LovesList, 0, len(songs)) + for _, song := range songs { love := SongAsLove(*song, b.client.User) if love.Created.Unix() > oldestTimestamp.Unix() { - loves[i] = love + loves = append(loves, love) } } From ca3b8492b0716aaff224d0baa0581e6d3813a69a Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 3 Dec 2023 16:56:12 +0100 Subject: [PATCH 019/150] Unified export result types --- internal/backends/deezer/deezer.go | 4 ++-- internal/backends/dump/dump.go | 4 ++-- internal/backends/funkwhale/funkwhale.go | 4 ++-- internal/backends/jspf/jspf.go | 4 ++-- internal/backends/lastfm/lastfm.go | 10 +++++----- internal/backends/listenbrainz/listenbrainz.go | 14 +++++++------- internal/backends/maloja/maloja.go | 4 ++-- internal/backends/process.go | 4 ++-- internal/backends/scrobblerlog/scrobblerlog.go | 6 +++--- internal/backends/spotify/spotify.go | 4 ++-- internal/backends/subsonic/subsonic.go | 2 +- internal/models/models.go | 12 +++++------- 12 files changed, 35 insertions(+), 37 deletions(-) diff --git a/internal/backends/deezer/deezer.go b/internal/backends/deezer/deezer.go index c35b8ae..df36280 100644 --- a/internal/backends/deezer/deezer.go +++ b/internal/backends/deezer/deezer.go @@ -113,7 +113,7 @@ out: } sort.Sort(listens) - results <- models.ListensResult{Listens: listens, Total: totalCount} + results <- models.ListensResult{Items: listens, Total: totalCount} p.Elapsed += int64(count) progress <- p @@ -180,7 +180,7 @@ out: } sort.Sort(loves) - results <- models.LovesResult{Loves: loves, Total: totalCount} + results <- models.LovesResult{Items: loves, Total: totalCount} p.Elapsed += int64(count) progress <- p diff --git a/internal/backends/dump/dump.go b/internal/backends/dump/dump.go index a5c3751..e85495f 100644 --- a/internal/backends/dump/dump.go +++ b/internal/backends/dump/dump.go @@ -33,7 +33,7 @@ func (b *DumpBackend) StartImport() error { return nil } func (b *DumpBackend) FinishImport() error { return nil } func (b *DumpBackend) ImportListens(export models.ListensResult, importResult models.ImportResult, progress chan models.Progress) (models.ImportResult, error) { - for _, listen := range export.Listens { + for _, listen := range export.Items { importResult.UpdateTimestamp(listen.ListenedAt) importResult.ImportCount += 1 progress <- models.Progress{}.FromImportResult(importResult) @@ -45,7 +45,7 @@ func (b *DumpBackend) ImportListens(export models.ListensResult, importResult mo } func (b *DumpBackend) ImportLoves(export models.LovesResult, importResult models.ImportResult, progress chan models.Progress) (models.ImportResult, error) { - for _, love := range export.Loves { + for _, love := range export.Items { importResult.UpdateTimestamp(love.Created) importResult.ImportCount += 1 progress <- models.Progress{}.FromImportResult(importResult) diff --git a/internal/backends/funkwhale/funkwhale.go b/internal/backends/funkwhale/funkwhale.go index 617b90a..4a4bfb1 100644 --- a/internal/backends/funkwhale/funkwhale.go +++ b/internal/backends/funkwhale/funkwhale.go @@ -88,7 +88,7 @@ out: sort.Sort(listens) progress <- p.Complete() - results <- models.ListensResult{Listens: listens} + results <- models.ListensResult{Items: listens} } func (b *FunkwhaleApiBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.Progress) { @@ -137,7 +137,7 @@ out: sort.Sort(loves) progress <- p.Complete() - results <- models.LovesResult{Loves: loves} + results <- models.LovesResult{Items: loves} } func (l Listening) AsListen() models.Listen { diff --git a/internal/backends/jspf/jspf.go b/internal/backends/jspf/jspf.go index 26bc57f..6aeb5ca 100644 --- a/internal/backends/jspf/jspf.go +++ b/internal/backends/jspf/jspf.go @@ -52,7 +52,7 @@ func (b *JSPFBackend) FinishImport() error { } func (b *JSPFBackend) ImportListens(export models.ListensResult, importResult models.ImportResult, progress chan models.Progress) (models.ImportResult, error) { - for _, listen := range export.Listens { + for _, listen := range export.Items { track := listenAsTrack(listen) b.tracks = append(b.tracks, track) importResult.ImportCount += 1 @@ -64,7 +64,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.Loves { + for _, love := range export.Items { track := loveAsTrack(love) b.tracks = append(b.tracks, track) importResult.ImportCount += 1 diff --git a/internal/backends/lastfm/lastfm.go b/internal/backends/lastfm/lastfm.go index e79c82c..d68ed6f 100644 --- a/internal/backends/lastfm/lastfm.go +++ b/internal/backends/lastfm/lastfm.go @@ -146,7 +146,7 @@ out: page -= 1 results <- models.ListensResult{ - Listens: listens, + Items: listens, Total: result.Total, OldestTimestamp: minTime, } @@ -160,9 +160,9 @@ out: } func (b *LastfmApiBackend) ImportListens(export models.ListensResult, importResult models.ImportResult, progress chan models.Progress) (models.ImportResult, error) { - total := len(export.Listens) + total := len(export.Items) for i := 0; i < total; i += MaxListensPerSubmission { - listens := export.Listens[i:min(i+MaxListensPerSubmission, total)] + listens := export.Items[i:min(i+MaxListensPerSubmission, total)] count := len(listens) if count == 0 { break @@ -302,12 +302,12 @@ out: } sort.Sort(loves) - results <- models.LovesResult{Loves: loves, Total: totalCount} + results <- models.LovesResult{Items: loves, Total: totalCount} progress <- p.Complete() } func (b *LastfmApiBackend) ImportLoves(export models.LovesResult, importResult models.ImportResult, progress chan models.Progress) (models.ImportResult, error) { - for _, love := range export.Loves { + for _, love := range export.Items { err := b.client.Track.Love(lastfm.P{ "track": love.TrackName, "artist": love.ArtistName(), diff --git a/internal/backends/listenbrainz/listenbrainz.go b/internal/backends/listenbrainz/listenbrainz.go index 0b29767..27c8102 100644 --- a/internal/backends/listenbrainz/listenbrainz.go +++ b/internal/backends/listenbrainz/listenbrainz.go @@ -95,7 +95,7 @@ func (b *ListenBrainzApiBackend) ExportListens(oldestTimestamp time.Time, result sort.Sort(listens) p.Elapsed = int64(totalDuration.Seconds() - remainingTime.Seconds()) progress <- p - results <- models.ListensResult{Listens: listens, OldestTimestamp: minTime} + results <- models.ListensResult{Items: listens, OldestTimestamp: minTime} } results <- models.ListensResult{OldestTimestamp: minTime} @@ -103,9 +103,9 @@ 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.Listens) + total := len(export.Items) for i := 0; i < total; i += MaxListensPerRequest { - listens := export.Listens[i:min(i+MaxListensPerRequest, total)] + listens := export.Items[i:min(i+MaxListensPerRequest, total)] count := len(listens) if count == 0 { break @@ -184,7 +184,7 @@ out: sort.Sort(loves) progress <- p.Complete() - results <- models.LovesResult{Loves: loves} + results <- models.LovesResult{Items: loves} } func (b *ListenBrainzApiBackend) ImportLoves(export models.LovesResult, importResult models.ImportResult, progress chan models.Progress) (models.ImportResult, error) { @@ -197,13 +197,13 @@ func (b *ListenBrainzApiBackend) ImportLoves(export models.LovesResult, importRe } // TODO: Store MBIDs directly - b.existingMbids = make(map[string]bool, len(existingLoves.Loves)) - for _, love := range existingLoves.Loves { + b.existingMbids = make(map[string]bool, len(existingLoves.Items)) + for _, love := range existingLoves.Items { b.existingMbids[string(love.RecordingMbid)] = true } } - for _, love := range export.Loves { + for _, love := range export.Items { recordingMbid := string(love.RecordingMbid) if recordingMbid == "" { diff --git a/internal/backends/maloja/maloja.go b/internal/backends/maloja/maloja.go index 58a70b7..4fa57ad 100644 --- a/internal/backends/maloja/maloja.go +++ b/internal/backends/maloja/maloja.go @@ -85,11 +85,11 @@ out: sort.Sort(listens) progress <- p.Complete() - results <- models.ListensResult{Listens: listens} + results <- models.ListensResult{Items: listens} } func (b *MalojaApiBackend) ImportListens(export models.ListensResult, importResult models.ImportResult, progress chan models.Progress) (models.ImportResult, error) { - for _, listen := range export.Listens { + for _, listen := range export.Items { scrobble := NewScrobble{ Title: listen.TrackName, Artists: listen.ArtistNames, diff --git a/internal/backends/process.go b/internal/backends/process.go index 1143cc0..2e8cb2e 100644 --- a/internal/backends/process.go +++ b/internal/backends/process.go @@ -39,7 +39,7 @@ func ProcessListensImports(importer models.ListensImport, results chan models.Li if exportResult.Total > 0 { result.TotalCount = exportResult.Total } else { - result.TotalCount += len(exportResult.Listens) + result.TotalCount += len(exportResult.Items) } importResult, err := importer.ImportListens(exportResult, result, progress) if err != nil { @@ -81,7 +81,7 @@ func ProcessLovesImports(importer models.LovesImport, results chan models.LovesR if exportResult.Total > 0 { result.TotalCount = exportResult.Total } else { - result.TotalCount += len(exportResult.Loves) + result.TotalCount += len(exportResult.Items) } importResult, err := importer.ImportLoves(exportResult, result, progress) if err != nil { diff --git a/internal/backends/scrobblerlog/scrobblerlog.go b/internal/backends/scrobblerlog/scrobblerlog.go index 1c4f652..cbd0aa8 100644 --- a/internal/backends/scrobblerlog/scrobblerlog.go +++ b/internal/backends/scrobblerlog/scrobblerlog.go @@ -119,17 +119,17 @@ func (b *ScrobblerLogBackend) ExportListens(oldestTimestamp time.Time, results c listens := log.Listens.NewerThan(oldestTimestamp) sort.Sort(listens) progress <- models.Progress{Elapsed: int64(len(listens))}.Complete() - results <- models.ListensResult{Listens: listens} + 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.Listens) + lastTimestamp, err := Write(b.file, export.Items) if err != nil { return importResult, err } importResult.UpdateTimestamp(lastTimestamp) - importResult.ImportCount += len(export.Listens) + importResult.ImportCount += len(export.Items) progress <- models.Progress{}.FromImportResult(importResult) return importResult, nil diff --git a/internal/backends/spotify/spotify.go b/internal/backends/spotify/spotify.go index 1c797c8..e584cc1 100644 --- a/internal/backends/spotify/spotify.go +++ b/internal/backends/spotify/spotify.go @@ -137,7 +137,7 @@ func (b *SpotifyApiBackend) ExportListens(oldestTimestamp time.Time, results cha sort.Sort(listens) p.Elapsed = int64(totalDuration.Seconds() - remainingTime.Seconds()) progress <- p - results <- models.ListensResult{Listens: listens, OldestTimestamp: minTime} + results <- models.ListensResult{Items: listens, OldestTimestamp: minTime} } results <- models.ListensResult{OldestTimestamp: minTime} @@ -193,7 +193,7 @@ out: } sort.Sort(loves) - results <- models.LovesResult{Loves: loves, Total: totalCount} + results <- models.LovesResult{Items: loves, Total: totalCount} p.Elapsed += int64(count) progress <- p diff --git a/internal/backends/subsonic/subsonic.go b/internal/backends/subsonic/subsonic.go index 2d42fff..1167f87 100644 --- a/internal/backends/subsonic/subsonic.go +++ b/internal/backends/subsonic/subsonic.go @@ -62,7 +62,7 @@ func (b *SubsonicApiBackend) ExportLoves(oldestTimestamp time.Time, results chan } progress <- models.Progress{Elapsed: int64(len(starred.Song))}.Complete() - results <- models.LovesResult{Loves: b.filterSongs(starred.Song, oldestTimestamp)} + results <- models.LovesResult{Items: b.filterSongs(starred.Song, oldestTimestamp)} } func (b *SubsonicApiBackend) filterSongs(songs []*subsonic.Child, oldestTimestamp time.Time) models.LovesList { diff --git a/internal/models/models.go b/internal/models/models.go index 2175ea1..086b922 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -147,18 +147,16 @@ func (l LovesList) Swap(i, j int) { l[i], l[j] = l[j], l[i] } -type ListensResult struct { +type ExportResult[T LovesList | ListensList] struct { + Items T Total int - Listens ListensList OldestTimestamp time.Time Error error } -type LovesResult struct { - Total int - Loves LovesList - Error error -} +type ListensResult ExportResult[ListensList] + +type LovesResult ExportResult[LovesList] type ImportResult struct { TotalCount int From a87686af572c9187eb83931206bd0c3723e6eabf Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 3 Dec 2023 16:56:32 +0100 Subject: [PATCH 020/150] Reduced code redundancy in import processing --- cmd/listens.go | 9 ++- cmd/loves.go | 9 ++- internal/backends/import.go | 121 +++++++++++++++++++++++++++++++++++ internal/backends/process.go | 110 ------------------------------- 4 files changed, 133 insertions(+), 116 deletions(-) create mode 100644 internal/backends/import.go delete mode 100644 internal/backends/process.go diff --git a/cmd/listens.go b/cmd/listens.go index c18bace..9093dbc 100644 --- a/cmd/listens.go +++ b/cmd/listens.go @@ -70,12 +70,15 @@ var listensCmd = &cobra.Command{ progress := progressBar(&wg, exportProgress, importProgress) // Export from source - listensChan := make(chan models.ListensResult, 1000) - go exportBackend.ExportListens(timestamp, listensChan, exportProgress) + exportChan := make(chan models.ListensResult, 1000) + go exportBackend.ExportListens(timestamp, exportChan, exportProgress) // Import into target resultChan := make(chan models.ImportResult) - go backends.ProcessListensImports(importBackend, listensChan, resultChan, importProgress) + var processor = backends.ListensImportProcessor{ + Backend: importBackend, + } + go processor.Process(exportChan, resultChan, importProgress) result := <-resultChan close(exportProgress) wg.Wait() diff --git a/cmd/loves.go b/cmd/loves.go index 941290f..623b477 100644 --- a/cmd/loves.go +++ b/cmd/loves.go @@ -70,12 +70,15 @@ var lovesCmd = &cobra.Command{ progress := progressBar(&wg, exportProgress, importProgress) // Export from source - lovesChan := make(chan models.LovesResult, 1000) - go exportBackend.ExportLoves(timestamp, lovesChan, exportProgress) + exportChan := make(chan models.LovesResult, 1000) + go exportBackend.ExportLoves(timestamp, exportChan, exportProgress) // Import into target resultChan := make(chan models.ImportResult) - go backends.ProcessLovesImports(importBackend, lovesChan, resultChan, importProgress) + var processor = backends.LovesImportProcessor{ + Backend: importBackend, + } + go processor.Process(exportChan, resultChan, importProgress) result := <-resultChan close(exportProgress) wg.Wait() diff --git a/internal/backends/import.go b/internal/backends/import.go new file mode 100644 index 0000000..cd47960 --- /dev/null +++ b/internal/backends/import.go @@ -0,0 +1,121 @@ +/* +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 backends + +import "go.uploadedlobster.com/scotty/internal/models" + +type ImportProcessor[T models.ListensResult | models.LovesResult] interface { + ImportBackend() models.ImportBackend + Process(results chan T, out chan models.ImportResult, progress chan models.Progress) + Import(export T, result models.ImportResult, out chan models.ImportResult, progress chan models.Progress) models.ImportResult +} + +type ListensImportProcessor struct { + Backend models.ListensImport +} + +func (p ListensImportProcessor) ImportBackend() models.ImportBackend { + return p.Backend +} + +func (p ListensImportProcessor) Process(results chan models.ListensResult, out chan models.ImportResult, progress chan models.Progress) { + process(p, results, out, progress) +} + +func (p ListensImportProcessor) Import(export models.ListensResult, result models.ImportResult, out chan models.ImportResult, progress chan models.Progress) models.ImportResult { + if export.Error != nil { + handleError(result, export.Error, out, progress) + return result + } + + if export.Total > 0 { + result.TotalCount = export.Total + } else { + result.TotalCount += len(export.Items) + } + importResult, err := p.Backend.ImportListens(export, result, progress) + if err != nil { + handleError(importResult, err, out, progress) + return result + } + return importResult +} + +type LovesImportProcessor struct { + Backend models.LovesImport +} + +func (p LovesImportProcessor) ImportBackend() models.ImportBackend { + return p.Backend +} + +func (p LovesImportProcessor) Process(results chan models.LovesResult, out chan models.ImportResult, progress chan models.Progress) { + process(p, results, out, progress) +} + +func (p LovesImportProcessor) Import(export models.LovesResult, result models.ImportResult, out chan models.ImportResult, progress chan models.Progress) models.ImportResult { + if export.Error != nil { + handleError(result, export.Error, out, progress) + return result + } + + if export.Total > 0 { + result.TotalCount = export.Total + } else { + result.TotalCount += len(export.Items) + } + importResult, err := p.Backend.ImportLoves(export, result, progress) + if err != nil { + handleError(importResult, err, out, progress) + return result + } + return importResult +} + +func process[R models.LovesResult | models.ListensResult, P ImportProcessor[R]](processor P, results chan R, out chan models.ImportResult, progress chan models.Progress) { + defer close(out) + defer close(progress) + result := models.ImportResult{} + + err := processor.ImportBackend().StartImport() + if err != nil { + handleError(result, err, out, progress) + return + } + + for exportResult := range results { + importResult := processor.Import(exportResult, result, out, progress) + result.Update(importResult) + progress <- models.Progress{}.FromImportResult(result) + } + + err = processor.ImportBackend().FinishImport() + if err != nil { + handleError(result, err, out, progress) + return + } + + progress <- models.Progress{}.FromImportResult(result).Complete() + out <- result +} + +func handleError(result models.ImportResult, err error, out chan models.ImportResult, progress chan models.Progress) { + result.Error = err + progress <- models.Progress{}.FromImportResult(result).Complete() + out <- result +} diff --git a/internal/backends/process.go b/internal/backends/process.go deleted file mode 100644 index 2e8cb2e..0000000 --- a/internal/backends/process.go +++ /dev/null @@ -1,110 +0,0 @@ -/* -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 backends - -import "go.uploadedlobster.com/scotty/internal/models" - -func ProcessListensImports(importer models.ListensImport, results chan models.ListensResult, out chan models.ImportResult, progress chan models.Progress) { - defer close(out) - defer close(progress) - result := models.ImportResult{} - - err := importer.StartImport() - if err != nil { - handleError(result, err, out, progress) - return - } - - for exportResult := range results { - if exportResult.Error != nil { - handleError(result, exportResult.Error, out, progress) - return - } - - if exportResult.Total > 0 { - result.TotalCount = exportResult.Total - } else { - result.TotalCount += len(exportResult.Items) - } - importResult, err := importer.ImportListens(exportResult, result, progress) - if err != nil { - handleError(importResult, err, out, progress) - return - } - - result.Update(importResult) - progress <- models.Progress{}.FromImportResult(result) - } - - err = importer.FinishImport() - if err != nil { - handleError(result, err, out, progress) - return - } - - progress <- models.Progress{}.FromImportResult(result).Complete() - out <- result -} - -func ProcessLovesImports(importer models.LovesImport, results chan models.LovesResult, out chan models.ImportResult, progress chan models.Progress) { - defer close(out) - defer close(progress) - result := models.ImportResult{} - - err := importer.StartImport() - if err != nil { - handleError(result, err, out, progress) - return - } - - for exportResult := range results { - if exportResult.Error != nil { - handleError(result, exportResult.Error, out, progress) - return - } - - if exportResult.Total > 0 { - result.TotalCount = exportResult.Total - } else { - result.TotalCount += len(exportResult.Items) - } - importResult, err := importer.ImportLoves(exportResult, result, progress) - if err != nil { - handleError(importResult, err, out, progress) - return - } - - result.Update(importResult) - progress <- models.Progress{}.FromImportResult(result) - } - - err = importer.FinishImport() - if err != nil { - handleError(result, err, out, progress) - return - } - - progress <- models.Progress{}.FromImportResult(result).Complete() - out <- result -} - -func handleError(result models.ImportResult, err error, out chan models.ImportResult, progress chan models.Progress) { - result.Error = err - progress <- models.Progress{}.FromImportResult(result).Complete() - out <- result -} From 7c85ba05abc7ba9c255f93478b97da7babd5aa9d Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Mon, 4 Dec 2023 08:03:32 +0100 Subject: [PATCH 021/150] refactor: generic common cmd processing --- cmd/common.go | 116 ++++++++++++++++++++++++++++++++++++ cmd/listens.go | 80 ++----------------------- cmd/loves.go | 80 ++----------------------- internal/backends/export.go | 51 ++++++++++++++++ internal/backends/import.go | 4 +- 5 files changed, 182 insertions(+), 149 deletions(-) create mode 100644 internal/backends/export.go diff --git a/cmd/common.go b/cmd/common.go index 05059b4..cee8c16 100644 --- a/cmd/common.go +++ b/cmd/common.go @@ -18,9 +18,14 @@ package cmd import ( "fmt" + "sync" + "time" "github.com/spf13/cobra" "github.com/spf13/viper" + "go.uploadedlobster.com/scotty/internal/backends" + "go.uploadedlobster.com/scotty/internal/models" + "go.uploadedlobster.com/scotty/internal/storage" ) func getConfigFromFlag(cmd *cobra.Command, flagName string) (string, *viper.Viper) { @@ -43,3 +48,114 @@ func getInt64FromFlag(cmd *cobra.Command, flagName string) (result int64) { } return } + +type backendInfo[T models.Backend, R models.ListensResult | models.LovesResult] struct { + configName string + backend T +} + +type exportBackendInfo[T models.Backend, R models.ListensResult | models.LovesResult] struct { + backendInfo[T, R] + processor backends.ExportProcessor[R] +} + +type importBackendInfo[T models.Backend, R models.ListensResult | models.LovesResult] struct { + backendInfo[T, R] + processor backends.ImportProcessor[R] +} + +func resolveBackends[E models.Backend, I models.ImportBackend, R models.ListensResult | models.LovesResult](cmd *cobra.Command) (*exportBackendInfo[E, R], *importBackendInfo[I, R], error) { + sourceName, sourceConfig := getConfigFromFlag(cmd, "from") + targetName, targetConfig := getConfigFromFlag(cmd, "to") + // Initialize backends + exportBackend, err := backends.ResolveBackend[E](sourceConfig) + if err != nil { + return nil, nil, err + } + importBackend, err := backends.ResolveBackend[I](targetConfig) + if err != nil { + return nil, nil, err + } + + exportInfo := exportBackendInfo[E, R]{ + backendInfo: backendInfo[E, R]{ + configName: sourceName, + backend: exportBackend, + }, + } + + importInfo := importBackendInfo[I, R]{ + backendInfo: backendInfo[I, R]{ + configName: targetName, + backend: importBackend, + }, + } + + return &exportInfo, &importInfo, nil +} + +func cmdExportImport[E models.Backend, I models.ImportBackend, R models.ListensResult | models.LovesResult](cmd *cobra.Command, entity string, exp *exportBackendInfo[E, R], imp *importBackendInfo[I, R]) { + sourceName := exp.configName + targetName := imp.configName + fmt.Printf("Transferring %s from %s to %s...\n", entity, sourceName, targetName) + + // Setup database + db, err := storage.New(viper.GetString("database")) + cobra.CheckErr(err) + + // Authenticate backends, if needed + config := viper.GetViper() + _, err = backends.Authenticate(sourceName, exp.backend, db, config) + cobra.CheckErr(err) + + _, err = backends.Authenticate(targetName, imp.backend, db, config) + cobra.CheckErr(err) + + // Read timestamp + timestamp := time.Unix(getInt64FromFlag(cmd, "timestamp"), 0) + if timestamp == time.Unix(0, 0) { + timestamp, err = db.GetImportTimestamp(sourceName, targetName, entity) + cobra.CheckErr(err) + } + fmt.Printf("From timestamp: %v (%v)\n", timestamp, timestamp.Unix()) + + // Prepare progress bars + exportProgress := make(chan models.Progress) + importProgress := make(chan models.Progress) + var wg sync.WaitGroup + progress := progressBar(&wg, exportProgress, importProgress) + + // Export from source + exportChan := make(chan R, 1000) + go exp.processor.Process(timestamp, exportChan, exportProgress) + + // Import into target + resultChan := make(chan models.ImportResult) + go imp.processor.Process(exportChan, resultChan, importProgress) + result := <-resultChan + close(exportProgress) + wg.Wait() + progress.Wait() + if result.Error != nil { + fmt.Printf("Import failed, last reported timestamp was %v (%v)\n", result.LastTimestamp, result.LastTimestamp.Unix()) + cobra.CheckErr(result.Error) + } + fmt.Printf("Imported %v of %v %s into %v.\n", + result.ImportCount, result.TotalCount, entity, targetName) + + // Update timestamp + if result.LastTimestamp.Unix() < timestamp.Unix() { + result.LastTimestamp = timestamp + } + fmt.Printf("Latest timestamp: %v (%v)\n", result.LastTimestamp, result.LastTimestamp.Unix()) + err = db.SetImportTimestamp(sourceName, targetName, entity, result.LastTimestamp) + cobra.CheckErr(err) + + // Print errors + if len(result.ImportErrors) > 0 { + fmt.Printf("\nDuring the import the following errors occurred:\n") + for _, err := range result.ImportErrors { + fmt.Printf("Error: %v\n", err) + } + } +} diff --git a/cmd/listens.go b/cmd/listens.go index 9093dbc..1d52dc5 100644 --- a/cmd/listens.go +++ b/cmd/listens.go @@ -17,15 +17,9 @@ Scotty. If not, see . package cmd import ( - "fmt" - "sync" - "time" - "github.com/spf13/cobra" - "github.com/spf13/viper" "go.uploadedlobster.com/scotty/internal/backends" "go.uploadedlobster.com/scotty/internal/models" - "go.uploadedlobster.com/scotty/internal/storage" ) // listensCmd represents the listens command @@ -34,77 +28,15 @@ var listensCmd = &cobra.Command{ Short: "Transfer listens between two services", Long: `Transfers listens between two configured services.`, Run: func(cmd *cobra.Command, args []string) { - sourceName, sourceConfig := getConfigFromFlag(cmd, "from") - targetName, targetConfig := getConfigFromFlag(cmd, "to") - fmt.Printf("Transferring listens from %s to %s...\n", sourceName, targetName) - - // Setup database - db, err := storage.New(viper.GetString("database")) + exp, imp, err := resolveBackends[models.ListensExport, models.ListensImport, models.ListensResult](cmd) cobra.CheckErr(err) - - // Initialize backends - exportBackend, err := backends.ResolveBackend[models.ListensExport](sourceConfig) - cobra.CheckErr(err) - importBackend, err := backends.ResolveBackend[models.ListensImport](targetConfig) - cobra.CheckErr(err) - - // Authenticate backends, if needed - _, err = backends.Authenticate(sourceName, exportBackend, db, viper.GetViper()) - cobra.CheckErr(err) - - _, err = backends.Authenticate(targetName, importBackend, db, viper.GetViper()) - cobra.CheckErr(err) - - // Read timestamp - timestamp := time.Unix(getInt64FromFlag(cmd, "timestamp"), 0) - if timestamp == time.Unix(0, 0) { - timestamp, err = db.GetImportTimestamp(sourceName, targetName, "listens") - cobra.CheckErr(err) + exp.processor = backends.ListensExportProcessor{ + Backend: exp.backend, } - fmt.Printf("From timestamp: %v (%v)\n", timestamp, timestamp.Unix()) - - // Prepare progress bars - exportProgress := make(chan models.Progress) - importProgress := make(chan models.Progress) - var wg sync.WaitGroup - progress := progressBar(&wg, exportProgress, importProgress) - - // Export from source - exportChan := make(chan models.ListensResult, 1000) - go exportBackend.ExportListens(timestamp, exportChan, exportProgress) - - // Import into target - resultChan := make(chan models.ImportResult) - var processor = backends.ListensImportProcessor{ - Backend: importBackend, - } - go processor.Process(exportChan, resultChan, importProgress) - result := <-resultChan - close(exportProgress) - wg.Wait() - progress.Wait() - if result.Error != nil { - fmt.Printf("Import failed, last reported timestamp was %v (%v)\n", result.LastTimestamp, result.LastTimestamp.Unix()) - cobra.CheckErr(result.Error) - } - fmt.Printf("Imported %v of %v listens into %v.\n", - result.ImportCount, result.TotalCount, targetName) - - // Update timestamp - if result.LastTimestamp.Unix() < timestamp.Unix() { - result.LastTimestamp = timestamp - } - fmt.Printf("Latest timestamp: %v (%v)\n", result.LastTimestamp, result.LastTimestamp.Unix()) - err = db.SetImportTimestamp(sourceName, targetName, "listens", result.LastTimestamp) - cobra.CheckErr(err) - - // Print errors - if len(result.ImportErrors) > 0 { - fmt.Printf("\nDuring the import the following errors occurred:\n") - for _, err := range result.ImportErrors { - fmt.Printf("Error: %v\n", err) - } + imp.processor = backends.ListensImportProcessor{ + Backend: imp.backend, } + cmdExportImport(cmd, "listens", exp, imp) }, } diff --git a/cmd/loves.go b/cmd/loves.go index 623b477..ea257a0 100644 --- a/cmd/loves.go +++ b/cmd/loves.go @@ -17,15 +17,9 @@ Scotty. If not, see . package cmd import ( - "fmt" - "sync" - "time" - "github.com/spf13/cobra" - "github.com/spf13/viper" "go.uploadedlobster.com/scotty/internal/backends" "go.uploadedlobster.com/scotty/internal/models" - "go.uploadedlobster.com/scotty/internal/storage" ) // lovesCmd represents the loves command @@ -34,77 +28,15 @@ var lovesCmd = &cobra.Command{ Short: "Transfer loves between two services", Long: `Transfers loves between two configured services.`, Run: func(cmd *cobra.Command, args []string) { - sourceName, sourceConfig := getConfigFromFlag(cmd, "from") - targetName, targetConfig := getConfigFromFlag(cmd, "to") - fmt.Printf("Transferring loves from %s to %s...\n", sourceName, targetName) - - // Setup database - db, err := storage.New(viper.GetString("database")) + exp, imp, err := resolveBackends[models.LovesExport, models.LovesImport, models.LovesResult](cmd) cobra.CheckErr(err) - - // Initialize backends - exportBackend, err := backends.ResolveBackend[models.LovesExport](sourceConfig) - cobra.CheckErr(err) - importBackend, err := backends.ResolveBackend[models.LovesImport](targetConfig) - cobra.CheckErr(err) - - // Authenticate backends, if needed - _, err = backends.Authenticate(sourceName, exportBackend, db, viper.GetViper()) - cobra.CheckErr(err) - - _, err = backends.Authenticate(targetName, importBackend, db, viper.GetViper()) - cobra.CheckErr(err) - - // Read timestamp - timestamp := time.Unix(getInt64FromFlag(cmd, "timestamp"), 0) - if timestamp == time.Unix(0, 0) { - timestamp, err = db.GetImportTimestamp(sourceName, targetName, "loves") - cobra.CheckErr(err) + exp.processor = backends.LovesExportProcessor{ + Backend: exp.backend, } - fmt.Printf("From timestamp: %v (%v)\n", timestamp, timestamp.Unix()) - - // Prepare progress bars - exportProgress := make(chan models.Progress) - importProgress := make(chan models.Progress) - var wg sync.WaitGroup - progress := progressBar(&wg, exportProgress, importProgress) - - // Export from source - exportChan := make(chan models.LovesResult, 1000) - go exportBackend.ExportLoves(timestamp, exportChan, exportProgress) - - // Import into target - resultChan := make(chan models.ImportResult) - var processor = backends.LovesImportProcessor{ - Backend: importBackend, - } - go processor.Process(exportChan, resultChan, importProgress) - result := <-resultChan - close(exportProgress) - wg.Wait() - progress.Wait() - if result.Error != nil { - fmt.Printf("Import failed, last reported timestamp was %v (%v)\n", result.LastTimestamp, result.LastTimestamp.Unix()) - cobra.CheckErr(result.Error) - } - fmt.Printf("Imported %v of %v loves into %v.\n", - result.ImportCount, result.TotalCount, targetName) - - // Update timestamp - if result.LastTimestamp.Unix() < timestamp.Unix() { - result.LastTimestamp = timestamp - } - fmt.Printf("Latest timestamp: %v (%v)\n", result.LastTimestamp, result.LastTimestamp.Unix()) - err = db.SetImportTimestamp(sourceName, targetName, "loves", result.LastTimestamp) - cobra.CheckErr(err) - - // Print errors - if len(result.ImportErrors) > 0 { - fmt.Printf("\nDuring the import the following errors occurred:\n") - for _, err := range result.ImportErrors { - fmt.Printf("Error: %v\n", err) - } + imp.processor = backends.LovesImportProcessor{ + Backend: imp.backend, } + cmdExportImport(cmd, "loves", exp, imp) }, } diff --git a/internal/backends/export.go b/internal/backends/export.go new file mode 100644 index 0000000..9bdb8a1 --- /dev/null +++ b/internal/backends/export.go @@ -0,0 +1,51 @@ +/* +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 backends + +import ( + "time" + + "go.uploadedlobster.com/scotty/internal/models" +) + +type ExportProcessor[T models.ListensResult | models.LovesResult] interface { + ExportBackend() models.Backend + Process(oldestTimestamp time.Time, results chan T, progress chan models.Progress) +} + +type ListensExportProcessor struct { + Backend models.ListensExport +} + +func (p ListensExportProcessor) ExportBackend() models.Backend { + return p.Backend +} + +func (p ListensExportProcessor) Process(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.Progress) { + p.Backend.ExportListens(oldestTimestamp, results, progress) +} + +type LovesExportProcessor struct { + Backend models.LovesExport +} + +func (p LovesExportProcessor) ExportBackend() models.Backend { + return p.Backend +} + +func (p LovesExportProcessor) Process(oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.Progress) { + p.Backend.ExportLoves(oldestTimestamp, results, progress) +} diff --git a/internal/backends/import.go b/internal/backends/import.go index cd47960..6365cbb 100644 --- a/internal/backends/import.go +++ b/internal/backends/import.go @@ -17,7 +17,9 @@ Scotty. If not, see . package backends -import "go.uploadedlobster.com/scotty/internal/models" +import ( + "go.uploadedlobster.com/scotty/internal/models" +) type ImportProcessor[T models.ListensResult | models.LovesResult] interface { ImportBackend() models.ImportBackend From a9e07915ce30690b64cdb8c20b71085f59fad325 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 5 Dec 2023 08:10:38 +0100 Subject: [PATCH 022/150] Transfer command refactoring Make transfer command processing even more modular --- cmd/auth.go | 3 +- cmd/common.go | 161 ------------------------------ cmd/listens.go | 22 ++-- cmd/loves.go | 22 ++-- internal/cli/common.go | 44 ++++++++ {cmd => internal/cli}/progress.go | 2 +- internal/cli/transfer.go | 161 ++++++++++++++++++++++++++++++ 7 files changed, 236 insertions(+), 179 deletions(-) delete mode 100644 cmd/common.go create mode 100644 internal/cli/common.go rename {cmd => internal/cli}/progress.go (99%) create mode 100644 internal/cli/transfer.go diff --git a/cmd/auth.go b/cmd/auth.go index f7876fc..2b618bf 100644 --- a/cmd/auth.go +++ b/cmd/auth.go @@ -25,6 +25,7 @@ import ( "github.com/spf13/viper" "go.uploadedlobster.com/scotty/internal/auth" "go.uploadedlobster.com/scotty/internal/backends" + "go.uploadedlobster.com/scotty/internal/cli" "go.uploadedlobster.com/scotty/internal/models" "go.uploadedlobster.com/scotty/internal/storage" "golang.org/x/oauth2" @@ -36,7 +37,7 @@ var authCmd = &cobra.Command{ Short: "Authenticate with a backend", Long: `For backends requiring authentication this command can be used to authenticate.`, Run: func(cmd *cobra.Command, args []string) { - serviceName, serviceConfig := getConfigFromFlag(cmd, "service") + serviceName, serviceConfig := cli.GetConfigFromFlag(cmd, "service") backend, err := backends.ResolveBackend[models.OAuth2Authenticator](serviceConfig) cobra.CheckErr(err) diff --git a/cmd/common.go b/cmd/common.go deleted file mode 100644 index cee8c16..0000000 --- a/cmd/common.go +++ /dev/null @@ -1,161 +0,0 @@ -/* -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 cmd - -import ( - "fmt" - "sync" - "time" - - "github.com/spf13/cobra" - "github.com/spf13/viper" - "go.uploadedlobster.com/scotty/internal/backends" - "go.uploadedlobster.com/scotty/internal/models" - "go.uploadedlobster.com/scotty/internal/storage" -) - -func getConfigFromFlag(cmd *cobra.Command, flagName string) (string, *viper.Viper) { - configName := cmd.Flag(flagName).Value.String() - var config *viper.Viper - servicesConfig := viper.Sub("service") - if servicesConfig != nil { - config = servicesConfig.Sub(configName) - } - if config == nil { - cobra.CheckErr(fmt.Sprintf("Invalid source configuration \"%s\"", configName)) - } - return configName, config -} - -func getInt64FromFlag(cmd *cobra.Command, flagName string) (result int64) { - result, err := cmd.Flags().GetInt64(flagName) - if err != nil { - result = 0 - } - return -} - -type backendInfo[T models.Backend, R models.ListensResult | models.LovesResult] struct { - configName string - backend T -} - -type exportBackendInfo[T models.Backend, R models.ListensResult | models.LovesResult] struct { - backendInfo[T, R] - processor backends.ExportProcessor[R] -} - -type importBackendInfo[T models.Backend, R models.ListensResult | models.LovesResult] struct { - backendInfo[T, R] - processor backends.ImportProcessor[R] -} - -func resolveBackends[E models.Backend, I models.ImportBackend, R models.ListensResult | models.LovesResult](cmd *cobra.Command) (*exportBackendInfo[E, R], *importBackendInfo[I, R], error) { - sourceName, sourceConfig := getConfigFromFlag(cmd, "from") - targetName, targetConfig := getConfigFromFlag(cmd, "to") - // Initialize backends - exportBackend, err := backends.ResolveBackend[E](sourceConfig) - if err != nil { - return nil, nil, err - } - importBackend, err := backends.ResolveBackend[I](targetConfig) - if err != nil { - return nil, nil, err - } - - exportInfo := exportBackendInfo[E, R]{ - backendInfo: backendInfo[E, R]{ - configName: sourceName, - backend: exportBackend, - }, - } - - importInfo := importBackendInfo[I, R]{ - backendInfo: backendInfo[I, R]{ - configName: targetName, - backend: importBackend, - }, - } - - return &exportInfo, &importInfo, nil -} - -func cmdExportImport[E models.Backend, I models.ImportBackend, R models.ListensResult | models.LovesResult](cmd *cobra.Command, entity string, exp *exportBackendInfo[E, R], imp *importBackendInfo[I, R]) { - sourceName := exp.configName - targetName := imp.configName - fmt.Printf("Transferring %s from %s to %s...\n", entity, sourceName, targetName) - - // Setup database - db, err := storage.New(viper.GetString("database")) - cobra.CheckErr(err) - - // Authenticate backends, if needed - config := viper.GetViper() - _, err = backends.Authenticate(sourceName, exp.backend, db, config) - cobra.CheckErr(err) - - _, err = backends.Authenticate(targetName, imp.backend, db, config) - cobra.CheckErr(err) - - // Read timestamp - timestamp := time.Unix(getInt64FromFlag(cmd, "timestamp"), 0) - if timestamp == time.Unix(0, 0) { - timestamp, err = db.GetImportTimestamp(sourceName, targetName, entity) - cobra.CheckErr(err) - } - fmt.Printf("From timestamp: %v (%v)\n", timestamp, timestamp.Unix()) - - // Prepare progress bars - exportProgress := make(chan models.Progress) - importProgress := make(chan models.Progress) - var wg sync.WaitGroup - progress := progressBar(&wg, exportProgress, importProgress) - - // Export from source - exportChan := make(chan R, 1000) - go exp.processor.Process(timestamp, exportChan, exportProgress) - - // Import into target - resultChan := make(chan models.ImportResult) - go imp.processor.Process(exportChan, resultChan, importProgress) - result := <-resultChan - close(exportProgress) - wg.Wait() - progress.Wait() - if result.Error != nil { - fmt.Printf("Import failed, last reported timestamp was %v (%v)\n", result.LastTimestamp, result.LastTimestamp.Unix()) - cobra.CheckErr(result.Error) - } - fmt.Printf("Imported %v of %v %s into %v.\n", - result.ImportCount, result.TotalCount, entity, targetName) - - // Update timestamp - if result.LastTimestamp.Unix() < timestamp.Unix() { - result.LastTimestamp = timestamp - } - fmt.Printf("Latest timestamp: %v (%v)\n", result.LastTimestamp, result.LastTimestamp.Unix()) - err = db.SetImportTimestamp(sourceName, targetName, entity, result.LastTimestamp) - cobra.CheckErr(err) - - // Print errors - if len(result.ImportErrors) > 0 { - fmt.Printf("\nDuring the import the following errors occurred:\n") - for _, err := range result.ImportErrors { - fmt.Printf("Error: %v\n", err) - } - } -} diff --git a/cmd/listens.go b/cmd/listens.go index 1d52dc5..8bf3c3b 100644 --- a/cmd/listens.go +++ b/cmd/listens.go @@ -18,8 +18,11 @@ package cmd import ( "github.com/spf13/cobra" + "github.com/spf13/viper" "go.uploadedlobster.com/scotty/internal/backends" + "go.uploadedlobster.com/scotty/internal/cli" "go.uploadedlobster.com/scotty/internal/models" + "go.uploadedlobster.com/scotty/internal/storage" ) // listensCmd represents the listens command @@ -28,15 +31,18 @@ var listensCmd = &cobra.Command{ Short: "Transfer listens between two services", Long: `Transfers listens between two configured services.`, Run: func(cmd *cobra.Command, args []string) { - exp, imp, err := resolveBackends[models.ListensExport, models.ListensImport, models.ListensResult](cmd) + db, err := storage.New(viper.GetString("database")) + cobra.CheckErr(err) + c, err := cli.NewTransferCmd[ + models.ListensExport, + models.ListensImport, + models.ListensResult, + ](cmd, &db, "listens") + cobra.CheckErr(err) + exp := backends.ListensExportProcessor{Backend: c.ExpBackend} + imp := backends.ListensImportProcessor{Backend: c.ImpBackend} + err = c.Transfer(exp, imp) cobra.CheckErr(err) - exp.processor = backends.ListensExportProcessor{ - Backend: exp.backend, - } - imp.processor = backends.ListensImportProcessor{ - Backend: imp.backend, - } - cmdExportImport(cmd, "listens", exp, imp) }, } diff --git a/cmd/loves.go b/cmd/loves.go index ea257a0..f44cb5b 100644 --- a/cmd/loves.go +++ b/cmd/loves.go @@ -18,8 +18,11 @@ package cmd import ( "github.com/spf13/cobra" + "github.com/spf13/viper" "go.uploadedlobster.com/scotty/internal/backends" + "go.uploadedlobster.com/scotty/internal/cli" "go.uploadedlobster.com/scotty/internal/models" + "go.uploadedlobster.com/scotty/internal/storage" ) // lovesCmd represents the loves command @@ -28,15 +31,18 @@ var lovesCmd = &cobra.Command{ Short: "Transfer loves between two services", Long: `Transfers loves between two configured services.`, Run: func(cmd *cobra.Command, args []string) { - exp, imp, err := resolveBackends[models.LovesExport, models.LovesImport, models.LovesResult](cmd) + db, err := storage.New(viper.GetString("database")) + cobra.CheckErr(err) + c, err := cli.NewTransferCmd[ + models.LovesExport, + models.LovesImport, + models.LovesResult, + ](cmd, &db, "loves") + cobra.CheckErr(err) + exp := backends.LovesExportProcessor{Backend: c.ExpBackend} + imp := backends.LovesImportProcessor{Backend: c.ImpBackend} + err = c.Transfer(exp, imp) cobra.CheckErr(err) - exp.processor = backends.LovesExportProcessor{ - Backend: exp.backend, - } - imp.processor = backends.LovesImportProcessor{ - Backend: imp.backend, - } - cmdExportImport(cmd, "loves", exp, imp) }, } diff --git a/internal/cli/common.go b/internal/cli/common.go new file mode 100644 index 0000000..a26253d --- /dev/null +++ b/internal/cli/common.go @@ -0,0 +1,44 @@ +/* +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 cli + +import ( + "fmt" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func GetConfigFromFlag(cmd *cobra.Command, flagName string) (string, *viper.Viper) { + configName := cmd.Flag(flagName).Value.String() + var config *viper.Viper + servicesConfig := viper.Sub("service") + if servicesConfig != nil { + config = servicesConfig.Sub(configName) + } + if config == nil { + cobra.CheckErr(fmt.Sprintf("Invalid source configuration \"%s\"", configName)) + } + return configName, config +} + +func getInt64FromFlag(cmd *cobra.Command, flagName string) (result int64) { + result, err := cmd.Flags().GetInt64(flagName) + if err != nil { + result = 0 + } + return +} diff --git a/cmd/progress.go b/internal/cli/progress.go similarity index 99% rename from cmd/progress.go rename to internal/cli/progress.go index b328f29..457383c 100644 --- a/cmd/progress.go +++ b/internal/cli/progress.go @@ -15,7 +15,7 @@ You should have received a copy of the GNU General Public License along with Scotty. If not, see . */ -package cmd +package cli import ( "sync" diff --git a/internal/cli/transfer.go b/internal/cli/transfer.go new file mode 100644 index 0000000..35add01 --- /dev/null +++ b/internal/cli/transfer.go @@ -0,0 +1,161 @@ +/* +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 cli + +import ( + "fmt" + "sync" + "time" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + "go.uploadedlobster.com/scotty/internal/backends" + "go.uploadedlobster.com/scotty/internal/models" + "go.uploadedlobster.com/scotty/internal/storage" +) + +func NewTransferCmd[ + E models.Backend, + I models.ImportBackend, + R models.ListensResult | models.LovesResult, +]( + cmd *cobra.Command, + db *storage.Database, + entity string, +) (TransferCmd[E, I, R], error) { + c := TransferCmd[E, I, R]{ + cmd: cmd, + db: db, + entity: entity, + } + err := c.resolveBackends() + if err != nil { + return c, err + } + return c, nil +} + +type TransferCmd[E models.Backend, I models.ImportBackend, R models.ListensResult | models.LovesResult] struct { + cmd *cobra.Command + db *storage.Database + entity string + sourceName string + targetName string + ExpBackend E + ImpBackend I +} + +func (c *TransferCmd[E, I, R]) resolveBackends() error { + sourceName, sourceConfig := GetConfigFromFlag(c.cmd, "from") + targetName, targetConfig := GetConfigFromFlag(c.cmd, "to") + + // Initialize backends + expBackend, err := backends.ResolveBackend[E](sourceConfig) + if err != nil { + return err + } + impBackend, err := backends.ResolveBackend[I](targetConfig) + if err != nil { + return err + } + + c.sourceName = sourceName + c.targetName = targetName + c.ExpBackend = expBackend + c.ImpBackend = impBackend + return nil +} + +func (c *TransferCmd[E, I, R]) Transfer(exp backends.ExportProcessor[R], imp backends.ImportProcessor[R]) error { + fmt.Printf("Transferring %s from %s to %s...\n", c.entity, c.sourceName, c.targetName) + + // Authenticate backends, if needed + config := viper.GetViper() + _, err := backends.Authenticate(c.sourceName, c.ExpBackend, *c.db, config) + if err != nil { + return err + } + + _, err = backends.Authenticate(c.targetName, c.ImpBackend, *c.db, config) + if err != nil { + return err + } + + // Read timestamp + timestamp, err := c.timestamp() + if err != nil { + return err + } + fmt.Printf("From timestamp: %v (%v)\n", timestamp, timestamp.Unix()) + + // Prepare progress bars + exportProgress := make(chan models.Progress) + importProgress := make(chan models.Progress) + var wg sync.WaitGroup + progress := progressBar(&wg, exportProgress, importProgress) + + // Export from source + exportChan := make(chan R, 1000) + go exp.Process(timestamp, exportChan, exportProgress) + + // Import into target + resultChan := make(chan models.ImportResult) + go imp.Process(exportChan, resultChan, importProgress) + result := <-resultChan + close(exportProgress) + wg.Wait() + progress.Wait() + if result.Error != nil { + fmt.Printf("Import failed, last reported timestamp was %v (%v)\n", result.LastTimestamp, result.LastTimestamp.Unix()) + return result.Error + } + fmt.Printf("Imported %v of %v %s into %v.\n", + result.ImportCount, result.TotalCount, c.entity, c.targetName) + + // Update timestamp + err = c.updateTimestamp(result, timestamp) + if err != nil { + return err + } + + // Print errors + if len(result.ImportErrors) > 0 { + fmt.Printf("\nDuring the import the following errors occurred:\n") + for _, err := range result.ImportErrors { + fmt.Printf("Error: %v\n", err) + } + } + + return nil +} + +func (c *TransferCmd[E, I, R]) timestamp() (time.Time, error) { + timestamp := time.Unix(getInt64FromFlag(c.cmd, "timestamp"), 0) + if timestamp == time.Unix(0, 0) { + timestamp, err := c.db.GetImportTimestamp(c.sourceName, c.targetName, c.entity) + return timestamp, err + } + return timestamp, nil +} + +func (c *TransferCmd[E, I, R]) updateTimestamp(result models.ImportResult, oldTimestamp time.Time) error { + if result.LastTimestamp.Unix() < oldTimestamp.Unix() { + result.LastTimestamp = oldTimestamp + } + fmt.Printf("Latest timestamp: %v (%v)\n", result.LastTimestamp, result.LastTimestamp.Unix()) + err := c.db.SetImportTimestamp(c.sourceName, c.targetName, c.entity, result.LastTimestamp) + return err +} From 28ed1183e40fbabe518f9ad911a464a3fbc24cb6 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 5 Dec 2023 08:17:46 +0100 Subject: [PATCH 023/150] Use fmt.Errorf, error strings should start lowercase --- internal/backends/backends.go | 6 ++---- internal/backends/backends_test.go | 4 ++-- internal/backends/lastfm/lastfm.go | 5 ++--- internal/backends/scrobblerlog/parser.go | 10 ++++------ 4 files changed, 10 insertions(+), 15 deletions(-) diff --git a/internal/backends/backends.go b/internal/backends/backends.go index 029cca7..02f29ae 100644 --- a/internal/backends/backends.go +++ b/internal/backends/backends.go @@ -17,7 +17,6 @@ Scotty. If not, see . package backends import ( - "errors" "fmt" "reflect" "strings" @@ -54,8 +53,7 @@ func ResolveBackend[T interface{}](config *viper.Viper) (T, error) { if implements { result = backend.(T) } else { - err = errors.New( - fmt.Sprintf("Backend %s does not implement %s", backendName, interfaceName)) + err = fmt.Errorf("backend %s does not implement %s", backendName, interfaceName) } return result, err @@ -93,7 +91,7 @@ func resolveBackend(config *viper.Viper) (string, models.Backend, error) { backendName := config.GetString("backend") backendType := knownBackends[backendName] if backendType == nil { - return backendName, nil, fmt.Errorf("Unknown backend %s", backendName) + return backendName, nil, fmt.Errorf("unknown backend %s", backendName) } return backendName, backendType().FromConfig(config), nil } diff --git a/internal/backends/backends_test.go b/internal/backends/backends_test.go index 1af09a4..5e64b0f 100644 --- a/internal/backends/backends_test.go +++ b/internal/backends/backends_test.go @@ -49,14 +49,14 @@ func TestResolveBackendUnknown(t *testing.T) { config := viper.New() config.Set("backend", "foo") _, err := backends.ResolveBackend[models.ListensImport](config) - assert.EqualError(t, err, "Unknown backend foo") + assert.EqualError(t, err, "unknown backend foo") } func TestResolveBackendInvalidInterface(t *testing.T) { config := viper.New() config.Set("backend", "dump") _, err := backends.ResolveBackend[models.ListensExport](config) - assert.EqualError(t, err, "Backend dump does not implement ListensExport") + assert.EqualError(t, err, "backend dump does not implement ListensExport") } func TestGetBackends(t *testing.T) { diff --git a/internal/backends/lastfm/lastfm.go b/internal/backends/lastfm/lastfm.go index d68ed6f..326be6e 100644 --- a/internal/backends/lastfm/lastfm.go +++ b/internal/backends/lastfm/lastfm.go @@ -16,7 +16,6 @@ Scotty. If not, see . package lastfm import ( - "errors" "fmt" "net/url" "sort" @@ -223,8 +222,8 @@ func (b *LastfmApiBackend) ImportListens(export models.ListensResult, importResu importResult.ImportErrors = append(importResult.ImportErrors, ignoreMsg) } } - errMsg := fmt.Sprintf("Last.fm import ignored %v scrobbles", count-accepted) - return importResult, errors.New(errMsg) + err := fmt.Errorf("last.fm import ignored %v scrobbles", count-accepted) + return importResult, err } importResult.UpdateTimestamp(listens[count-1].ListenedAt) diff --git a/internal/backends/scrobblerlog/parser.go b/internal/backends/scrobblerlog/parser.go index 8687c6a..a503d35 100644 --- a/internal/backends/scrobblerlog/parser.go +++ b/internal/backends/scrobblerlog/parser.go @@ -24,7 +24,6 @@ package scrobblerlog import ( "bufio" "encoding/csv" - "errors" "fmt" "io" "strconv" @@ -71,8 +70,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, errors.New(fmt.Sprintf( - "Invalid record in scrobblerlog line %v", line)) + return result, fmt.Errorf("invalid record in scrobblerlog line %v", line) } rating := row[5] @@ -132,15 +130,15 @@ func ReadHeader(reader *bufio.Reader, log *ScrobblerLog) error { } if len(line) == 0 || line[0] != '#' { - err = errors.New(fmt.Sprintf("Unexpected header (line %v)", i)) + err = fmt.Errorf("unexpected header (line %v)", i) } else { text := string(line) if i == 0 && !strings.HasPrefix(text, "#AUDIOSCROBBLER/1") { - err = errors.New(fmt.Sprintf("Not a scrobbler log file")) + err = fmt.Errorf("not a scrobbler log file") } timezone, found := strings.CutPrefix(text, "#TZ/") - if strings.HasPrefix(text, "#TZ/") { + if found { log.Timezone = timezone } From 7a2fcc091e7a887a1875c551075c1252faa292dd Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 5 Dec 2023 09:10:25 +0100 Subject: [PATCH 024/150] spotify: fixed loves export count --- internal/backends/spotify/spotify.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/internal/backends/spotify/spotify.go b/internal/backends/spotify/spotify.go index e584cc1..454c1fa 100644 --- a/internal/backends/spotify/spotify.go +++ b/internal/backends/spotify/spotify.go @@ -153,7 +153,8 @@ func (b *SpotifyApiBackend) ExportLoves(oldestTimestamp time.Time, results chan defer close(results) p := models.Progress{Total: int64(perPage)} - var totalCount int + totalCount := 0 + exportCount := 0 out: for { @@ -187,11 +188,11 @@ out: if love.Created.Unix() > oldestTimestamp.Unix() { loves = append(loves, love) } else { - totalCount -= 1 - break + continue } } + exportCount += len(loves) sort.Sort(loves) results <- models.LovesResult{Items: loves, Total: totalCount} p.Elapsed += int64(count) @@ -208,6 +209,7 @@ out: } } + results <- models.LovesResult{Total: exportCount} progress <- p.Complete() } From ed4c0aa1f89ce9ce12e7bdb12ec883c9eff37033 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Thu, 7 Dec 2023 08:53:01 +0100 Subject: [PATCH 025/150] jspf: add MB playlist extension, public field must be boolean --- internal/backends/jspf/jspf.go | 6 ++++++ pkg/jspf/extensions.go | 5 ++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/internal/backends/jspf/jspf.go b/internal/backends/jspf/jspf.go index 6aeb5ca..47bd2ec 100644 --- a/internal/backends/jspf/jspf.go +++ b/internal/backends/jspf/jspf.go @@ -149,6 +149,12 @@ func (b JSPFBackend) writeJSPF(tracks []jspf.Track) error { Identifier: b.identifier, Date: time.Now(), Tracks: tracks, + Extension: map[string]any{ + jspf.MusicBrainzPlaylistExtensionId: jspf.MusicBrainzPlaylistExtension{ + LastModifiedAt: time.Now(), + Public: true, + }, + }, }, } diff --git a/pkg/jspf/extensions.go b/pkg/jspf/extensions.go index fe5fef2..07cd3c1 100644 --- a/pkg/jspf/extensions.go +++ b/pkg/jspf/extensions.go @@ -51,9 +51,8 @@ type MusicBrainzPlaylistExtension struct { // deleted, this field will be set to true and the copied_from field will not // be returned. CopiedFromDeleted bool `json:"copied_from_deleted,omitempty"` - // Indicates if this playlist is public or private. Must contain the value - // "true" or "false". - Public string `json:"public,omitempty"` + // Indicates if this playlist is public or private. + Public bool `json:"public,omitempty"` // The timestamp for when this playlist was last modified. LastModifiedAt time.Time `json:"last_modified_at,omitempty"` // This dict allows a playlist creator to submit additional track metadata From c101749faa170638bce969a5bec67927c71d1971 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Thu, 7 Dec 2023 13:21:56 +0100 Subject: [PATCH 026/150] Link to SourceHut project page --- README.md | 2 +- pages/scotty/index.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3432851..12e69af 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ subsonic | ⨯ | ⨯ | ✓ | - ## Contribute -The source code for Scotty is available on [SourceHut](https://git.sr.ht/~phw/scotty). To report issues or feature requests please [create a ticket](https://todo.sr.ht/~phw/scotty). +The source code for Scotty is available on [SourceHut](https://sr.ht/~phw/scotty/). To report issues or feature requests please [create a ticket](https://todo.sr.ht/~phw/scotty). Patches can be submitted to the mailing list [~phw/musicbrainz@lists.sr.ht](https://lists.sr.ht/~phw/musicbrainz). You can clone the repository directly on SourceHut and submit your changes with the "Prepare patchset" button. Please see SourceHut's [documentation for sending patches upstream](https://man.sr.ht/git.sr.ht/#sending-patches-upstream) for details. diff --git a/pages/scotty/index.html b/pages/scotty/index.html index 2918ec5..23f6c8b 100644 --- a/pages/scotty/index.html +++ b/pages/scotty/index.html @@ -4,13 +4,13 @@ - + scotty - Redirecting to scotty... + Redirecting to scotty... From ce5cdceb1f735cee3ea6dfefc76254dc2ed2b709 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 5 Dec 2023 16:31:52 +0100 Subject: [PATCH 027/150] Initialize config if it does not exist, set database relative to config dir --- cmd/auth.go | 3 +- cmd/listens.go | 4 +- cmd/loves.go | 4 +- cmd/root.go | 27 +++--------- internal/cli/common.go | 2 +- internal/config/config.go | 87 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 100 insertions(+), 27 deletions(-) create mode 100644 internal/config/config.go diff --git a/cmd/auth.go b/cmd/auth.go index 2b618bf..595262d 100644 --- a/cmd/auth.go +++ b/cmd/auth.go @@ -26,6 +26,7 @@ import ( "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/models" "go.uploadedlobster.com/scotty/internal/storage" "golang.org/x/oauth2" @@ -76,7 +77,7 @@ var authCmd = &cobra.Command{ cobra.CheckErr(err) // Store the retrieved token in the database - db, err := storage.New(viper.GetString("database")) + db, err := storage.New(config.DatabasePath()) cobra.CheckErr(err) err = db.SetOAuth2Token(serviceName, tok) diff --git a/cmd/listens.go b/cmd/listens.go index 8bf3c3b..558259e 100644 --- a/cmd/listens.go +++ b/cmd/listens.go @@ -18,9 +18,9 @@ package cmd import ( "github.com/spf13/cobra" - "github.com/spf13/viper" "go.uploadedlobster.com/scotty/internal/backends" "go.uploadedlobster.com/scotty/internal/cli" + "go.uploadedlobster.com/scotty/internal/config" "go.uploadedlobster.com/scotty/internal/models" "go.uploadedlobster.com/scotty/internal/storage" ) @@ -31,7 +31,7 @@ var listensCmd = &cobra.Command{ Short: "Transfer listens between two services", Long: `Transfers listens between two configured services.`, Run: func(cmd *cobra.Command, args []string) { - db, err := storage.New(viper.GetString("database")) + db, err := storage.New(config.DatabasePath()) cobra.CheckErr(err) c, err := cli.NewTransferCmd[ models.ListensExport, diff --git a/cmd/loves.go b/cmd/loves.go index f44cb5b..a802c42 100644 --- a/cmd/loves.go +++ b/cmd/loves.go @@ -18,9 +18,9 @@ package cmd import ( "github.com/spf13/cobra" - "github.com/spf13/viper" "go.uploadedlobster.com/scotty/internal/backends" "go.uploadedlobster.com/scotty/internal/cli" + "go.uploadedlobster.com/scotty/internal/config" "go.uploadedlobster.com/scotty/internal/models" "go.uploadedlobster.com/scotty/internal/storage" ) @@ -31,7 +31,7 @@ var lovesCmd = &cobra.Command{ Short: "Transfer loves between two services", Long: `Transfers loves between two configured services.`, Run: func(cmd *cobra.Command, args []string) { - db, err := storage.New(viper.GetString("database")) + db, err := storage.New(config.DatabasePath()) cobra.CheckErr(err) c, err := cli.NewTransferCmd[ models.LovesExport, diff --git a/cmd/root.go b/cmd/root.go index 10dc528..8b4fdf8 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -19,10 +19,10 @@ package cmd import ( "fmt" "os" - "path" "github.com/spf13/cobra" "github.com/spf13/viper" + "go.uploadedlobster.com/scotty/internal/config" "go.uploadedlobster.com/scotty/internal/version" ) @@ -56,7 +56,7 @@ func init() { // Cobra supports persistent flags, which, if defined here, // will be global for your application. - configDir := defaultConfigDir() + configDir := config.DefaultConfigDir() rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", fmt.Sprintf("config file (default is %s/scotty.yaml)", configDir)) @@ -65,27 +65,12 @@ func init() { // rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") } -func defaultConfigDir() string { - configDir, err := os.UserConfigDir() - cobra.CheckErr(err) - return path.Join(configDir, version.AppName) -} - // initConfig reads in config file and ENV variables if set. func initConfig() { - if cfgFile != "" { - // Use config file from the flag. - viper.SetConfigFile(cfgFile) - } else { - viper.AddConfigPath(defaultConfigDir()) - viper.SetConfigType("toml") - viper.SetConfigName(version.AppName) - } - - viper.AutomaticEnv() // read in environment variables that match - // If a config file is found, read it in. - if err := viper.ReadInConfig(); err == nil { - fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed()) + if err := config.InitConfig(cfgFile); err != nil { + fmt.Fprintln(os.Stderr, "Failed reading config:", err) + } else { + fmt.Println("Using config file:", viper.ConfigFileUsed()) } } diff --git a/internal/cli/common.go b/internal/cli/common.go index a26253d..e7fe19d 100644 --- a/internal/cli/common.go +++ b/internal/cli/common.go @@ -30,7 +30,7 @@ func GetConfigFromFlag(cmd *cobra.Command, flagName string) (string, *viper.Vipe config = servicesConfig.Sub(configName) } if config == nil { - cobra.CheckErr(fmt.Sprintf("Invalid source configuration \"%s\"", configName)) + cobra.CheckErr(fmt.Sprintf("invalid configuration \"%s\"", configName)) } return configName, config } diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..ece3544 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,87 @@ +/* +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 config + +import ( + "os" + "path" + "path/filepath" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + "go.uploadedlobster.com/scotty/internal/version" +) + +const ( + defaultDatabase = "scotty.sqlite3" + defaultOAuthHost = "127.0.0.1:2369" +) + +func DefaultConfigDir() string { + configDir, err := os.UserConfigDir() + cobra.CheckErr(err) + return path.Join(configDir, version.AppName) +} + +// initConfig reads in config file and ENV variables if set. +func InitConfig(cfgFile string) error { + configDir := DefaultConfigDir() + if cfgFile != "" { + // Use given config file + viper.SetConfigFile(cfgFile) + } else { + viper.AddConfigPath(configDir) + viper.SetConfigType("toml") + viper.SetConfigName(version.AppName) + viper.SetConfigPermissions(0640) + } + + setDefaults() + + // Create global config if it does not exist + if viper.ConfigFileUsed() == "" && cfgFile == "" { + if err := os.MkdirAll(configDir, 0750); err == nil { + viper.SafeWriteConfig() + } + } + + // read in environment variables that match + viper.AutomaticEnv() + + // If a config file is found, read it in. + return viper.ReadInConfig() +} + +func DatabasePath() string { + path := viper.GetString("database") + if filepath.IsAbs(path) { + return path + } + + return filepath.Join(getConfigDir(), path) +} + +func setDefaults() { + viper.SetDefault("database", defaultDatabase) + viper.SetDefault("oauth-host", defaultOAuthHost) + + // Always configure the dump backend as a default service + viper.SetDefault("service.dump.backend", "dump") +} + +func getConfigDir() string { + return filepath.Dir(viper.ConfigFileUsed()) +} From ae5f1c5f269b8380af55be3fc7bb7c987efcf97b Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 5 Dec 2023 17:41:15 +0100 Subject: [PATCH 028/150] Basic TUI to add new service configuration --- cmd/add.go | 88 ++++++++++++++++++++++++++++++++++ cmd/config.go | 48 +++++++++++++++++++ go.mod | 2 + go.sum | 4 ++ internal/backends/backends.go | 24 +++++++++- internal/config/config.go | 13 +++++ internal/config/config_test.go | 32 +++++++++++++ internal/config/services.go | 33 +++++++++++++ 8 files changed, 242 insertions(+), 2 deletions(-) create mode 100644 cmd/add.go create mode 100644 cmd/config.go create mode 100644 internal/config/config_test.go create mode 100644 internal/config/services.go diff --git a/cmd/add.go b/cmd/add.go new file mode 100644 index 0000000..1803e9b --- /dev/null +++ b/cmd/add.go @@ -0,0 +1,88 @@ +/* +Copyright © 2023 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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ +package cmd + +import ( + "fmt" + + "github.com/manifoldco/promptui" + "github.com/spf13/cobra" + "go.uploadedlobster.com/scotty/internal/backends" + "go.uploadedlobster.com/scotty/internal/config" +) + +// addCmd represents the add command +var addCmd = &cobra.Command{ + Use: "add", + Short: "Add a service configuration", + Long: `Add a service configuration.`, + Run: func(cmd *cobra.Command, args []string) { + // Select backend + sel := promptui.Select{ + Label: "Backend", + Items: backends.GetBackends(), + Size: 10, + } + _, backend, err := sel.Run() + if err != nil { + fmt.Printf("Prompt failed %v\n", err) + return + } + + // Set service name + prompt := promptui.Prompt{ + Label: "Service name", + Validate: config.ValidateKey, + Default: backend, + } + + name, err := prompt.Run() + if err != nil { + fmt.Printf("Prompt failed %v\n", err) + return + } + + // Save the service config + service := config.ServiceConfig{ + Name: name, + Backend: backend, + } + err = service.Save() + cobra.CheckErr(err) + + fmt.Printf("Saved service %v using backend %v\n", service.Name, service.Backend) + }, +} + +func init() { + configCmd.AddCommand(addCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // addCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // addCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/cmd/config.go b/cmd/config.go new file mode 100644 index 0000000..ced8585 --- /dev/null +++ b/cmd/config.go @@ -0,0 +1,48 @@ +/* +Copyright © 2023 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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ +package cmd + +import ( + "github.com/spf13/cobra" +) + +// configCmd represents the config command +var configCmd = &cobra.Command{ + Use: "config", + Short: "Manage the configuration", + Long: `Manage the scotty configuration using the subcommands to add, remove +or edit services.`, +} + +func init() { + rootCmd.AddCommand(configCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // configCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // configCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/go.mod b/go.mod index e8feb7c..4404fda 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( require ( github.com/VividCortex/ewma v1.2.0 // indirect github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // 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 github.com/fsnotify/fsnotify v1.6.0 // indirect @@ -35,6 +36,7 @@ require ( 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/manifoldco/promptui v0.9.0 // 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 diff --git a/go.sum b/go.sum index 51d651e..7716b50 100644 --- a/go.sum +++ b/go.sum @@ -44,6 +44,7 @@ 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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 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/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo= @@ -186,6 +187,8 @@ 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= @@ -376,6 +379,7 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ 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= diff --git a/internal/backends/backends.go b/internal/backends/backends.go index 02f29ae..3f4e2c9 100644 --- a/internal/backends/backends.go +++ b/internal/backends/backends.go @@ -19,6 +19,7 @@ package backends import ( "fmt" "reflect" + "sort" "strings" "github.com/spf13/viper" @@ -41,6 +42,24 @@ type BackendInfo struct { ImportCapabilities []Capability } +func (b BackendInfo) String() string { + return b.Name +} + +type BackendList []BackendInfo + +func (l BackendList) Len() int { + return len(l) +} + +func (l BackendList) Less(i, j int) bool { + return l[i].Name < l[j].Name +} + +func (l BackendList) Swap(i, j int) { + l[i], l[j] = l[j], l[i] +} + type Capability = string func ResolveBackend[T interface{}](config *viper.Viper) (T, error) { @@ -59,8 +78,8 @@ func ResolveBackend[T interface{}](config *viper.Viper) (T, error) { return result, err } -func GetBackends() []BackendInfo { - backends := make([]BackendInfo, 0) +func GetBackends() BackendList { + backends := make(BackendList, 0) for name, backendFunc := range knownBackends { backend := backendFunc() info := BackendInfo{ @@ -71,6 +90,7 @@ func GetBackends() []BackendInfo { backends = append(backends, info) } + sort.Sort(backends) return backends } diff --git a/internal/config/config.go b/internal/config/config.go index ece3544..746e79c 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -16,9 +16,11 @@ Scotty. If not, see . package config import ( + "fmt" "os" "path" "path/filepath" + "regexp" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -74,6 +76,17 @@ func DatabasePath() string { return filepath.Join(getConfigDir(), path) } +func ValidateKey(key string) error { + found, err := regexp.MatchString("^[A-Za-z0-9_-]+$", key) + if err != nil { + return err + } else if found { + return nil + } else { + return fmt.Errorf("key must only consist of A-Za-z0-9_-") + } +} + func setDefaults() { viper.SetDefault("database", defaultDatabase) viper.SetDefault("oauth-host", defaultOAuthHost) diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..4616857 --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,32 @@ +/* +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 config_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "go.uploadedlobster.com/scotty/internal/config" +) + +func TestTimestampUpdate(t *testing.T) { + assert := assert.New(t) + assert.Nil(config.ValidateKey("foo")) + assert.Nil(config.ValidateKey("foo123")) + assert.Nil(config.ValidateKey("foo_bar-123")) + assert.NotNil(config.ValidateKey("foo/bar")) + assert.NotNil(config.ValidateKey("bär")) +} diff --git a/internal/config/services.go b/internal/config/services.go new file mode 100644 index 0000000..e3a15e8 --- /dev/null +++ b/internal/config/services.go @@ -0,0 +1,33 @@ +/* +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 config + +import "github.com/spf13/viper" + +type ServiceConfig struct { + Name string + Backend string + ConfigValues map[string]any +} + +func (c *ServiceConfig) Save() error { + key := "service." + c.Name + viper.Set(key+".backend", c.Backend) + for k, v := range c.ConfigValues { + viper.Set(key+"."+k, v) + } + return viper.WriteConfig() +} From c9fa21be73f539654e3124170b3145c2db53b11c Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 5 Dec 2023 23:25:15 +0100 Subject: [PATCH 029/150] Dynamic per-backend configuration options --- cmd/add.go | 86 ++++++++++++++++--- internal/backends/backends.go | 20 +++-- internal/backends/deezer/deezer.go | 12 +++ internal/backends/dump/dump.go | 2 + internal/backends/funkwhale/funkwhale.go | 16 ++++ internal/backends/jspf/jspf.go | 20 +++++ internal/backends/lastfm/lastfm.go | 16 ++++ .../backends/listenbrainz/listenbrainz.go | 12 +++ internal/backends/maloja/maloja.go | 16 ++++ .../backends/scrobblerlog/scrobblerlog.go | 16 ++++ internal/backends/spotify/spotify.go | 12 +++ internal/backends/subsonic/subsonic.go | 16 ++++ internal/models/interfaces.go | 3 + internal/models/options.go | 32 +++++++ 14 files changed, 262 insertions(+), 17 deletions(-) create mode 100644 internal/models/options.go diff --git a/cmd/add.go b/cmd/add.go index 1803e9b..a0867d0 100644 --- a/cmd/add.go +++ b/cmd/add.go @@ -28,6 +28,7 @@ import ( "github.com/spf13/cobra" "go.uploadedlobster.com/scotty/internal/backends" "go.uploadedlobster.com/scotty/internal/config" + "go.uploadedlobster.com/scotty/internal/models" ) // addCmd represents the add command @@ -43,10 +44,7 @@ var addCmd = &cobra.Command{ Size: 10, } _, backend, err := sel.Run() - if err != nil { - fmt.Printf("Prompt failed %v\n", err) - return - } + cobra.CheckErr(err) // Set service name prompt := promptui.Prompt{ @@ -54,21 +52,22 @@ var addCmd = &cobra.Command{ Validate: config.ValidateKey, Default: backend, } - name, err := prompt.Run() - if err != nil { - fmt.Printf("Prompt failed %v\n", err) - return - } + cobra.CheckErr(err) - // Save the service config + // Prepate service config service := config.ServiceConfig{ Name: name, Backend: backend, } - err = service.Save() + + // Additional options + err = extraOptions(&service) cobra.CheckErr(err) + // Save the service config + err = service.Save() + cobra.CheckErr(err) fmt.Printf("Saved service %v using backend %v\n", service.Name, service.Backend) }, } @@ -86,3 +85,68 @@ func init() { // is called directly, e.g.: // addCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") } + +func extraOptions(config *config.ServiceConfig) error { + backend, err := backends.BackendByName(config.Backend) + if err != nil { + return err + } + opts := backend.Options() + if opts == nil { + return nil + } + + values := make(map[string]any, len(*opts)) + for _, opt := range *opts { + var val any + var err error + switch opt.Type { + case models.Bool: + val, err = promptBool(opt) + case models.Secret: + val, err = promptSecret(opt) + case models.String: + val, err = promptString(opt) + } + if err != nil { + return err + } + values[opt.Name] = val + + } + + config.ConfigValues = values + return nil +} + +func promptString(opt models.BackendOption) (string, error) { + prompt := promptui.Prompt{ + Label: opt.Label, + Validate: opt.Validate, + Default: opt.Default, + } + + val, err := prompt.Run() + return val, err +} + +func promptSecret(opt models.BackendOption) (string, error) { + prompt := promptui.Prompt{ + Label: opt.Label, + Validate: opt.Validate, + Default: opt.Default, + Mask: '*', + } + + val, err := prompt.Run() + return val, err +} + +func promptBool(opt models.BackendOption) (bool, error) { + sel := promptui.Select{ + Label: opt.Label, + Items: []string{"Yes", "No"}, + } + _, val, err := sel.Run() + return val == "Yes", err +} diff --git a/internal/backends/backends.go b/internal/backends/backends.go index 3f4e2c9..089f8ab 100644 --- a/internal/backends/backends.go +++ b/internal/backends/backends.go @@ -63,7 +63,7 @@ func (l BackendList) Swap(i, j int) { type Capability = string func ResolveBackend[T interface{}](config *viper.Viper) (T, error) { - backendName, backend, err := resolveBackend(config) + backendName, backend, err := backendWithConfig(config) var result T if err != nil { return result, err @@ -78,6 +78,14 @@ func ResolveBackend[T interface{}](config *viper.Viper) (T, error) { return result, err } +func BackendByName(backendName string) (models.Backend, error) { + backendType := knownBackends[backendName] + if backendType == nil { + return nil, fmt.Errorf("unknown backend %s", backendName) + } + return backendType(), nil +} + func GetBackends() BackendList { backends := make(BackendList, 0) for name, backendFunc := range knownBackends { @@ -107,13 +115,13 @@ var knownBackends = map[string]func() models.Backend{ "subsonic": func() models.Backend { return &subsonic.SubsonicApiBackend{} }, } -func resolveBackend(config *viper.Viper) (string, models.Backend, error) { +func backendWithConfig(config *viper.Viper) (string, models.Backend, error) { backendName := config.GetString("backend") - backendType := knownBackends[backendName] - if backendType == nil { - return backendName, nil, fmt.Errorf("unknown backend %s", backendName) + backend, err := BackendByName(backendName) + if err != nil { + return backendName, nil, err } - return backendName, backendType().FromConfig(config), nil + return backendName, backend.FromConfig(config), 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 df36280..6fcff52 100644 --- a/internal/backends/deezer/deezer.go +++ b/internal/backends/deezer/deezer.go @@ -36,6 +36,18 @@ type DeezerApiBackend struct { func (b *DeezerApiBackend) Name() string { return "deezer" } +func (b *DeezerApiBackend) Options() *[]models.BackendOption { + return &[]models.BackendOption{{ + Name: "client-id", + Label: "Client ID", + Type: models.String, + }, { + Name: "client-secret", + Label: "Client secret", + Type: models.Secret, + }} +} + func (b *DeezerApiBackend) FromConfig(config *viper.Viper) models.Backend { b.clientId = config.GetString("client-id") b.clientSecret = config.GetString("client-secret") diff --git a/internal/backends/dump/dump.go b/internal/backends/dump/dump.go index e85495f..6a600ee 100644 --- a/internal/backends/dump/dump.go +++ b/internal/backends/dump/dump.go @@ -25,6 +25,8 @@ type DumpBackend struct{} func (b *DumpBackend) Name() string { return "dump" } +func (b *DumpBackend) Options() *[]models.BackendOption { return nil } + func (b *DumpBackend) FromConfig(config *viper.Viper) models.Backend { return b } diff --git a/internal/backends/funkwhale/funkwhale.go b/internal/backends/funkwhale/funkwhale.go index 4a4bfb1..7388b62 100644 --- a/internal/backends/funkwhale/funkwhale.go +++ b/internal/backends/funkwhale/funkwhale.go @@ -33,6 +33,22 @@ type FunkwhaleApiBackend struct { func (b *FunkwhaleApiBackend) Name() string { return "funkwhale" } +func (b *FunkwhaleApiBackend) Options() *[]models.BackendOption { + return &[]models.BackendOption{{ + Name: "server-url", + Label: "Server URL", + Type: models.String, + }, { + Name: "username", + Label: "User name", + Type: models.String, + }, { + Name: "token", + Label: "Access token", + Type: models.Secret, + }} +} + func (b *FunkwhaleApiBackend) FromConfig(config *viper.Viper) models.Backend { b.client = NewClient( config.GetString("server-url"), diff --git a/internal/backends/jspf/jspf.go b/internal/backends/jspf/jspf.go index 47bd2ec..38a1697 100644 --- a/internal/backends/jspf/jspf.go +++ b/internal/backends/jspf/jspf.go @@ -36,6 +36,26 @@ type JSPFBackend struct { func (b *JSPFBackend) Name() string { return "jspf" } +func (b *JSPFBackend) Options() *[]models.BackendOption { + return &[]models.BackendOption{{ + Name: "file-path", + Label: "File path", + Type: models.String, + }, { + Name: "title", + Label: "Playlist title", + Type: models.String, + }, { + Name: "username", + Label: "User name", + Type: models.String, + }, { + Name: "identifier", + Label: "Unique playlist identifier", + Type: models.String, + }} +} + func (b *JSPFBackend) FromConfig(config *viper.Viper) models.Backend { b.filePath = config.GetString("file-path") b.title = config.GetString("title") diff --git a/internal/backends/lastfm/lastfm.go b/internal/backends/lastfm/lastfm.go index 326be6e..8154e38 100644 --- a/internal/backends/lastfm/lastfm.go +++ b/internal/backends/lastfm/lastfm.go @@ -43,6 +43,22 @@ type LastfmApiBackend struct { func (b *LastfmApiBackend) Name() string { return "lastfm" } +func (b *LastfmApiBackend) Options() *[]models.BackendOption { + return &[]models.BackendOption{{ + Name: "username", + Label: "User name", + Type: models.String, + }, { + Name: "client-id", + Label: "Client ID", + Type: models.String, + }, { + Name: "client-secret", + Label: "Client secret", + Type: models.Secret, + }} +} + func (b *LastfmApiBackend) FromConfig(config *viper.Viper) models.Backend { clientId := config.GetString("client-id") clientSecret := config.GetString("client-secret") diff --git a/internal/backends/listenbrainz/listenbrainz.go b/internal/backends/listenbrainz/listenbrainz.go index 27c8102..dbc1022 100644 --- a/internal/backends/listenbrainz/listenbrainz.go +++ b/internal/backends/listenbrainz/listenbrainz.go @@ -34,6 +34,18 @@ type ListenBrainzApiBackend struct { func (b *ListenBrainzApiBackend) Name() string { return "listenbrainz" } +func (b *ListenBrainzApiBackend) Options() *[]models.BackendOption { + return &[]models.BackendOption{{ + Name: "username", + Label: "User name", + Type: models.String, + }, { + Name: "token", + Label: "Access token", + Type: models.Secret, + }} +} + func (b *ListenBrainzApiBackend) FromConfig(config *viper.Viper) models.Backend { b.client = NewClient(config.GetString("token")) b.client.MaxResults = MaxItemsPerGet diff --git a/internal/backends/maloja/maloja.go b/internal/backends/maloja/maloja.go index 4fa57ad..80d9369 100644 --- a/internal/backends/maloja/maloja.go +++ b/internal/backends/maloja/maloja.go @@ -33,6 +33,22 @@ type MalojaApiBackend struct { func (b *MalojaApiBackend) Name() string { return "maloja" } +func (b *MalojaApiBackend) Options() *[]models.BackendOption { + return &[]models.BackendOption{{ + Name: "server-url", + Label: "Server URL", + Type: models.String, + }, { + Name: "token", + Label: "Access token", + Type: models.Secret, + }, { + Name: "nofix", + Label: "Disable auto correction of submitted listens", + Type: models.Bool, + }} +} + func (b *MalojaApiBackend) FromConfig(config *viper.Viper) models.Backend { b.client = NewClient( config.GetString("server-url"), diff --git a/internal/backends/scrobblerlog/scrobblerlog.go b/internal/backends/scrobblerlog/scrobblerlog.go index cbd0aa8..0267b0e 100644 --- a/internal/backends/scrobblerlog/scrobblerlog.go +++ b/internal/backends/scrobblerlog/scrobblerlog.go @@ -36,6 +36,22 @@ type ScrobblerLogBackend struct { func (b *ScrobblerLogBackend) Name() string { return "scrobbler-log" } +func (b *ScrobblerLogBackend) Options() *[]models.BackendOption { + return &[]models.BackendOption{{ + Name: "file-path", + Label: "File path", + Type: models.String, + }, { + Name: "include-skipped", + Label: "Include skipped listens", + Type: models.Bool, + }, { + Name: "append", + Label: "Append to file", + Type: models.Bool, + }} +} + func (b *ScrobblerLogBackend) FromConfig(config *viper.Viper) models.Backend { b.filePath = config.GetString("file-path") b.includeSkipped = config.GetBool("include-skipped") diff --git a/internal/backends/spotify/spotify.go b/internal/backends/spotify/spotify.go index 454c1fa..9131841 100644 --- a/internal/backends/spotify/spotify.go +++ b/internal/backends/spotify/spotify.go @@ -39,6 +39,18 @@ type SpotifyApiBackend struct { func (b *SpotifyApiBackend) Name() string { return "spotify" } +func (b *SpotifyApiBackend) Options() *[]models.BackendOption { + return &[]models.BackendOption{{ + Name: "client-id", + Label: "Client ID", + Type: models.String, + }, { + Name: "client-secret", + Label: "Client secret", + Type: models.Secret, + }} +} + func (b *SpotifyApiBackend) FromConfig(config *viper.Viper) models.Backend { b.clientId = config.GetString("client-id") b.clientSecret = config.GetString("client-secret") diff --git a/internal/backends/subsonic/subsonic.go b/internal/backends/subsonic/subsonic.go index 1167f87..bd2acbc 100644 --- a/internal/backends/subsonic/subsonic.go +++ b/internal/backends/subsonic/subsonic.go @@ -34,6 +34,22 @@ type SubsonicApiBackend struct { func (b *SubsonicApiBackend) Name() string { return "subsonic" } +func (b *SubsonicApiBackend) Options() *[]models.BackendOption { + return &[]models.BackendOption{{ + Name: "server-url", + Label: "Server URL", + Type: models.String, + }, { + Name: "username", + Label: "User name", + Type: models.String, + }, { + Name: "token", + Label: "Access token", + Type: models.Secret, + }} +} + func (b *SubsonicApiBackend) FromConfig(config *viper.Viper) models.Backend { b.client = subsonic.Client{ Client: &http.Client{}, diff --git a/internal/models/interfaces.go b/internal/models/interfaces.go index 9078eba..ab2339b 100644 --- a/internal/models/interfaces.go +++ b/internal/models/interfaces.go @@ -33,6 +33,9 @@ type Backend interface { // Initialize the backend from a config. FromConfig(config *viper.Viper) Backend + + // Return configuration options + Options() *[]BackendOption } type ImportBackend interface { diff --git a/internal/models/options.go b/internal/models/options.go new file mode 100644 index 0000000..0763032 --- /dev/null +++ b/internal/models/options.go @@ -0,0 +1,32 @@ +/* +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 models + +type OptionType string + +const ( + Bool OptionType = "bool" + Secret OptionType = "secret" + String OptionType = "string" +) + +type BackendOption struct { + Name string + Label string + Type OptionType + Default string + Validate func(string) error +} From 8743a9d81c44d1c81bd739f642214c0d563995d6 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Thu, 7 Dec 2023 22:32:08 +0100 Subject: [PATCH 030/150] Moved prompt helpers into cli package --- cmd/add.go | 45 ++-------------------------- internal/cli/prompt.go | 68 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 43 deletions(-) create mode 100644 internal/cli/prompt.go diff --git a/cmd/add.go b/cmd/add.go index a0867d0..830ff12 100644 --- a/cmd/add.go +++ b/cmd/add.go @@ -27,8 +27,8 @@ import ( "github.com/manifoldco/promptui" "github.com/spf13/cobra" "go.uploadedlobster.com/scotty/internal/backends" + "go.uploadedlobster.com/scotty/internal/cli" "go.uploadedlobster.com/scotty/internal/config" - "go.uploadedlobster.com/scotty/internal/models" ) // addCmd represents the add command @@ -98,16 +98,7 @@ func extraOptions(config *config.ServiceConfig) error { values := make(map[string]any, len(*opts)) for _, opt := range *opts { - var val any - var err error - switch opt.Type { - case models.Bool: - val, err = promptBool(opt) - case models.Secret: - val, err = promptSecret(opt) - case models.String: - val, err = promptString(opt) - } + val, err := cli.Prompt(opt) if err != nil { return err } @@ -118,35 +109,3 @@ func extraOptions(config *config.ServiceConfig) error { config.ConfigValues = values return nil } - -func promptString(opt models.BackendOption) (string, error) { - prompt := promptui.Prompt{ - Label: opt.Label, - Validate: opt.Validate, - Default: opt.Default, - } - - val, err := prompt.Run() - return val, err -} - -func promptSecret(opt models.BackendOption) (string, error) { - prompt := promptui.Prompt{ - Label: opt.Label, - Validate: opt.Validate, - Default: opt.Default, - Mask: '*', - } - - val, err := prompt.Run() - return val, err -} - -func promptBool(opt models.BackendOption) (bool, error) { - sel := promptui.Select{ - Label: opt.Label, - Items: []string{"Yes", "No"}, - } - _, val, err := sel.Run() - return val == "Yes", err -} diff --git a/internal/cli/prompt.go b/internal/cli/prompt.go new file mode 100644 index 0000000..0fbe979 --- /dev/null +++ b/internal/cli/prompt.go @@ -0,0 +1,68 @@ +/* +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 cli + +import ( + "fmt" + + "github.com/manifoldco/promptui" + "go.uploadedlobster.com/scotty/internal/models" +) + +func Prompt(opt models.BackendOption) (any, error) { + switch opt.Type { + case models.Bool: + return PromptBool(opt) + case models.Secret: + return PromptSecret(opt) + case models.String: + return PromptString(opt) + default: + return nil, fmt.Errorf("unknown prompt type %v", opt.Type) + } +} + +func PromptString(opt models.BackendOption) (string, error) { + prompt := promptui.Prompt{ + Label: opt.Label, + Validate: opt.Validate, + Default: opt.Default, + } + + val, err := prompt.Run() + return val, err +} + +func PromptSecret(opt models.BackendOption) (string, error) { + prompt := promptui.Prompt{ + Label: opt.Label, + Validate: opt.Validate, + Default: opt.Default, + Mask: '*', + } + + val, err := prompt.Run() + return val, err +} + +func PromptBool(opt models.BackendOption) (bool, error) { + sel := promptui.Select{ + Label: opt.Label, + Items: []string{"Yes", "No"}, + } + _, val, err := sel.Run() + return val == "Yes", err +} From 091b3c2f49bd91e149dc44d8b44582bd324f7086 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Thu, 7 Dec 2023 23:09:16 +0100 Subject: [PATCH 031/150] Prevent adding duplicate services --- cmd/add.go | 15 ++++++++---- internal/config/services.go | 47 ++++++++++++++++++++++++++++++++++++- 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/cmd/add.go b/cmd/add.go index 830ff12..0d0bc8c 100644 --- a/cmd/add.go +++ b/cmd/add.go @@ -22,6 +22,7 @@ THE SOFTWARE. package cmd import ( + "errors" "fmt" "github.com/manifoldco/promptui" @@ -48,14 +49,20 @@ var addCmd = &cobra.Command{ // Set service name prompt := promptui.Prompt{ - Label: "Service name", - Validate: config.ValidateKey, - Default: backend, + Label: "Service name", + Default: backend, + Validate: func(s string) error { + srv, _ := config.GetService(s) + if srv != nil { + return errors.New("a service with this name already exists") + } + return config.ValidateKey(s) + }, } name, err := prompt.Run() cobra.CheckErr(err) - // Prepate service config + // Prepare service config service := config.ServiceConfig{ Name: name, Backend: backend, diff --git a/internal/config/services.go b/internal/config/services.go index e3a15e8..cb64f7f 100644 --- a/internal/config/services.go +++ b/internal/config/services.go @@ -15,7 +15,11 @@ Scotty. If not, see . package config -import "github.com/spf13/viper" +import ( + "fmt" + + "github.com/spf13/viper" +) type ServiceConfig struct { Name string @@ -23,6 +27,22 @@ type ServiceConfig struct { ConfigValues map[string]any } +func NewServiceConfig(name string, config *viper.Viper) ServiceConfig { + service := ServiceConfig{ + Name: name, + Backend: viper.GetString("backend"), + ConfigValues: make(map[string]any), + } + + for key, val := range viper.AllSettings() { + if key != "backend" { + service.ConfigValues[key] = val + } + } + + return service +} + func (c *ServiceConfig) Save() error { key := "service." + c.Name viper.Set(key+".backend", c.Backend) @@ -31,3 +51,28 @@ func (c *ServiceConfig) Save() error { } return viper.WriteConfig() } + +func AllServices() map[string]ServiceConfig { + services := make(map[string]ServiceConfig) + config := viper.Sub("service") + if config != nil { + for k, v := range config.AllSettings() { + s, ok := v.(*viper.Viper) + if ok { + services[k] = NewServiceConfig(k, s) + } + } + } + return services +} + +func GetService(name string) (*ServiceConfig, error) { + key := "service." + name + config := viper.Sub(key) + if config != nil { + service := NewServiceConfig(name, config) + return &service, nil + } + + return nil, fmt.Errorf("no service configuration \"%v\"", name) +} From 9c363cc06dfba2d0d5af9ebe6c015fc6c3fca815 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Thu, 7 Dec 2023 23:44:58 +0100 Subject: [PATCH 032/150] Use config.ServiceConfig across API --- cmd/auth.go | 10 +++++--- go.mod | 4 ++-- go.sum | 2 ++ internal/backends/backends.go | 19 +++++++-------- internal/backends/backends_test.go | 24 +++++++++++-------- internal/backends/deezer/deezer.go | 4 ++-- internal/backends/deezer/deezer_test.go | 10 ++++---- internal/backends/dump/dump.go | 4 ++-- internal/backends/funkwhale/funkwhale.go | 4 ++-- internal/backends/funkwhale/funkwhale_test.go | 8 ++++--- internal/backends/jspf/jspf.go | 4 ++-- internal/backends/jspf/jspf_test.go | 18 +++++++------- internal/backends/lastfm/lastfm.go | 4 ++-- .../backends/listenbrainz/listenbrainz.go | 4 ++-- .../listenbrainz/listenbrainz_test.go | 8 ++++--- internal/backends/maloja/maloja.go | 4 ++-- internal/backends/maloja/maloja_test.go | 8 ++++--- .../backends/scrobblerlog/scrobblerlog.go | 4 ++-- .../scrobblerlog/scrobblerlog_test.go | 8 ++++--- internal/backends/spotify/spotify.go | 4 ++-- internal/backends/spotify/spotify_test.go | 10 ++++---- internal/backends/subsonic/subsonic.go | 4 ++-- internal/backends/subsonic/subsonic_test.go | 10 ++++---- internal/cli/common.go | 20 +++++----------- internal/cli/transfer.go | 15 ++++++++---- internal/config/services.go | 18 ++++++++++++-- internal/models/interfaces.go | 4 ++-- 27 files changed, 137 insertions(+), 99 deletions(-) diff --git a/cmd/auth.go b/cmd/auth.go index 595262d..c0a1129 100644 --- a/cmd/auth.go +++ b/cmd/auth.go @@ -17,6 +17,7 @@ Scotty. If not, see . package cmd import ( + "errors" "fmt" "os" @@ -38,7 +39,10 @@ var authCmd = &cobra.Command{ Short: "Authenticate with a backend", Long: `For backends requiring authentication this command can be used to authenticate.`, Run: func(cmd *cobra.Command, args []string) { - serviceName, serviceConfig := cli.GetConfigFromFlag(cmd, "service") + serviceConfig := cli.GetServiceConfigFromFlag(cmd, "service") + if serviceConfig == nil { + cobra.CheckErr(errors.New("failed loading service configuration")) + } backend, err := backends.ResolveBackend[models.OAuth2Authenticator](serviceConfig) cobra.CheckErr(err) @@ -80,10 +84,10 @@ var authCmd = &cobra.Command{ db, err := storage.New(config.DatabasePath()) cobra.CheckErr(err) - err = db.SetOAuth2Token(serviceName, tok) + err = db.SetOAuth2Token(serviceConfig.Name, tok) cobra.CheckErr(err) - fmt.Printf("Access token received, you can use %v now.\n\n", serviceName) + fmt.Printf("Access token received, you can use %v now.\n\n", serviceConfig.Name) }, } diff --git a/go.mod b/go.mod index 4404fda..04b84d4 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,9 @@ require ( github.com/glebarez/sqlite v1.10.0 github.com/go-resty/resty/v2 v2.10.0 github.com/jarcoal/httpmock v1.3.1 + github.com/manifoldco/promptui v0.9.0 github.com/shkh/lastfm-go v0.0.0-20191215035245-89a801c244e0 + github.com/spf13/cast v1.5.1 github.com/spf13/cobra v1.8.0 github.com/spf13/viper v1.17.0 github.com/stretchr/testify v1.8.4 @@ -36,7 +38,6 @@ require ( 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/manifoldco/promptui v0.9.0 // 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 @@ -49,7 +50,6 @@ require ( 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/cast v1.5.1 // 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 diff --git a/go.sum b/go.sum index 7716b50..91c80bb 100644 --- a/go.sum +++ b/go.sum @@ -43,9 +43,11 @@ github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAU 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/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/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/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo= github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk= diff --git a/internal/backends/backends.go b/internal/backends/backends.go index 089f8ab..8c17d10 100644 --- a/internal/backends/backends.go +++ b/internal/backends/backends.go @@ -22,7 +22,6 @@ import ( "sort" "strings" - "github.com/spf13/viper" "go.uploadedlobster.com/scotty/internal/backends/deezer" "go.uploadedlobster.com/scotty/internal/backends/dump" "go.uploadedlobster.com/scotty/internal/backends/funkwhale" @@ -33,6 +32,7 @@ import ( "go.uploadedlobster.com/scotty/internal/backends/scrobblerlog" "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/models" ) @@ -62,8 +62,8 @@ func (l BackendList) Swap(i, j int) { type Capability = string -func ResolveBackend[T interface{}](config *viper.Viper) (T, error) { - backendName, backend, err := backendWithConfig(config) +func ResolveBackend[T interface{}](config *config.ServiceConfig) (T, error) { + backend, err := backendWithConfig(config) var result T if err != nil { return result, err @@ -72,7 +72,7 @@ func ResolveBackend[T interface{}](config *viper.Viper) (T, error) { if implements { result = backend.(T) } else { - err = fmt.Errorf("backend %s does not implement %s", backendName, interfaceName) + err = fmt.Errorf("backend %s does not implement %s", config.Backend, interfaceName) } return result, err @@ -81,7 +81,7 @@ func ResolveBackend[T interface{}](config *viper.Viper) (T, error) { func BackendByName(backendName string) (models.Backend, error) { backendType := knownBackends[backendName] if backendType == nil { - return nil, fmt.Errorf("unknown backend %s", backendName) + return nil, fmt.Errorf("unknown backend \"%s\"", backendName) } return backendType(), nil } @@ -115,13 +115,12 @@ var knownBackends = map[string]func() models.Backend{ "subsonic": func() models.Backend { return &subsonic.SubsonicApiBackend{} }, } -func backendWithConfig(config *viper.Viper) (string, models.Backend, error) { - backendName := config.GetString("backend") - backend, err := BackendByName(backendName) +func backendWithConfig(config *config.ServiceConfig) (models.Backend, error) { + backend, err := BackendByName(config.Backend) if err != nil { - return backendName, nil, err + return nil, err } - return backendName, backend.FromConfig(config), nil + return backend.FromConfig(config), nil } func ImplementsInterface[T interface{}](backend *models.Backend) (bool, string) { diff --git a/internal/backends/backends_test.go b/internal/backends/backends_test.go index 5e64b0f..0e57616 100644 --- a/internal/backends/backends_test.go +++ b/internal/backends/backends_test.go @@ -34,28 +34,32 @@ import ( "go.uploadedlobster.com/scotty/internal/backends/scrobblerlog" "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/models" ) func TestResolveBackend(t *testing.T) { - config := viper.New() - config.Set("backend", "dump") - backend, err := backends.ResolveBackend[models.ListensImport](config) + c := viper.New() + c.Set("backend", "dump") + service := config.NewServiceConfig("test", c) + backend, err := backends.ResolveBackend[models.ListensImport](&service) assert.NoError(t, err) assert.IsType(t, &dump.DumpBackend{}, backend) } func TestResolveBackendUnknown(t *testing.T) { - config := viper.New() - config.Set("backend", "foo") - _, err := backends.ResolveBackend[models.ListensImport](config) - assert.EqualError(t, err, "unknown backend foo") + c := viper.New() + c.Set("backend", "foo") + service := config.NewServiceConfig("test", c) + _, err := backends.ResolveBackend[models.ListensImport](&service) + assert.EqualError(t, err, "unknown backend \"foo\"") } func TestResolveBackendInvalidInterface(t *testing.T) { - config := viper.New() - config.Set("backend", "dump") - _, err := backends.ResolveBackend[models.ListensExport](config) + c := viper.New() + 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") } diff --git a/internal/backends/deezer/deezer.go b/internal/backends/deezer/deezer.go index 6fcff52..89d82c2 100644 --- a/internal/backends/deezer/deezer.go +++ b/internal/backends/deezer/deezer.go @@ -22,8 +22,8 @@ import ( "sort" "time" - "github.com/spf13/viper" "go.uploadedlobster.com/scotty/internal/auth" + "go.uploadedlobster.com/scotty/internal/config" "go.uploadedlobster.com/scotty/internal/models" "golang.org/x/oauth2" ) @@ -48,7 +48,7 @@ func (b *DeezerApiBackend) Options() *[]models.BackendOption { }} } -func (b *DeezerApiBackend) FromConfig(config *viper.Viper) models.Backend { +func (b *DeezerApiBackend) FromConfig(config *config.ServiceConfig) models.Backend { b.clientId = config.GetString("client-id") b.clientSecret = config.GetString("client-secret") return b diff --git a/internal/backends/deezer/deezer_test.go b/internal/backends/deezer/deezer_test.go index c1bc23d..c50d4a7 100644 --- a/internal/backends/deezer/deezer_test.go +++ b/internal/backends/deezer/deezer_test.go @@ -25,13 +25,15 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uploadedlobster.com/scotty/internal/backends/deezer" + "go.uploadedlobster.com/scotty/internal/config" ) func TestFromConfig(t *testing.T) { - config := viper.New() - config.Set("client-id", "someclientid") - config.Set("client-secret", "someclientsecret") - backend := (&deezer.DeezerApiBackend{}).FromConfig(config) + 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) } diff --git a/internal/backends/dump/dump.go b/internal/backends/dump/dump.go index 6a600ee..aedcaaf 100644 --- a/internal/backends/dump/dump.go +++ b/internal/backends/dump/dump.go @@ -17,7 +17,7 @@ Scotty. If not, see . package dump import ( - "github.com/spf13/viper" + "go.uploadedlobster.com/scotty/internal/config" "go.uploadedlobster.com/scotty/internal/models" ) @@ -27,7 +27,7 @@ func (b *DumpBackend) Name() string { return "dump" } func (b *DumpBackend) Options() *[]models.BackendOption { return nil } -func (b *DumpBackend) FromConfig(config *viper.Viper) models.Backend { +func (b *DumpBackend) FromConfig(config *config.ServiceConfig) models.Backend { return b } diff --git a/internal/backends/funkwhale/funkwhale.go b/internal/backends/funkwhale/funkwhale.go index 7388b62..429fc11 100644 --- a/internal/backends/funkwhale/funkwhale.go +++ b/internal/backends/funkwhale/funkwhale.go @@ -20,7 +20,7 @@ import ( "sort" "time" - "github.com/spf13/viper" + "go.uploadedlobster.com/scotty/internal/config" "go.uploadedlobster.com/scotty/internal/models" ) @@ -49,7 +49,7 @@ func (b *FunkwhaleApiBackend) Options() *[]models.BackendOption { }} } -func (b *FunkwhaleApiBackend) FromConfig(config *viper.Viper) models.Backend { +func (b *FunkwhaleApiBackend) FromConfig(config *config.ServiceConfig) models.Backend { b.client = NewClient( config.GetString("server-url"), config.GetString("token"), diff --git a/internal/backends/funkwhale/funkwhale_test.go b/internal/backends/funkwhale/funkwhale_test.go index 1047671..12b2b48 100644 --- a/internal/backends/funkwhale/funkwhale_test.go +++ b/internal/backends/funkwhale/funkwhale_test.go @@ -24,13 +24,15 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uploadedlobster.com/scotty/internal/backends/funkwhale" + "go.uploadedlobster.com/scotty/internal/config" "go.uploadedlobster.com/scotty/internal/models" ) func TestFromConfig(t *testing.T) { - config := viper.New() - config.Set("token", "thetoken") - backend := (&funkwhale.FunkwhaleApiBackend{}).FromConfig(config) + c := viper.New() + c.Set("token", "thetoken") + service := config.NewServiceConfig("test", c) + backend := (&funkwhale.FunkwhaleApiBackend{}).FromConfig(&service) assert.IsType(t, &funkwhale.FunkwhaleApiBackend{}, backend) } diff --git a/internal/backends/jspf/jspf.go b/internal/backends/jspf/jspf.go index 38a1697..82deaf6 100644 --- a/internal/backends/jspf/jspf.go +++ b/internal/backends/jspf/jspf.go @@ -21,7 +21,7 @@ import ( "os" "time" - "github.com/spf13/viper" + "go.uploadedlobster.com/scotty/internal/config" "go.uploadedlobster.com/scotty/internal/models" "go.uploadedlobster.com/scotty/pkg/jspf" ) @@ -56,7 +56,7 @@ func (b *JSPFBackend) Options() *[]models.BackendOption { }} } -func (b *JSPFBackend) FromConfig(config *viper.Viper) models.Backend { +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") diff --git a/internal/backends/jspf/jspf_test.go b/internal/backends/jspf/jspf_test.go index 08c5b2e..31b5370 100644 --- a/internal/backends/jspf/jspf_test.go +++ b/internal/backends/jspf/jspf_test.go @@ -22,15 +22,17 @@ import ( "github.com/spf13/viper" "github.com/stretchr/testify/assert" - "go.uploadedlobster.com/scotty/internal/backends/scrobblerlog" + "go.uploadedlobster.com/scotty/internal/backends/jspf" + "go.uploadedlobster.com/scotty/internal/config" ) func TestFromConfig(t *testing.T) { - config := viper.New() - config.Set("file-path", "/foo/bar.jspf") - config.Set("title", "My Playlist") - config.Set("username", "outsidecontext") - config.Set("identifier", "http://example.com/playlist1") - backend := (&scrobblerlog.ScrobblerLogBackend{}).FromConfig(config) - assert.IsType(t, &scrobblerlog.ScrobblerLogBackend{}, backend) + 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) } diff --git a/internal/backends/lastfm/lastfm.go b/internal/backends/lastfm/lastfm.go index 8154e38..06a1d2c 100644 --- a/internal/backends/lastfm/lastfm.go +++ b/internal/backends/lastfm/lastfm.go @@ -23,8 +23,8 @@ import ( "time" "github.com/shkh/lastfm-go/lastfm" - "github.com/spf13/viper" "go.uploadedlobster.com/scotty/internal/auth" + "go.uploadedlobster.com/scotty/internal/config" "go.uploadedlobster.com/scotty/internal/models" "golang.org/x/oauth2" ) @@ -59,7 +59,7 @@ func (b *LastfmApiBackend) Options() *[]models.BackendOption { }} } -func (b *LastfmApiBackend) FromConfig(config *viper.Viper) models.Backend { +func (b *LastfmApiBackend) FromConfig(config *config.ServiceConfig) models.Backend { clientId := config.GetString("client-id") clientSecret := config.GetString("client-secret") b.client = lastfm.New(clientId, clientSecret) diff --git a/internal/backends/listenbrainz/listenbrainz.go b/internal/backends/listenbrainz/listenbrainz.go index dbc1022..2e20e0a 100644 --- a/internal/backends/listenbrainz/listenbrainz.go +++ b/internal/backends/listenbrainz/listenbrainz.go @@ -21,7 +21,7 @@ import ( "sort" "time" - "github.com/spf13/viper" + "go.uploadedlobster.com/scotty/internal/config" "go.uploadedlobster.com/scotty/internal/models" "go.uploadedlobster.com/scotty/internal/version" ) @@ -46,7 +46,7 @@ func (b *ListenBrainzApiBackend) Options() *[]models.BackendOption { }} } -func (b *ListenBrainzApiBackend) FromConfig(config *viper.Viper) models.Backend { +func (b *ListenBrainzApiBackend) FromConfig(config *config.ServiceConfig) models.Backend { b.client = NewClient(config.GetString("token")) b.client.MaxResults = MaxItemsPerGet b.username = config.GetString("username") diff --git a/internal/backends/listenbrainz/listenbrainz_test.go b/internal/backends/listenbrainz/listenbrainz_test.go index 03592a3..f67280e 100644 --- a/internal/backends/listenbrainz/listenbrainz_test.go +++ b/internal/backends/listenbrainz/listenbrainz_test.go @@ -24,13 +24,15 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uploadedlobster.com/scotty/internal/backends/listenbrainz" + "go.uploadedlobster.com/scotty/internal/config" "go.uploadedlobster.com/scotty/internal/models" ) func TestFromConfig(t *testing.T) { - config := viper.New() - config.Set("token", "thetoken") - backend := (&listenbrainz.ListenBrainzApiBackend{}).FromConfig(config) + c := viper.New() + c.Set("token", "thetoken") + service := config.NewServiceConfig("test", c) + backend := (&listenbrainz.ListenBrainzApiBackend{}).FromConfig(&service) assert.IsType(t, &listenbrainz.ListenBrainzApiBackend{}, backend) } diff --git a/internal/backends/maloja/maloja.go b/internal/backends/maloja/maloja.go index 80d9369..8595ee4 100644 --- a/internal/backends/maloja/maloja.go +++ b/internal/backends/maloja/maloja.go @@ -22,7 +22,7 @@ import ( "strings" "time" - "github.com/spf13/viper" + "go.uploadedlobster.com/scotty/internal/config" "go.uploadedlobster.com/scotty/internal/models" ) @@ -49,7 +49,7 @@ func (b *MalojaApiBackend) Options() *[]models.BackendOption { }} } -func (b *MalojaApiBackend) FromConfig(config *viper.Viper) models.Backend { +func (b *MalojaApiBackend) FromConfig(config *config.ServiceConfig) models.Backend { b.client = NewClient( config.GetString("server-url"), config.GetString("token"), diff --git a/internal/backends/maloja/maloja_test.go b/internal/backends/maloja/maloja_test.go index bb0dc16..52be58c 100644 --- a/internal/backends/maloja/maloja_test.go +++ b/internal/backends/maloja/maloja_test.go @@ -23,12 +23,14 @@ import ( "github.com/spf13/viper" "github.com/stretchr/testify/assert" "go.uploadedlobster.com/scotty/internal/backends/maloja" + "go.uploadedlobster.com/scotty/internal/config" ) func TestFromConfig(t *testing.T) { - config := viper.New() - config.Set("token", "thetoken") - backend := (&maloja.MalojaApiBackend{}).FromConfig(config) + c := viper.New() + c.Set("token", "thetoken") + service := config.NewServiceConfig("test", c) + backend := (&maloja.MalojaApiBackend{}).FromConfig(&service) assert.IsType(t, &maloja.MalojaApiBackend{}, backend) } diff --git a/internal/backends/scrobblerlog/scrobblerlog.go b/internal/backends/scrobblerlog/scrobblerlog.go index 0267b0e..963f628 100644 --- a/internal/backends/scrobblerlog/scrobblerlog.go +++ b/internal/backends/scrobblerlog/scrobblerlog.go @@ -22,7 +22,7 @@ import ( "sort" "time" - "github.com/spf13/viper" + "go.uploadedlobster.com/scotty/internal/config" "go.uploadedlobster.com/scotty/internal/models" ) @@ -52,7 +52,7 @@ func (b *ScrobblerLogBackend) Options() *[]models.BackendOption { }} } -func (b *ScrobblerLogBackend) FromConfig(config *viper.Viper) models.Backend { +func (b *ScrobblerLogBackend) FromConfig(config *config.ServiceConfig) models.Backend { b.filePath = config.GetString("file-path") b.includeSkipped = config.GetBool("include-skipped") b.append = true diff --git a/internal/backends/scrobblerlog/scrobblerlog_test.go b/internal/backends/scrobblerlog/scrobblerlog_test.go index 4e6c600..04e76c1 100644 --- a/internal/backends/scrobblerlog/scrobblerlog_test.go +++ b/internal/backends/scrobblerlog/scrobblerlog_test.go @@ -22,11 +22,13 @@ import ( "github.com/spf13/viper" "github.com/stretchr/testify/assert" "go.uploadedlobster.com/scotty/internal/backends/scrobblerlog" + "go.uploadedlobster.com/scotty/internal/config" ) func TestFromConfig(t *testing.T) { - config := viper.New() - config.Set("token", "thetoken") - backend := (&scrobblerlog.ScrobblerLogBackend{}).FromConfig(config) + c := viper.New() + c.Set("token", "thetoken") + service := config.NewServiceConfig("test", c) + backend := (&scrobblerlog.ScrobblerLogBackend{}).FromConfig(&service) assert.IsType(t, &scrobblerlog.ScrobblerLogBackend{}, backend) } diff --git a/internal/backends/spotify/spotify.go b/internal/backends/spotify/spotify.go index 9131841..e0bc646 100644 --- a/internal/backends/spotify/spotify.go +++ b/internal/backends/spotify/spotify.go @@ -24,8 +24,8 @@ import ( "strconv" "time" - "github.com/spf13/viper" "go.uploadedlobster.com/scotty/internal/auth" + "go.uploadedlobster.com/scotty/internal/config" "go.uploadedlobster.com/scotty/internal/models" "golang.org/x/oauth2" "golang.org/x/oauth2/spotify" @@ -51,7 +51,7 @@ func (b *SpotifyApiBackend) Options() *[]models.BackendOption { }} } -func (b *SpotifyApiBackend) FromConfig(config *viper.Viper) models.Backend { +func (b *SpotifyApiBackend) FromConfig(config *config.ServiceConfig) models.Backend { b.clientId = config.GetString("client-id") b.clientSecret = config.GetString("client-secret") return b diff --git a/internal/backends/spotify/spotify_test.go b/internal/backends/spotify/spotify_test.go index 5f1d544..496922a 100644 --- a/internal/backends/spotify/spotify_test.go +++ b/internal/backends/spotify/spotify_test.go @@ -27,13 +27,15 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uploadedlobster.com/scotty/internal/backends/spotify" + "go.uploadedlobster.com/scotty/internal/config" ) func TestFromConfig(t *testing.T) { - config := viper.New() - config.Set("client-id", "someclientid") - config.Set("client-secret", "someclientsecret") - backend := (&spotify.SpotifyApiBackend{}).FromConfig(config) + 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) } diff --git a/internal/backends/subsonic/subsonic.go b/internal/backends/subsonic/subsonic.go index bd2acbc..b78f489 100644 --- a/internal/backends/subsonic/subsonic.go +++ b/internal/backends/subsonic/subsonic.go @@ -22,7 +22,7 @@ import ( "time" "github.com/delucks/go-subsonic" - "github.com/spf13/viper" + "go.uploadedlobster.com/scotty/internal/config" "go.uploadedlobster.com/scotty/internal/models" "go.uploadedlobster.com/scotty/internal/version" ) @@ -50,7 +50,7 @@ func (b *SubsonicApiBackend) Options() *[]models.BackendOption { }} } -func (b *SubsonicApiBackend) FromConfig(config *viper.Viper) models.Backend { +func (b *SubsonicApiBackend) FromConfig(config *config.ServiceConfig) models.Backend { b.client = subsonic.Client{ Client: &http.Client{}, BaseUrl: config.GetString("server-url"), diff --git a/internal/backends/subsonic/subsonic_test.go b/internal/backends/subsonic/subsonic_test.go index d87c173..68641cd 100644 --- a/internal/backends/subsonic/subsonic_test.go +++ b/internal/backends/subsonic/subsonic_test.go @@ -24,13 +24,15 @@ import ( "github.com/spf13/viper" "github.com/stretchr/testify/assert" "go.uploadedlobster.com/scotty/internal/backends/subsonic" + "go.uploadedlobster.com/scotty/internal/config" ) func TestFromConfig(t *testing.T) { - config := viper.New() - config.Set("server-url", "https://subsonic.example.com") - config.Set("token", "thetoken") - backend := (&subsonic.SubsonicApiBackend{}).FromConfig(config) + 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) } diff --git a/internal/cli/common.go b/internal/cli/common.go index e7fe19d..52b2360 100644 --- a/internal/cli/common.go +++ b/internal/cli/common.go @@ -16,23 +16,15 @@ Scotty. If not, see . package cli import ( - "fmt" - "github.com/spf13/cobra" - "github.com/spf13/viper" + "go.uploadedlobster.com/scotty/internal/config" ) -func GetConfigFromFlag(cmd *cobra.Command, flagName string) (string, *viper.Viper) { - configName := cmd.Flag(flagName).Value.String() - var config *viper.Viper - servicesConfig := viper.Sub("service") - if servicesConfig != nil { - config = servicesConfig.Sub(configName) - } - if config == nil { - cobra.CheckErr(fmt.Sprintf("invalid configuration \"%s\"", configName)) - } - return configName, config +func GetServiceConfigFromFlag(cmd *cobra.Command, flagName string) *config.ServiceConfig { + name := cmd.Flag(flagName).Value.String() + config, err := config.GetService(name) + cobra.CheckErr(err) + return config } func getInt64FromFlag(cmd *cobra.Command, flagName string) (result int64) { diff --git a/internal/cli/transfer.go b/internal/cli/transfer.go index 35add01..cd8944e 100644 --- a/internal/cli/transfer.go +++ b/internal/cli/transfer.go @@ -16,6 +16,7 @@ Scotty. If not, see . package cli import ( + "errors" "fmt" "sync" "time" @@ -59,8 +60,14 @@ type TransferCmd[E models.Backend, I models.ImportBackend, R models.ListensResul } func (c *TransferCmd[E, I, R]) resolveBackends() error { - sourceName, sourceConfig := GetConfigFromFlag(c.cmd, "from") - targetName, targetConfig := GetConfigFromFlag(c.cmd, "to") + sourceConfig := GetServiceConfigFromFlag(c.cmd, "from") + if sourceConfig == nil { + cobra.CheckErr(errors.New("failed loading service configuration")) + } + targetConfig := GetServiceConfigFromFlag(c.cmd, "to") + if targetConfig == nil { + cobra.CheckErr(errors.New("failed loading service configuration")) + } // Initialize backends expBackend, err := backends.ResolveBackend[E](sourceConfig) @@ -72,8 +79,8 @@ func (c *TransferCmd[E, I, R]) resolveBackends() error { return err } - c.sourceName = sourceName - c.targetName = targetName + c.sourceName = sourceConfig.Name + c.targetName = targetConfig.Name c.ExpBackend = expBackend c.ImpBackend = impBackend return nil diff --git a/internal/config/services.go b/internal/config/services.go index cb64f7f..a0dbb13 100644 --- a/internal/config/services.go +++ b/internal/config/services.go @@ -18,6 +18,7 @@ package config import ( "fmt" + "github.com/spf13/cast" "github.com/spf13/viper" ) @@ -30,11 +31,11 @@ type ServiceConfig struct { func NewServiceConfig(name string, config *viper.Viper) ServiceConfig { service := ServiceConfig{ Name: name, - Backend: viper.GetString("backend"), + Backend: config.GetString("backend"), ConfigValues: make(map[string]any), } - for key, val := range viper.AllSettings() { + for key, val := range config.AllSettings() { if key != "backend" { service.ConfigValues[key] = val } @@ -43,6 +44,19 @@ func NewServiceConfig(name string, config *viper.Viper) ServiceConfig { return service } +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) IsSet(key string) bool { + _, ok := c.ConfigValues[key] + return ok +} + func (c *ServiceConfig) Save() error { key := "service." + c.Name viper.Set(key+".backend", c.Backend) diff --git a/internal/models/interfaces.go b/internal/models/interfaces.go index ab2339b..d2c05df 100644 --- a/internal/models/interfaces.go +++ b/internal/models/interfaces.go @@ -20,8 +20,8 @@ import ( "net/url" "time" - "github.com/spf13/viper" "go.uploadedlobster.com/scotty/internal/auth" + "go.uploadedlobster.com/scotty/internal/config" "golang.org/x/oauth2" ) @@ -32,7 +32,7 @@ type Backend interface { Name() string // Initialize the backend from a config. - FromConfig(config *viper.Viper) Backend + FromConfig(config *config.ServiceConfig) Backend // Return configuration options Options() *[]BackendOption From c6c0723e27201672409e18a682b54a2fa08015d8 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Thu, 7 Dec 2023 23:49:19 +0100 Subject: [PATCH 033/150] Renamed config command to service, make auth a subcommand --- cmd/add.go | 2 +- cmd/auth.go | 3 +-- cmd/{config.go => service.go} | 13 ++++++------- 3 files changed, 8 insertions(+), 10 deletions(-) rename cmd/{config.go => service.go} (83%) diff --git a/cmd/add.go b/cmd/add.go index 0d0bc8c..ab2f4fb 100644 --- a/cmd/add.go +++ b/cmd/add.go @@ -80,7 +80,7 @@ var addCmd = &cobra.Command{ } func init() { - configCmd.AddCommand(addCmd) + serviceCmd.AddCommand(addCmd) // Here you will define your flags and configuration settings. diff --git a/cmd/auth.go b/cmd/auth.go index c0a1129..70f4f81 100644 --- a/cmd/auth.go +++ b/cmd/auth.go @@ -33,7 +33,6 @@ import ( "golang.org/x/oauth2" ) -// authCmd represents the auth command var authCmd = &cobra.Command{ Use: "auth", Short: "Authenticate with a backend", @@ -92,7 +91,7 @@ var authCmd = &cobra.Command{ } func init() { - rootCmd.AddCommand(authCmd) + serviceCmd.AddCommand(authCmd) authCmd.Flags().StringP("service", "s", "", "Service configuration (required)") authCmd.MarkFlagRequired("service") diff --git a/cmd/config.go b/cmd/service.go similarity index 83% rename from cmd/config.go rename to cmd/service.go index ced8585..7fba51e 100644 --- a/cmd/config.go +++ b/cmd/service.go @@ -25,24 +25,23 @@ import ( "github.com/spf13/cobra" ) -// configCmd represents the config command -var configCmd = &cobra.Command{ - Use: "config", - Short: "Manage the configuration", +var serviceCmd = &cobra.Command{ + Use: "service", + Short: "Manage the service configuration", Long: `Manage the scotty configuration using the subcommands to add, remove or edit services.`, } func init() { - rootCmd.AddCommand(configCmd) + rootCmd.AddCommand(serviceCmd) // Here you will define your flags and configuration settings. // Cobra supports Persistent Flags which will work for this command // and all subcommands, e.g.: - // configCmd.PersistentFlags().String("foo", "", "A help for foo") + // serviceCmd.PersistentFlags().String("foo", "", "A help for foo") // Cobra supports local flags which will only run when this command // is called directly, e.g.: - // configCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") + // serviceCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") } From 58a47a43e702cfabeaa20d4e784ad00fceee0ed6 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Fri, 8 Dec 2023 08:38:17 +0100 Subject: [PATCH 034/150] Implemented service edit command --- cmd/add.go | 39 ++------ cmd/edit.go | 68 ++++++++++++++ internal/backends/deezer/deezer.go | 4 +- internal/backends/dump/dump.go | 2 +- internal/backends/funkwhale/funkwhale.go | 4 +- internal/backends/jspf/jspf.go | 4 +- internal/backends/lastfm/lastfm.go | 4 +- .../backends/listenbrainz/listenbrainz.go | 4 +- internal/backends/maloja/maloja.go | 4 +- .../backends/scrobblerlog/scrobblerlog.go | 4 +- internal/backends/spotify/spotify.go | 4 +- internal/backends/subsonic/subsonic.go | 4 +- internal/cli/services.go | 88 +++++++++++++++++++ internal/config/services.go | 35 +++++++- internal/models/interfaces.go | 2 +- 15 files changed, 213 insertions(+), 57 deletions(-) create mode 100644 cmd/edit.go create mode 100644 internal/cli/services.go diff --git a/cmd/add.go b/cmd/add.go index ab2f4fb..dc96474 100644 --- a/cmd/add.go +++ b/cmd/add.go @@ -27,7 +27,6 @@ import ( "github.com/manifoldco/promptui" "github.com/spf13/cobra" - "go.uploadedlobster.com/scotty/internal/backends" "go.uploadedlobster.com/scotty/internal/cli" "go.uploadedlobster.com/scotty/internal/config" ) @@ -39,12 +38,7 @@ var addCmd = &cobra.Command{ Long: `Add a service configuration.`, Run: func(cmd *cobra.Command, args []string) { // Select backend - sel := promptui.Select{ - Label: "Backend", - Items: backends.GetBackends(), - Size: 10, - } - _, backend, err := sel.Run() + backend, err := cli.SelectBackend("") cobra.CheckErr(err) // Set service name @@ -64,12 +58,13 @@ var addCmd = &cobra.Command{ // Prepare service config service := config.ServiceConfig{ - Name: name, - Backend: backend, + Name: name, + Backend: backend, + ConfigValues: make(map[string]any), } // Additional options - err = extraOptions(&service) + service, err = cli.PromptExtraOptions(service) cobra.CheckErr(err) // Save the service config @@ -92,27 +87,3 @@ func init() { // is called directly, e.g.: // addCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") } - -func extraOptions(config *config.ServiceConfig) error { - backend, err := backends.BackendByName(config.Backend) - if err != nil { - return err - } - opts := backend.Options() - if opts == nil { - return nil - } - - values := make(map[string]any, len(*opts)) - for _, opt := range *opts { - val, err := cli.Prompt(opt) - if err != nil { - return err - } - values[opt.Name] = val - - } - - config.ConfigValues = values - return nil -} diff --git a/cmd/edit.go b/cmd/edit.go new file mode 100644 index 0000000..bbc45d6 --- /dev/null +++ b/cmd/edit.go @@ -0,0 +1,68 @@ +/* +Copyright © 2023 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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" + "go.uploadedlobster.com/scotty/internal/cli" +) + +// editCmd represents the add command +var editCmd = &cobra.Command{ + Use: "edit", + Short: "Edit existing service configuration", + Long: `Edit an existing service configuration.`, + Run: func(cmd *cobra.Command, args []string) { + service, err := cli.SelectService() + cobra.CheckErr(err) + + // Select backend + backend, err := cli.SelectBackend(service.Backend) + cobra.CheckErr(err) + service.Backend = backend + + // Additional options + service, err = cli.PromptExtraOptions(service) + cobra.CheckErr(err) + + // Save the service config + err = service.Save() + cobra.CheckErr(err) + fmt.Printf("Updated service %v using backend %v\n", service.Name, service.Backend) + }, +} + +func init() { + serviceCmd.AddCommand(editCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // editCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // editCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/internal/backends/deezer/deezer.go b/internal/backends/deezer/deezer.go index 89d82c2..05145fb 100644 --- a/internal/backends/deezer/deezer.go +++ b/internal/backends/deezer/deezer.go @@ -36,8 +36,8 @@ type DeezerApiBackend struct { func (b *DeezerApiBackend) Name() string { return "deezer" } -func (b *DeezerApiBackend) Options() *[]models.BackendOption { - return &[]models.BackendOption{{ +func (b *DeezerApiBackend) Options() []models.BackendOption { + return []models.BackendOption{{ Name: "client-id", Label: "Client ID", Type: models.String, diff --git a/internal/backends/dump/dump.go b/internal/backends/dump/dump.go index aedcaaf..32a957b 100644 --- a/internal/backends/dump/dump.go +++ b/internal/backends/dump/dump.go @@ -25,7 +25,7 @@ type DumpBackend struct{} func (b *DumpBackend) Name() string { return "dump" } -func (b *DumpBackend) Options() *[]models.BackendOption { return nil } +func (b *DumpBackend) Options() []models.BackendOption { return nil } func (b *DumpBackend) FromConfig(config *config.ServiceConfig) models.Backend { return b diff --git a/internal/backends/funkwhale/funkwhale.go b/internal/backends/funkwhale/funkwhale.go index 429fc11..8e808f9 100644 --- a/internal/backends/funkwhale/funkwhale.go +++ b/internal/backends/funkwhale/funkwhale.go @@ -33,8 +33,8 @@ type FunkwhaleApiBackend struct { func (b *FunkwhaleApiBackend) Name() string { return "funkwhale" } -func (b *FunkwhaleApiBackend) Options() *[]models.BackendOption { - return &[]models.BackendOption{{ +func (b *FunkwhaleApiBackend) Options() []models.BackendOption { + return []models.BackendOption{{ Name: "server-url", Label: "Server URL", Type: models.String, diff --git a/internal/backends/jspf/jspf.go b/internal/backends/jspf/jspf.go index 82deaf6..29ddcdc 100644 --- a/internal/backends/jspf/jspf.go +++ b/internal/backends/jspf/jspf.go @@ -36,8 +36,8 @@ type JSPFBackend struct { func (b *JSPFBackend) Name() string { return "jspf" } -func (b *JSPFBackend) Options() *[]models.BackendOption { - return &[]models.BackendOption{{ +func (b *JSPFBackend) Options() []models.BackendOption { + return []models.BackendOption{{ Name: "file-path", Label: "File path", Type: models.String, diff --git a/internal/backends/lastfm/lastfm.go b/internal/backends/lastfm/lastfm.go index 06a1d2c..c823248 100644 --- a/internal/backends/lastfm/lastfm.go +++ b/internal/backends/lastfm/lastfm.go @@ -43,8 +43,8 @@ type LastfmApiBackend struct { func (b *LastfmApiBackend) Name() string { return "lastfm" } -func (b *LastfmApiBackend) Options() *[]models.BackendOption { - return &[]models.BackendOption{{ +func (b *LastfmApiBackend) Options() []models.BackendOption { + return []models.BackendOption{{ Name: "username", Label: "User name", Type: models.String, diff --git a/internal/backends/listenbrainz/listenbrainz.go b/internal/backends/listenbrainz/listenbrainz.go index 2e20e0a..2d3dc95 100644 --- a/internal/backends/listenbrainz/listenbrainz.go +++ b/internal/backends/listenbrainz/listenbrainz.go @@ -34,8 +34,8 @@ type ListenBrainzApiBackend struct { func (b *ListenBrainzApiBackend) Name() string { return "listenbrainz" } -func (b *ListenBrainzApiBackend) Options() *[]models.BackendOption { - return &[]models.BackendOption{{ +func (b *ListenBrainzApiBackend) Options() []models.BackendOption { + return []models.BackendOption{{ Name: "username", Label: "User name", Type: models.String, diff --git a/internal/backends/maloja/maloja.go b/internal/backends/maloja/maloja.go index 8595ee4..c47f67a 100644 --- a/internal/backends/maloja/maloja.go +++ b/internal/backends/maloja/maloja.go @@ -33,8 +33,8 @@ type MalojaApiBackend struct { func (b *MalojaApiBackend) Name() string { return "maloja" } -func (b *MalojaApiBackend) Options() *[]models.BackendOption { - return &[]models.BackendOption{{ +func (b *MalojaApiBackend) Options() []models.BackendOption { + return []models.BackendOption{{ Name: "server-url", Label: "Server URL", Type: models.String, diff --git a/internal/backends/scrobblerlog/scrobblerlog.go b/internal/backends/scrobblerlog/scrobblerlog.go index 963f628..6964b7e 100644 --- a/internal/backends/scrobblerlog/scrobblerlog.go +++ b/internal/backends/scrobblerlog/scrobblerlog.go @@ -36,8 +36,8 @@ type ScrobblerLogBackend struct { func (b *ScrobblerLogBackend) Name() string { return "scrobbler-log" } -func (b *ScrobblerLogBackend) Options() *[]models.BackendOption { - return &[]models.BackendOption{{ +func (b *ScrobblerLogBackend) Options() []models.BackendOption { + return []models.BackendOption{{ Name: "file-path", Label: "File path", Type: models.String, diff --git a/internal/backends/spotify/spotify.go b/internal/backends/spotify/spotify.go index e0bc646..8a0bc09 100644 --- a/internal/backends/spotify/spotify.go +++ b/internal/backends/spotify/spotify.go @@ -39,8 +39,8 @@ type SpotifyApiBackend struct { func (b *SpotifyApiBackend) Name() string { return "spotify" } -func (b *SpotifyApiBackend) Options() *[]models.BackendOption { - return &[]models.BackendOption{{ +func (b *SpotifyApiBackend) Options() []models.BackendOption { + return []models.BackendOption{{ Name: "client-id", Label: "Client ID", Type: models.String, diff --git a/internal/backends/subsonic/subsonic.go b/internal/backends/subsonic/subsonic.go index b78f489..7defb87 100644 --- a/internal/backends/subsonic/subsonic.go +++ b/internal/backends/subsonic/subsonic.go @@ -34,8 +34,8 @@ type SubsonicApiBackend struct { func (b *SubsonicApiBackend) Name() string { return "subsonic" } -func (b *SubsonicApiBackend) Options() *[]models.BackendOption { - return &[]models.BackendOption{{ +func (b *SubsonicApiBackend) Options() []models.BackendOption { + return []models.BackendOption{{ Name: "server-url", Label: "Server URL", Type: models.String, diff --git a/internal/cli/services.go b/internal/cli/services.go new file mode 100644 index 0000000..d755607 --- /dev/null +++ b/internal/cli/services.go @@ -0,0 +1,88 @@ +/* +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 cli + +import ( + "errors" + "fmt" + "slices" + + "github.com/manifoldco/promptui" + "go.uploadedlobster.com/scotty/internal/backends" + "go.uploadedlobster.com/scotty/internal/config" +) + +func SelectService() (config.ServiceConfig, error) { + services := config.AllServicesAsList() + if len(services) == 0 { + err := errors.New("no existing service configurations") + return config.ServiceConfig{}, err + } + sel := promptui.Select{ + Label: "Service", + Items: services, + Size: 10, + } + i, _, err := sel.Run() + if err != nil { + return config.ServiceConfig{}, err + } + return services[i], nil +} + +func SelectBackend(selected string) (string, error) { + backendList := backends.GetBackends() + i := slices.IndexFunc(backendList, func(b backends.BackendInfo) bool { + return b.Name == selected + }) + sel := promptui.Select{ + Label: "Backend", + Items: backendList, + CursorPos: i, + Size: 10, + } + _, backend, err := sel.Run() + return backend, err +} + +func PromptExtraOptions(config config.ServiceConfig) (config.ServiceConfig, error) { + backend, err := backends.BackendByName(config.Backend) + if err != nil { + return config, err + } + opts := backend.Options() + if opts == nil { + return config, nil + } + + values := make(map[string]any, len(opts)) + for _, opt := range opts { + // Use current value as default + current, exists := config.ConfigValues[opt.Name] + if exists { + opt.Default = fmt.Sprintf("%v", current) + } + + val, err := Prompt(opt) + if err != nil { + return config, err + } + values[opt.Name] = val + } + + config.ConfigValues = values + return config, nil +} diff --git a/internal/config/services.go b/internal/config/services.go index a0dbb13..eed2678 100644 --- a/internal/config/services.go +++ b/internal/config/services.go @@ -17,6 +17,7 @@ package config import ( "fmt" + "sort" "github.com/spf13/cast" "github.com/spf13/viper" @@ -44,6 +45,10 @@ func NewServiceConfig(name string, config *viper.Viper) ServiceConfig { return service } +func (c ServiceConfig) String() string { + return c.Name +} + func (c *ServiceConfig) GetString(key string) string { return cast.ToString(c.ConfigValues[key]) } @@ -66,13 +71,27 @@ func (c *ServiceConfig) Save() error { return viper.WriteConfig() } +type ServiceList []ServiceConfig + +func (l ServiceList) Len() int { + return len(l) +} + +func (l ServiceList) Less(i, j int) bool { + return l[i].Name < l[j].Name +} + +func (l ServiceList) Swap(i, j int) { + l[i], l[j] = l[j], l[i] +} + func AllServices() map[string]ServiceConfig { services := make(map[string]ServiceConfig) config := viper.Sub("service") if config != nil { - for k, v := range config.AllSettings() { - s, ok := v.(*viper.Viper) - if ok { + for k := range config.AllSettings() { + s := config.Sub(k) + if s != nil { services[k] = NewServiceConfig(k, s) } } @@ -80,6 +99,16 @@ func AllServices() map[string]ServiceConfig { return services } +func AllServicesAsList() ServiceList { + services := AllServices() + list := make(ServiceList, 0, len(services)) + for _, s := range services { + list = append(list, s) + } + sort.Sort(list) + return list +} + func GetService(name string) (*ServiceConfig, error) { key := "service." + name config := viper.Sub(key) diff --git a/internal/models/interfaces.go b/internal/models/interfaces.go index d2c05df..cc52ead 100644 --- a/internal/models/interfaces.go +++ b/internal/models/interfaces.go @@ -35,7 +35,7 @@ type Backend interface { FromConfig(config *config.ServiceConfig) Backend // Return configuration options - Options() *[]BackendOption + Options() []BackendOption } type ImportBackend interface { From 7f2db58462a231a46a8ac32c179f673d56ca5dda Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Fri, 8 Dec 2023 08:56:10 +0100 Subject: [PATCH 035/150] Implemented service list command --- cmd/list.go | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 cmd/list.go diff --git a/cmd/list.go b/cmd/list.go new file mode 100644 index 0000000..730904d --- /dev/null +++ b/cmd/list.go @@ -0,0 +1,63 @@ +/* +Copyright © 2023 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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" + "go.uploadedlobster.com/scotty/internal/config" +) + +// listCmd represents the add command +var listCmd = &cobra.Command{ + Use: "list", + Short: "List existing service configurations", + Long: `List existing service configurations.`, + Run: func(cmd *cobra.Command, args []string) { + verbose, _ := cmd.Flags().GetBool("verbose") + for _, s := range config.AllServicesAsList() { + fmt.Printf("%v\n", s.Name) + if verbose { + fmt.Printf("\tbackend: %v\n", s.Backend) + for k, v := range s.ConfigValues { + fmt.Printf("\t%v: %v\n", k, v) + } + fmt.Println() + } + } + }, +} + +func init() { + serviceCmd.AddCommand(listCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // listCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + listCmd.Flags().BoolP("verbose", "v", false, "Verbose output") +} From 543a9c666d7a96c219d215565bc781fd3f53d0b4 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Fri, 8 Dec 2023 17:27:04 +0100 Subject: [PATCH 036/150] Implemented service delete command --- cmd/delete.go | 68 +++++++++++++++++++++++++++++++++++++ go.mod | 2 +- internal/cli/prompt.go | 6 +++- internal/config/config.go | 43 ++++++++++++++++++++++- internal/config/services.go | 8 ++++- 5 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 cmd/delete.go diff --git a/cmd/delete.go b/cmd/delete.go new file mode 100644 index 0000000..19ccad6 --- /dev/null +++ b/cmd/delete.go @@ -0,0 +1,68 @@ +/* +Copyright © 2023 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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" + "go.uploadedlobster.com/scotty/internal/cli" +) + +// deleteCmd represents the add command +var deleteCmd = &cobra.Command{ + Use: "delete", + Short: "Delete existing service configuration", + Long: `Delete an existing service configuration.`, + Run: func(cmd *cobra.Command, args []string) { + service, err := cli.SelectService() + cobra.CheckErr(err) + + // Prompt for deletion + delete, err := cli.PromptYesNo(fmt.Sprintf("Delete the service configuration \"%v\"?", service)) + cobra.CheckErr(err) + + if !delete { + fmt.Println("Aborted") + return + } + + // Delete the service config + err = service.Delete() + cobra.CheckErr(err) + fmt.Printf("Service \"%v\" deleted\n", service.Name) + }, +} + +func init() { + serviceCmd.AddCommand(deleteCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // deleteCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // deleteCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/go.mod b/go.mod index 04b84d4..a827f0f 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/go-resty/resty/v2 v2.10.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/shkh/lastfm-go v0.0.0-20191215035245-89a801c244e0 github.com/spf13/cast v1.5.1 github.com/spf13/cobra v1.8.0 @@ -42,7 +43,6 @@ 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/pelletier/go-toml/v2 v2.1.0 // 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 diff --git a/internal/cli/prompt.go b/internal/cli/prompt.go index 0fbe979..19038e9 100644 --- a/internal/cli/prompt.go +++ b/internal/cli/prompt.go @@ -59,8 +59,12 @@ func PromptSecret(opt models.BackendOption) (string, error) { } func PromptBool(opt models.BackendOption) (bool, error) { + return PromptYesNo(opt.Label) +} + +func PromptYesNo(label string) (bool, error) { sel := promptui.Select{ - Label: opt.Label, + Label: label, Items: []string{"Yes", "No"}, } _, val, err := sel.Run() diff --git a/internal/config/config.go b/internal/config/config.go index 746e79c..6d68d00 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -16,12 +16,15 @@ Scotty. If not, see . package config import ( + "errors" "fmt" "os" "path" "path/filepath" "regexp" + "strings" + "github.com/pelletier/go-toml/v2" "github.com/spf13/cobra" "github.com/spf13/viper" "go.uploadedlobster.com/scotty/internal/version" @@ -30,6 +33,7 @@ import ( const ( defaultDatabase = "scotty.sqlite3" defaultOAuthHost = "127.0.0.1:2369" + fileMode = 0640 ) func DefaultConfigDir() string { @@ -48,7 +52,7 @@ func InitConfig(cfgFile string) error { viper.AddConfigPath(configDir) viper.SetConfigType("toml") viper.SetConfigName(version.AppName) - viper.SetConfigPermissions(0640) + viper.SetConfigPermissions(fileMode) } setDefaults() @@ -67,6 +71,43 @@ func InitConfig(cfgFile string) error { return viper.ReadInConfig() } +// Write the configuration except for removedKeys +func WriteConfig(removedKeys ...string) error { + file := viper.ConfigFileUsed() + if len(file) == 0 { + return errors.New("no configuration file defined, cannot write config") + } + + configMap := viper.AllSettings() + for _, key := range removedKeys { + c := configMap + ok := true + subKeys := strings.Split(key, ".") + keyLen := len(subKeys) + // Deep search the key in the config and delete the deepest key, if it exists + for i, s := range subKeys { + if i == keyLen-1 { + // This is the final key, delete it from the map + delete(c, s) + } else { + // Use the child for next iteration if it is a map + c, ok = c[s].(map[string]any) + if !ok { + // Child is not a map, can't search deeper + break + } + } + } + } + + content, err := toml.Marshal(configMap) + if err != nil { + return err + } + + return os.WriteFile(file, content, fileMode) +} + func DatabasePath() string { path := viper.GetString("database") if filepath.IsAbs(path) { diff --git a/internal/config/services.go b/internal/config/services.go index eed2678..8a191f3 100644 --- a/internal/config/services.go +++ b/internal/config/services.go @@ -68,7 +68,13 @@ func (c *ServiceConfig) Save() error { for k, v := range c.ConfigValues { viper.Set(key+"."+k, v) } - return viper.WriteConfig() + return WriteConfig() +} + +// Deletes the service configuration from the config file +func (c *ServiceConfig) Delete() error { + key := "service." + c.Name + return WriteConfig(key) } type ServiceList []ServiceConfig From 3ab0ce1cc65be6605429d757ce1fa9845c960f62 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Fri, 8 Dec 2023 17:37:53 +0100 Subject: [PATCH 037/150] Restructured cmd files --- cmd/backends.go | 1 - cmd/beam.go | 1 - cmd/{listens.go => beam_listens.go} | 9 ++++----- cmd/{loves.go => beam_loves.go} | 9 ++++----- cmd/{add.go => service_add.go} | 9 ++++----- cmd/{auth.go => service_auth.go} | 8 ++++---- cmd/{delete.go => service_delete.go} | 9 ++++----- cmd/{edit.go => service_edit.go} | 9 ++++----- cmd/{list.go => service_list.go} | 9 ++++----- 9 files changed, 28 insertions(+), 36 deletions(-) rename cmd/{listens.go => beam_listens.go} (87%) rename cmd/{loves.go => beam_loves.go} (88%) rename cmd/{add.go => service_add.go} (91%) rename cmd/{auth.go => service_auth.go} (93%) rename cmd/{delete.go => service_delete.go} (89%) rename cmd/{edit.go => service_edit.go} (89%) rename cmd/{list.go => service_list.go} (89%) diff --git a/cmd/backends.go b/cmd/backends.go index 7d6383e..43cae08 100644 --- a/cmd/backends.go +++ b/cmd/backends.go @@ -24,7 +24,6 @@ import ( "go.uploadedlobster.com/scotty/internal/backends" ) -// backendsCmd represents the backends command var backendsCmd = &cobra.Command{ Use: "backends", Short: "List available backends", diff --git a/cmd/beam.go b/cmd/beam.go index 63b5975..2a43495 100644 --- a/cmd/beam.go +++ b/cmd/beam.go @@ -20,7 +20,6 @@ import ( "github.com/spf13/cobra" ) -// beamCmd represents the beam command var beamCmd = &cobra.Command{ Use: "beam", Short: "Transfer data between two services", diff --git a/cmd/listens.go b/cmd/beam_listens.go similarity index 87% rename from cmd/listens.go rename to cmd/beam_listens.go index 558259e..499654d 100644 --- a/cmd/listens.go +++ b/cmd/beam_listens.go @@ -25,8 +25,7 @@ import ( "go.uploadedlobster.com/scotty/internal/storage" ) -// listensCmd represents the listens command -var listensCmd = &cobra.Command{ +var beamListensCmd = &cobra.Command{ Use: "listens", Short: "Transfer listens between two services", Long: `Transfers listens between two configured services.`, @@ -47,14 +46,14 @@ var listensCmd = &cobra.Command{ } func init() { - beamCmd.AddCommand(listensCmd) + beamCmd.AddCommand(beamListensCmd) // Here you will define your flags and configuration settings. // Cobra supports Persistent Flags which will work for this command // and all subcommands, e.g.: - // listensCmd.PersistentFlags().String("foo", "", "A help for foo") + // beamListensCmd.PersistentFlags().String("foo", "", "A help for foo") // Cobra supports local flags which will only run when this command // is called directly, e.g.: - // listensCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") + // beamListensCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") } diff --git a/cmd/loves.go b/cmd/beam_loves.go similarity index 88% rename from cmd/loves.go rename to cmd/beam_loves.go index a802c42..d632719 100644 --- a/cmd/loves.go +++ b/cmd/beam_loves.go @@ -25,8 +25,7 @@ import ( "go.uploadedlobster.com/scotty/internal/storage" ) -// lovesCmd represents the loves command -var lovesCmd = &cobra.Command{ +var beamLovesCmd = &cobra.Command{ Use: "loves", Short: "Transfer loves between two services", Long: `Transfers loves between two configured services.`, @@ -47,15 +46,15 @@ var lovesCmd = &cobra.Command{ } func init() { - beamCmd.AddCommand(lovesCmd) + beamCmd.AddCommand(beamLovesCmd) // Here you will define your flags and configuration settings. // Cobra supports Persistent Flags which will work for this command // and all subcommands, e.g.: - // lovesCmd.PersistentFlags().String("foo", "", "A help for foo") + // beamLovesCmd.PersistentFlags().String("foo", "", "A help for foo") // Cobra supports local flags which will only run when this command // is called directly, e.g.: - // lovesCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") + // beamLovesCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") } diff --git a/cmd/add.go b/cmd/service_add.go similarity index 91% rename from cmd/add.go rename to cmd/service_add.go index dc96474..d995d46 100644 --- a/cmd/add.go +++ b/cmd/service_add.go @@ -31,8 +31,7 @@ import ( "go.uploadedlobster.com/scotty/internal/config" ) -// addCmd represents the add command -var addCmd = &cobra.Command{ +var serviceAddCmd = &cobra.Command{ Use: "add", Short: "Add a service configuration", Long: `Add a service configuration.`, @@ -75,15 +74,15 @@ var addCmd = &cobra.Command{ } func init() { - serviceCmd.AddCommand(addCmd) + serviceCmd.AddCommand(serviceAddCmd) // Here you will define your flags and configuration settings. // Cobra supports Persistent Flags which will work for this command // and all subcommands, e.g.: - // addCmd.PersistentFlags().String("foo", "", "A help for foo") + // serviceAddCmd.PersistentFlags().String("foo", "", "A help for foo") // Cobra supports local flags which will only run when this command // is called directly, e.g.: - // addCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") + // serviceAddCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") } diff --git a/cmd/auth.go b/cmd/service_auth.go similarity index 93% rename from cmd/auth.go rename to cmd/service_auth.go index 70f4f81..cee8e2f 100644 --- a/cmd/auth.go +++ b/cmd/service_auth.go @@ -33,7 +33,7 @@ import ( "golang.org/x/oauth2" ) -var authCmd = &cobra.Command{ +var serviceAuthCmd = &cobra.Command{ Use: "auth", Short: "Authenticate with a backend", Long: `For backends requiring authentication this command can be used to authenticate.`, @@ -91,8 +91,8 @@ var authCmd = &cobra.Command{ } func init() { - serviceCmd.AddCommand(authCmd) + serviceCmd.AddCommand(serviceAuthCmd) - authCmd.Flags().StringP("service", "s", "", "Service configuration (required)") - authCmd.MarkFlagRequired("service") + serviceAuthCmd.Flags().StringP("service", "s", "", "Service configuration (required)") + serviceAuthCmd.MarkFlagRequired("service") } diff --git a/cmd/delete.go b/cmd/service_delete.go similarity index 89% rename from cmd/delete.go rename to cmd/service_delete.go index 19ccad6..dd7b431 100644 --- a/cmd/delete.go +++ b/cmd/service_delete.go @@ -28,8 +28,7 @@ import ( "go.uploadedlobster.com/scotty/internal/cli" ) -// deleteCmd represents the add command -var deleteCmd = &cobra.Command{ +var serviceDeleteCmd = &cobra.Command{ Use: "delete", Short: "Delete existing service configuration", Long: `Delete an existing service configuration.`, @@ -54,15 +53,15 @@ var deleteCmd = &cobra.Command{ } func init() { - serviceCmd.AddCommand(deleteCmd) + serviceCmd.AddCommand(serviceDeleteCmd) // Here you will define your flags and configuration settings. // Cobra supports Persistent Flags which will work for this command // and all subcommands, e.g.: - // deleteCmd.PersistentFlags().String("foo", "", "A help for foo") + // serviceDeleteCmd.PersistentFlags().String("foo", "", "A help for foo") // Cobra supports local flags which will only run when this command // is called directly, e.g.: - // deleteCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") + // serviceDeleteCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") } diff --git a/cmd/edit.go b/cmd/service_edit.go similarity index 89% rename from cmd/edit.go rename to cmd/service_edit.go index bbc45d6..9f4accf 100644 --- a/cmd/edit.go +++ b/cmd/service_edit.go @@ -28,8 +28,7 @@ import ( "go.uploadedlobster.com/scotty/internal/cli" ) -// editCmd represents the add command -var editCmd = &cobra.Command{ +var serviceEditCmd = &cobra.Command{ Use: "edit", Short: "Edit existing service configuration", Long: `Edit an existing service configuration.`, @@ -54,15 +53,15 @@ var editCmd = &cobra.Command{ } func init() { - serviceCmd.AddCommand(editCmd) + serviceCmd.AddCommand(serviceEditCmd) // Here you will define your flags and configuration settings. // Cobra supports Persistent Flags which will work for this command // and all subcommands, e.g.: - // editCmd.PersistentFlags().String("foo", "", "A help for foo") + // serviceEditCmd.PersistentFlags().String("foo", "", "A help for foo") // Cobra supports local flags which will only run when this command // is called directly, e.g.: - // editCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") + // serviceEditCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") } diff --git a/cmd/list.go b/cmd/service_list.go similarity index 89% rename from cmd/list.go rename to cmd/service_list.go index 730904d..23d64d9 100644 --- a/cmd/list.go +++ b/cmd/service_list.go @@ -28,8 +28,7 @@ import ( "go.uploadedlobster.com/scotty/internal/config" ) -// listCmd represents the add command -var listCmd = &cobra.Command{ +var serviceListCmd = &cobra.Command{ Use: "list", Short: "List existing service configurations", Long: `List existing service configurations.`, @@ -49,15 +48,15 @@ var listCmd = &cobra.Command{ } func init() { - serviceCmd.AddCommand(listCmd) + serviceCmd.AddCommand(serviceListCmd) // Here you will define your flags and configuration settings. // Cobra supports Persistent Flags which will work for this command // and all subcommands, e.g.: - // listCmd.PersistentFlags().String("foo", "", "A help for foo") + // serviceListCmd.PersistentFlags().String("foo", "", "A help for foo") // Cobra supports local flags which will only run when this command // is called directly, e.g.: - listCmd.Flags().BoolP("verbose", "v", false, "Verbose output") + serviceListCmd.Flags().BoolP("verbose", "v", false, "Verbose output") } From 20f173285880000f3ab3ed1e08da6cf646ac5e5a Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Fri, 8 Dec 2023 17:59:37 +0100 Subject: [PATCH 038/150] Use positional arguments for source and target in beam commands --- cmd/beam.go | 6 +----- cmd/beam_listens.go | 11 +++++++---- cmd/beam_loves.go | 6 ++++-- internal/cli/transfer.go | 19 ++++++++----------- 4 files changed, 20 insertions(+), 22 deletions(-) diff --git a/cmd/beam.go b/cmd/beam.go index 2a43495..51dd1c5 100644 --- a/cmd/beam.go +++ b/cmd/beam.go @@ -37,11 +37,7 @@ func init() { // Cobra supports Persistent Flags which will work for this command // and all subcommands, e.g.: - beamCmd.PersistentFlags().StringP("from", "f", "", "Source service configuration (required)") - beamCmd.MarkPersistentFlagRequired("from") - beamCmd.PersistentFlags().StringP("to", "t", "", "Target service configuration (required)") - beamCmd.MarkPersistentFlagRequired("to") - beamCmd.PersistentFlags().Int64P("timestamp", "s", 0, "Only import data newer then given Unix timestamp") + // beamCmd.PersistentFlags().String("foo", "", "A help for foo") // Cobra supports local flags which will only run when this command // is called directly, e.g.: diff --git a/cmd/beam_listens.go b/cmd/beam_listens.go index 499654d..1b83e29 100644 --- a/cmd/beam_listens.go +++ b/cmd/beam_listens.go @@ -26,9 +26,11 @@ import ( ) var beamListensCmd = &cobra.Command{ - Use: "listens", - Short: "Transfer listens between two services", - Long: `Transfers listens between two configured services.`, + Use: "listens SOURCE TARGET", + Short: "Transfer listens between two services", + Long: `Transfers listens between two configured services.`, + Args: cobra.ExactArgs(2), + ArgAliases: []string{"source", "target"}, Run: func(cmd *cobra.Command, args []string) { db, err := storage.New(config.DatabasePath()) cobra.CheckErr(err) @@ -36,7 +38,7 @@ var beamListensCmd = &cobra.Command{ models.ListensExport, models.ListensImport, models.ListensResult, - ](cmd, &db, "listens") + ](cmd, &db, "listens", args[0], args[1]) cobra.CheckErr(err) exp := backends.ListensExportProcessor{Backend: c.ExpBackend} imp := backends.ListensImportProcessor{Backend: c.ImpBackend} @@ -56,4 +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", 0, "Only import listens newer then given Unix timestamp") } diff --git a/cmd/beam_loves.go b/cmd/beam_loves.go index d632719..9c2669a 100644 --- a/cmd/beam_loves.go +++ b/cmd/beam_loves.go @@ -26,9 +26,10 @@ import ( ) var beamLovesCmd = &cobra.Command{ - Use: "loves", + Use: "loves SOURCE TARGET", Short: "Transfer loves between two services", Long: `Transfers loves between two configured services.`, + Args: cobra.ExactArgs(2), Run: func(cmd *cobra.Command, args []string) { db, err := storage.New(config.DatabasePath()) cobra.CheckErr(err) @@ -36,7 +37,7 @@ var beamLovesCmd = &cobra.Command{ models.LovesExport, models.LovesImport, models.LovesResult, - ](cmd, &db, "loves") + ](cmd, &db, "loves", args[0], args[1]) cobra.CheckErr(err) exp := backends.LovesExportProcessor{Backend: c.ExpBackend} imp := backends.LovesImportProcessor{Backend: c.ImpBackend} @@ -57,4 +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", 0, "Only import loves newer then given Unix timestamp") } diff --git a/internal/cli/transfer.go b/internal/cli/transfer.go index cd8944e..0ad67f1 100644 --- a/internal/cli/transfer.go +++ b/internal/cli/transfer.go @@ -16,7 +16,6 @@ Scotty. If not, see . package cli import ( - "errors" "fmt" "sync" "time" @@ -24,6 +23,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" "go.uploadedlobster.com/scotty/internal/backends" + "go.uploadedlobster.com/scotty/internal/config" "go.uploadedlobster.com/scotty/internal/models" "go.uploadedlobster.com/scotty/internal/storage" ) @@ -36,13 +36,15 @@ func NewTransferCmd[ cmd *cobra.Command, db *storage.Database, entity string, + source string, + target string, ) (TransferCmd[E, I, R], error) { c := TransferCmd[E, I, R]{ cmd: cmd, db: db, entity: entity, } - err := c.resolveBackends() + err := c.resolveBackends(source, target) if err != nil { return c, err } @@ -59,15 +61,10 @@ type TransferCmd[E models.Backend, I models.ImportBackend, R models.ListensResul ImpBackend I } -func (c *TransferCmd[E, I, R]) resolveBackends() error { - sourceConfig := GetServiceConfigFromFlag(c.cmd, "from") - if sourceConfig == nil { - cobra.CheckErr(errors.New("failed loading service configuration")) - } - targetConfig := GetServiceConfigFromFlag(c.cmd, "to") - if targetConfig == nil { - cobra.CheckErr(errors.New("failed loading service configuration")) - } +func (c *TransferCmd[E, I, R]) resolveBackends(source string, target string) error { + sourceConfig, err := config.GetService(source) + cobra.CheckErr(err) + targetConfig, err := config.GetService(target) // Initialize backends expBackend, err := backends.ResolveBackend[E](sourceConfig) From a41318d822aeb8d433b253df3400d154b48f5500 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sat, 9 Dec 2023 11:46:32 +0100 Subject: [PATCH 039/150] Basic i18n setup --- go.mod | 3 +- go.sum | 12 ++ internal/cli/transfer.go | 19 +- internal/i18n/i18n.go | 39 ++++ internal/i18n/localizer.go | 37 ++++ internal/translations/catalog.go | 75 ++++++++ .../locales/de/messages.gotext.json | 164 ++++++++++++++++ .../translations/locales/de/out.gotext.json | 164 ++++++++++++++++ .../translations/locales/en/out.gotext.json | 178 ++++++++++++++++++ internal/translations/translations.go | 18 ++ 10 files changed, 700 insertions(+), 9 deletions(-) create mode 100644 internal/i18n/i18n.go create mode 100644 internal/i18n/localizer.go create mode 100644 internal/translations/catalog.go create mode 100644 internal/translations/locales/de/messages.gotext.json create mode 100644 internal/translations/locales/de/out.gotext.json create mode 100644 internal/translations/locales/en/out.gotext.json create mode 100644 internal/translations/translations.go diff --git a/go.mod b/go.mod index a827f0f..4c5487f 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module go.uploadedlobster.com/scotty go 1.21.1 require ( + github.com/Xuanwo/go-locale v1.1.0 github.com/cli/browser v1.3.0 github.com/delucks/go-subsonic v0.0.0-20220915164742-2744002c4be5 github.com/fatih/color v1.16.0 @@ -19,6 +20,7 @@ require ( github.com/vbauerster/mpb/v8 v8.6.2 golang.org/x/exp v0.0.0-20230905200255-921286631fa9 golang.org/x/oauth2 v0.14.0 + golang.org/x/text v0.14.0 gorm.io/datatypes v1.2.0 gorm.io/gorm v1.25.5 ) @@ -56,7 +58,6 @@ require ( go.uber.org/multierr v1.9.0 // indirect golang.org/x/net v0.18.0 // indirect golang.org/x/sys v0.14.0 // indirect - golang.org/x/text v0.14.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index 91c80bb..fd1332d 100644 --- a/go.sum +++ b/go.sum @@ -40,6 +40,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 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= +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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -155,6 +157,8 @@ github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ 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/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= @@ -178,6 +182,8 @@ 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= @@ -229,6 +235,10 @@ github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6g 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.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY= @@ -416,6 +426,7 @@ golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7w 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-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= @@ -455,6 +466,7 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3 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= diff --git a/internal/cli/transfer.go b/internal/cli/transfer.go index 0ad67f1..e073864 100644 --- a/internal/cli/transfer.go +++ b/internal/cli/transfer.go @@ -24,6 +24,7 @@ import ( "github.com/spf13/viper" "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" ) @@ -84,7 +85,7 @@ func (c *TransferCmd[E, I, R]) resolveBackends(source string, target string) err } func (c *TransferCmd[E, I, R]) Transfer(exp backends.ExportProcessor[R], imp backends.ImportProcessor[R]) error { - fmt.Printf("Transferring %s from %s to %s...\n", c.entity, c.sourceName, c.targetName) + fmt.Println(i18n.Tr("Transferring %s from %s to %s...", c.entity, c.sourceName, c.targetName)) // Authenticate backends, if needed config := viper.GetViper() @@ -103,7 +104,7 @@ func (c *TransferCmd[E, I, R]) Transfer(exp backends.ExportProcessor[R], imp bac if err != nil { return err } - fmt.Printf("From timestamp: %v (%v)\n", timestamp, timestamp.Unix()) + fmt.Println(i18n.Tr("From timestamp: %v (%v)", timestamp, timestamp.Unix())) // Prepare progress bars exportProgress := make(chan models.Progress) @@ -123,11 +124,12 @@ func (c *TransferCmd[E, I, R]) Transfer(exp backends.ExportProcessor[R], imp bac wg.Wait() progress.Wait() if result.Error != nil { - fmt.Printf("Import failed, last reported timestamp was %v (%v)\n", result.LastTimestamp, result.LastTimestamp.Unix()) + fmt.Println(i18n.Tr("Import failed, last reported timestamp was %v (%v)", + result.LastTimestamp, result.LastTimestamp.Unix())) return result.Error } - fmt.Printf("Imported %v of %v %s into %v.\n", - result.ImportCount, result.TotalCount, c.entity, c.targetName) + fmt.Println(i18n.Tr("Imported %v of %v %s into %v.", + result.ImportCount, result.TotalCount, c.entity, c.targetName)) // Update timestamp err = c.updateTimestamp(result, timestamp) @@ -137,9 +139,10 @@ func (c *TransferCmd[E, I, R]) Transfer(exp backends.ExportProcessor[R], imp bac // Print errors if len(result.ImportErrors) > 0 { - fmt.Printf("\nDuring the import the following errors occurred:\n") + fmt.Println() + fmt.Println(i18n.Tr("During the import the following errors occurred:")) for _, err := range result.ImportErrors { - fmt.Printf("Error: %v\n", err) + fmt.Println(i18n.Tr("Error: %v\n", err)) } } @@ -159,7 +162,7 @@ func (c *TransferCmd[E, I, R]) updateTimestamp(result models.ImportResult, oldTi if result.LastTimestamp.Unix() < oldTimestamp.Unix() { result.LastTimestamp = oldTimestamp } - fmt.Printf("Latest timestamp: %v (%v)\n", result.LastTimestamp, result.LastTimestamp.Unix()) + fmt.Println(i18n.Tr("Latest timestamp: %v (%v)\n", result.LastTimestamp, result.LastTimestamp.Unix())) err := c.db.SetImportTimestamp(c.sourceName, c.targetName, c.entity, result.LastTimestamp) return err } diff --git a/internal/i18n/i18n.go b/internal/i18n/i18n.go new file mode 100644 index 0000000..cbfd516 --- /dev/null +++ b/internal/i18n/i18n.go @@ -0,0 +1,39 @@ +/* +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 i18n + +import ( + "log" + + "github.com/Xuanwo/go-locale" + _ "go.uploadedlobster.com/scotty/internal/translations" + + "golang.org/x/text/message" +) + +var localizer Localizer + +func init() { + tag, err := locale.Detect() + if err != nil { + log.Fatal(err) + } + localizer = New(tag) +} + +func Tr(key message.Reference, args ...interface{}) string { + return localizer.Translate(key, args...) +} diff --git a/internal/i18n/localizer.go b/internal/i18n/localizer.go new file mode 100644 index 0000000..7e839a2 --- /dev/null +++ b/internal/i18n/localizer.go @@ -0,0 +1,37 @@ +/* +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 i18n + +import ( + "golang.org/x/text/language" + "golang.org/x/text/message" +) + +type Localizer struct { + printer *message.Printer +} + +// Create a new Localizer for a language tag +func New(lang language.Tag) Localizer { + return Localizer{ + printer: message.NewPrinter(lang), + } +} + +// Return the translated string, with variables replaced. +func (l Localizer) Translate(key message.Reference, args ...interface{}) string { + return l.printer.Sprintf(key, args...) +} diff --git a/internal/translations/catalog.go b/internal/translations/catalog.go new file mode 100644 index 0000000..276eb87 --- /dev/null +++ b/internal/translations/catalog.go @@ -0,0 +1,75 @@ +// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT. + +package translations + +import ( + "golang.org/x/text/language" + "golang.org/x/text/message" + "golang.org/x/text/message/catalog" +) + +type dictionary struct { + index []uint32 + data string +} + +func (d *dictionary) Lookup(key string) (data string, ok bool) { + p, ok := messageKeyToIndex[key] + if !ok { + return "", false + } + start, end := d.index[p], d.index[p+1] + if start == end { + return "", false + } + return d.data[start:end], true +} + +func init() { + dict := map[string]catalog.Dictionary{ + "de": &dictionary{index: deIndex, data: deData}, + "en": &dictionary{index: enIndex, data: enData}, + } + fallback := language.MustParse("en") + cat, err := catalog.NewFromMap(dict, catalog.Fallback(fallback)) + if err != nil { + panic(err) + } + message.DefaultCatalog = cat +} + +var messageKeyToIndex = map[string]int{ + "During the import the following errors occurred:": 4, + "Error: %v\n": 5, + "From timestamp: %v (%v)": 1, + "Import failed, last reported timestamp was %v (%v)": 2, + "Imported %v of %v %s into %v.": 3, + "Latest timestamp: %v (%v)\n": 6, + "Transferring %s from %s to %s...": 0, +} + +var deIndex = []uint32{ // 8 elements + 0x00000000, 0x00000029, 0x00000047, 0x00000087, + 0x000000b2, 0x000000e9, 0x000000fc, 0x00000125, +} // Size: 56 bytes + +const deData string = "" + // Size: 293 bytes + "\x02Übertrage %[1]s von %[2]s nach %[3]s...\x02Ab Zeitstempel: %[1]v (%[" + + "2]v)\x02Import fehlgeschlagen, der letzte Zeitstempel war %[1]v (%[2]v)" + + "\x02%[1]v von %[2]v %[3]s in %[4]v importiert.\x02Während des Imports si" + + "nd folgende Fehler aufgetreten:\x04\x00\x01\x0a\x0e\x02Fehler: %[1]v\x04" + + "\x00\x01\x0a$\x02Neuester Zeitstempel: %[1]v (%[2]v)" + +var enIndex = []uint32{ // 8 elements + 0x00000000, 0x0000002a, 0x00000048, 0x00000081, + 0x000000ab, 0x000000dc, 0x000000ee, 0x00000113, +} // Size: 56 bytes + +const enData string = "" + // Size: 275 bytes + "\x02Transferring %[1]s from %[2]s to %[3]s...\x02From timestamp: %[1]v (" + + "%[2]v)\x02Import failed, last reported timestamp was %[1]v (%[2]v)\x02Im" + + "ported %[1]v of %[2]v %[3]s into %[4]v.\x02During the import the followi" + + "ng errors occurred:\x04\x00\x01\x0a\x0d\x02Error: %[1]v\x04\x00\x01\x0a " + + "\x02Latest timestamp: %[1]v (%[2]v)" + + // Total table size 680 bytes (0KiB); checksum: D0CBF9F5 diff --git a/internal/translations/locales/de/messages.gotext.json b/internal/translations/locales/de/messages.gotext.json new file mode 100644 index 0000000..7e8940e --- /dev/null +++ b/internal/translations/locales/de/messages.gotext.json @@ -0,0 +1,164 @@ +{ + "language": "de", + "messages": [ + { + "id": "Transferring {Entity} from {SourceName} to {TargetName}...", + "message": "Transferring {Entity} from {SourceName} to {TargetName}...", + "translation": "Übertrage {Entity} von {SourceName} nach {TargetName}...", + "placeholders": [ + { + "id": "Entity", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "c.entity" + }, + { + "id": "SourceName", + "string": "%[2]s", + "type": "string", + "underlyingType": "string", + "argNum": 2, + "expr": "c.sourceName" + }, + { + "id": "TargetName", + "string": "%[3]s", + "type": "string", + "underlyingType": "string", + "argNum": 3, + "expr": "c.targetName" + } + ] + }, + { + "id": "From timestamp: {Timestamp} ({Unix})", + "message": "From timestamp: {Timestamp} ({Unix})", + "translation": "Ab Zeitstempel: {Timestamp} ({Unix})", + "placeholders": [ + { + "id": "Timestamp", + "string": "%[1]v", + "type": "time.Time", + "underlyingType": "struct{wall uint64; ext int64; loc *time.Location}", + "argNum": 1, + "expr": "timestamp" + }, + { + "id": "Unix", + "string": "%[2]v", + "type": "int64", + "underlyingType": "int64", + "argNum": 2, + "expr": "timestamp.Unix()" + } + ] + }, + { + "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})", + "placeholders": [ + { + "id": "LastTimestamp", + "string": "%[1]v", + "type": "time.Time", + "underlyingType": "struct{wall uint64; ext int64; loc *time.Location}", + "argNum": 1, + "expr": "result.LastTimestamp" + }, + { + "id": "Unix", + "string": "%[2]v", + "type": "int64", + "underlyingType": "int64", + "argNum": 2, + "expr": "result.LastTimestamp.Unix()" + } + ] + }, + { + "id": "Imported {ImportCount} of {TotalCount} {Entity} into {TargetName}.", + "message": "Imported {ImportCount} of {TotalCount} {Entity} into {TargetName}.", + "translation": "{ImportCount} von {TotalCount} {Entity} in {TargetName} importiert.", + "placeholders": [ + { + "id": "ImportCount", + "string": "%[1]v", + "type": "int", + "underlyingType": "int", + "argNum": 1, + "expr": "result.ImportCount" + }, + { + "id": "TotalCount", + "string": "%[2]v", + "type": "int", + "underlyingType": "int", + "argNum": 2, + "expr": "result.TotalCount" + }, + { + "id": "Entity", + "string": "%[3]s", + "type": "string", + "underlyingType": "string", + "argNum": 3, + "expr": "c.entity" + }, + { + "id": "TargetName", + "string": "%[4]v", + "type": "string", + "underlyingType": "string", + "argNum": 4, + "expr": "c.targetName" + } + ] + }, + { + "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": "Error: {Err}", + "message": "Error: {Err}", + "translation": "Fehler: {Err}", + "placeholders": [ + { + "id": "Err", + "string": "%[1]v", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "err" + } + ] + }, + { + "id": "Latest timestamp: {LastTimestamp} ({Unix})", + "message": "Latest timestamp: {LastTimestamp} ({Unix})", + "translation": "Neuester Zeitstempel: {LastTimestamp} ({Unix})", + "placeholders": [ + { + "id": "LastTimestamp", + "string": "%[1]v", + "type": "time.Time", + "underlyingType": "struct{wall uint64; ext int64; loc *time.Location}", + "argNum": 1, + "expr": "result.LastTimestamp" + }, + { + "id": "Unix", + "string": "%[2]v", + "type": "int64", + "underlyingType": "int64", + "argNum": 2, + "expr": "result.LastTimestamp.Unix()" + } + ] + } + ] +} diff --git a/internal/translations/locales/de/out.gotext.json b/internal/translations/locales/de/out.gotext.json new file mode 100644 index 0000000..6788b8a --- /dev/null +++ b/internal/translations/locales/de/out.gotext.json @@ -0,0 +1,164 @@ +{ + "language": "de", + "messages": [ + { + "id": "Transferring {Entity} from {SourceName} to {TargetName}...", + "message": "Transferring {Entity} from {SourceName} to {TargetName}...", + "translation": "Übertrage {Entity} von {SourceName} nach {TargetName}...", + "placeholders": [ + { + "id": "Entity", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "c.entity" + }, + { + "id": "SourceName", + "string": "%[2]s", + "type": "string", + "underlyingType": "string", + "argNum": 2, + "expr": "c.sourceName" + }, + { + "id": "TargetName", + "string": "%[3]s", + "type": "string", + "underlyingType": "string", + "argNum": 3, + "expr": "c.targetName" + } + ] + }, + { + "id": "From timestamp: {Timestamp} ({Unix})", + "message": "From timestamp: {Timestamp} ({Unix})", + "translation": "Ab Zeitstempel: {Timestamp} ({Unix})", + "placeholders": [ + { + "id": "Timestamp", + "string": "%[1]v", + "type": "time.Time", + "underlyingType": "struct{wall uint64; ext int64; loc *time.Location}", + "argNum": 1, + "expr": "timestamp" + }, + { + "id": "Unix", + "string": "%[2]v", + "type": "int64", + "underlyingType": "int64", + "argNum": 2, + "expr": "timestamp.Unix()" + } + ] + }, + { + "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})", + "placeholders": [ + { + "id": "LastTimestamp", + "string": "%[1]v", + "type": "time.Time", + "underlyingType": "struct{wall uint64; ext int64; loc *time.Location}", + "argNum": 1, + "expr": "result.LastTimestamp" + }, + { + "id": "Unix", + "string": "%[2]v", + "type": "int64", + "underlyingType": "int64", + "argNum": 2, + "expr": "result.LastTimestamp.Unix()" + } + ] + }, + { + "id": "Imported {ImportCount} of {TotalCount} {Entity} into {TargetName}.", + "message": "Imported {ImportCount} of {TotalCount} {Entity} into {TargetName}.", + "translation": "{ImportCount} von {TotalCount} {Entity} in {TargetName} importiert.", + "placeholders": [ + { + "id": "ImportCount", + "string": "%[1]v", + "type": "int", + "underlyingType": "int", + "argNum": 1, + "expr": "result.ImportCount" + }, + { + "id": "TotalCount", + "string": "%[2]v", + "type": "int", + "underlyingType": "int", + "argNum": 2, + "expr": "result.TotalCount" + }, + { + "id": "Entity", + "string": "%[3]s", + "type": "string", + "underlyingType": "string", + "argNum": 3, + "expr": "c.entity" + }, + { + "id": "TargetName", + "string": "%[4]v", + "type": "string", + "underlyingType": "string", + "argNum": 4, + "expr": "c.targetName" + } + ] + }, + { + "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": "Error: {Err}", + "message": "Error: {Err}", + "translation": "Fehler: {Err}", + "placeholders": [ + { + "id": "Err", + "string": "%[1]v", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "err" + } + ] + }, + { + "id": "Latest timestamp: {LastTimestamp} ({Unix})", + "message": "Latest timestamp: {LastTimestamp} ({Unix})", + "translation": "Neuester Zeitstempel: {LastTimestamp} ({Unix})", + "placeholders": [ + { + "id": "LastTimestamp", + "string": "%[1]v", + "type": "time.Time", + "underlyingType": "struct{wall uint64; ext int64; loc *time.Location}", + "argNum": 1, + "expr": "result.LastTimestamp" + }, + { + "id": "Unix", + "string": "%[2]v", + "type": "int64", + "underlyingType": "int64", + "argNum": 2, + "expr": "result.LastTimestamp.Unix()" + } + ] + } + ] +} \ No newline at end of file diff --git a/internal/translations/locales/en/out.gotext.json b/internal/translations/locales/en/out.gotext.json new file mode 100644 index 0000000..44e776d --- /dev/null +++ b/internal/translations/locales/en/out.gotext.json @@ -0,0 +1,178 @@ +{ + "language": "en", + "messages": [ + { + "id": "Transferring {Entity} from {SourceName} to {TargetName}...", + "message": "Transferring {Entity} from {SourceName} to {TargetName}...", + "translation": "Transferring {Entity} from {SourceName} to {TargetName}...", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Entity", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "c.entity" + }, + { + "id": "SourceName", + "string": "%[2]s", + "type": "string", + "underlyingType": "string", + "argNum": 2, + "expr": "c.sourceName" + }, + { + "id": "TargetName", + "string": "%[3]s", + "type": "string", + "underlyingType": "string", + "argNum": 3, + "expr": "c.targetName" + } + ], + "fuzzy": true + }, + { + "id": "From timestamp: {Timestamp} ({Unix})", + "message": "From timestamp: {Timestamp} ({Unix})", + "translation": "From timestamp: {Timestamp} ({Unix})", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Timestamp", + "string": "%[1]v", + "type": "time.Time", + "underlyingType": "struct{wall uint64; ext int64; loc *time.Location}", + "argNum": 1, + "expr": "timestamp" + }, + { + "id": "Unix", + "string": "%[2]v", + "type": "int64", + "underlyingType": "int64", + "argNum": 2, + "expr": "timestamp.Unix()" + } + ], + "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})", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "LastTimestamp", + "string": "%[1]v", + "type": "time.Time", + "underlyingType": "struct{wall uint64; ext int64; loc *time.Location}", + "argNum": 1, + "expr": "result.LastTimestamp" + }, + { + "id": "Unix", + "string": "%[2]v", + "type": "int64", + "underlyingType": "int64", + "argNum": 2, + "expr": "result.LastTimestamp.Unix()" + } + ], + "fuzzy": true + }, + { + "id": "Imported {ImportCount} of {TotalCount} {Entity} into {TargetName}.", + "message": "Imported {ImportCount} of {TotalCount} {Entity} into {TargetName}.", + "translation": "Imported {ImportCount} of {TotalCount} {Entity} into {TargetName}.", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "ImportCount", + "string": "%[1]v", + "type": "int", + "underlyingType": "int", + "argNum": 1, + "expr": "result.ImportCount" + }, + { + "id": "TotalCount", + "string": "%[2]v", + "type": "int", + "underlyingType": "int", + "argNum": 2, + "expr": "result.TotalCount" + }, + { + "id": "Entity", + "string": "%[3]s", + "type": "string", + "underlyingType": "string", + "argNum": 3, + "expr": "c.entity" + }, + { + "id": "TargetName", + "string": "%[4]v", + "type": "string", + "underlyingType": "string", + "argNum": 4, + "expr": "c.targetName" + } + ], + "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:", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Error: {Err}", + "message": "Error: {Err}", + "translation": "Error: {Err}", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Err", + "string": "%[1]v", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "err" + } + ], + "fuzzy": true + }, + { + "id": "Latest timestamp: {LastTimestamp} ({Unix})", + "message": "Latest timestamp: {LastTimestamp} ({Unix})", + "translation": "Latest timestamp: {LastTimestamp} ({Unix})", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "LastTimestamp", + "string": "%[1]v", + "type": "time.Time", + "underlyingType": "struct{wall uint64; ext int64; loc *time.Location}", + "argNum": 1, + "expr": "result.LastTimestamp" + }, + { + "id": "Unix", + "string": "%[2]v", + "type": "int64", + "underlyingType": "int64", + "argNum": 2, + "expr": "result.LastTimestamp.Unix()" + } + ], + "fuzzy": true + } + ] +} \ No newline at end of file diff --git a/internal/translations/translations.go b/internal/translations/translations.go new file mode 100644 index 0000000..76173f6 --- /dev/null +++ b/internal/translations/translations.go @@ -0,0 +1,18 @@ +/* +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 translations + +//go:generate gotext -srclang=en update -out=catalog.go -lang=en,de go.uploadedlobster.com/scotty From 1f3247c3ff45a5165146b00693f951af1decdb2d Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sat, 9 Dec 2023 12:01:04 +0100 Subject: [PATCH 040/150] Translate progress bar --- internal/cli/progress.go | 7 +- internal/translations/catalog.go | 19 +- .../translations/locales/de/out.gotext.json | 15 ++ .../locales/en/messages.gotext.json | 199 ++++++++++++++++++ .../translations/locales/en/out.gotext.json | 21 ++ 5 files changed, 251 insertions(+), 10 deletions(-) create mode 100644 internal/translations/locales/en/messages.gotext.json diff --git a/internal/cli/progress.go b/internal/cli/progress.go index 457383c..7b4ad17 100644 --- a/internal/cli/progress.go +++ b/internal/cli/progress.go @@ -24,6 +24,7 @@ import ( "github.com/fatih/color" "github.com/vbauerster/mpb/v8" "github.com/vbauerster/mpb/v8/decor" + "go.uploadedlobster.com/scotty/internal/i18n" "go.uploadedlobster.com/scotty/internal/models" ) @@ -35,8 +36,8 @@ func progressBar(wg *sync.WaitGroup, exportProgress chan models.Progress, import mpb.WithAutoRefresh(), ) - exportBar := setupProgressBar(p, "exporting") - importBar := setupProgressBar(p, "importing") + exportBar := setupProgressBar(p, i18n.Tr("exporting")) + importBar := setupProgressBar(p, i18n.Tr("importing")) go updateProgressBar(exportBar, wg, exportProgress) go updateProgressBar(importBar, wg, importProgress) @@ -58,7 +59,7 @@ func setupProgressBar(p *mpb.Progress, name string) *mpb.Bar { mpb.AppendDecorators( decor.OnComplete( decor.EwmaETA(decor.ET_STYLE_GO, 0, decor.WC{C: decor.DSyncWidth}), - "done", + i18n.Tr("done"), ), // decor.OnComplete(decor.Percentage(decor.WC{W: 5, C: decor.DSyncWidthR}), "done"), decor.Name(" "), diff --git a/internal/translations/catalog.go b/internal/translations/catalog.go index 276eb87..d9ac879 100644 --- a/internal/translations/catalog.go +++ b/internal/translations/catalog.go @@ -46,12 +46,16 @@ var messageKeyToIndex = map[string]int{ "Imported %v of %v %s into %v.": 3, "Latest timestamp: %v (%v)\n": 6, "Transferring %s from %s to %s...": 0, + "done": 9, + "exporting": 7, + "importing": 8, } -var deIndex = []uint32{ // 8 elements +var deIndex = []uint32{ // 11 elements 0x00000000, 0x00000029, 0x00000047, 0x00000087, 0x000000b2, 0x000000e9, 0x000000fc, 0x00000125, -} // Size: 56 bytes + 0x00000125, 0x00000125, 0x00000125, +} // Size: 68 bytes const deData string = "" + // Size: 293 bytes "\x02Übertrage %[1]s von %[2]s nach %[3]s...\x02Ab Zeitstempel: %[1]v (%[" + @@ -60,16 +64,17 @@ const deData string = "" + // Size: 293 bytes "nd folgende Fehler aufgetreten:\x04\x00\x01\x0a\x0e\x02Fehler: %[1]v\x04" + "\x00\x01\x0a$\x02Neuester Zeitstempel: %[1]v (%[2]v)" -var enIndex = []uint32{ // 8 elements +var enIndex = []uint32{ // 11 elements 0x00000000, 0x0000002a, 0x00000048, 0x00000081, 0x000000ab, 0x000000dc, 0x000000ee, 0x00000113, -} // Size: 56 bytes + 0x0000011d, 0x00000127, 0x0000012c, +} // Size: 68 bytes -const enData string = "" + // Size: 275 bytes +const enData string = "" + // Size: 300 bytes "\x02Transferring %[1]s from %[2]s to %[3]s...\x02From timestamp: %[1]v (" + "%[2]v)\x02Import failed, last reported timestamp was %[1]v (%[2]v)\x02Im" + "ported %[1]v of %[2]v %[3]s into %[4]v.\x02During the import the followi" + "ng errors occurred:\x04\x00\x01\x0a\x0d\x02Error: %[1]v\x04\x00\x01\x0a " + - "\x02Latest timestamp: %[1]v (%[2]v)" + "\x02Latest timestamp: %[1]v (%[2]v)\x02exporting\x02importing\x02done" - // Total table size 680 bytes (0KiB); checksum: D0CBF9F5 + // Total table size 729 bytes (0KiB); checksum: A7E71D11 diff --git a/internal/translations/locales/de/out.gotext.json b/internal/translations/locales/de/out.gotext.json index 6788b8a..ac41552 100644 --- a/internal/translations/locales/de/out.gotext.json +++ b/internal/translations/locales/de/out.gotext.json @@ -1,6 +1,21 @@ { "language": "de", "messages": [ + { + "id": "exporting", + "message": "exporting", + "translation": "" + }, + { + "id": "importing", + "message": "importing", + "translation": "" + }, + { + "id": "done", + "message": "done", + "translation": "" + }, { "id": "Transferring {Entity} from {SourceName} to {TargetName}...", "message": "Transferring {Entity} from {SourceName} to {TargetName}...", diff --git a/internal/translations/locales/en/messages.gotext.json b/internal/translations/locales/en/messages.gotext.json new file mode 100644 index 0000000..0b8d9c4 --- /dev/null +++ b/internal/translations/locales/en/messages.gotext.json @@ -0,0 +1,199 @@ +{ + "language": "en", + "messages": [ + { + "id": "exporting", + "message": "exporting", + "translation": "exporting", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "importing", + "message": "importing", + "translation": "importing", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "done", + "message": "done", + "translation": "done", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Transferring {Entity} from {SourceName} to {TargetName}...", + "message": "Transferring {Entity} from {SourceName} to {TargetName}...", + "translation": "Transferring {Entity} from {SourceName} to {TargetName}...", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Entity", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "c.entity" + }, + { + "id": "SourceName", + "string": "%[2]s", + "type": "string", + "underlyingType": "string", + "argNum": 2, + "expr": "c.sourceName" + }, + { + "id": "TargetName", + "string": "%[3]s", + "type": "string", + "underlyingType": "string", + "argNum": 3, + "expr": "c.targetName" + } + ], + "fuzzy": true + }, + { + "id": "From timestamp: {Timestamp} ({Unix})", + "message": "From timestamp: {Timestamp} ({Unix})", + "translation": "From timestamp: {Timestamp} ({Unix})", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Timestamp", + "string": "%[1]v", + "type": "time.Time", + "underlyingType": "struct{wall uint64; ext int64; loc *time.Location}", + "argNum": 1, + "expr": "timestamp" + }, + { + "id": "Unix", + "string": "%[2]v", + "type": "int64", + "underlyingType": "int64", + "argNum": 2, + "expr": "timestamp.Unix()" + } + ], + "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})", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "LastTimestamp", + "string": "%[1]v", + "type": "time.Time", + "underlyingType": "struct{wall uint64; ext int64; loc *time.Location}", + "argNum": 1, + "expr": "result.LastTimestamp" + }, + { + "id": "Unix", + "string": "%[2]v", + "type": "int64", + "underlyingType": "int64", + "argNum": 2, + "expr": "result.LastTimestamp.Unix()" + } + ], + "fuzzy": true + }, + { + "id": "Imported {ImportCount} of {TotalCount} {Entity} into {TargetName}.", + "message": "Imported {ImportCount} of {TotalCount} {Entity} into {TargetName}.", + "translation": "Imported {ImportCount} of {TotalCount} {Entity} into {TargetName}.", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "ImportCount", + "string": "%[1]v", + "type": "int", + "underlyingType": "int", + "argNum": 1, + "expr": "result.ImportCount" + }, + { + "id": "TotalCount", + "string": "%[2]v", + "type": "int", + "underlyingType": "int", + "argNum": 2, + "expr": "result.TotalCount" + }, + { + "id": "Entity", + "string": "%[3]s", + "type": "string", + "underlyingType": "string", + "argNum": 3, + "expr": "c.entity" + }, + { + "id": "TargetName", + "string": "%[4]v", + "type": "string", + "underlyingType": "string", + "argNum": 4, + "expr": "c.targetName" + } + ], + "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:", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Error: {Err}", + "message": "Error: {Err}", + "translation": "Error: {Err}", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Err", + "string": "%[1]v", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "err" + } + ], + "fuzzy": true + }, + { + "id": "Latest timestamp: {LastTimestamp} ({Unix})", + "message": "Latest timestamp: {LastTimestamp} ({Unix})", + "translation": "Latest timestamp: {LastTimestamp} ({Unix})", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "LastTimestamp", + "string": "%[1]v", + "type": "time.Time", + "underlyingType": "struct{wall uint64; ext int64; loc *time.Location}", + "argNum": 1, + "expr": "result.LastTimestamp" + }, + { + "id": "Unix", + "string": "%[2]v", + "type": "int64", + "underlyingType": "int64", + "argNum": 2, + "expr": "result.LastTimestamp.Unix()" + } + ], + "fuzzy": true + } + ] +} diff --git a/internal/translations/locales/en/out.gotext.json b/internal/translations/locales/en/out.gotext.json index 44e776d..651e329 100644 --- a/internal/translations/locales/en/out.gotext.json +++ b/internal/translations/locales/en/out.gotext.json @@ -1,6 +1,27 @@ { "language": "en", "messages": [ + { + "id": "exporting", + "message": "exporting", + "translation": "exporting", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "importing", + "message": "importing", + "translation": "importing", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "done", + "message": "done", + "translation": "done", + "translatorComment": "Copied from source.", + "fuzzy": true + }, { "id": "Transferring {Entity} from {SourceName} to {TargetName}...", "message": "Transferring {Entity} from {SourceName} to {TargetName}...", From 2006725311363c468915fa385842770ab23a7e8b Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sat, 9 Dec 2023 11:02:24 +0000 Subject: [PATCH 041/150] Translated using Weblate (German) Currently translated at 100.0% (10 of 10 strings) Translation: Scotty/app Translate-URL: https://translate.uploadedlobster.com/projects/scotty/app/de/ --- .../locales/de/messages.gotext.json | 333 ++++++++++-------- 1 file changed, 177 insertions(+), 156 deletions(-) diff --git a/internal/translations/locales/de/messages.gotext.json b/internal/translations/locales/de/messages.gotext.json index 7e8940e..a2cd62c 100644 --- a/internal/translations/locales/de/messages.gotext.json +++ b/internal/translations/locales/de/messages.gotext.json @@ -1,164 +1,185 @@ { - "language": "de", - "messages": [ - { - "id": "Transferring {Entity} from {SourceName} to {TargetName}...", - "message": "Transferring {Entity} from {SourceName} to {TargetName}...", - "translation": "Übertrage {Entity} von {SourceName} nach {TargetName}...", - "placeholders": [ + "language": "de", + "messages": [ { - "id": "Entity", - "string": "%[1]s", - "type": "string", - "underlyingType": "string", - "argNum": 1, - "expr": "c.entity" + "id": "Transferring {Entity} from {SourceName} to {TargetName}...", + "message": "Transferring {Entity} from {SourceName} to {TargetName}...", + "translation": "Übertrage {Entity} von {SourceName} nach {TargetName}...", + "placeholders": [ + { + "id": "Entity", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "c.entity" + }, + { + "id": "SourceName", + "string": "%[2]s", + "type": "string", + "underlyingType": "string", + "argNum": 2, + "expr": "c.sourceName" + }, + { + "id": "TargetName", + "string": "%[3]s", + "type": "string", + "underlyingType": "string", + "argNum": 3, + "expr": "c.targetName" + } + ] }, { - "id": "SourceName", - "string": "%[2]s", - "type": "string", - "underlyingType": "string", - "argNum": 2, - "expr": "c.sourceName" + "id": "From timestamp: {Timestamp} ({Unix})", + "message": "From timestamp: {Timestamp} ({Unix})", + "translation": "Ab Zeitstempel: {Timestamp} ({Unix})", + "placeholders": [ + { + "id": "Timestamp", + "string": "%[1]v", + "type": "time.Time", + "underlyingType": "struct{wall uint64; ext int64; loc *time.Location}", + "argNum": 1, + "expr": "timestamp" + }, + { + "id": "Unix", + "string": "%[2]v", + "type": "int64", + "underlyingType": "int64", + "argNum": 2, + "expr": "timestamp.Unix()" + } + ] }, { - "id": "TargetName", - "string": "%[3]s", - "type": "string", - "underlyingType": "string", - "argNum": 3, - "expr": "c.targetName" + "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})", + "placeholders": [ + { + "id": "LastTimestamp", + "string": "%[1]v", + "type": "time.Time", + "underlyingType": "struct{wall uint64; ext int64; loc *time.Location}", + "argNum": 1, + "expr": "result.LastTimestamp" + }, + { + "id": "Unix", + "string": "%[2]v", + "type": "int64", + "underlyingType": "int64", + "argNum": 2, + "expr": "result.LastTimestamp.Unix()" + } + ] + }, + { + "id": "Imported {ImportCount} of {TotalCount} {Entity} into {TargetName}.", + "message": "Imported {ImportCount} of {TotalCount} {Entity} into {TargetName}.", + "translation": "{ImportCount} von {TotalCount} {Entity} in {TargetName} importiert.", + "placeholders": [ + { + "id": "ImportCount", + "string": "%[1]v", + "type": "int", + "underlyingType": "int", + "argNum": 1, + "expr": "result.ImportCount" + }, + { + "id": "TotalCount", + "string": "%[2]v", + "type": "int", + "underlyingType": "int", + "argNum": 2, + "expr": "result.TotalCount" + }, + { + "id": "Entity", + "string": "%[3]s", + "type": "string", + "underlyingType": "string", + "argNum": 3, + "expr": "c.entity" + }, + { + "id": "TargetName", + "string": "%[4]v", + "type": "string", + "underlyingType": "string", + "argNum": 4, + "expr": "c.targetName" + } + ] + }, + { + "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": "Error: {Err}", + "message": "Error: {Err}", + "translation": "Fehler: {Err}", + "placeholders": [ + { + "id": "Err", + "string": "%[1]v", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "err" + } + ] + }, + { + "id": "Latest timestamp: {LastTimestamp} ({Unix})", + "message": "Latest timestamp: {LastTimestamp} ({Unix})", + "translation": "Neuester Zeitstempel: {LastTimestamp} ({Unix})", + "placeholders": [ + { + "id": "LastTimestamp", + "string": "%[1]v", + "type": "time.Time", + "underlyingType": "struct{wall uint64; ext int64; loc *time.Location}", + "argNum": 1, + "expr": "result.LastTimestamp" + }, + { + "id": "Unix", + "string": "%[2]v", + "type": "int64", + "underlyingType": "int64", + "argNum": 2, + "expr": "result.LastTimestamp.Unix()" + } + ] + }, + { + "id": "exporting", + "message": "exporting", + "translatorComment": "Copied from source.", + "fuzzy": true, + "translation": "exportiere" + }, + { + "id": "importing", + "message": "importing", + "translatorComment": "Copied from source.", + "fuzzy": true, + "translation": "importiere" + }, + { + "id": "done", + "message": "done", + "translatorComment": "Copied from source.", + "fuzzy": true, + "translation": "fertig" } - ] - }, - { - "id": "From timestamp: {Timestamp} ({Unix})", - "message": "From timestamp: {Timestamp} ({Unix})", - "translation": "Ab Zeitstempel: {Timestamp} ({Unix})", - "placeholders": [ - { - "id": "Timestamp", - "string": "%[1]v", - "type": "time.Time", - "underlyingType": "struct{wall uint64; ext int64; loc *time.Location}", - "argNum": 1, - "expr": "timestamp" - }, - { - "id": "Unix", - "string": "%[2]v", - "type": "int64", - "underlyingType": "int64", - "argNum": 2, - "expr": "timestamp.Unix()" - } - ] - }, - { - "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})", - "placeholders": [ - { - "id": "LastTimestamp", - "string": "%[1]v", - "type": "time.Time", - "underlyingType": "struct{wall uint64; ext int64; loc *time.Location}", - "argNum": 1, - "expr": "result.LastTimestamp" - }, - { - "id": "Unix", - "string": "%[2]v", - "type": "int64", - "underlyingType": "int64", - "argNum": 2, - "expr": "result.LastTimestamp.Unix()" - } - ] - }, - { - "id": "Imported {ImportCount} of {TotalCount} {Entity} into {TargetName}.", - "message": "Imported {ImportCount} of {TotalCount} {Entity} into {TargetName}.", - "translation": "{ImportCount} von {TotalCount} {Entity} in {TargetName} importiert.", - "placeholders": [ - { - "id": "ImportCount", - "string": "%[1]v", - "type": "int", - "underlyingType": "int", - "argNum": 1, - "expr": "result.ImportCount" - }, - { - "id": "TotalCount", - "string": "%[2]v", - "type": "int", - "underlyingType": "int", - "argNum": 2, - "expr": "result.TotalCount" - }, - { - "id": "Entity", - "string": "%[3]s", - "type": "string", - "underlyingType": "string", - "argNum": 3, - "expr": "c.entity" - }, - { - "id": "TargetName", - "string": "%[4]v", - "type": "string", - "underlyingType": "string", - "argNum": 4, - "expr": "c.targetName" - } - ] - }, - { - "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": "Error: {Err}", - "message": "Error: {Err}", - "translation": "Fehler: {Err}", - "placeholders": [ - { - "id": "Err", - "string": "%[1]v", - "type": "string", - "underlyingType": "string", - "argNum": 1, - "expr": "err" - } - ] - }, - { - "id": "Latest timestamp: {LastTimestamp} ({Unix})", - "message": "Latest timestamp: {LastTimestamp} ({Unix})", - "translation": "Neuester Zeitstempel: {LastTimestamp} ({Unix})", - "placeholders": [ - { - "id": "LastTimestamp", - "string": "%[1]v", - "type": "time.Time", - "underlyingType": "struct{wall uint64; ext int64; loc *time.Location}", - "argNum": 1, - "expr": "result.LastTimestamp" - }, - { - "id": "Unix", - "string": "%[2]v", - "type": "int64", - "underlyingType": "int64", - "argNum": 2, - "expr": "result.LastTimestamp.Unix()" - } - ] - } - ] + ] } From 7df08bfaaa8cd66acab0b796593eaa6aa94b29cb Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sat, 9 Dec 2023 12:07:09 +0100 Subject: [PATCH 042/150] Trigger weblate update on CI build --- .build.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.build.yml b/.build.yml index 1286795..8be4e81 100644 --- a/.build.yml +++ b/.build.yml @@ -3,8 +3,13 @@ packages: - go - goreleaser-bin - hut + - weblate-wlc +secrets: + - 2a17e258-3e99-4093-9527-832c350d9c53 oauth: pages.sr.ht/PAGES:RW tasks: + - weblate-update: | + wlc --format text pull scotty - test: | cd scotty go build -v . From 511b71b90989032607230a8805bca2fbd03ddae8 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sat, 9 Dec 2023 15:41:43 +0100 Subject: [PATCH 043/150] Documented translation and set translation license to CC0 --- README.md | 4 + internal/translations/COPYING | 121 ++++++++++++++++++ internal/translations/README.md | 34 +++++ internal/translations/catalog.go | 57 +++++---- .../translations/locales/de/out.gotext.json | 12 +- internal/translations/translations.go | 15 +-- 6 files changed, 200 insertions(+), 43 deletions(-) create mode 100644 internal/translations/COPYING create mode 100644 internal/translations/README.md diff --git a/README.md b/README.md index 12e69af..50eb0c7 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,8 @@ The source code for Scotty is available on [SourceHut](https://sr.ht/~phw/scotty Patches can be submitted to the mailing list [~phw/musicbrainz@lists.sr.ht](https://lists.sr.ht/~phw/musicbrainz). You can clone the repository directly on SourceHut and submit your changes with the "Prepare patchset" button. Please see SourceHut's [documentation for sending patches upstream](https://man.sr.ht/git.sr.ht/#sending-patches-upstream) for details. +You can help translate this project into your language with [Weblate](https://translate.uploadedlobster.com/projects/scotty/). Please request new languages on the mailing list [~phw/musicbrainz@lists.sr.ht](https://lists.sr.ht/~phw/musicbrainz). See [internal/translations/README.md](internal/translations/README.md) for details. + ## License @@ -75,3 +77,5 @@ You should have received a copy of the GNU General Public License along with Sco See [COPYING](./COPYING) for details. Some source files in Scotty are licensed under the MIT license. Please see the license notice in the headers of the individual files for more information. + +All user interface strings and their translations are published under the conditions of [CC0 1.0 Universal (CC0 1.0)](https://creativecommons.org/publicdomain/zero/1.0/). diff --git a/internal/translations/COPYING b/internal/translations/COPYING new file mode 100644 index 0000000..0e259d4 --- /dev/null +++ b/internal/translations/COPYING @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/internal/translations/README.md b/internal/translations/README.md new file mode 100644 index 0000000..aa302aa --- /dev/null +++ b/internal/translations/README.md @@ -0,0 +1,34 @@ +# Scotty translation + +This package provides the translation files for [Scotty](https://sr.ht/~phw/scotty/). + + +## Contribute + +You can help translate this project into your language with [Weblate](https://translate.uploadedlobster.com/projects/scotty/). If there are missing or wrong translations in your language you can directly improve the translations there. + +If your language is not yet available on Weblate please request the language to be added. Use the mailing list [~phw/musicbrainz@lists.sr.ht](https://lists.sr.ht/~phw/musicbrainz) or [Start new translation](https://translate.uploadedlobster.com/new-lang/scotty/app/) in Weblate to do so. + +It is also possible to directly edit the `messages.gotext.json` file located in the `locales/[language-code]` directories. If you do so please send the modified file or a git patch to the mailing list. *Please do not use the `out.gotext.json` files.* + + +## Maintenance + +After strings have been added or modified in the code or after changes have been made to the `messages.gotext.json` files the new strings need to be extracted and changed translations need to be merged. + +This requires the gotext tool to be installed: + +``` +go install golang.org/x/text/cmd/gotext@latest +``` + +The following command will extract all strings and merge the translations: + +``` +go generate ./internal/translations/translations.go +``` + + +## License + +All Scotty user interface strings and their translations included in this package are published under the conditions of [CC0 1.0 Universal (CC0 1.0)](https://creativecommons.org/publicdomain/zero/1.0/). diff --git a/internal/translations/catalog.go b/internal/translations/catalog.go index d9ac879..797ce2f 100644 --- a/internal/translations/catalog.go +++ b/internal/translations/catalog.go @@ -39,42 +39,43 @@ func init() { } var messageKeyToIndex = map[string]int{ - "During the import the following errors occurred:": 4, - "Error: %v\n": 5, - "From timestamp: %v (%v)": 1, - "Import failed, last reported timestamp was %v (%v)": 2, - "Imported %v of %v %s into %v.": 3, - "Latest timestamp: %v (%v)\n": 6, - "Transferring %s from %s to %s...": 0, - "done": 9, - "exporting": 7, - "importing": 8, + "During the import the following errors occurred:": 7, + "Error: %v\n": 8, + "From timestamp: %v (%v)": 4, + "Import failed, last reported timestamp was %v (%v)": 5, + "Imported %v of %v %s into %v.": 6, + "Latest timestamp: %v (%v)\n": 9, + "Transferring %s from %s to %s...": 3, + "done": 2, + "exporting": 0, + "importing": 1, } var deIndex = []uint32{ // 11 elements - 0x00000000, 0x00000029, 0x00000047, 0x00000087, - 0x000000b2, 0x000000e9, 0x000000fc, 0x00000125, - 0x00000125, 0x00000125, 0x00000125, + 0x00000000, 0x0000000b, 0x00000016, 0x0000001d, + 0x00000046, 0x00000064, 0x000000a4, 0x000000cf, + 0x00000106, 0x00000119, 0x00000142, } // Size: 68 bytes -const deData string = "" + // Size: 293 bytes - "\x02Übertrage %[1]s von %[2]s nach %[3]s...\x02Ab Zeitstempel: %[1]v (%[" + - "2]v)\x02Import fehlgeschlagen, der letzte Zeitstempel war %[1]v (%[2]v)" + - "\x02%[1]v von %[2]v %[3]s in %[4]v importiert.\x02Während des Imports si" + - "nd folgende Fehler aufgetreten:\x04\x00\x01\x0a\x0e\x02Fehler: %[1]v\x04" + - "\x00\x01\x0a$\x02Neuester Zeitstempel: %[1]v (%[2]v)" +const deData string = "" + // Size: 322 bytes + "\x02exportiere\x02importiere\x02fertig\x02Übertrage %[1]s von %[2]s nach" + + " %[3]s...\x02Ab Zeitstempel: %[1]v (%[2]v)\x02Import fehlgeschlagen, der" + + " letzte Zeitstempel war %[1]v (%[2]v)\x02%[1]v von %[2]v %[3]s in %[4]v " + + "importiert.\x02Während des Imports sind folgende Fehler aufgetreten:\x04" + + "\x00\x01\x0a\x0e\x02Fehler: %[1]v\x04\x00\x01\x0a$\x02Neuester Zeitstemp" + + "el: %[1]v (%[2]v)" var enIndex = []uint32{ // 11 elements - 0x00000000, 0x0000002a, 0x00000048, 0x00000081, - 0x000000ab, 0x000000dc, 0x000000ee, 0x00000113, - 0x0000011d, 0x00000127, 0x0000012c, + 0x00000000, 0x0000000a, 0x00000014, 0x00000019, + 0x00000043, 0x00000061, 0x0000009a, 0x000000c4, + 0x000000f5, 0x00000107, 0x0000012c, } // Size: 68 bytes const enData string = "" + // Size: 300 bytes - "\x02Transferring %[1]s from %[2]s to %[3]s...\x02From timestamp: %[1]v (" + - "%[2]v)\x02Import failed, last reported timestamp was %[1]v (%[2]v)\x02Im" + - "ported %[1]v of %[2]v %[3]s into %[4]v.\x02During the import the followi" + - "ng errors occurred:\x04\x00\x01\x0a\x0d\x02Error: %[1]v\x04\x00\x01\x0a " + - "\x02Latest timestamp: %[1]v (%[2]v)\x02exporting\x02importing\x02done" + "\x02exporting\x02importing\x02done\x02Transferring %[1]s from %[2]s to %" + + "[3]s...\x02From timestamp: %[1]v (%[2]v)\x02Import failed, last reported" + + " timestamp was %[1]v (%[2]v)\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\x04\x00\x01\x0a \x02Latest timestamp: %[1]v (%[2]v)" - // Total table size 729 bytes (0KiB); checksum: A7E71D11 + // Total table size 758 bytes (0KiB); checksum: D85383CE diff --git a/internal/translations/locales/de/out.gotext.json b/internal/translations/locales/de/out.gotext.json index ac41552..321d448 100644 --- a/internal/translations/locales/de/out.gotext.json +++ b/internal/translations/locales/de/out.gotext.json @@ -4,17 +4,23 @@ { "id": "exporting", "message": "exporting", - "translation": "" + "translation": "exportiere", + "translatorComment": "Copied from source.", + "fuzzy": true }, { "id": "importing", "message": "importing", - "translation": "" + "translation": "importiere", + "translatorComment": "Copied from source.", + "fuzzy": true }, { "id": "done", "message": "done", - "translation": "" + "translation": "fertig", + "translatorComment": "Copied from source.", + "fuzzy": true }, { "id": "Transferring {Entity} from {SourceName} to {TargetName}...", diff --git a/internal/translations/translations.go b/internal/translations/translations.go index 76173f6..c555d32 100644 --- a/internal/translations/translations.go +++ b/internal/translations/translations.go @@ -1,16 +1,7 @@ /* -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 . +All Scotty user interface strings and their translations included in this +package are published under the conditions of CC0 1.0 Universal (CC0 1.0) +. */ package translations From d6ca8d33f7607046776cf436c21ecb17012ab12e Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sat, 9 Dec 2023 16:43:14 +0100 Subject: [PATCH 044/150] Mark user interface strings For now exclude command help, as cobra itself is not localizable yet. --- cmd/backends.go | 5 +- cmd/beam.go | 3 +- cmd/root.go | 10 +- cmd/service_add.go | 9 +- cmd/service_auth.go | 19 +- cmd/service_delete.go | 9 +- cmd/service_edit.go | 5 +- cmd/service_list.go | 3 +- internal/auth/callback.go | 4 +- internal/backends/backends.go | 5 +- internal/backends/deezer/deezer.go | 5 +- internal/backends/funkwhale/funkwhale.go | 7 +- internal/backends/jspf/jspf.go | 9 +- internal/backends/lastfm/lastfm.go | 7 +- .../backends/listenbrainz/listenbrainz.go | 5 +- internal/backends/maloja/maloja.go | 7 +- .../backends/scrobblerlog/scrobblerlog.go | 7 +- internal/backends/spotify/spotify.go | 5 +- internal/backends/subsonic/subsonic.go | 7 +- internal/cli/prompt.go | 7 +- internal/cli/services.go | 7 +- internal/config/config.go | 5 +- internal/config/services.go | 3 +- internal/translations/catalog.go | 98 ++++- .../locales/de/messages.gotext.json | 97 +++- .../translations/locales/de/out.gotext.json | 339 ++++++++++++++ .../translations/locales/en/out.gotext.json | 413 ++++++++++++++++++ 27 files changed, 1005 insertions(+), 95 deletions(-) diff --git a/cmd/backends.go b/cmd/backends.go index 43cae08..bf5c7d3 100644 --- a/cmd/backends.go +++ b/cmd/backends.go @@ -22,6 +22,7 @@ import ( "github.com/spf13/cobra" "go.uploadedlobster.com/scotty/internal/backends" + "go.uploadedlobster.com/scotty/internal/i18n" ) var backendsCmd = &cobra.Command{ @@ -32,8 +33,8 @@ var backendsCmd = &cobra.Command{ backends := backends.GetBackends() for _, info := range backends { fmt.Printf("%s:\n", info.Name) - fmt.Printf("\texport: %s\n", strings.Join(info.ExportCapabilities, ", ")) - fmt.Printf("\timport: %s\n\n", strings.Join(info.ImportCapabilities, ", ")) + fmt.Println(i18n.Tr("\texport: %s", strings.Join(info.ExportCapabilities, ", "))) + fmt.Println(i18n.Tr("\timport: %s\n", strings.Join(info.ImportCapabilities, ", "))) } }, } diff --git a/cmd/beam.go b/cmd/beam.go index 51dd1c5..83d51db 100644 --- a/cmd/beam.go +++ b/cmd/beam.go @@ -26,7 +26,8 @@ var beamCmd = &cobra.Command{ Long: `Transfers data (listens, loves) between two configured services. The services must be configured and be able to handle export and import of -the data.`, +the data. See "scotty backends" for a list of backends and their supported +features.`, // Run: func(cmd *cobra.Command, args []string) { }, } diff --git a/cmd/root.go b/cmd/root.go index 8b4fdf8..50ada8f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -21,8 +21,8 @@ import ( "os" "github.com/spf13/cobra" - "github.com/spf13/viper" "go.uploadedlobster.com/scotty/internal/config" + "go.uploadedlobster.com/scotty/internal/i18n" "go.uploadedlobster.com/scotty/internal/version" ) @@ -32,8 +32,8 @@ var cfgFile string var rootCmd = &cobra.Command{ Use: version.AppName, Short: "Beam data between music listening services", - Long: `Scotty transfers your listens/scrobbles between ListenBrainz and -various other listening and streaming services.`, + Long: `Scotty transfers listens and loves between different listening and streaming +services. Run "scotty backends" for a list of supported service backends.`, Version: version.AppVersion, // Uncomment the following line if your bare application // has an action associated with it: @@ -69,8 +69,6 @@ func init() { func initConfig() { // If a config file is found, read it in. if err := config.InitConfig(cfgFile); err != nil { - fmt.Fprintln(os.Stderr, "Failed reading config:", err) - } else { - fmt.Println("Using config file:", viper.ConfigFileUsed()) + fmt.Fprintln(os.Stderr, i18n.Tr("Failed reading config: %v", err)) } } diff --git a/cmd/service_add.go b/cmd/service_add.go index d995d46..64c905b 100644 --- a/cmd/service_add.go +++ b/cmd/service_add.go @@ -29,12 +29,13 @@ import ( "github.com/spf13/cobra" "go.uploadedlobster.com/scotty/internal/cli" "go.uploadedlobster.com/scotty/internal/config" + "go.uploadedlobster.com/scotty/internal/i18n" ) var serviceAddCmd = &cobra.Command{ Use: "add", Short: "Add a service configuration", - Long: `Add a service configuration.`, + Long: `Interactively add a service to the configuration file.`, Run: func(cmd *cobra.Command, args []string) { // Select backend backend, err := cli.SelectBackend("") @@ -42,12 +43,12 @@ var serviceAddCmd = &cobra.Command{ // Set service name prompt := promptui.Prompt{ - Label: "Service name", + Label: i18n.Tr("Service name"), Default: backend, Validate: func(s string) error { srv, _ := config.GetService(s) if srv != nil { - return errors.New("a service with this name already exists") + return errors.New(i18n.Tr("a service with this name already exists")) } return config.ValidateKey(s) }, @@ -69,7 +70,7 @@ var serviceAddCmd = &cobra.Command{ // Save the service config err = service.Save() cobra.CheckErr(err) - fmt.Printf("Saved service %v using backend %v\n", service.Name, service.Backend) + fmt.Println(i18n.Tr("Saved service %v using backend %v", service.Name, service.Backend)) }, } diff --git a/cmd/service_auth.go b/cmd/service_auth.go index cee8e2f..12da9dd 100644 --- a/cmd/service_auth.go +++ b/cmd/service_auth.go @@ -28,6 +28,7 @@ import ( "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" "go.uploadedlobster.com/scotty/internal/storage" "golang.org/x/oauth2" @@ -35,12 +36,16 @@ import ( var serviceAuthCmd = &cobra.Command{ Use: "auth", - Short: "Authenticate with a backend", - Long: `For backends requiring authentication this command can be used to authenticate.`, + Short: "Authenticate a service", + Long: `For backends requiring authentication this command can be used to authenticate. + +Authentication is always done per configured service. That means you can have +multiple services using the same backend but different authentication.`, Run: func(cmd *cobra.Command, args []string) { serviceConfig := cli.GetServiceConfigFromFlag(cmd, "service") if serviceConfig == nil { - cobra.CheckErr(errors.New("failed loading service configuration")) + err := errors.New(i18n.Tr("failed loading service configuration")) + cobra.CheckErr(err) } backend, err := backends.ResolveBackend[models.OAuth2Authenticator](serviceConfig) cobra.CheckErr(err) @@ -64,14 +69,14 @@ var serviceAuthCmd = &cobra.Command{ auth.RunOauth2CallbackServer(*redirectURL, authUrl.Param, responseChan) // Open the URL - fmt.Printf("Visit the URL for the auth dialog: %v\n", authUrl.Url) + fmt.Println(i18n.Tr("Visit the URL for authorization: %v", authUrl.Url)) err = browser.OpenURL(authUrl.Url) cobra.CheckErr(err) // Retrieve the code from the authentication callback code := <-responseChan if code.State != authUrl.State { - cobra.CompErrorln("Error: oauth state mismatch") + cobra.CompErrorln(i18n.Tr("Error: OAuth state mismatch")) os.Exit(1) } @@ -86,13 +91,13 @@ var serviceAuthCmd = &cobra.Command{ err = db.SetOAuth2Token(serviceConfig.Name, tok) cobra.CheckErr(err) - fmt.Printf("Access token received, you can use %v now.\n\n", serviceConfig.Name) + fmt.Println(i18n.Tr("Access token received, you can use %v now.\n", serviceConfig.Name)) }, } func init() { serviceCmd.AddCommand(serviceAuthCmd) - serviceAuthCmd.Flags().StringP("service", "s", "", "Service configuration (required)") + serviceAuthCmd.Flags().StringP("service", "s", "", "service configuration (required)") serviceAuthCmd.MarkFlagRequired("service") } diff --git a/cmd/service_delete.go b/cmd/service_delete.go index dd7b431..519e9b6 100644 --- a/cmd/service_delete.go +++ b/cmd/service_delete.go @@ -26,29 +26,30 @@ import ( "github.com/spf13/cobra" "go.uploadedlobster.com/scotty/internal/cli" + "go.uploadedlobster.com/scotty/internal/i18n" ) var serviceDeleteCmd = &cobra.Command{ Use: "delete", Short: "Delete existing service configuration", - Long: `Delete an existing service configuration.`, + Long: `Delete an existing service from the configuration file.`, Run: func(cmd *cobra.Command, args []string) { service, err := cli.SelectService() cobra.CheckErr(err) // Prompt for deletion - delete, err := cli.PromptYesNo(fmt.Sprintf("Delete the service configuration \"%v\"?", service)) + delete, err := cli.PromptYesNo(i18n.Tr("Delete the service configuration \"%v\"?", service)) cobra.CheckErr(err) if !delete { - fmt.Println("Aborted") + fmt.Println(i18n.Tr("Aborted")) return } // Delete the service config err = service.Delete() cobra.CheckErr(err) - fmt.Printf("Service \"%v\" deleted\n", service.Name) + fmt.Println(i18n.Tr("Service \"%v\" deleted\n", service.Name)) }, } diff --git a/cmd/service_edit.go b/cmd/service_edit.go index 9f4accf..873fe76 100644 --- a/cmd/service_edit.go +++ b/cmd/service_edit.go @@ -26,12 +26,13 @@ import ( "github.com/spf13/cobra" "go.uploadedlobster.com/scotty/internal/cli" + "go.uploadedlobster.com/scotty/internal/i18n" ) var serviceEditCmd = &cobra.Command{ Use: "edit", Short: "Edit existing service configuration", - Long: `Edit an existing service configuration.`, + Long: `Edit an existing service in the configuration file.`, Run: func(cmd *cobra.Command, args []string) { service, err := cli.SelectService() cobra.CheckErr(err) @@ -48,7 +49,7 @@ var serviceEditCmd = &cobra.Command{ // Save the service config err = service.Save() cobra.CheckErr(err) - fmt.Printf("Updated service %v using backend %v\n", service.Name, service.Backend) + fmt.Println(i18n.Tr("Updated service %v using backend %v\n", service.Name, service.Backend)) }, } diff --git a/cmd/service_list.go b/cmd/service_list.go index 23d64d9..f51ff00 100644 --- a/cmd/service_list.go +++ b/cmd/service_list.go @@ -26,6 +26,7 @@ import ( "github.com/spf13/cobra" "go.uploadedlobster.com/scotty/internal/config" + "go.uploadedlobster.com/scotty/internal/i18n" ) var serviceListCmd = &cobra.Command{ @@ -37,7 +38,7 @@ var serviceListCmd = &cobra.Command{ for _, s := range config.AllServicesAsList() { fmt.Printf("%v\n", s.Name) if verbose { - fmt.Printf("\tbackend: %v\n", s.Backend) + fmt.Println(i18n.Tr("\tbackend: %v", s.Backend)) for k, v := range s.ConfigValues { fmt.Printf("\t%v: %v\n", k, v) } diff --git a/internal/auth/callback.go b/internal/auth/callback.go index 0ad9c9d..6ae0128 100644 --- a/internal/auth/callback.go +++ b/internal/auth/callback.go @@ -19,13 +19,15 @@ import ( "fmt" "net/http" "net/url" + + "go.uploadedlobster.com/scotty/internal/i18n" ) func RunOauth2CallbackServer(redirectURL url.URL, param string, responseChan chan CodeResponse) { http.HandleFunc(redirectURL.Path, func(w http.ResponseWriter, r *http.Request) { code := r.URL.Query().Get(param) state := r.URL.Query().Get("state") - fmt.Fprint(w, "Token received, you can close this window now.") + fmt.Fprint(w, i18n.Tr("Token received, you can close this window now.")) responseChan <- CodeResponse{ Code: code, State: state, diff --git a/internal/backends/backends.go b/internal/backends/backends.go index 8c17d10..8cec9e0 100644 --- a/internal/backends/backends.go +++ b/internal/backends/backends.go @@ -33,6 +33,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" ) @@ -72,7 +73,7 @@ func ResolveBackend[T interface{}](config *config.ServiceConfig) (T, error) { if implements { result = backend.(T) } else { - err = fmt.Errorf("backend %s does not implement %s", config.Backend, interfaceName) + err = fmt.Errorf(i18n.Tr("backend %s does not implement %s", config.Backend, interfaceName)) } return result, err @@ -81,7 +82,7 @@ func ResolveBackend[T interface{}](config *config.ServiceConfig) (T, error) { func BackendByName(backendName string) (models.Backend, error) { backendType := knownBackends[backendName] if backendType == nil { - return nil, fmt.Errorf("unknown backend \"%s\"", backendName) + return nil, fmt.Errorf(i18n.Tr("unknown backend \"%s\"", backendName)) } return backendType(), nil } diff --git a/internal/backends/deezer/deezer.go b/internal/backends/deezer/deezer.go index 05145fb..802d1ab 100644 --- a/internal/backends/deezer/deezer.go +++ b/internal/backends/deezer/deezer.go @@ -24,6 +24,7 @@ import ( "go.uploadedlobster.com/scotty/internal/auth" "go.uploadedlobster.com/scotty/internal/config" + "go.uploadedlobster.com/scotty/internal/i18n" "go.uploadedlobster.com/scotty/internal/models" "golang.org/x/oauth2" ) @@ -39,11 +40,11 @@ func (b *DeezerApiBackend) Name() string { return "deezer" } func (b *DeezerApiBackend) Options() []models.BackendOption { return []models.BackendOption{{ Name: "client-id", - Label: "Client ID", + Label: i18n.Tr("Client ID"), Type: models.String, }, { Name: "client-secret", - Label: "Client secret", + Label: i18n.Tr("Client secret"), Type: models.Secret, }} } diff --git a/internal/backends/funkwhale/funkwhale.go b/internal/backends/funkwhale/funkwhale.go index 8e808f9..05017d3 100644 --- a/internal/backends/funkwhale/funkwhale.go +++ b/internal/backends/funkwhale/funkwhale.go @@ -21,6 +21,7 @@ import ( "time" "go.uploadedlobster.com/scotty/internal/config" + "go.uploadedlobster.com/scotty/internal/i18n" "go.uploadedlobster.com/scotty/internal/models" ) @@ -36,15 +37,15 @@ func (b *FunkwhaleApiBackend) Name() string { return "funkwhale" } func (b *FunkwhaleApiBackend) Options() []models.BackendOption { return []models.BackendOption{{ Name: "server-url", - Label: "Server URL", + Label: i18n.Tr("Server URL"), Type: models.String, }, { Name: "username", - Label: "User name", + Label: i18n.Tr("User name"), Type: models.String, }, { Name: "token", - Label: "Access token", + Label: i18n.Tr("Access token"), Type: models.Secret, }} } diff --git a/internal/backends/jspf/jspf.go b/internal/backends/jspf/jspf.go index 29ddcdc..944c6e6 100644 --- a/internal/backends/jspf/jspf.go +++ b/internal/backends/jspf/jspf.go @@ -22,6 +22,7 @@ import ( "time" "go.uploadedlobster.com/scotty/internal/config" + "go.uploadedlobster.com/scotty/internal/i18n" "go.uploadedlobster.com/scotty/internal/models" "go.uploadedlobster.com/scotty/pkg/jspf" ) @@ -39,19 +40,19 @@ func (b *JSPFBackend) Name() string { return "jspf" } func (b *JSPFBackend) Options() []models.BackendOption { return []models.BackendOption{{ Name: "file-path", - Label: "File path", + Label: i18n.Tr("File path"), Type: models.String, }, { Name: "title", - Label: "Playlist title", + Label: i18n.Tr("Playlist title"), Type: models.String, }, { Name: "username", - Label: "User name", + Label: i18n.Tr("User name"), Type: models.String, }, { Name: "identifier", - Label: "Unique playlist identifier", + Label: i18n.Tr("Unique playlist identifier"), Type: models.String, }} } diff --git a/internal/backends/lastfm/lastfm.go b/internal/backends/lastfm/lastfm.go index c823248..c4c2ec3 100644 --- a/internal/backends/lastfm/lastfm.go +++ b/internal/backends/lastfm/lastfm.go @@ -25,6 +25,7 @@ import ( "github.com/shkh/lastfm-go/lastfm" "go.uploadedlobster.com/scotty/internal/auth" "go.uploadedlobster.com/scotty/internal/config" + "go.uploadedlobster.com/scotty/internal/i18n" "go.uploadedlobster.com/scotty/internal/models" "golang.org/x/oauth2" ) @@ -46,15 +47,15 @@ func (b *LastfmApiBackend) Name() string { return "lastfm" } func (b *LastfmApiBackend) Options() []models.BackendOption { return []models.BackendOption{{ Name: "username", - Label: "User name", + Label: i18n.Tr("User name"), Type: models.String, }, { Name: "client-id", - Label: "Client ID", + Label: i18n.Tr("Client ID"), Type: models.String, }, { Name: "client-secret", - Label: "Client secret", + Label: i18n.Tr("Client secret"), Type: models.Secret, }} } diff --git a/internal/backends/listenbrainz/listenbrainz.go b/internal/backends/listenbrainz/listenbrainz.go index 2d3dc95..02b6fb4 100644 --- a/internal/backends/listenbrainz/listenbrainz.go +++ b/internal/backends/listenbrainz/listenbrainz.go @@ -22,6 +22,7 @@ import ( "time" "go.uploadedlobster.com/scotty/internal/config" + "go.uploadedlobster.com/scotty/internal/i18n" "go.uploadedlobster.com/scotty/internal/models" "go.uploadedlobster.com/scotty/internal/version" ) @@ -37,11 +38,11 @@ func (b *ListenBrainzApiBackend) Name() string { return "listenbrainz" } func (b *ListenBrainzApiBackend) Options() []models.BackendOption { return []models.BackendOption{{ Name: "username", - Label: "User name", + Label: i18n.Tr("User name"), Type: models.String, }, { Name: "token", - Label: "Access token", + Label: i18n.Tr("Access token"), Type: models.Secret, }} } diff --git a/internal/backends/maloja/maloja.go b/internal/backends/maloja/maloja.go index c47f67a..8057200 100644 --- a/internal/backends/maloja/maloja.go +++ b/internal/backends/maloja/maloja.go @@ -23,6 +23,7 @@ import ( "time" "go.uploadedlobster.com/scotty/internal/config" + "go.uploadedlobster.com/scotty/internal/i18n" "go.uploadedlobster.com/scotty/internal/models" ) @@ -36,15 +37,15 @@ func (b *MalojaApiBackend) Name() string { return "maloja" } func (b *MalojaApiBackend) Options() []models.BackendOption { return []models.BackendOption{{ Name: "server-url", - Label: "Server URL", + Label: i18n.Tr("Server URL"), Type: models.String, }, { Name: "token", - Label: "Access token", + Label: i18n.Tr("Access token"), Type: models.Secret, }, { Name: "nofix", - Label: "Disable auto correction of submitted listens", + Label: i18n.Tr("Disable auto correction of submitted listens"), Type: models.Bool, }} } diff --git a/internal/backends/scrobblerlog/scrobblerlog.go b/internal/backends/scrobblerlog/scrobblerlog.go index 6964b7e..e09f100 100644 --- a/internal/backends/scrobblerlog/scrobblerlog.go +++ b/internal/backends/scrobblerlog/scrobblerlog.go @@ -23,6 +23,7 @@ import ( "time" "go.uploadedlobster.com/scotty/internal/config" + "go.uploadedlobster.com/scotty/internal/i18n" "go.uploadedlobster.com/scotty/internal/models" ) @@ -39,15 +40,15 @@ func (b *ScrobblerLogBackend) Name() string { return "scrobbler-log" } func (b *ScrobblerLogBackend) Options() []models.BackendOption { return []models.BackendOption{{ Name: "file-path", - Label: "File path", + Label: i18n.Tr("File path"), Type: models.String, }, { Name: "include-skipped", - Label: "Include skipped listens", + Label: i18n.Tr("Include skipped listens"), Type: models.Bool, }, { Name: "append", - Label: "Append to file", + Label: i18n.Tr("Append to file"), Type: models.Bool, }} } diff --git a/internal/backends/spotify/spotify.go b/internal/backends/spotify/spotify.go index 8a0bc09..9b623cb 100644 --- a/internal/backends/spotify/spotify.go +++ b/internal/backends/spotify/spotify.go @@ -26,6 +26,7 @@ import ( "go.uploadedlobster.com/scotty/internal/auth" "go.uploadedlobster.com/scotty/internal/config" + "go.uploadedlobster.com/scotty/internal/i18n" "go.uploadedlobster.com/scotty/internal/models" "golang.org/x/oauth2" "golang.org/x/oauth2/spotify" @@ -42,11 +43,11 @@ func (b *SpotifyApiBackend) Name() string { return "spotify" } func (b *SpotifyApiBackend) Options() []models.BackendOption { return []models.BackendOption{{ Name: "client-id", - Label: "Client ID", + Label: i18n.Tr("Client ID"), Type: models.String, }, { Name: "client-secret", - Label: "Client secret", + Label: i18n.Tr("Client secret"), Type: models.Secret, }} } diff --git a/internal/backends/subsonic/subsonic.go b/internal/backends/subsonic/subsonic.go index 7defb87..6560319 100644 --- a/internal/backends/subsonic/subsonic.go +++ b/internal/backends/subsonic/subsonic.go @@ -23,6 +23,7 @@ import ( "github.com/delucks/go-subsonic" "go.uploadedlobster.com/scotty/internal/config" + "go.uploadedlobster.com/scotty/internal/i18n" "go.uploadedlobster.com/scotty/internal/models" "go.uploadedlobster.com/scotty/internal/version" ) @@ -37,15 +38,15 @@ func (b *SubsonicApiBackend) Name() string { return "subsonic" } func (b *SubsonicApiBackend) Options() []models.BackendOption { return []models.BackendOption{{ Name: "server-url", - Label: "Server URL", + Label: i18n.Tr("Server URL"), Type: models.String, }, { Name: "username", - Label: "User name", + Label: i18n.Tr("User name"), Type: models.String, }, { Name: "token", - Label: "Access token", + Label: i18n.Tr("Access token"), Type: models.Secret, }} } diff --git a/internal/cli/prompt.go b/internal/cli/prompt.go index 19038e9..9a4ba70 100644 --- a/internal/cli/prompt.go +++ b/internal/cli/prompt.go @@ -19,6 +19,7 @@ import ( "fmt" "github.com/manifoldco/promptui" + "go.uploadedlobster.com/scotty/internal/i18n" "go.uploadedlobster.com/scotty/internal/models" ) @@ -63,10 +64,12 @@ func PromptBool(opt models.BackendOption) (bool, error) { } func PromptYesNo(label string) (bool, error) { + yes := i18n.Tr("Yes") + no := i18n.Tr("No") sel := promptui.Select{ Label: label, - Items: []string{"Yes", "No"}, + Items: []string{yes, no}, } _, val, err := sel.Run() - return val == "Yes", err + return val == yes, err } diff --git a/internal/cli/services.go b/internal/cli/services.go index d755607..a8bd813 100644 --- a/internal/cli/services.go +++ b/internal/cli/services.go @@ -23,16 +23,17 @@ import ( "github.com/manifoldco/promptui" "go.uploadedlobster.com/scotty/internal/backends" "go.uploadedlobster.com/scotty/internal/config" + "go.uploadedlobster.com/scotty/internal/i18n" ) func SelectService() (config.ServiceConfig, error) { services := config.AllServicesAsList() if len(services) == 0 { - err := errors.New("no existing service configurations") + err := errors.New(i18n.Tr("no existing service configurations")) return config.ServiceConfig{}, err } sel := promptui.Select{ - Label: "Service", + Label: i18n.Tr("Service"), Items: services, Size: 10, } @@ -49,7 +50,7 @@ func SelectBackend(selected string) (string, error) { return b.Name == selected }) sel := promptui.Select{ - Label: "Backend", + Label: i18n.Tr("Backend"), Items: backendList, CursorPos: i, Size: 10, diff --git a/internal/config/config.go b/internal/config/config.go index 6d68d00..f52de99 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -27,6 +27,7 @@ import ( "github.com/pelletier/go-toml/v2" "github.com/spf13/cobra" "github.com/spf13/viper" + "go.uploadedlobster.com/scotty/internal/i18n" "go.uploadedlobster.com/scotty/internal/version" ) @@ -75,7 +76,7 @@ func InitConfig(cfgFile string) error { func WriteConfig(removedKeys ...string) error { file := viper.ConfigFileUsed() if len(file) == 0 { - return errors.New("no configuration file defined, cannot write config") + return errors.New(i18n.Tr("no configuration file defined, cannot write config")) } configMap := viper.AllSettings() @@ -124,7 +125,7 @@ func ValidateKey(key string) error { } else if found { return nil } else { - return fmt.Errorf("key must only consist of A-Za-z0-9_-") + return fmt.Errorf(i18n.Tr("key must only consist of A-Za-z0-9_-")) } } diff --git a/internal/config/services.go b/internal/config/services.go index 8a191f3..2895cb2 100644 --- a/internal/config/services.go +++ b/internal/config/services.go @@ -21,6 +21,7 @@ import ( "github.com/spf13/cast" "github.com/spf13/viper" + "go.uploadedlobster.com/scotty/internal/i18n" ) type ServiceConfig struct { @@ -123,5 +124,5 @@ func GetService(name string) (*ServiceConfig, error) { return &service, nil } - return nil, fmt.Errorf("no service configuration \"%v\"", name) + return nil, fmt.Errorf(i18n.Tr("no service configuration \"%v\"", name)) } diff --git a/internal/translations/catalog.go b/internal/translations/catalog.go index 797ce2f..7ccdaa0 100644 --- a/internal/translations/catalog.go +++ b/internal/translations/catalog.go @@ -39,23 +39,71 @@ func init() { } var messageKeyToIndex = map[string]int{ + "\tbackend: %v": 24, + "\texport: %s": 10, + "\timport: %s\n": 11, + "Aborted": 21, + "Access token": 32, + "Access token received, you can use %v now.\n": 19, + "Append to file": 38, + "Backend": 43, + "Client ID": 28, + "Client secret": 29, + "Delete the service configuration \"%v\"?": 20, + "Disable auto correction of submitted listens": 36, "During the import the following errors occurred:": 7, - "Error: %v\n": 8, - "From timestamp: %v (%v)": 4, + "Error: %v\n": 8, + "Error: OAuth state mismatch": 18, + "Failed reading config: %v": 12, + "File path": 33, + "From timestamp: %v (%v)": 4, "Import failed, last reported timestamp was %v (%v)": 5, "Imported %v of %v %s into %v.": 6, + "Include skipped listens": 37, "Latest timestamp: %v (%v)\n": 9, + "No": 40, + "Playlist title": 34, + "Saved service %v using backend %v": 15, + "Server URL": 30, + "Service": 42, + "Service \"%v\" deleted\n": 22, + "Service name": 13, + "Token received, you can close this window now.": 25, "Transferring %s from %s to %s...": 3, + "Unique playlist identifier": 35, + "Updated service %v using backend %v\n": 23, + "User name": 31, + "Visit the URL for authorization: %v": 17, + "Yes": 39, + "a service with this name already exists": 14, + "backend %s does not implement %s": 26, "done": 2, "exporting": 0, + "failed loading service configuration": 16, "importing": 1, + "key must only consist of A-Za-z0-9_-": 45, + "no configuration file defined, cannot write config": 44, + "no existing service configurations": 41, + "no service configuration \"%v\"": 46, + "unknown backend \"%s\"": 27, } -var deIndex = []uint32{ // 11 elements +var deIndex = []uint32{ // 48 elements + // Entry 0 - 1F 0x00000000, 0x0000000b, 0x00000016, 0x0000001d, 0x00000046, 0x00000064, 0x000000a4, 0x000000cf, - 0x00000106, 0x00000119, 0x00000142, -} // Size: 68 bytes + 0x00000106, 0x00000119, 0x00000142, 0x00000142, + 0x00000142, 0x00000142, 0x00000142, 0x00000142, + 0x00000142, 0x00000142, 0x00000142, 0x00000142, + 0x00000142, 0x00000142, 0x00000142, 0x00000142, + 0x00000142, 0x00000142, 0x00000142, 0x00000142, + 0x00000142, 0x00000142, 0x00000142, 0x00000142, + // Entry 20 - 3F + 0x00000142, 0x00000142, 0x00000142, 0x00000142, + 0x00000142, 0x00000142, 0x00000142, 0x00000142, + 0x00000142, 0x00000142, 0x00000142, 0x00000142, + 0x00000142, 0x00000142, 0x00000142, 0x00000142, +} // Size: 216 bytes const deData string = "" + // Size: 322 bytes "\x02exportiere\x02importiere\x02fertig\x02Übertrage %[1]s von %[2]s nach" + @@ -65,17 +113,45 @@ const deData string = "" + // Size: 322 bytes "\x00\x01\x0a\x0e\x02Fehler: %[1]v\x04\x00\x01\x0a$\x02Neuester Zeitstemp" + "el: %[1]v (%[2]v)" -var enIndex = []uint32{ // 11 elements +var enIndex = []uint32{ // 48 elements + // Entry 0 - 1F 0x00000000, 0x0000000a, 0x00000014, 0x00000019, 0x00000043, 0x00000061, 0x0000009a, 0x000000c4, - 0x000000f5, 0x00000107, 0x0000012c, -} // Size: 68 bytes + 0x000000f5, 0x00000107, 0x0000012c, 0x0000013f, + 0x00000153, 0x00000170, 0x0000017d, 0x000001a5, + 0x000001cd, 0x000001f2, 0x00000219, 0x00000235, + 0x00000268, 0x00000292, 0x0000029a, 0x000002b7, + 0x000002e6, 0x000002fa, 0x00000329, 0x00000350, + 0x00000368, 0x00000372, 0x00000380, 0x0000038b, + // Entry 20 - 3F + 0x00000395, 0x000003a2, 0x000003ac, 0x000003bb, + 0x000003d6, 0x00000403, 0x0000041b, 0x0000042a, + 0x0000042e, 0x00000431, 0x00000454, 0x0000045c, + 0x00000464, 0x00000497, 0x000004bc, 0x000004dd, +} // Size: 216 bytes -const enData string = "" + // Size: 300 bytes +const enData string = "" + // Size: 1245 bytes "\x02exporting\x02importing\x02done\x02Transferring %[1]s from %[2]s to %" + "[3]s...\x02From timestamp: %[1]v (%[2]v)\x02Import failed, last reported" + " timestamp was %[1]v (%[2]v)\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\x04\x00\x01\x0a \x02Latest timestamp: %[1]v (%[2]v)" + "\x0d\x02Error: %[1]v\x04\x00\x01\x0a \x02Latest timestamp: %[1]v (%[2]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" + + " this name already exists\x02Saved service %[1]v using backend %[2]v\x02" + + "failed loading service configuration\x02Visit the URL for authorization:" + + " %[1]v\x02Error: OAuth state mismatch\x04\x00\x01\x0a.\x02Access token r" + + "eceived, you can use %[1]v now.\x02Delete the service configuration \x22" + + "%[1]v\x22?\x02Aborted\x04\x00\x01\x0a\x18\x02Service \x22%[1]v\x22 delet" + + "ed\x04\x00\x01\x0a*\x02Updated service %[1]v using backend %[2]v\x04\x01" + + "\x09\x00\x0f\x02backend: %[1]v\x02Token received, you can close this win" + + "dow now.\x02backend %[1]s does not implement %[2]s\x02unknown backend " + + "\x22%[1]s\x22\x02Client ID\x02Client secret\x02Server URL\x02User name" + + "\x02Access token\x02File path\x02Playlist title\x02Unique playlist ident" + + "ifier\x02Disable auto correction of submitted listens\x02Include skipped" + + " listens\x02Append to file\x02Yes\x02No\x02no existing service configura" + + "tions\x02Service\x02Backend\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" - // Total table size 758 bytes (0KiB); checksum: D85383CE + // Total table size 1999 bytes (1KiB); checksum: FFEA1B2A diff --git a/internal/translations/locales/de/messages.gotext.json b/internal/translations/locales/de/messages.gotext.json index a2cd62c..0c41b77 100644 --- a/internal/translations/locales/de/messages.gotext.json +++ b/internal/translations/locales/de/messages.gotext.json @@ -1,6 +1,82 @@ { "language": "de", "messages": [ + { + "id": "Authenticate a service", + "message": "Authenticate a service", + "translation": "An einem Service anmelden" + }, + { + "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.", + "translation": "" + }, + { + "id": "failed loading service configuration", + "message": "failed loading service configuration", + "translation": "" + }, + { + "id": "Visit the URL for authorization: {Url}", + "message": "Visit the URL for authorization: {Url}", + "translation": "", + "placeholders": [ + { + "id": "Url", + "string": "%[1]v", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "authUrl.Url" + } + ] + }, + { + "id": "Error: OAuth state mismatch", + "message": "Error: OAuth state mismatch", + "translation": "" + }, + { + "id": "Access token received, you can use {Name} now.", + "message": "Access token received, you can use {Name} now.", + "translation": "", + "placeholders": [ + { + "id": "Name", + "string": "%[1]v", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "serviceConfig.Name" + } + ] + }, + { + "id": "service configuration (required)", + "message": "service configuration (required)", + "translation": "Servicekonfiguration (notwendig)" + }, + { + "id": "exporting", + "message": "exporting", + "translation": "exportiere", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "importing", + "message": "importing", + "translation": "importiere", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "done", + "message": "done", + "translation": "fertig", + "translatorComment": "Copied from source.", + "fuzzy": true + }, { "id": "Transferring {Entity} from {SourceName} to {TargetName}...", "message": "Transferring {Entity} from {SourceName} to {TargetName}...", @@ -159,27 +235,6 @@ "expr": "result.LastTimestamp.Unix()" } ] - }, - { - "id": "exporting", - "message": "exporting", - "translatorComment": "Copied from source.", - "fuzzy": true, - "translation": "exportiere" - }, - { - "id": "importing", - "message": "importing", - "translatorComment": "Copied from source.", - "fuzzy": true, - "translation": "importiere" - }, - { - "id": "done", - "message": "done", - "translatorComment": "Copied from source.", - "fuzzy": true, - "translation": "fertig" } ] } diff --git a/internal/translations/locales/de/out.gotext.json b/internal/translations/locales/de/out.gotext.json index 321d448..6efac3d 100644 --- a/internal/translations/locales/de/out.gotext.json +++ b/internal/translations/locales/de/out.gotext.json @@ -1,6 +1,295 @@ { "language": "de", "messages": [ + { + "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": "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": "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": "failed loading service configuration", + "message": "failed loading service configuration", + "translation": "" + }, + { + "id": "Visit the URL for authorization: {Url}", + "message": "Visit the URL for authorization: {Url}", + "translation": "", + "placeholders": [ + { + "id": "Url", + "string": "%[1]v", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "authUrl.Url" + } + ] + }, + { + "id": "Error: OAuth state mismatch", + "message": "Error: OAuth state mismatch", + "translation": "" + }, + { + "id": "Access token received, you can use {Name} now.", + "message": "Access token received, you can use {Name} now.", + "translation": "", + "placeholders": [ + { + "id": "Name", + "string": "%[1]v", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "serviceConfig.Name" + } + ] + }, + { + "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": "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": "" + }, + { + "id": "Append to file", + "message": "Append to file", + "translation": "" + }, { "id": "exporting", "message": "exporting", @@ -22,6 +311,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}...", @@ -180,6 +494,31 @@ "expr": "result.LastTimestamp.Unix()" } ] + }, + { + "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" + } + ] } ] } \ 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 651e329..23d2710 100644 --- a/internal/translations/locales/en/out.gotext.json +++ b/internal/translations/locales/en/out.gotext.json @@ -1,6 +1,353 @@ { "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": "failed loading service configuration", + "message": "failed loading service configuration", + "translation": "failed loading service configuration", + "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": "serviceConfig.Name" + } + ], + "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": "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": "Append to file", + "message": "Append to file", + "translation": "Append to file", + "translatorComment": "Copied from source.", + "fuzzy": true + }, { "id": "exporting", "message": "exporting", @@ -22,6 +369,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}...", @@ -194,6 +576,37 @@ } ], "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 } ] } \ No newline at end of file From 0b1806367f27de35edc06f640caae0771e6b7761 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sat, 9 Dec 2023 17:41:38 +0100 Subject: [PATCH 045/150] Documented general configuration and usage --- README.md | 87 +++++++++++++++++++++++++++++++-- config.example.toml | 6 +-- internal/translations/README.md | 8 +-- 3 files changed, 87 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 50eb0c7..414f498 100644 --- a/README.md +++ b/README.md @@ -28,22 +28,97 @@ This requires `go` to be installed on your system. You can get it from https://g ## Configuration -Scotty requires the configuration of the services in a configuration file in TOML format. See [config.example.toml](./config.example.toml) for details. +To use Scotty you need to configure at least two services (e.g. ListenBrainz, Last.fm, Funkwhale or Spotify). + +By default Scotty stores the configuration in a platform dependent configuration directory (e.g. on Unix like system this is `$HOME/.config/scotty/scotty.toml`), but you can also run it with a different configuration file using the `--config` command line parameter. + +New services can be configured interactively using the `service add`, `service edit` and `service delete` commands. Run `scotty service --help` for tails. + +The configuration file in TOML format can also be edited manually. For a full example see [config.example.toml](./config.example.toml). ## Usage -Run `scotty --help` for command line help. +Run `scotty --help` for general command line help. + +### Tutorial +As a full example consider that you want to transfer your listen history and loved tracks from Deezer to ListenBrainz. You first need to configure these services. Let's start with adding ListenBrainz. Run `scotty service add`. Scotty will allow you to interactively configure the service. First you need to select "listenbrainz" as the backend: + +``` +$ scotty service add +Use the arrow keys to navigate: ↓ ↑ → ← +? Backend: + deezer + dump + funkwhale + jspf + lastfm + ▸ listenbrainz + maloja + scrobbler-log + spotify + subsonic +``` + +Next Scotty will ask how to name this service. You can accept the suggested name "listenbrainz". Naming services differently can be useful when you configure multiple services with the same backend (e.g. multiple separate accounts). + +``` +✔ listenbrainz +✔ Service name: listenbrainz█ +``` + +Next you need to provide your ListenBrainz user name and [user token](https://listenbrainz.org/profile/): + +``` +✔ listenbrainz +✔ Service name: listenbrainz +✔ User name: outsidecontext +✔ Access token: ************************************* +Saved service listenbrainz using backend listenbrainz +``` + +*Hint: If you made a mistake and want to change a value, run `scotty service edit` to change the configuration of existing services.* + +For Deezer we need access to the Deezer API. You need a Deezer account for which you have to [register an application](https://developers.deezer.com/myapps) in the Deezer developer portal. Give this application any name (e.g. Scotty) and use `http://127.0.0.1:2369/callback/deezer` as the "Redirect URL after authentication". After creating the application note the "Application ID" and "Secret Key". + +Now you can add a new service by running `scotty service add` again. Choose the "deezer" backend and set a name (let's use the default "deezer") as well as the Application ID and Secret Key you obtained before. + +Before you can use Deezer you need to authorize Scotty to access your account. For this run `scotty service auth --service deezer`. If your Application ID and Secret Key were correct your browser should open with Deezer's login and authorization page. Confirm the access. On success the browser will show "Token received, you can close this window now.". Close the browser window and return to your terminal. + +Running `scotty service list` should now show the two services "deezer" and "listenbrainz". + +Now you can use these services to transfer data between them. To transfer the loved tracks from Deezer to ListenBrainz run: + +``` +scotty beam loves deezer listenbrainz +``` + +The output will look something like this: + +``` +Transferring loves from deezer to listenbrainz... +From timestamp: 1970-01-01 01:00:01 +0100 CET (1) + ✓ exporting [=======================================================] done + ✓ importing [=======================================================] done +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. + +For example to import listens starting at a specific timestamp use: + +``` +scotty beam listens deezer listenbrainz --timestamp 1701872784 +``` -## Supported backends - +### Supported backends The following table lists the available backends and the currently supported features. Backend | Listens Export | Listens Import | Loves Export | Loves Import ---------------|----------------|----------------|--------------|------------- deezer | ✓ | ⨯ | ✓ | - -dump | ⨯ | ✓ | ⨯ | ✓ funkwhale | ✓ | ⨯ | ✓ | - jspf | - | ✓ | - | ✓ lastfm | ✓ | ✓ | ✓ | ✓ @@ -55,6 +130,8 @@ subsonic | ⨯ | ⨯ | ✓ | - **✓** implemented **-** not yet implemented **⨯** unavailable / not planned +See the comments in [config.example.toml](./config.example.toml) for a description of each backend's available configuration options. + ## Contribute The source code for Scotty is available on [SourceHut](https://sr.ht/~phw/scotty/). To report issues or feature requests please [create a ticket](https://todo.sr.ht/~phw/scotty). diff --git a/config.example.toml b/config.example.toml index 391fda5..b7b82fc 100644 --- a/config.example.toml +++ b/config.example.toml @@ -71,7 +71,7 @@ identifier = "" backend = "spotify" # You need to register an application on https://developer.spotify.com/ # and set the client ID and client secret below. -# When registering use "http://127.0.0.1:2222/callback/spotify" as the +# When registering use "http://127.0.0.1:2369/callback/spotify" as the # callback URI and enable "Web API". client-id = "" client-secret = "" @@ -81,7 +81,7 @@ client-secret = "" backend = "deezer" # You need to register an application on https://developers.deezer.com/myapps # and set the client ID and client secret below. -# When registering use "http://127.0.0.1:2222/callback/deezer" as the +# When registering use "http://127.0.0.1:2369/callback/deezer" as the # callback URI. client-id = "" client-secret = "" @@ -92,7 +92,7 @@ backend = "lastfm" username = "" # You need to register an application on https://www.last.fm/api/account/create # and set the API ID and shared secret below. -# When registering use "http://127.0.0.1:2222/callback/lastfm" as the +# When registering use "http://127.0.0.1:2369/callback/lastfm" as the # callback URI. client-id = "" client-secret = "" diff --git a/internal/translations/README.md b/internal/translations/README.md index aa302aa..77cd8fe 100644 --- a/internal/translations/README.md +++ b/internal/translations/README.md @@ -18,15 +18,11 @@ After strings have been added or modified in the code or after changes have been This requires the gotext tool to be installed: -``` -go install golang.org/x/text/cmd/gotext@latest -``` + go install golang.org/x/text/cmd/gotext@latest The following command will extract all strings and merge the translations: -``` -go generate ./internal/translations/translations.go -``` + go generate ./internal/translations/translations.go ## License From 08514e5eac1dd4a9836e257d6a2952a7e37e7062 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sat, 9 Dec 2023 17:42:03 +0100 Subject: [PATCH 046/150] Fixed file extension for config file in paramter documentation --- cmd/root.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/root.go b/cmd/root.go index 50ada8f..20f01ee 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -58,7 +58,7 @@ func init() { configDir := config.DefaultConfigDir() rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", - fmt.Sprintf("config file (default is %s/scotty.yaml)", configDir)) + fmt.Sprintf("config file (default is %s/scotty.toml)", configDir)) // Cobra also supports local flags, which will only run // when this action is called directly. From 3e9ebfb9af353fc9fe8c1e4844bbd531de9be78e Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sat, 9 Dec 2023 18:10:34 +0100 Subject: [PATCH 047/150] Fixed Deezer listen export count --- internal/backends/deezer/deezer.go | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/internal/backends/deezer/deezer.go b/internal/backends/deezer/deezer.go index 802d1ab..c796af3 100644 --- a/internal/backends/deezer/deezer.go +++ b/internal/backends/deezer/deezer.go @@ -83,10 +83,14 @@ func (b *DeezerApiBackend) ExportListens(oldestTimestamp time.Time, results chan offset := math.MaxInt32 perPage := MaxItemsPerGet + startTime := time.Now() + minTime := oldestTimestamp + + totalDuration := startTime.Sub(oldestTimestamp) + defer close(results) - p := models.Progress{Total: int64(perPage)} - var totalCount int + p := models.Progress{Total: int64(totalDuration.Seconds())} out: for { @@ -101,7 +105,6 @@ out: // and continue. if offset >= result.Total { p.Total = int64(result.Total) - totalCount = result.Total offset = result.Total - perPage if offset < 0 { offset = 0 @@ -120,15 +123,19 @@ out: if listen.ListenedAt.Unix() > oldestTimestamp.Unix() { listens = append(listens, listen) } else { - totalCount -= 1 break } } sort.Sort(listens) - results <- models.ListensResult{Items: listens, Total: totalCount} - p.Elapsed += int64(count) + if len(listens) > 0 { + minTime = listens[0].ListenedAt + } + + remainingTime := startTime.Sub(minTime) + p.Elapsed = int64(totalDuration.Seconds() - remainingTime.Seconds()) progress <- p + results <- models.ListensResult{Items: listens, OldestTimestamp: minTime} if offset <= 0 { // This was the last request, no further results @@ -141,6 +148,7 @@ out: } } + results <- models.ListensResult{OldestTimestamp: minTime} progress <- p.Complete() } From b07c393a22e0b3568ebe4ea9d1694050c2d47afa Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sat, 9 Dec 2023 18:37:52 +0100 Subject: [PATCH 048/150] Fixed import error handling --- internal/backends/import.go | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/internal/backends/import.go b/internal/backends/import.go index 6365cbb..6173a53 100644 --- a/internal/backends/import.go +++ b/internal/backends/import.go @@ -24,7 +24,7 @@ import ( type ImportProcessor[T models.ListensResult | models.LovesResult] interface { ImportBackend() models.ImportBackend Process(results chan T, out chan models.ImportResult, progress chan models.Progress) - Import(export T, result models.ImportResult, out chan models.ImportResult, progress chan models.Progress) models.ImportResult + Import(export T, result models.ImportResult, out chan models.ImportResult, progress chan models.Progress) (models.ImportResult, error) } type ListensImportProcessor struct { @@ -39,10 +39,9 @@ func (p ListensImportProcessor) Process(results chan models.ListensResult, out c process(p, results, out, progress) } -func (p ListensImportProcessor) Import(export models.ListensResult, result models.ImportResult, out chan models.ImportResult, progress chan models.Progress) models.ImportResult { +func (p ListensImportProcessor) Import(export models.ListensResult, result models.ImportResult, out chan models.ImportResult, progress chan models.Progress) (models.ImportResult, error) { if export.Error != nil { - handleError(result, export.Error, out, progress) - return result + return handleError(result, export.Error, progress), export.Error } if export.Total > 0 { @@ -52,10 +51,9 @@ func (p ListensImportProcessor) Import(export models.ListensResult, result model } importResult, err := p.Backend.ImportListens(export, result, progress) if err != nil { - handleError(importResult, err, out, progress) - return result + return handleError(result, err, progress), err } - return importResult + return importResult, nil } type LovesImportProcessor struct { @@ -70,10 +68,9 @@ func (p LovesImportProcessor) Process(results chan models.LovesResult, out chan process(p, results, out, progress) } -func (p LovesImportProcessor) Import(export models.LovesResult, result models.ImportResult, out chan models.ImportResult, progress chan models.Progress) models.ImportResult { +func (p LovesImportProcessor) Import(export models.LovesResult, result models.ImportResult, out chan models.ImportResult, progress chan models.Progress) (models.ImportResult, error) { if export.Error != nil { - handleError(result, export.Error, out, progress) - return result + return handleError(result, export.Error, progress), export.Error } if export.Total > 0 { @@ -83,10 +80,9 @@ func (p LovesImportProcessor) Import(export models.LovesResult, result models.Im } importResult, err := p.Backend.ImportLoves(export, result, progress) if err != nil { - handleError(importResult, err, out, progress) - return result + return handleError(importResult, err, progress), err } - return importResult + return importResult, nil } func process[R models.LovesResult | models.ListensResult, P ImportProcessor[R]](processor P, results chan R, out chan models.ImportResult, progress chan models.Progress) { @@ -96,19 +92,23 @@ func process[R models.LovesResult | models.ListensResult, P ImportProcessor[R]]( err := processor.ImportBackend().StartImport() if err != nil { - handleError(result, err, out, progress) + out <- handleError(result, err, progress) return } for exportResult := range results { - importResult := processor.Import(exportResult, result, out, progress) + importResult, err := processor.Import(exportResult, result, out, progress) + if err != nil { + out <- handleError(result, err, progress) + return + } result.Update(importResult) progress <- models.Progress{}.FromImportResult(result) } err = processor.ImportBackend().FinishImport() if err != nil { - handleError(result, err, out, progress) + out <- handleError(result, err, progress) return } @@ -116,8 +116,8 @@ func process[R models.LovesResult | models.ListensResult, P ImportProcessor[R]]( out <- result } -func handleError(result models.ImportResult, err error, out chan models.ImportResult, progress chan models.Progress) { +func handleError(result models.ImportResult, err error, progress chan models.Progress) models.ImportResult { result.Error = err progress <- models.Progress{}.FromImportResult(result).Complete() - out <- result + return result } From 2307e6247b58a5908a6531b36459615700fbf324 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sat, 9 Dec 2023 18:41:53 +0100 Subject: [PATCH 049/150] Even on error the last import timestamp should be set to the previous timestamp --- internal/cli/transfer.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/cli/transfer.go b/internal/cli/transfer.go index e073864..757b912 100644 --- a/internal/cli/transfer.go +++ b/internal/cli/transfer.go @@ -120,6 +120,9 @@ func (c *TransferCmd[E, I, R]) Transfer(exp backends.ExportProcessor[R], imp bac resultChan := make(chan models.ImportResult) go imp.Process(exportChan, resultChan, importProgress) result := <-resultChan + if result.LastTimestamp.Unix() < timestamp.Unix() { + result.LastTimestamp = timestamp + } close(exportProgress) wg.Wait() progress.Wait() From e7d596b4e06de3175334e491f7d9580a097e1091 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sat, 9 Dec 2023 18:56:23 +0100 Subject: [PATCH 050/150] Allow specifying --timestamp 0 --- cmd/beam_listens.go | 4 +++- internal/cli/transfer.go | 8 +++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/cmd/beam_listens.go b/cmd/beam_listens.go index 1b83e29..149319f 100644 --- a/cmd/beam_listens.go +++ b/cmd/beam_listens.go @@ -17,6 +17,8 @@ Scotty. If not, see . package cmd import ( + "math" + "github.com/spf13/cobra" "go.uploadedlobster.com/scotty/internal/backends" "go.uploadedlobster.com/scotty/internal/cli" @@ -58,5 +60,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", 0, "Only import listens newer then given Unix timestamp") + beamListensCmd.Flags().Int64P("timestamp", "t", math.MinInt64, "Only import listens newer then given Unix timestamp") } diff --git a/internal/cli/transfer.go b/internal/cli/transfer.go index 757b912..6636bce 100644 --- a/internal/cli/transfer.go +++ b/internal/cli/transfer.go @@ -17,6 +17,7 @@ package cli import ( "fmt" + "math" "sync" "time" @@ -153,12 +154,13 @@ func (c *TransferCmd[E, I, R]) Transfer(exp backends.ExportProcessor[R], imp bac } func (c *TransferCmd[E, I, R]) timestamp() (time.Time, error) { - timestamp := time.Unix(getInt64FromFlag(c.cmd, "timestamp"), 0) - if timestamp == time.Unix(0, 0) { + flagValue, err := c.cmd.Flags().GetInt64("timestamp") + if err == nil && flagValue > math.MinInt64 { + return time.Unix(flagValue, 0), nil + } else { timestamp, err := c.db.GetImportTimestamp(c.sourceName, c.targetName, c.entity) return timestamp, err } - return timestamp, nil } func (c *TransferCmd[E, I, R]) updateTimestamp(result models.ImportResult, oldTimestamp time.Time) error { From 6c1cf2101d49c405212dd6ac3ec79697b47a3f4b Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sat, 9 Dec 2023 21:59:54 +0100 Subject: [PATCH 051/150] Bump version to 0.3.0 --- internal/version/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/version/version.go b/internal/version/version.go index 9a4f9fc..f3b30fa 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -17,7 +17,7 @@ package version const ( AppName = "scotty" - AppVersion = "0.2.0" + AppVersion = "0.3.0" ) func UserAgent() string { From 76fd7cfeb4ad21aef4ca101ccf61af4356ab2302 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sat, 9 Dec 2023 22:15:40 +0100 Subject: [PATCH 052/150] Allow default value for boolean select Default for both service delete conformation and disabling Maloja autofix is now "no". --- cmd/service_delete.go | 5 ++++- internal/backends/maloja/maloja.go | 7 ++++--- internal/cli/prompt.go | 13 +++++++++---- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/cmd/service_delete.go b/cmd/service_delete.go index 519e9b6..1d5c0ba 100644 --- a/cmd/service_delete.go +++ b/cmd/service_delete.go @@ -38,7 +38,10 @@ var serviceDeleteCmd = &cobra.Command{ cobra.CheckErr(err) // Prompt for deletion - delete, err := cli.PromptYesNo(i18n.Tr("Delete the service configuration \"%v\"?", service)) + delete, err := cli.PromptYesNo( + i18n.Tr("Delete the service configuration \"%v\"?", service), + false, + ) cobra.CheckErr(err) if !delete { diff --git a/internal/backends/maloja/maloja.go b/internal/backends/maloja/maloja.go index 8057200..9c91115 100644 --- a/internal/backends/maloja/maloja.go +++ b/internal/backends/maloja/maloja.go @@ -44,9 +44,10 @@ func (b *MalojaApiBackend) Options() []models.BackendOption { Label: i18n.Tr("Access token"), Type: models.Secret, }, { - Name: "nofix", - Label: i18n.Tr("Disable auto correction of submitted listens"), - Type: models.Bool, + Name: "nofix", + Label: i18n.Tr("Disable auto correction of submitted listens"), + Type: models.Bool, + Default: "false", }} } diff --git a/internal/cli/prompt.go b/internal/cli/prompt.go index 9a4ba70..fd0676d 100644 --- a/internal/cli/prompt.go +++ b/internal/cli/prompt.go @@ -60,15 +60,20 @@ func PromptSecret(opt models.BackendOption) (string, error) { } func PromptBool(opt models.BackendOption) (bool, error) { - return PromptYesNo(opt.Label) + return PromptYesNo(opt.Label, opt.Default == "true") } -func PromptYesNo(label string) (bool, error) { +func PromptYesNo(label string, defaultValue bool) (bool, error) { yes := i18n.Tr("Yes") no := i18n.Tr("No") + selected := 1 + if defaultValue { + selected = 0 + } sel := promptui.Select{ - Label: label, - Items: []string{yes, no}, + Label: label, + Items: []string{yes, no}, + CursorPos: selected, } _, val, err := sel.Run() return val == yes, err From 1698568d0eb2f6df81812835b9a10e2febe80227 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sat, 9 Dec 2023 22:29:15 +0100 Subject: [PATCH 053/150] Fixed crash with invalid target config name in beam commands --- internal/backends/backends.go | 6 +++++- internal/cli/transfer.go | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/backends/backends.go b/internal/backends/backends.go index 8cec9e0..a970cf5 100644 --- a/internal/backends/backends.go +++ b/internal/backends/backends.go @@ -64,8 +64,12 @@ func (l BackendList) Swap(i, j int) { type Capability = string func ResolveBackend[T interface{}](config *config.ServiceConfig) (T, error) { - backend, err := backendWithConfig(config) var result T + if config == nil { + err := fmt.Errorf("config must not be nil") + return result, err + } + backend, err := backendWithConfig(config) if err != nil { return result, err } diff --git a/internal/cli/transfer.go b/internal/cli/transfer.go index 6636bce..7f3c3c8 100644 --- a/internal/cli/transfer.go +++ b/internal/cli/transfer.go @@ -67,6 +67,7 @@ func (c *TransferCmd[E, I, R]) resolveBackends(source string, target string) err sourceConfig, err := config.GetService(source) cobra.CheckErr(err) targetConfig, err := config.GetService(target) + cobra.CheckErr(err) // Initialize backends expBackend, err := backends.ResolveBackend[E](sourceConfig) From 5755ccef01fd23f2edc44390a13168f5d2fdc64b Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sat, 9 Dec 2023 22:39:08 +0100 Subject: [PATCH 054/150] Do not apply locale formatting to Unix timestamps --- internal/cli/transfer.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/internal/cli/transfer.go b/internal/cli/transfer.go index 7f3c3c8..3816d96 100644 --- a/internal/cli/transfer.go +++ b/internal/cli/transfer.go @@ -18,6 +18,7 @@ package cli import ( "fmt" "math" + "strconv" "sync" "time" @@ -106,7 +107,7 @@ func (c *TransferCmd[E, I, R]) Transfer(exp backends.ExportProcessor[R], imp bac if err != nil { return err } - fmt.Println(i18n.Tr("From timestamp: %v (%v)", timestamp, timestamp.Unix())) + printTimestamp("From timestamp: %v (%v)", timestamp) // Prepare progress bars exportProgress := make(chan models.Progress) @@ -129,8 +130,7 @@ func (c *TransferCmd[E, I, R]) Transfer(exp backends.ExportProcessor[R], imp bac wg.Wait() progress.Wait() if result.Error != nil { - fmt.Println(i18n.Tr("Import failed, last reported timestamp was %v (%v)", - result.LastTimestamp, result.LastTimestamp.Unix())) + printTimestamp("Import failed, last reported timestamp was %v (%s)", result.LastTimestamp) return result.Error } fmt.Println(i18n.Tr("Imported %v of %v %s into %v.", @@ -168,7 +168,11 @@ func (c *TransferCmd[E, I, R]) updateTimestamp(result models.ImportResult, oldTi if result.LastTimestamp.Unix() < oldTimestamp.Unix() { result.LastTimestamp = oldTimestamp } - fmt.Println(i18n.Tr("Latest timestamp: %v (%v)\n", result.LastTimestamp, result.LastTimestamp.Unix())) + printTimestamp("Latest timestamp: %v (%v)", result.LastTimestamp) err := c.db.SetImportTimestamp(c.sourceName, c.targetName, c.entity, result.LastTimestamp) return err } + +func printTimestamp(s string, t time.Time) { + fmt.Println(i18n.Tr(s, t, strconv.FormatInt(t.Unix(), 10))) +} From c21715d36b0f1399285e4dfff607bdd7cec2df32 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sat, 9 Dec 2023 22:42:04 +0100 Subject: [PATCH 055/150] Fixed last timestamp for beam loves not getting loaded --- cmd/beam_loves.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd/beam_loves.go b/cmd/beam_loves.go index 9c2669a..19063db 100644 --- a/cmd/beam_loves.go +++ b/cmd/beam_loves.go @@ -17,6 +17,8 @@ Scotty. If not, see . package cmd import ( + "math" + "github.com/spf13/cobra" "go.uploadedlobster.com/scotty/internal/backends" "go.uploadedlobster.com/scotty/internal/cli" @@ -58,5 +60,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", 0, "Only import loves newer then given Unix timestamp") + beamLovesCmd.Flags().Int64P("timestamp", "t", math.MinInt64, "Only import loves newer then given Unix timestamp") } From 9449a29fb16b80b4933d9ec2d92ebbf61d95db8f Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sat, 9 Dec 2023 22:59:33 +0100 Subject: [PATCH 056/150] service auth, edit and delete now all support --service flag If a service name is given, this will be used. If not the user is prompted to select one. --- cmd/service_add.go | 4 ++-- cmd/service_auth.go | 12 +++--------- cmd/service_delete.go | 13 ++----------- cmd/service_edit.go | 13 ++----------- internal/backends/backends.go | 10 +++------- internal/backends/backends_test.go | 6 +++--- internal/cli/common.go | 6 ++---- internal/cli/services.go | 10 +++++++++- internal/config/services.go | 6 +++--- 9 files changed, 29 insertions(+), 51 deletions(-) diff --git a/cmd/service_add.go b/cmd/service_add.go index 64c905b..634a511 100644 --- a/cmd/service_add.go +++ b/cmd/service_add.go @@ -46,8 +46,8 @@ var serviceAddCmd = &cobra.Command{ Label: i18n.Tr("Service name"), Default: backend, Validate: func(s string) error { - srv, _ := config.GetService(s) - if srv != nil { + _, err := config.GetService(s) + if err == nil { return errors.New(i18n.Tr("a service with this name already exists")) } return config.ValidateKey(s) diff --git a/cmd/service_auth.go b/cmd/service_auth.go index 12da9dd..91125ad 100644 --- a/cmd/service_auth.go +++ b/cmd/service_auth.go @@ -17,7 +17,6 @@ Scotty. If not, see . package cmd import ( - "errors" "fmt" "os" @@ -42,11 +41,8 @@ var serviceAuthCmd = &cobra.Command{ Authentication is always done per configured service. That means you can have multiple services using the same backend but different authentication.`, Run: func(cmd *cobra.Command, args []string) { - serviceConfig := cli.GetServiceConfigFromFlag(cmd, "service") - if serviceConfig == nil { - err := errors.New(i18n.Tr("failed loading service configuration")) - cobra.CheckErr(err) - } + serviceConfig, err := cli.SelectService(cmd) + cobra.CheckErr(err) backend, err := backends.ResolveBackend[models.OAuth2Authenticator](serviceConfig) cobra.CheckErr(err) @@ -97,7 +93,5 @@ multiple services using the same backend but different authentication.`, func init() { serviceCmd.AddCommand(serviceAuthCmd) - - serviceAuthCmd.Flags().StringP("service", "s", "", "service configuration (required)") - serviceAuthCmd.MarkFlagRequired("service") + serviceAuthCmd.Flags().StringP("service", "s", "", "service configuration") } diff --git a/cmd/service_delete.go b/cmd/service_delete.go index 1d5c0ba..3a2738d 100644 --- a/cmd/service_delete.go +++ b/cmd/service_delete.go @@ -34,7 +34,7 @@ var serviceDeleteCmd = &cobra.Command{ Short: "Delete existing service configuration", Long: `Delete an existing service from the configuration file.`, Run: func(cmd *cobra.Command, args []string) { - service, err := cli.SelectService() + service, err := cli.SelectService(cmd) cobra.CheckErr(err) // Prompt for deletion @@ -58,14 +58,5 @@ var serviceDeleteCmd = &cobra.Command{ func init() { serviceCmd.AddCommand(serviceDeleteCmd) - - // Here you will define your flags and configuration settings. - - // Cobra supports Persistent Flags which will work for this command - // and all subcommands, e.g.: - // serviceDeleteCmd.PersistentFlags().String("foo", "", "A help for foo") - - // Cobra supports local flags which will only run when this command - // is called directly, e.g.: - // serviceDeleteCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") + serviceDeleteCmd.Flags().StringP("service", "s", "", "service configuration") } diff --git a/cmd/service_edit.go b/cmd/service_edit.go index 873fe76..abd790b 100644 --- a/cmd/service_edit.go +++ b/cmd/service_edit.go @@ -34,7 +34,7 @@ var serviceEditCmd = &cobra.Command{ Short: "Edit existing service configuration", Long: `Edit an existing service in the configuration file.`, Run: func(cmd *cobra.Command, args []string) { - service, err := cli.SelectService() + service, err := cli.SelectService(cmd) cobra.CheckErr(err) // Select backend @@ -55,14 +55,5 @@ var serviceEditCmd = &cobra.Command{ func init() { serviceCmd.AddCommand(serviceEditCmd) - - // Here you will define your flags and configuration settings. - - // Cobra supports Persistent Flags which will work for this command - // and all subcommands, e.g.: - // serviceEditCmd.PersistentFlags().String("foo", "", "A help for foo") - - // Cobra supports local flags which will only run when this command - // is called directly, e.g.: - // serviceEditCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") + serviceEditCmd.Flags().StringP("service", "s", "", "service configuration") } diff --git a/internal/backends/backends.go b/internal/backends/backends.go index a970cf5..f257f3d 100644 --- a/internal/backends/backends.go +++ b/internal/backends/backends.go @@ -63,12 +63,8 @@ func (l BackendList) Swap(i, j int) { type Capability = string -func ResolveBackend[T interface{}](config *config.ServiceConfig) (T, error) { +func ResolveBackend[T interface{}](config config.ServiceConfig) (T, error) { var result T - if config == nil { - err := fmt.Errorf("config must not be nil") - return result, err - } backend, err := backendWithConfig(config) if err != nil { return result, err @@ -120,12 +116,12 @@ var knownBackends = map[string]func() models.Backend{ "subsonic": func() models.Backend { return &subsonic.SubsonicApiBackend{} }, } -func backendWithConfig(config *config.ServiceConfig) (models.Backend, error) { +func backendWithConfig(config config.ServiceConfig) (models.Backend, error) { backend, err := BackendByName(config.Backend) if err != nil { return nil, err } - return backend.FromConfig(config), nil + return backend.FromConfig(&config), nil } func ImplementsInterface[T interface{}](backend *models.Backend) (bool, string) { diff --git a/internal/backends/backends_test.go b/internal/backends/backends_test.go index 0e57616..ee71e6e 100644 --- a/internal/backends/backends_test.go +++ b/internal/backends/backends_test.go @@ -42,7 +42,7 @@ func TestResolveBackend(t *testing.T) { c := viper.New() c.Set("backend", "dump") service := config.NewServiceConfig("test", c) - backend, err := backends.ResolveBackend[models.ListensImport](&service) + backend, err := backends.ResolveBackend[models.ListensImport](service) assert.NoError(t, err) assert.IsType(t, &dump.DumpBackend{}, backend) } @@ -51,7 +51,7 @@ func TestResolveBackendUnknown(t *testing.T) { c := viper.New() c.Set("backend", "foo") service := config.NewServiceConfig("test", c) - _, err := backends.ResolveBackend[models.ListensImport](&service) + _, err := backends.ResolveBackend[models.ListensImport](service) assert.EqualError(t, err, "unknown backend \"foo\"") } @@ -59,7 +59,7 @@ func TestResolveBackendInvalidInterface(t *testing.T) { c := viper.New() c.Set("backend", "dump") service := config.NewServiceConfig("test", c) - _, err := backends.ResolveBackend[models.ListensExport](&service) + _, err := backends.ResolveBackend[models.ListensExport](service) assert.EqualError(t, err, "backend dump does not implement ListensExport") } diff --git a/internal/cli/common.go b/internal/cli/common.go index 52b2360..6c8786a 100644 --- a/internal/cli/common.go +++ b/internal/cli/common.go @@ -20,11 +20,9 @@ import ( "go.uploadedlobster.com/scotty/internal/config" ) -func GetServiceConfigFromFlag(cmd *cobra.Command, flagName string) *config.ServiceConfig { +func GetServiceConfigFromFlag(cmd *cobra.Command, flagName string) (config.ServiceConfig, error) { name := cmd.Flag(flagName).Value.String() - config, err := config.GetService(name) - cobra.CheckErr(err) - return config + return config.GetService(name) } func getInt64FromFlag(cmd *cobra.Command, flagName string) (result int64) { diff --git a/internal/cli/services.go b/internal/cli/services.go index a8bd813..df27833 100644 --- a/internal/cli/services.go +++ b/internal/cli/services.go @@ -21,12 +21,20 @@ import ( "slices" "github.com/manifoldco/promptui" + "github.com/spf13/cobra" "go.uploadedlobster.com/scotty/internal/backends" "go.uploadedlobster.com/scotty/internal/config" "go.uploadedlobster.com/scotty/internal/i18n" ) -func SelectService() (config.ServiceConfig, error) { +func SelectService(cmd *cobra.Command) (config.ServiceConfig, error) { + // First try to load service from command line flag + service, err := GetServiceConfigFromFlag(cmd, "service") + if err == nil { + return service, nil + } + + // Prompt the user to select a service services := config.AllServicesAsList() if len(services) == 0 { err := errors.New(i18n.Tr("no existing service configurations")) diff --git a/internal/config/services.go b/internal/config/services.go index 2895cb2..783adc4 100644 --- a/internal/config/services.go +++ b/internal/config/services.go @@ -116,13 +116,13 @@ func AllServicesAsList() ServiceList { return list } -func GetService(name string) (*ServiceConfig, error) { +func GetService(name string) (ServiceConfig, error) { key := "service." + name config := viper.Sub(key) if config != nil { service := NewServiceConfig(name, config) - return &service, nil + return service, nil } - return nil, fmt.Errorf(i18n.Tr("no service configuration \"%v\"", name)) + return ServiceConfig{}, fmt.Errorf(i18n.Tr("no service configuration \"%v\"", name)) } From ab0e50f7aaf7f4559667c4416e465f4b74664ed6 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sat, 9 Dec 2023 23:17:43 +0100 Subject: [PATCH 057/150] Prompt user for auth after service requireing auth added --- cmd/service_add.go | 28 ++++++++++++++++ cmd/service_auth.go | 54 +------------------------------ internal/cli/auth.go | 77 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 53 deletions(-) create mode 100644 internal/cli/auth.go diff --git a/cmd/service_add.go b/cmd/service_add.go index 634a511..0bf671d 100644 --- a/cmd/service_add.go +++ b/cmd/service_add.go @@ -27,9 +27,11 @@ import ( "github.com/manifoldco/promptui" "github.com/spf13/cobra" + "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{ @@ -71,6 +73,10 @@ var serviceAddCmd = &cobra.Command{ err = service.Save() cobra.CheckErr(err) fmt.Println(i18n.Tr("Saved service %v using backend %v", service.Name, service.Backend)) + + // Check whether authentication is required + err = promptForAuth(service) + cobra.CheckErr(err) }, } @@ -87,3 +93,25 @@ func init() { // is called directly, e.g.: // serviceAddCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") } + +func promptForAuth(service config.ServiceConfig) error { + backend, err := backends.ResolveBackend[models.OAuth2Authenticator](service) + if err != nil { + // No authentication required, return + return nil + } + + doAuth, err := cli.PromptYesNo( + i18n.Tr("The backend %v requires authentication. Authenticate now?", service.Backend), + true, + ) + if err != nil { + return err + } + if !doAuth { + return nil + } + + cli.AuthenticationFlow(service, backend) + return nil +} diff --git a/cmd/service_auth.go b/cmd/service_auth.go index 91125ad..ddab35d 100644 --- a/cmd/service_auth.go +++ b/cmd/service_auth.go @@ -17,20 +17,10 @@ Scotty. If not, see . package cmd import ( - "fmt" - "os" - - "github.com/cli/browser" "github.com/spf13/cobra" - "github.com/spf13/viper" - "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" - "go.uploadedlobster.com/scotty/internal/storage" - "golang.org/x/oauth2" ) var serviceAuthCmd = &cobra.Command{ @@ -45,49 +35,7 @@ multiple services using the same backend but different authentication.`, cobra.CheckErr(err) backend, err := backends.ResolveBackend[models.OAuth2Authenticator](serviceConfig) cobra.CheckErr(err) - - redirectURL, err := backends.BuildRedirectURL(viper.GetViper(), backend.Name()) - cobra.CheckErr(err) - - // The backend must provide an authentication strategy - strategy := backend.OAuth2Strategy(redirectURL) - - // use PKCE to protect against CSRF attacks - // https://www.ietf.org/archive/id/draft-ietf-oauth-security-topics-22.html#name-countermeasures-6 - verifier := oauth2.GenerateVerifier() - - state := auth.RandomState() - // Redirect user to consent page to ask for permission specified scopes. - authUrl := strategy.AuthCodeURL(verifier, state) - - // Start an HTTP server to listen for the response - responseChan := make(chan auth.CodeResponse) - auth.RunOauth2CallbackServer(*redirectURL, authUrl.Param, responseChan) - - // Open the URL - fmt.Println(i18n.Tr("Visit the URL for authorization: %v", authUrl.Url)) - err = browser.OpenURL(authUrl.Url) - cobra.CheckErr(err) - - // Retrieve the code from the authentication callback - code := <-responseChan - if code.State != authUrl.State { - cobra.CompErrorln(i18n.Tr("Error: OAuth state mismatch")) - os.Exit(1) - } - - // Exchange the code for the authentication token - tok, err := strategy.ExchangeToken(code, verifier) - cobra.CheckErr(err) - - // Store the retrieved token in the database - db, err := storage.New(config.DatabasePath()) - cobra.CheckErr(err) - - err = db.SetOAuth2Token(serviceConfig.Name, tok) - cobra.CheckErr(err) - - fmt.Println(i18n.Tr("Access token received, you can use %v now.\n", serviceConfig.Name)) + cli.AuthenticationFlow(serviceConfig, backend) }, } diff --git a/internal/cli/auth.go b/internal/cli/auth.go new file mode 100644 index 0000000..fc5c889 --- /dev/null +++ b/internal/cli/auth.go @@ -0,0 +1,77 @@ +/* +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 cli + +import ( + "fmt" + "os" + + "github.com/cli/browser" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "go.uploadedlobster.com/scotty/internal/auth" + "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) { + redirectURL, err := backends.BuildRedirectURL(viper.GetViper(), backend.Name()) + cobra.CheckErr(err) + + // The backend must provide an authentication strategy + strategy := backend.OAuth2Strategy(redirectURL) + + // use PKCE to protect against CSRF attacks + // https://www.ietf.org/archive/id/draft-ietf-oauth-security-topics-22.html#name-countermeasures-6 + verifier := oauth2.GenerateVerifier() + + state := auth.RandomState() + // Redirect user to consent page to ask for permission specified scopes. + authUrl := strategy.AuthCodeURL(verifier, state) + + // Start an HTTP server to listen for the response + responseChan := make(chan auth.CodeResponse) + auth.RunOauth2CallbackServer(*redirectURL, authUrl.Param, responseChan) + + // Open the URL + fmt.Println(i18n.Tr("Visit the URL for authorization: %v", authUrl.Url)) + err = browser.OpenURL(authUrl.Url) + cobra.CheckErr(err) + + // Retrieve the code from the authentication callback + code := <-responseChan + if code.State != authUrl.State { + cobra.CompErrorln(i18n.Tr("Error: OAuth state mismatch")) + os.Exit(1) + } + + // Exchange the code for the authentication token + tok, err := strategy.ExchangeToken(code, verifier) + cobra.CheckErr(err) + + // Store the retrieved token in the database + db, err := storage.New(config.DatabasePath()) + cobra.CheckErr(err) + + err = db.SetOAuth2Token(service.Name, tok) + cobra.CheckErr(err) + + fmt.Println(i18n.Tr("Access token received, you can use %v now.\n", service.Name)) +} From 3d52f8779af8a5deb944bec747a246b578a650f7 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sat, 9 Dec 2023 23:19:23 +0100 Subject: [PATCH 058/150] Update translations --- internal/translations/catalog.go | 190 +++++++++--------- .../translations/locales/de/out.gotext.json | 142 ++++++------- .../translations/locales/en/out.gotext.json | 156 +++++++------- 3 files changed, 247 insertions(+), 241 deletions(-) diff --git a/internal/translations/catalog.go b/internal/translations/catalog.go index 7ccdaa0..40dee94 100644 --- a/internal/translations/catalog.go +++ b/internal/translations/catalog.go @@ -39,119 +39,117 @@ func init() { } var messageKeyToIndex = map[string]int{ - "\tbackend: %v": 24, - "\texport: %s": 10, - "\timport: %s\n": 11, - "Aborted": 21, - "Access token": 32, - "Access token received, you can use %v now.\n": 19, - "Append to file": 38, - "Backend": 43, - "Client ID": 28, - "Client secret": 29, - "Delete the service configuration \"%v\"?": 20, - "Disable auto correction of submitted listens": 36, - "During the import the following errors occurred:": 7, - "Error: %v\n": 8, - "Error: OAuth state mismatch": 18, - "Failed reading config: %v": 12, - "File path": 33, - "From timestamp: %v (%v)": 4, - "Import failed, last reported timestamp was %v (%v)": 5, - "Imported %v of %v %s into %v.": 6, - "Include skipped listens": 37, - "Latest timestamp: %v (%v)\n": 9, - "No": 40, - "Playlist title": 34, - "Saved service %v using backend %v": 15, - "Server URL": 30, - "Service": 42, - "Service \"%v\" deleted\n": 22, - "Service name": 13, - "Token received, you can close this window now.": 25, - "Transferring %s from %s to %s...": 3, - "Unique playlist identifier": 35, - "Updated service %v using backend %v\n": 23, - "User name": 31, - "Visit the URL for authorization: %v": 17, - "Yes": 39, - "a service with this name already exists": 14, - "backend %s does not implement %s": 26, - "done": 2, - "exporting": 0, - "failed loading service configuration": 16, - "importing": 1, - "key must only consist of A-Za-z0-9_-": 45, - "no configuration file defined, cannot write config": 44, - "no existing service configurations": 41, - "no service configuration \"%v\"": 46, - "unknown backend \"%s\"": 27, + "\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": 32, + "Backend": 40, + "Client ID": 22, + "Client secret": 23, + "Delete the service configuration \"%v\"?": 14, + "Disable auto correction of submitted listens": 30, + "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": 31, + "Latest timestamp: %v (%v)": 43, + "No": 37, + "Playlist title": 28, + "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": 29, + "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, + "key must only consist of A-Za-z0-9_-": 45, + "no configuration file defined, cannot write config": 44, + "no existing service configurations": 38, + "no service configuration \"%v\"": 46, + "unknown backend \"%s\"": 21, } var deIndex = []uint32{ // 48 elements // Entry 0 - 1F 0x00000000, 0x0000000b, 0x00000016, 0x0000001d, - 0x00000046, 0x00000064, 0x000000a4, 0x000000cf, - 0x00000106, 0x00000119, 0x00000142, 0x00000142, - 0x00000142, 0x00000142, 0x00000142, 0x00000142, - 0x00000142, 0x00000142, 0x00000142, 0x00000142, - 0x00000142, 0x00000142, 0x00000142, 0x00000142, - 0x00000142, 0x00000142, 0x00000142, 0x00000142, - 0x00000142, 0x00000142, 0x00000142, 0x00000142, + 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, // Entry 20 - 3F - 0x00000142, 0x00000142, 0x00000142, 0x00000142, - 0x00000142, 0x00000142, 0x00000142, 0x00000142, - 0x00000142, 0x00000142, 0x00000142, 0x00000142, - 0x00000142, 0x00000142, 0x00000142, 0x00000142, + 0x000000bb, 0x000000bb, 0x000000bb, 0x000000bb, + 0x000000bb, 0x000000bb, 0x000000bb, 0x000000bb, + 0x000000bb, 0x000000bb, 0x000000bb, 0x000000bb, + 0x000000bb, 0x000000bb, 0x000000bb, 0x000000bb, } // Size: 216 bytes -const deData string = "" + // Size: 322 bytes +const deData string = "" + // Size: 187 bytes "\x02exportiere\x02importiere\x02fertig\x02Übertrage %[1]s von %[2]s nach" + - " %[3]s...\x02Ab Zeitstempel: %[1]v (%[2]v)\x02Import fehlgeschlagen, der" + - " letzte Zeitstempel war %[1]v (%[2]v)\x02%[1]v von %[2]v %[3]s in %[4]v " + - "importiert.\x02Während des Imports sind folgende Fehler aufgetreten:\x04" + - "\x00\x01\x0a\x0e\x02Fehler: %[1]v\x04\x00\x01\x0a$\x02Neuester Zeitstemp" + - "el: %[1]v (%[2]v)" + " %[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" var enIndex = []uint32{ // 48 elements // Entry 0 - 1F 0x00000000, 0x0000000a, 0x00000014, 0x00000019, - 0x00000043, 0x00000061, 0x0000009a, 0x000000c4, - 0x000000f5, 0x00000107, 0x0000012c, 0x0000013f, - 0x00000153, 0x00000170, 0x0000017d, 0x000001a5, - 0x000001cd, 0x000001f2, 0x00000219, 0x00000235, - 0x00000268, 0x00000292, 0x0000029a, 0x000002b7, - 0x000002e6, 0x000002fa, 0x00000329, 0x00000350, - 0x00000368, 0x00000372, 0x00000380, 0x0000038b, + 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, 0x000002fc, 0x00000329, // Entry 20 - 3F - 0x00000395, 0x000003a2, 0x000003ac, 0x000003bb, - 0x000003d6, 0x00000403, 0x0000041b, 0x0000042a, - 0x0000042e, 0x00000431, 0x00000454, 0x0000045c, - 0x00000464, 0x00000497, 0x000004bc, 0x000004dd, + 0x00000341, 0x00000350, 0x00000377, 0x00000393, + 0x000003c6, 0x000003ca, 0x000003cd, 0x000003f0, + 0x000003f8, 0x00000400, 0x0000041e, 0x00000457, + 0x00000477, 0x000004aa, 0x000004cf, 0x000004f0, } // Size: 216 bytes -const enData string = "" + // Size: 1245 bytes +const enData string = "" + // Size: 1264 bytes "\x02exporting\x02importing\x02done\x02Transferring %[1]s from %[2]s to %" + - "[3]s...\x02From timestamp: %[1]v (%[2]v)\x02Import failed, last reported" + - " timestamp was %[1]v (%[2]v)\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\x04\x00\x01\x0a \x02Latest timestamp: %[1]v (%[2]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" + - " this name already exists\x02Saved service %[1]v using backend %[2]v\x02" + - "failed loading service configuration\x02Visit the URL for authorization:" + - " %[1]v\x02Error: OAuth state mismatch\x04\x00\x01\x0a.\x02Access token r" + - "eceived, you can use %[1]v now.\x02Delete the service configuration \x22" + - "%[1]v\x22?\x02Aborted\x04\x00\x01\x0a\x18\x02Service \x22%[1]v\x22 delet" + - "ed\x04\x00\x01\x0a*\x02Updated service %[1]v using backend %[2]v\x04\x01" + - "\x09\x00\x0f\x02backend: %[1]v\x02Token received, you can close this win" + - "dow now.\x02backend %[1]s does not implement %[2]s\x02unknown backend " + - "\x22%[1]s\x22\x02Client ID\x02Client secret\x02Server URL\x02User name" + - "\x02Access token\x02File path\x02Playlist title\x02Unique playlist ident" + - "ifier\x02Disable auto correction of submitted listens\x02Include skipped" + - " listens\x02Append to file\x02Yes\x02No\x02no existing service configura" + - "tions\x02Service\x02Backend\x02no configuration file defined, cannot wri" + + "[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\x02Playlist title\x02" + + "Unique playlist identifier\x02Disable auto correction of submitted liste" + + "ns\x02Include skipped listens\x02Append to file\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" - // Total table size 1999 bytes (1KiB); checksum: FFEA1B2A + // Total table size 1883 bytes (1KiB); checksum: 6875B9DE diff --git a/internal/translations/locales/de/out.gotext.json b/internal/translations/locales/de/out.gotext.json index 6efac3d..b608d02 100644 --- a/internal/translations/locales/de/out.gotext.json +++ b/internal/translations/locales/de/out.gotext.json @@ -80,42 +80,17 @@ ] }, { - "id": "failed loading service configuration", - "message": "failed loading service configuration", - "translation": "" - }, - { - "id": "Visit the URL for authorization: {Url}", - "message": "Visit the URL for authorization: {Url}", + "id": "The backend {Backend} requires authentication. Authenticate now?", + "message": "The backend {Backend} requires authentication. Authenticate now?", "translation": "", "placeholders": [ { - "id": "Url", + "id": "Backend", "string": "%[1]v", "type": "string", "underlyingType": "string", "argNum": 1, - "expr": "authUrl.Url" - } - ] - }, - { - "id": "Error: OAuth state mismatch", - "message": "Error: OAuth state mismatch", - "translation": "" - }, - { - "id": "Access token received, you can use {Name} now.", - "message": "Access token received, you can use {Name} now.", - "translation": "", - "placeholders": [ - { - "id": "Name", - "string": "%[1]v", - "type": "string", - "underlyingType": "string", - "argNum": 1, - "expr": "serviceConfig.Name" + "expr": "service.Backend" } ] }, @@ -290,6 +265,41 @@ "message": "Append to file", "translation": "" }, + { + "id": "Visit the URL for authorization: {Url}", + "message": "Visit the URL for authorization: {Url}", + "translation": "", + "placeholders": [ + { + "id": "Url", + "string": "%[1]v", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "authUrl.Url" + } + ] + }, + { + "id": "Error: OAuth state mismatch", + "message": "Error: OAuth state mismatch", + "translation": "" + }, + { + "id": "Access token received, you can use {Name} now.", + "message": "Access token received, you can use {Name} now.", + "translation": "", + "placeholders": [ + { + "id": "Name", + "string": "%[1]v", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "service.Name" + } + ] + }, { "id": "exporting", "message": "exporting", @@ -368,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 } ] }, @@ -473,25 +479,23 @@ ] }, { - "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 } ] }, diff --git a/internal/translations/locales/en/out.gotext.json b/internal/translations/locales/en/out.gotext.json index 23d2710..170000e 100644 --- a/internal/translations/locales/en/out.gotext.json +++ b/internal/translations/locales/en/out.gotext.json @@ -92,49 +92,18 @@ "fuzzy": true }, { - "id": "failed loading service configuration", - "message": "failed loading service configuration", - "translation": "failed loading service configuration", - "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}", + "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": "Url", + "id": "Backend", "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": "serviceConfig.Name" + "expr": "service.Backend" } ], "fuzzy": true @@ -348,6 +317,47 @@ "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", @@ -438,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 @@ -553,26 +559,24 @@ "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 From a751b7182cc3138f030d9d80102bd6763c929b32 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sat, 9 Dec 2023 23:20:12 +0100 Subject: [PATCH 059/150] Bump version to 0.3.1 --- internal/version/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/version/version.go b/internal/version/version.go index f3b30fa..82f11ab 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -17,7 +17,7 @@ package version const ( AppName = "scotty" - AppVersion = "0.3.0" + AppVersion = "0.3.1" ) func UserAgent() string { From 4d07a39b64ce9d2a9926e46a78652a78a2dd9fd2 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 10 Dec 2023 00:24:39 +0100 Subject: [PATCH 060/150] 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 061/150] 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 062/150] 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 063/150] 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 064/150] 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 065/150] 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 066/150] 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 067/150] 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 068/150] 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 069/150] 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 070/150] 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 071/150] 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 072/150] 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 073/150] 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 074/150] 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 075/150] 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 076/150] 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 077/150] 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 078/150] 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 079/150] 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 080/150] 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 081/150] 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 082/150] 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 083/150] 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 084/150] 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 085/150] 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 086/150] 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 087/150] 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 088/150] 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 089/150] 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 090/150] 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 091/150] 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 092/150] 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 093/150] 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 094/150] 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 095/150] 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 096/150] 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 097/150] 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 098/150] 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 099/150] 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 100/150] 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 101/150] 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 102/150] 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 103/150] 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 104/150] 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 105/150] 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 106/150] 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 107/150] 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 108/150] 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 109/150] 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 110/150] 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 111/150] 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 112/150] 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 113/150] 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 114/150] 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 115/150] 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 116/150] 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 117/150] 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 118/150] 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 119/150] 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 120/150] 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 121/150] 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 122/150] 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 123/150] 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 124/150] 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 125/150] 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 126/150] 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 127/150] 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 128/150] 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 129/150] 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 130/150] 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 131/150] 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 132/150] 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 133/150] 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 134/150] 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 135/150] 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 136/150] 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 137/150] 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 138/150] 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 139/150] 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 140/150] 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 141/150] 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 From d51c97c648883e9c29e7b7c80a2eca77186e11dc Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 29 Apr 2025 13:23:41 +0200 Subject: [PATCH 142/150] Code style: All uppercase acronyms URL, ISRC, ID, HTTP --- internal/auth/auth.go | 2 +- internal/auth/strategy.go | 12 +++--- internal/backends/deezer/auth.go | 6 +-- internal/backends/deezer/client.go | 6 +-- internal/backends/deezer/client_test.go | 6 +-- internal/backends/deezer/deezer.go | 14 +++---- internal/backends/deezer/models.go | 6 +-- internal/backends/funkwhale/client.go | 12 +++--- internal/backends/funkwhale/client_test.go | 22 +++++----- internal/backends/funkwhale/models.go | 12 +++--- internal/backends/jspf/jspf.go | 6 +-- internal/backends/lastfm/auth.go | 10 ++--- internal/backends/lastfm/lastfm.go | 8 ++-- internal/backends/listenbrainz/client.go | 14 +++---- internal/backends/listenbrainz/client_test.go | 14 +++---- internal/backends/listenbrainz/models_test.go | 2 +- internal/backends/maloja/client.go | 12 +++--- internal/backends/maloja/client_test.go | 16 ++++---- internal/backends/spotify/client.go | 8 ++-- internal/backends/spotify/client_test.go | 6 +-- internal/backends/spotify/models.go | 20 +++++----- internal/backends/spotify/spotify.go | 40 +++++++++---------- internal/backends/spotifyhistory/models.go | 4 +- internal/cli/auth.go | 10 ++--- pkg/jspf/extensions.go | 4 +- pkg/jspf/extensions_test.go | 2 +- 26 files changed, 137 insertions(+), 137 deletions(-) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 5ba05af..84c85b6 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -27,7 +27,7 @@ type OAuth2Authenticator interface { models.Backend // Returns OAuth2 config suitable for this backend - OAuth2Strategy(redirectUrl *url.URL) OAuth2Strategy + OAuth2Strategy(redirectURL *url.URL) OAuth2Strategy // Setup the OAuth2 client OAuth2Setup(token oauth2.TokenSource) error diff --git a/internal/auth/strategy.go b/internal/auth/strategy.go index 3d03fa4..7e3c265 100644 --- a/internal/auth/strategy.go +++ b/internal/auth/strategy.go @@ -24,14 +24,14 @@ import ( type OAuth2Strategy interface { Config() oauth2.Config - AuthCodeURL(verifier string, state string) AuthUrl + AuthCodeURL(verifier string, state string) AuthURL ExchangeToken(code CodeResponse, verifier string) (*oauth2.Token, error) } -type AuthUrl struct { +type AuthURL struct { // The URL the user must visit to approve access - Url string + URL string // Random state string passed on to the callback. // Leave empty if the service does not support state. State string @@ -56,10 +56,10 @@ func (s StandardStrategy) Config() oauth2.Config { return s.conf } -func (s StandardStrategy) AuthCodeURL(verifier string, state string) AuthUrl { +func (s StandardStrategy) AuthCodeURL(verifier string, state string) AuthURL { url := s.conf.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(verifier)) - return AuthUrl{ - Url: url, + return AuthURL{ + URL: url, State: state, Param: "code", } diff --git a/internal/backends/deezer/auth.go b/internal/backends/deezer/auth.go index aa30b04..0304dec 100644 --- a/internal/backends/deezer/auth.go +++ b/internal/backends/deezer/auth.go @@ -33,10 +33,10 @@ func (s deezerStrategy) Config() oauth2.Config { return s.conf } -func (s deezerStrategy) AuthCodeURL(verifier string, state string) auth.AuthUrl { +func (s deezerStrategy) AuthCodeURL(verifier string, state string) auth.AuthURL { url := s.conf.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(verifier)) - return auth.AuthUrl{ - Url: url, + return auth.AuthURL{ + URL: url, State: state, Param: "code", } diff --git a/internal/backends/deezer/client.go b/internal/backends/deezer/client.go index 3c3b740..05264ae 100644 --- a/internal/backends/deezer/client.go +++ b/internal/backends/deezer/client.go @@ -36,7 +36,7 @@ const MaxItemsPerGet = 1000 const DefaultRateLimitWaitSeconds = 5 type Client struct { - HttpClient *resty.Client + HTTPClient *resty.Client token oauth2.TokenSource } @@ -47,7 +47,7 @@ func NewClient(token oauth2.TokenSource) Client { client.SetHeader("User-Agent", version.UserAgent()) client.SetRetryCount(5) return Client{ - HttpClient: client, + HTTPClient: client, token: token, } } @@ -73,7 +73,7 @@ func (c Client) setToken(req *resty.Request) error { } func listRequest[T Result](c Client, path string, offset int, limit int) (result T, err error) { - request := c.HttpClient.R(). + request := c.HTTPClient.R(). SetQueryParams(map[string]string{ "index": strconv.Itoa(offset), "limit": strconv.Itoa(limit), diff --git a/internal/backends/deezer/client_test.go b/internal/backends/deezer/client_test.go index f8240f8..c90b01a 100644 --- a/internal/backends/deezer/client_test.go +++ b/internal/backends/deezer/client_test.go @@ -44,7 +44,7 @@ func TestGetUserHistory(t *testing.T) { token := oauth2.StaticTokenSource(&oauth2.Token{}) client := deezer.NewClient(token) - setupHttpMock(t, client.HttpClient.GetClient(), + setupHTTPMock(t, client.HTTPClient.GetClient(), "https://api.deezer.com/user/me/history", "testdata/user-history.json") @@ -65,7 +65,7 @@ func TestGetUserTracks(t *testing.T) { token := oauth2.StaticTokenSource(&oauth2.Token{}) client := deezer.NewClient(token) - setupHttpMock(t, client.HttpClient.GetClient(), + setupHTTPMock(t, client.HTTPClient.GetClient(), "https://api.deezer.com/user/me/tracks", "testdata/user-tracks.json") @@ -81,7 +81,7 @@ func TestGetUserTracks(t *testing.T) { assert.Equal("Outland", track1.Track.Album.Title) } -func setupHttpMock(t *testing.T, client *http.Client, url string, testDataPath string) { +func setupHTTPMock(t *testing.T, client *http.Client, url string, testDataPath string) { httpmock.ActivateNonDefault(client) responder, err := httpmock.NewJsonResponder(200, httpmock.File(testDataPath)) diff --git a/internal/backends/deezer/deezer.go b/internal/backends/deezer/deezer.go index e7d9762..7b6e1ad 100644 --- a/internal/backends/deezer/deezer.go +++ b/internal/backends/deezer/deezer.go @@ -31,7 +31,7 @@ import ( type DeezerApiBackend struct { client Client - clientId string + clientID string clientSecret string } @@ -50,19 +50,19 @@ func (b *DeezerApiBackend) Options() []models.BackendOption { } func (b *DeezerApiBackend) InitConfig(config *config.ServiceConfig) error { - b.clientId = config.GetString("client-id") + b.clientID = config.GetString("client-id") b.clientSecret = config.GetString("client-secret") return nil } -func (b *DeezerApiBackend) OAuth2Strategy(redirectUrl *url.URL) auth.OAuth2Strategy { +func (b *DeezerApiBackend) OAuth2Strategy(redirectURL *url.URL) auth.OAuth2Strategy { conf := oauth2.Config{ - ClientID: b.clientId, + ClientID: b.clientID, ClientSecret: b.clientSecret, Scopes: []string{ "offline_access,basic_access,listening_history", }, - RedirectURL: redirectUrl.String(), + RedirectURL: redirectURL.String(), Endpoint: oauth2.Endpoint{ AuthURL: "https://connect.deezer.com/oauth/auth.php", TokenURL: "https://connect.deezer.com/oauth/access_token.php", @@ -244,8 +244,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/album/%v", t.Album.Id) - info["deezer_artist_id"] = fmt.Sprintf("https://www.deezer.com/artist/%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/models.go b/internal/backends/deezer/models.go index 712cd73..85b569c 100644 --- a/internal/backends/deezer/models.go +++ b/internal/backends/deezer/models.go @@ -51,7 +51,7 @@ type HistoryResult struct { } type Track struct { - Id int `json:"id"` + ID int `json:"id"` Type string `json:"type"` Link string `json:"link"` Title string `json:"title"` @@ -75,7 +75,7 @@ type LovedTrack struct { } type Album struct { - Id int `json:"id"` + ID int `json:"id"` Type string `json:"type"` Link string `json:"link"` Title string `json:"title"` @@ -83,7 +83,7 @@ type Album struct { } type Artist struct { - Id int `json:"id"` + ID int `json:"id"` Type string `json:"type"` Link string `json:"link"` Name string `json:"name"` diff --git a/internal/backends/funkwhale/client.go b/internal/backends/funkwhale/client.go index 8f2848b..c231c94 100644 --- a/internal/backends/funkwhale/client.go +++ b/internal/backends/funkwhale/client.go @@ -33,13 +33,13 @@ import ( const MaxItemsPerGet = 50 type Client struct { - HttpClient *resty.Client + HTTPClient *resty.Client token string } -func NewClient(serverUrl string, token string) Client { +func NewClient(serverURL string, token string) Client { client := resty.New() - client.SetBaseURL(serverUrl) + client.SetBaseURL(serverURL) client.SetAuthScheme("Bearer") client.SetAuthToken(token) client.SetHeader("Accept", "application/json") @@ -49,14 +49,14 @@ func NewClient(serverUrl string, token string) Client { ratelimit.EnableHTTPHeaderRateLimit(client, "Retry-After") return Client{ - HttpClient: client, + HTTPClient: client, token: token, } } func (c Client) GetHistoryListenings(user string, page int, perPage int) (result ListeningsResult, err error) { const path = "/api/v1/history/listenings" - response, err := c.HttpClient.R(). + response, err := c.HTTPClient.R(). SetQueryParams(map[string]string{ "username": user, "page": strconv.Itoa(page), @@ -75,7 +75,7 @@ func (c Client) GetHistoryListenings(user string, page int, perPage int) (result func (c Client) GetFavoriteTracks(page int, perPage int) (result FavoriteTracksResult, err error) { const path = "/api/v1/favorites/tracks" - response, err := c.HttpClient.R(). + response, err := c.HTTPClient.R(). SetQueryParams(map[string]string{ "page": strconv.Itoa(page), "page_size": strconv.Itoa(perPage), diff --git a/internal/backends/funkwhale/client_test.go b/internal/backends/funkwhale/client_test.go index 89325cd..e850a4d 100644 --- a/internal/backends/funkwhale/client_test.go +++ b/internal/backends/funkwhale/client_test.go @@ -32,20 +32,20 @@ import ( ) func TestNewClient(t *testing.T) { - serverUrl := "https://funkwhale.example.com" + serverURL := "https://funkwhale.example.com" token := "foobar123" - client := funkwhale.NewClient(serverUrl, token) - assert.Equal(t, serverUrl, client.HttpClient.BaseURL) - assert.Equal(t, token, client.HttpClient.Token) + client := funkwhale.NewClient(serverURL, token) + assert.Equal(t, serverURL, client.HTTPClient.BaseURL) + assert.Equal(t, token, client.HTTPClient.Token) } func TestGetHistoryListenings(t *testing.T) { defer httpmock.DeactivateAndReset() - serverUrl := "https://funkwhale.example.com" + serverURL := "https://funkwhale.example.com" token := "thetoken" - client := funkwhale.NewClient(serverUrl, token) - setupHttpMock(t, client.HttpClient.GetClient(), + client := funkwhale.NewClient(serverURL, token) + setupHTTPMock(t, client.HTTPClient.GetClient(), "https://funkwhale.example.com/api/v1/history/listenings", "testdata/listenings.json") @@ -67,9 +67,9 @@ func TestGetFavoriteTracks(t *testing.T) { defer httpmock.DeactivateAndReset() token := "thetoken" - serverUrl := "https://funkwhale.example.com" - client := funkwhale.NewClient(serverUrl, token) - setupHttpMock(t, client.HttpClient.GetClient(), + serverURL := "https://funkwhale.example.com" + client := funkwhale.NewClient(serverURL, token) + setupHTTPMock(t, client.HTTPClient.GetClient(), "https://funkwhale.example.com/api/v1/favorites/tracks", "testdata/favorite-tracks.json") @@ -87,7 +87,7 @@ func TestGetFavoriteTracks(t *testing.T) { assert.Equal("phw", fav1.User.UserName) } -func setupHttpMock(t *testing.T, client *http.Client, url string, testDataPath string) { +func setupHTTPMock(t *testing.T, client *http.Client, url string, testDataPath string) { httpmock.ActivateNonDefault(client) responder, err := httpmock.NewJsonResponder(200, httpmock.File(testDataPath)) diff --git a/internal/backends/funkwhale/models.go b/internal/backends/funkwhale/models.go index 10d57d0..faaae12 100644 --- a/internal/backends/funkwhale/models.go +++ b/internal/backends/funkwhale/models.go @@ -31,7 +31,7 @@ type ListeningsResult struct { } type Listening struct { - Id int `json:"int"` + ID int `json:"int"` User User `json:"user"` Track Track `json:"track"` CreationDate string `json:"creation_date"` @@ -45,14 +45,14 @@ type FavoriteTracksResult struct { } type FavoriteTrack struct { - Id int `json:"int"` + ID int `json:"int"` User User `json:"user"` Track Track `json:"track"` CreationDate string `json:"creation_date"` } type Track struct { - Id int `json:"int"` + ID int `json:"int"` Artist Artist `json:"artist"` Album Album `json:"album"` Title string `json:"title"` @@ -64,13 +64,13 @@ type Track struct { } type Artist struct { - Id int `json:"int"` + ID int `json:"int"` Name string `json:"name"` ArtistMBID mbtypes.MBID `json:"mbid"` } type Album struct { - Id int `json:"int"` + ID int `json:"int"` Title string `json:"title"` AlbumArtist Artist `json:"artist"` ReleaseDate string `json:"release_date"` @@ -79,7 +79,7 @@ type Album struct { } type User struct { - Id int `json:"int"` + ID int `json:"int"` UserName string `json:"username"` } diff --git a/internal/backends/jspf/jspf.go b/internal/backends/jspf/jspf.go index 152c810..3e6866d 100644 --- a/internal/backends/jspf/jspf.go +++ b/internal/backends/jspf/jspf.go @@ -69,7 +69,7 @@ func (b *JSPFBackend) InitConfig(config *config.ServiceConfig) error { Identifier: config.GetString("identifier"), Tracks: make([]jspf.Track, 0), Extension: map[string]any{ - jspf.MusicBrainzPlaylistExtensionId: jspf.MusicBrainzPlaylistExtension{ + jspf.MusicBrainzPlaylistExtensionID: jspf.MusicBrainzPlaylistExtension{ LastModifiedAt: time.Now(), Public: true, }, @@ -116,7 +116,7 @@ func listenAsTrack(l models.Listen) jspf.Track { extension := makeMusicBrainzExtension(l.Track) extension.AddedAt = l.ListenedAt extension.AddedBy = l.UserName - track.Extension[jspf.MusicBrainzTrackExtensionId] = extension + track.Extension[jspf.MusicBrainzTrackExtensionID] = extension if l.RecordingMBID != "" { track.Identifier = append(track.Identifier, "https://musicbrainz.org/recording/"+string(l.RecordingMBID)) @@ -131,7 +131,7 @@ func loveAsTrack(l models.Love) jspf.Track { extension := makeMusicBrainzExtension(l.Track) extension.AddedAt = l.Created extension.AddedBy = l.UserName - track.Extension[jspf.MusicBrainzTrackExtensionId] = extension + track.Extension[jspf.MusicBrainzTrackExtensionID] = extension recordingMBID := l.Track.RecordingMBID if l.RecordingMBID != "" { diff --git a/internal/backends/lastfm/auth.go b/internal/backends/lastfm/auth.go index bbc65bc..9d43e0c 100644 --- a/internal/backends/lastfm/auth.go +++ b/internal/backends/lastfm/auth.go @@ -25,21 +25,21 @@ import ( type lastfmStrategy struct { client *lastfm.Api - redirectUrl *url.URL + redirectURL *url.URL } func (s lastfmStrategy) Config() oauth2.Config { return oauth2.Config{} } -func (s lastfmStrategy) AuthCodeURL(verifier string, state string) auth.AuthUrl { +func (s lastfmStrategy) AuthCodeURL(verifier string, state string) auth.AuthURL { // Last.fm does not use OAuth2, but the provided authorization flow with // callback URL is close enough we can shoehorn it into the existing // authentication strategy. // TODO: Investigate and use callback-less flow with api.GetAuthTokenUrl(token) - url := s.client.GetAuthRequestUrl(s.redirectUrl.String()) - return auth.AuthUrl{ - Url: url, + url := s.client.GetAuthRequestUrl(s.redirectURL.String()) + return auth.AuthURL{ + URL: url, State: "", // last.fm does not use state Param: "token", } diff --git a/internal/backends/lastfm/lastfm.go b/internal/backends/lastfm/lastfm.go index 2d4a9d5..76fe9c7 100644 --- a/internal/backends/lastfm/lastfm.go +++ b/internal/backends/lastfm/lastfm.go @@ -62,9 +62,9 @@ func (b *LastfmApiBackend) Options() []models.BackendOption { } func (b *LastfmApiBackend) InitConfig(config *config.ServiceConfig) error { - clientId := config.GetString("client-id") + clientID := config.GetString("client-id") clientSecret := config.GetString("client-secret") - b.client = lastfm.New(clientId, clientSecret) + b.client = lastfm.New(clientID, clientSecret) b.username = config.GetString("username") return nil } @@ -72,10 +72,10 @@ func (b *LastfmApiBackend) InitConfig(config *config.ServiceConfig) error { func (b *LastfmApiBackend) StartImport() error { return nil } func (b *LastfmApiBackend) FinishImport() error { return nil } -func (b *LastfmApiBackend) OAuth2Strategy(redirectUrl *url.URL) auth.OAuth2Strategy { +func (b *LastfmApiBackend) OAuth2Strategy(redirectURL *url.URL) auth.OAuth2Strategy { return lastfmStrategy{ client: b.client, - redirectUrl: redirectUrl, + redirectURL: redirectURL, } } diff --git a/internal/backends/listenbrainz/client.go b/internal/backends/listenbrainz/client.go index 1917bd4..fff476c 100644 --- a/internal/backends/listenbrainz/client.go +++ b/internal/backends/listenbrainz/client.go @@ -39,7 +39,7 @@ const ( ) type Client struct { - HttpClient *resty.Client + HTTPClient *resty.Client MaxResults int } @@ -55,7 +55,7 @@ func NewClient(token string) Client { ratelimit.EnableHTTPHeaderRateLimit(client, "X-RateLimit-Reset-In") return Client{ - HttpClient: client, + HTTPClient: client, MaxResults: DefaultItemsPerGet, } } @@ -63,7 +63,7 @@ func NewClient(token string) Client { func (c Client) GetListens(user string, maxTime time.Time, minTime time.Time) (result GetListensResult, err error) { const path = "/user/{username}/listens" errorResult := ErrorResult{} - response, err := c.HttpClient.R(). + response, err := c.HTTPClient.R(). SetPathParam("username", user). SetQueryParams(map[string]string{ "max_ts": strconv.FormatInt(maxTime.Unix(), 10), @@ -84,7 +84,7 @@ func (c Client) GetListens(user string, maxTime time.Time, minTime time.Time) (r func (c Client) SubmitListens(listens ListenSubmission) (result StatusResult, err error) { const path = "/submit-listens" errorResult := ErrorResult{} - response, err := c.HttpClient.R(). + response, err := c.HTTPClient.R(). SetBody(listens). SetResult(&result). SetError(&errorResult). @@ -100,7 +100,7 @@ func (c Client) SubmitListens(listens ListenSubmission) (result StatusResult, er func (c Client) GetFeedback(user string, status int, offset int) (result GetFeedbackResult, err error) { const path = "/feedback/user/{username}/get-feedback" errorResult := ErrorResult{} - response, err := c.HttpClient.R(). + response, err := c.HTTPClient.R(). SetPathParam("username", user). SetQueryParams(map[string]string{ "status": strconv.Itoa(status), @@ -122,7 +122,7 @@ func (c Client) GetFeedback(user string, status int, offset int) (result GetFeed func (c Client) SendFeedback(feedback Feedback) (result StatusResult, err error) { const path = "/feedback/recording-feedback" errorResult := ErrorResult{} - response, err := c.HttpClient.R(). + response, err := c.HTTPClient.R(). SetBody(feedback). SetResult(&result). SetError(&errorResult). @@ -138,7 +138,7 @@ func (c Client) SendFeedback(feedback Feedback) (result StatusResult, err error) func (c Client) Lookup(recordingName string, artistName string) (result LookupResult, err error) { const path = "/metadata/lookup" errorResult := ErrorResult{} - response, err := c.HttpClient.R(). + response, err := c.HTTPClient.R(). SetQueryParams(map[string]string{ "recording_name": recordingName, "artist_name": artistName, diff --git a/internal/backends/listenbrainz/client_test.go b/internal/backends/listenbrainz/client_test.go index 4e72756..2e841ae 100644 --- a/internal/backends/listenbrainz/client_test.go +++ b/internal/backends/listenbrainz/client_test.go @@ -36,7 +36,7 @@ import ( func TestNewClient(t *testing.T) { token := "foobar123" client := listenbrainz.NewClient(token) - assert.Equal(t, token, client.HttpClient.Token) + assert.Equal(t, token, client.HTTPClient.Token) assert.Equal(t, listenbrainz.DefaultItemsPerGet, client.MaxResults) } @@ -45,7 +45,7 @@ func TestGetListens(t *testing.T) { client := listenbrainz.NewClient("thetoken") client.MaxResults = 2 - setupHttpMock(t, client.HttpClient.GetClient(), + setupHTTPMock(t, client.HTTPClient.GetClient(), "https://api.listenbrainz.org/1/user/outsidecontext/listens", "testdata/listens.json") @@ -62,7 +62,7 @@ func TestGetListens(t *testing.T) { func TestSubmitListens(t *testing.T) { client := listenbrainz.NewClient("thetoken") - httpmock.ActivateNonDefault(client.HttpClient.GetClient()) + httpmock.ActivateNonDefault(client.HTTPClient.GetClient()) responder, err := httpmock.NewJsonResponder(200, listenbrainz.StatusResult{ Status: "ok", @@ -103,7 +103,7 @@ func TestGetFeedback(t *testing.T) { client := listenbrainz.NewClient("thetoken") client.MaxResults = 2 - setupHttpMock(t, client.HttpClient.GetClient(), + setupHTTPMock(t, client.HTTPClient.GetClient(), "https://api.listenbrainz.org/1/feedback/user/outsidecontext/get-feedback", "testdata/feedback.json") @@ -120,7 +120,7 @@ func TestGetFeedback(t *testing.T) { func TestSendFeedback(t *testing.T) { client := listenbrainz.NewClient("thetoken") - httpmock.ActivateNonDefault(client.HttpClient.GetClient()) + httpmock.ActivateNonDefault(client.HTTPClient.GetClient()) responder, err := httpmock.NewJsonResponder(200, listenbrainz.StatusResult{ Status: "ok", @@ -145,7 +145,7 @@ func TestLookup(t *testing.T) { defer httpmock.DeactivateAndReset() client := listenbrainz.NewClient("thetoken") - setupHttpMock(t, client.HttpClient.GetClient(), + setupHTTPMock(t, client.HTTPClient.GetClient(), "https://api.listenbrainz.org/1/metadata/lookup", "testdata/lookup.json") @@ -158,7 +158,7 @@ func TestLookup(t *testing.T) { assert.Equal(mbtypes.MBID("569436a1-234a-44bc-a370-8f4d252bef21"), result.RecordingMBID) } -func setupHttpMock(t *testing.T, client *http.Client, url string, testDataPath string) { +func setupHTTPMock(t *testing.T, client *http.Client, url string, testDataPath string) { httpmock.ActivateNonDefault(client) responder, err := httpmock.NewJsonResponder(200, httpmock.File(testDataPath)) diff --git a/internal/backends/listenbrainz/models_test.go b/internal/backends/listenbrainz/models_test.go index 9f5b14a..02cbe98 100644 --- a/internal/backends/listenbrainz/models_test.go +++ b/internal/backends/listenbrainz/models_test.go @@ -131,7 +131,7 @@ func TestTrackTrackNumberString(t *testing.T) { assert.Equal(t, 12, track.TrackNumber()) } -func TestTrackIsrc(t *testing.T) { +func TestTrackISRC(t *testing.T) { expected := mbtypes.ISRC("TCAEJ1934417") track := listenbrainz.Track{ AdditionalInfo: map[string]any{ diff --git a/internal/backends/maloja/client.go b/internal/backends/maloja/client.go index f373f93..249819a 100644 --- a/internal/backends/maloja/client.go +++ b/internal/backends/maloja/client.go @@ -32,25 +32,25 @@ import ( const MaxItemsPerGet = 1000 type Client struct { - HttpClient *resty.Client + HTTPClient *resty.Client token string } -func NewClient(serverUrl string, token string) Client { +func NewClient(serverURL string, token string) Client { client := resty.New() - client.SetBaseURL(serverUrl) + client.SetBaseURL(serverURL) client.SetHeader("Accept", "application/json") client.SetHeader("User-Agent", version.UserAgent()) client.SetRetryCount(5) return Client{ - HttpClient: client, + HTTPClient: client, token: token, } } func (c Client) GetScrobbles(page int, perPage int) (result GetScrobblesResult, err error) { const path = "/apis/mlj_1/scrobbles" - response, err := c.HttpClient.R(). + response, err := c.HTTPClient.R(). SetQueryParams(map[string]string{ "page": strconv.Itoa(page), "perpage": strconv.Itoa(perPage), @@ -68,7 +68,7 @@ func (c Client) GetScrobbles(page int, perPage int) (result GetScrobblesResult, func (c Client) NewScrobble(scrobble NewScrobble) (result NewScrobbleResult, err error) { const path = "/apis/mlj_1/newscrobble" scrobble.Key = c.token - response, err := c.HttpClient.R(). + response, err := c.HTTPClient.R(). SetBody(scrobble). SetResult(&result). Post(path) diff --git a/internal/backends/maloja/client_test.go b/internal/backends/maloja/client_test.go index 6a07adb..54316a8 100644 --- a/internal/backends/maloja/client_test.go +++ b/internal/backends/maloja/client_test.go @@ -32,19 +32,19 @@ import ( ) func TestNewClient(t *testing.T) { - serverUrl := "https://maloja.example.com" + serverURL := "https://maloja.example.com" token := "foobar123" - client := maloja.NewClient(serverUrl, token) - assert.Equal(t, serverUrl, client.HttpClient.BaseURL) + client := maloja.NewClient(serverURL, token) + assert.Equal(t, serverURL, client.HTTPClient.BaseURL) } func TestGetScrobbles(t *testing.T) { defer httpmock.DeactivateAndReset() - serverUrl := "https://maloja.example.com" + serverURL := "https://maloja.example.com" token := "thetoken" - client := maloja.NewClient(serverUrl, token) - setupHttpMock(t, client.HttpClient.GetClient(), + client := maloja.NewClient(serverURL, token) + setupHTTPMock(t, client.HTTPClient.GetClient(), "https://maloja.example.com/apis/mlj_1/scrobbles", "testdata/scrobbles.json") @@ -60,7 +60,7 @@ func TestGetScrobbles(t *testing.T) { func TestNewScrobble(t *testing.T) { server := "https://maloja.example.com" client := maloja.NewClient(server, "thetoken") - httpmock.ActivateNonDefault(client.HttpClient.GetClient()) + httpmock.ActivateNonDefault(client.HTTPClient.GetClient()) responder, err := httpmock.NewJsonResponder(200, httpmock.File("testdata/newscrobble-result.json")) if err != nil { @@ -80,7 +80,7 @@ func TestNewScrobble(t *testing.T) { assert.Equal(t, "success", result.Status) } -func setupHttpMock(t *testing.T, client *http.Client, url string, testDataPath string) { +func setupHTTPMock(t *testing.T, client *http.Client, url string, testDataPath string) { httpmock.ActivateNonDefault(client) responder, err := httpmock.NewJsonResponder(200, httpmock.File(testDataPath)) diff --git a/internal/backends/spotify/client.go b/internal/backends/spotify/client.go index 1c002c0..ff2b0a3 100644 --- a/internal/backends/spotify/client.go +++ b/internal/backends/spotify/client.go @@ -40,7 +40,7 @@ const ( ) type Client struct { - HttpClient *resty.Client + HTTPClient *resty.Client } func NewClient(token oauth2.TokenSource) Client { @@ -55,7 +55,7 @@ func NewClient(token oauth2.TokenSource) Client { ratelimit.EnableHTTPHeaderRateLimit(client, "Retry-After") return Client{ - HttpClient: client, + HTTPClient: client, } } @@ -69,7 +69,7 @@ func (c Client) RecentlyPlayedBefore(before time.Time, limit int) (RecentlyPlaye func (c Client) recentlyPlayed(after *time.Time, before *time.Time, limit int) (result RecentlyPlayedResult, err error) { const path = "/me/player/recently-played" - request := c.HttpClient.R(). + request := c.HTTPClient.R(). SetQueryParam("limit", strconv.Itoa(limit)). SetResult(&result) if after != nil { @@ -87,7 +87,7 @@ func (c Client) recentlyPlayed(after *time.Time, before *time.Time, limit int) ( func (c Client) UserTracks(offset int, limit int) (result TracksResult, err error) { const path = "/me/tracks" - response, err := c.HttpClient.R(). + response, err := c.HTTPClient.R(). SetQueryParams(map[string]string{ "offset": strconv.Itoa(offset), "limit": strconv.Itoa(limit), diff --git a/internal/backends/spotify/client_test.go b/internal/backends/spotify/client_test.go index 7d738bf..78ff063 100644 --- a/internal/backends/spotify/client_test.go +++ b/internal/backends/spotify/client_test.go @@ -43,7 +43,7 @@ func TestRecentlyPlayedAfter(t *testing.T) { defer httpmock.DeactivateAndReset() client := spotify.NewClient(nil) - setupHttpMock(t, client.HttpClient.GetClient(), + setupHTTPMock(t, client.HTTPClient.GetClient(), "https://api.spotify.com/v1/me/player/recently-played", "testdata/recently-played.json") @@ -63,7 +63,7 @@ func TestGetUserTracks(t *testing.T) { defer httpmock.DeactivateAndReset() client := spotify.NewClient(nil) - setupHttpMock(t, client.HttpClient.GetClient(), + setupHTTPMock(t, client.HTTPClient.GetClient(), "https://api.spotify.com/v1/me/tracks", "testdata/user-tracks.json") @@ -79,7 +79,7 @@ func TestGetUserTracks(t *testing.T) { assert.Equal("Zeal & Ardor", track1.Track.Album.Name) } -func setupHttpMock(t *testing.T, client *http.Client, url string, testDataPath string) { +func setupHTTPMock(t *testing.T, client *http.Client, url string, testDataPath string) { httpmock.ActivateNonDefault(client) responder, err := httpmock.NewJsonResponder(200, httpmock.File(testDataPath)) diff --git a/internal/backends/spotify/models.go b/internal/backends/spotify/models.go index a22de21..e279e15 100644 --- a/internal/backends/spotify/models.go +++ b/internal/backends/spotify/models.go @@ -58,7 +58,7 @@ type Listen struct { } type Track struct { - Id string `json:"id"` + ID string `json:"id"` Name string `json:"name"` Href string `json:"href"` Uri string `json:"uri"` @@ -69,14 +69,14 @@ type Track struct { Explicit bool `json:"explicit"` IsLocal bool `json:"is_local"` Popularity int `json:"popularity"` - ExternalIds ExternalIds `json:"external_ids"` - ExternalUrls ExternalUrls `json:"external_urls"` + ExternalIDs ExternalIDs `json:"external_ids"` + ExternalURLs ExternalURLs `json:"external_urls"` Album Album `json:"album"` Artists []Artist `json:"artists"` } type Album struct { - Id string `json:"id"` + ID string `json:"id"` Name string `json:"name"` Href string `json:"href"` Uri string `json:"uri"` @@ -85,32 +85,32 @@ type Album struct { ReleaseDate string `json:"release_date"` ReleaseDatePrecision string `json:"release_date_precision"` AlbumType string `json:"album_type"` - ExternalUrls ExternalUrls `json:"external_urls"` + ExternalURLs ExternalURLs `json:"external_urls"` Artists []Artist `json:"artists"` Images []Image `json:"images"` } type Artist struct { - Id string `json:"id"` + ID string `json:"id"` Name string `json:"name"` Href string `json:"href"` Uri string `json:"uri"` Type string `json:"type"` - ExternalUrls ExternalUrls `json:"external_urls"` + ExternalURLs ExternalURLs `json:"external_urls"` } -type ExternalIds struct { +type ExternalIDs struct { ISRC mbtypes.ISRC `json:"isrc"` EAN string `json:"ean"` UPC string `json:"upc"` } -type ExternalUrls struct { +type ExternalURLs struct { Spotify string `json:"spotify"` } type Image struct { - Url string `json:"url"` + URL string `json:"url"` Height int `json:"height"` Width int `json:"width"` } diff --git a/internal/backends/spotify/spotify.go b/internal/backends/spotify/spotify.go index ae2fc25..1827769 100644 --- a/internal/backends/spotify/spotify.go +++ b/internal/backends/spotify/spotify.go @@ -34,7 +34,7 @@ import ( type SpotifyApiBackend struct { client Client - clientId string + clientID string clientSecret string } @@ -53,14 +53,14 @@ func (b *SpotifyApiBackend) Options() []models.BackendOption { } func (b *SpotifyApiBackend) InitConfig(config *config.ServiceConfig) error { - b.clientId = config.GetString("client-id") + b.clientID = config.GetString("client-id") b.clientSecret = config.GetString("client-secret") return nil } -func (b *SpotifyApiBackend) OAuth2Strategy(redirectUrl *url.URL) auth.OAuth2Strategy { +func (b *SpotifyApiBackend) OAuth2Strategy(redirectURL *url.URL) auth.OAuth2Strategy { conf := oauth2.Config{ - ClientID: b.clientId, + ClientID: b.clientID, ClientSecret: b.clientSecret, Scopes: []string{ "user-read-currently-playing", @@ -68,16 +68,16 @@ func (b *SpotifyApiBackend) OAuth2Strategy(redirectUrl *url.URL) auth.OAuth2Stra "user-library-read", "user-library-modify", }, - RedirectURL: redirectUrl.String(), + RedirectURL: redirectURL.String(), Endpoint: spotify.Endpoint, } return auth.NewStandardStrategy(conf) } -func (b *SpotifyApiBackend) OAuth2Config(redirectUrl *url.URL) oauth2.Config { +func (b *SpotifyApiBackend) OAuth2Config(redirectURL *url.URL) oauth2.Config { return oauth2.Config{ - ClientID: b.clientId, + ClientID: b.clientID, ClientSecret: b.clientSecret, Scopes: []string{ "user-read-currently-playing", @@ -85,7 +85,7 @@ func (b *SpotifyApiBackend) OAuth2Config(redirectUrl *url.URL) oauth2.Config { "user-library-read", "user-library-modify", }, - RedirectURL: redirectUrl.String(), + RedirectURL: redirectURL.String(), Endpoint: spotify.Endpoint, } } @@ -251,7 +251,7 @@ func (t Track) AsTrack() models.Track { Duration: time.Duration(t.DurationMs * int(time.Millisecond)), TrackNumber: t.TrackNumber, DiscNumber: t.DiscNumber, - ISRC: t.ExternalIds.ISRC, + ISRC: t.ExternalIDs.ISRC, AdditionalInfo: map[string]any{}, } @@ -264,30 +264,30 @@ func (t Track) AsTrack() models.Track { info["music_service"] = "spotify.com" } - if t.ExternalUrls.Spotify != "" { - info["origin_url"] = t.ExternalUrls.Spotify - info["spotify_id"] = t.ExternalUrls.Spotify + if t.ExternalURLs.Spotify != "" { + info["origin_url"] = t.ExternalURLs.Spotify + info["spotify_id"] = t.ExternalURLs.Spotify } - if t.Album.ExternalUrls.Spotify != "" { - info["spotify_album_id"] = t.Album.ExternalUrls.Spotify + if t.Album.ExternalURLs.Spotify != "" { + info["spotify_album_id"] = t.Album.ExternalURLs.Spotify } if len(t.Artists) > 0 { - info["spotify_artist_ids"] = extractArtistIds(t.Artists) + info["spotify_artist_ids"] = extractArtistIDs(t.Artists) } if len(t.Album.Artists) > 0 { - info["spotify_album_artist_ids"] = extractArtistIds(t.Album.Artists) + info["spotify_album_artist_ids"] = extractArtistIDs(t.Album.Artists) } return track } -func extractArtistIds(artists []Artist) []string { - artistIds := make([]string, len(artists)) +func extractArtistIDs(artists []Artist) []string { + artistIDs := make([]string, len(artists)) for i, artist := range artists { - artistIds[i] = artist.ExternalUrls.Spotify + artistIDs[i] = artist.ExternalURLs.Spotify } - return artistIds + return artistIDs } diff --git a/internal/backends/spotifyhistory/models.go b/internal/backends/spotifyhistory/models.go index fad4fb1..a2eba23 100644 --- a/internal/backends/spotifyhistory/models.go +++ b/internal/backends/spotifyhistory/models.go @@ -92,8 +92,8 @@ func (i HistoryItem) AsListen() models.Listen { PlaybackDuration: time.Duration(i.MillisecondsPlayed * int(time.Millisecond)), UserName: i.UserName, } - if trackUrl, err := formatSpotifyUri(i.SpotifyTrackUri); err != nil { - listen.AdditionalInfo["spotify_id"] = trackUrl + if trackURL, err := formatSpotifyUri(i.SpotifyTrackUri); err != nil { + listen.AdditionalInfo["spotify_id"] = trackURL } return listen } diff --git a/internal/cli/auth.go b/internal/cli/auth.go index 828651a..ff14573 100644 --- a/internal/cli/auth.go +++ b/internal/cli/auth.go @@ -43,20 +43,20 @@ func AuthenticationFlow(service config.ServiceConfig, backend auth.OAuth2Authent state := auth.RandomState() // Redirect user to consent page to ask for permission specified scopes. - authUrl := strategy.AuthCodeURL(verifier, state) + authURL := strategy.AuthCodeURL(verifier, state) // Start an HTTP server to listen for the response responseChan := make(chan auth.CodeResponse) - auth.RunOauth2CallbackServer(*redirectURL, authUrl.Param, responseChan) + auth.RunOauth2CallbackServer(*redirectURL, authURL.Param, responseChan) // Open the URL - fmt.Println(i18n.Tr("Visit the URL for authorization: %v", authUrl.Url)) - err = browser.OpenURL(authUrl.Url) + fmt.Println(i18n.Tr("Visit the URL for authorization: %v", authURL.URL)) + err = browser.OpenURL(authURL.URL) cobra.CheckErr(err) // Retrieve the code from the authentication callback code := <-responseChan - if code.State != authUrl.State { + if code.State != authURL.State { cobra.CompErrorln(i18n.Tr("Error: OAuth state mismatch")) os.Exit(1) } diff --git a/pkg/jspf/extensions.go b/pkg/jspf/extensions.go index 07cd3c1..0f521c4 100644 --- a/pkg/jspf/extensions.go +++ b/pkg/jspf/extensions.go @@ -26,9 +26,9 @@ import "time" const ( // The identifier for the MusicBrainz / ListenBrainz JSPF playlist extension - MusicBrainzPlaylistExtensionId = "https://musicbrainz.org/doc/jspf#playlist" + MusicBrainzPlaylistExtensionID = "https://musicbrainz.org/doc/jspf#playlist" // The identifier for the MusicBrainz / ListenBrainz JSPF track extension - MusicBrainzTrackExtensionId = "https://musicbrainz.org/doc/jspf#track" + MusicBrainzTrackExtensionID = "https://musicbrainz.org/doc/jspf#track" ) // MusicBrainz / ListenBrainz JSPF track extension diff --git a/pkg/jspf/extensions_test.go b/pkg/jspf/extensions_test.go index 8d8653d..883301d 100644 --- a/pkg/jspf/extensions_test.go +++ b/pkg/jspf/extensions_test.go @@ -39,7 +39,7 @@ func ExampleMusicBrainzTrackExtension() { { Title: "Oweynagat", Extension: map[string]any{ - jspf.MusicBrainzTrackExtensionId: jspf.MusicBrainzTrackExtension{ + jspf.MusicBrainzTrackExtensionID: jspf.MusicBrainzTrackExtension{ AddedAt: time.Date(2023, 11, 24, 07, 47, 50, 0, time.UTC), AddedBy: "scotty", }, From bcb183499449e3fe9572895c8b8a1734b346c7d2 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 29 Apr 2025 13:29:00 +0200 Subject: [PATCH 143/150] scrobblerlog: use camelcase for constants --- internal/backends/scrobblerlog/scrobblerlog.go | 4 ++-- pkg/scrobblerlog/parser.go | 14 +++++++------- pkg/scrobblerlog/parser_test.go | 14 +++++++------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/internal/backends/scrobblerlog/scrobblerlog.go b/internal/backends/scrobblerlog/scrobblerlog.go index 26d417a..19ed30b 100644 --- a/internal/backends/scrobblerlog/scrobblerlog.go +++ b/internal/backends/scrobblerlog/scrobblerlog.go @@ -76,7 +76,7 @@ func (b *ScrobblerLogBackend) InitConfig(config *config.ServiceConfig) error { b.log.FallbackTimezone = location } b.log = scrobblerlog.ScrobblerLog{ - TZ: scrobblerlog.TZ_UTC, + TZ: scrobblerlog.TimezoneUTC, Client: "Rockbox unknown $Revision$", } return nil @@ -197,7 +197,7 @@ 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 + rating = scrobblerlog.RatingListened } else { rating = scrobblerlog.Rating(rating) } diff --git a/pkg/scrobblerlog/parser.go b/pkg/scrobblerlog/parser.go index 892f6e8..9c6471c 100644 --- a/pkg/scrobblerlog/parser.go +++ b/pkg/scrobblerlog/parser.go @@ -45,16 +45,16 @@ import ( type TZInfo string const ( - TZ_UNKNOWN TZInfo = "UNKNOWN" - TZ_UTC TZInfo = "UTC" + TimezoneUnknown TZInfo = "UNKNOWN" + TimezoneUTC TZInfo = "UTC" ) // L if listened at least 50% or S if skipped type Rating string const ( - RATING_LISTENED Rating = "L" - RATING_SKIPPED Rating = "S" + RatingListened Rating = "L" + RatingSkipped Rating = "S" ) // A single entry of a track in the scrobbler log file. @@ -75,7 +75,7 @@ type ScrobblerLog struct { Client string Records []Record // Timezone to be used for timestamps in the log file, - // if TZ is set to [TZ_UNKNOWN]. + // if TZ is set to [TimezoneUnknown]. FallbackTimezone *time.Location } @@ -116,7 +116,7 @@ func (l *ScrobblerLog) Parse(data io.Reader, ignoreSkipped bool) error { return err } - if ignoreSkipped && record.Rating == RATING_SKIPPED { + if ignoreSkipped && record.Rating == RatingSkipped { continue } @@ -224,7 +224,7 @@ func (l ScrobblerLog) rowToRecord(row []string) (Record, error) { } var timezone *time.Location = nil - if l.TZ == TZ_UNKNOWN { + if l.TZ == TimezoneUnknown { timezone = l.FallbackTimezone } diff --git a/pkg/scrobblerlog/parser_test.go b/pkg/scrobblerlog/parser_test.go index 7fd57c3..f4527cc 100644 --- a/pkg/scrobblerlog/parser_test.go +++ b/pkg/scrobblerlog/parser_test.go @@ -50,7 +50,7 @@ func TestParser(t *testing.T) { result := scrobblerlog.ScrobblerLog{} err := result.Parse(data, false) require.NoError(t, err) - assert.Equal(scrobblerlog.TZ_UNKNOWN, result.TZ) + assert.Equal(scrobblerlog.TimezoneUnknown, result.TZ) assert.Equal("Rockbox sansaclipplus $Revision$", result.Client) assert.Len(result.Records, 5) record1 := result.Records[0] @@ -59,11 +59,11 @@ func TestParser(t *testing.T) { 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(scrobblerlog.RatingListened, 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(scrobblerlog.RatingSkipped, record4.Rating) assert.Equal(mbtypes.MBID("385ba9e9-626d-4750-a607-58e541dca78e"), record4.MusicBrainzRecordingID) } @@ -76,7 +76,7 @@ func TestParserIgnoreSkipped(t *testing.T) { require.NoError(t, err) assert.Len(result.Records, 4) record4 := result.Records[3] - assert.Equal(scrobblerlog.RATING_LISTENED, record4.Rating) + assert.Equal(scrobblerlog.RatingListened, record4.Rating) assert.Equal(mbtypes.MBID("1262beaf-19f8-4534-b9ed-7eef9ca8e83f"), record4.MusicBrainzRecordingID) } @@ -101,7 +101,7 @@ func TestAppend(t *testing.T) { data := make([]byte, 0, 10) buffer := bytes.NewBuffer(data) log := scrobblerlog.ScrobblerLog{ - TZ: scrobblerlog.TZ_UNKNOWN, + TZ: scrobblerlog.TimezoneUnknown, Client: "Rockbox foo $Revision$", } records := []scrobblerlog.Record{ @@ -111,7 +111,7 @@ func TestAppend(t *testing.T) { TrackName: "Reign", TrackNumber: 1, Duration: 271 * time.Second, - Rating: scrobblerlog.RATING_LISTENED, + Rating: scrobblerlog.RatingListened, Timestamp: time.Unix(1699572072, 0), MusicBrainzRecordingID: mbtypes.MBID("b59cf4e7-caee-4019-a844-79d2c58d4dff"), }, @@ -139,7 +139,7 @@ func TestReadHeader(t *testing.T) { log := scrobblerlog.ScrobblerLog{} err := log.ReadHeader(reader) assert.NoError(t, err) - assert.Equal(t, log.TZ, scrobblerlog.TZ_UNKNOWN) + assert.Equal(t, log.TZ, scrobblerlog.TimezoneUnknown) assert.Equal(t, log.Client, "Rockbox sansaclipplus $Revision$") assert.Empty(t, log.Records) } From dff34b249c29916848ef5b700563126bd59a3eef Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 29 Apr 2025 15:46:14 +0200 Subject: [PATCH 144/150] Updated translation files --- internal/translations/catalog.go | 123 +++++++++--------- .../translations/locales/de/out.gotext.json | 26 ++-- .../translations/locales/en/out.gotext.json | 30 ++--- 3 files changed, 89 insertions(+), 90 deletions(-) diff --git a/internal/translations/catalog.go b/internal/translations/catalog.go index 987612a..c4a7937 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": 47, + "%v: %v": 45, "Aborted": 8, "Access token": 19, - "Access token received, you can use %v now.\n": 33, + "Access token received, you can use %v now.\n": 31, "Append to file": 21, - "Backend": 41, + "Backend": 39, "Check for duplicate listens on import (slower)": 24, "Client ID": 15, "Client secret": 16, "Delete the service configuration \"%v\"?": 7, "Directory path": 27, "Disable auto correction of submitted listens": 25, - "Error: OAuth state mismatch": 32, + "Error: OAuth state mismatch": 30, "Failed reading config: %v": 2, "File path": 20, - "From timestamp: %v (%v)": 43, + "From timestamp: %v (%v)": 41, "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, + "Ignore skipped listens": 26, + "Ignored duplicate listen %v: \"%v\" by %v (%v)": 51, + "Import failed, last reported timestamp was %v (%s)": 42, + "Import log:": 44, + "Imported %v of %v %s into %v.": 43, + "Latest timestamp: %v (%v)": 47, + "Minimum playback duration for skipped tracks (seconds)": 29, + "No": 36, "Playlist title": 22, "Saved service %v using backend %v": 5, "Server URL": 17, - "Service": 40, + "Service": 38, "Service \"%v\" deleted\n": 9, "Service name": 3, + "Specify a time zone for the listen timestamps": 52, "The backend %v requires authentication. Authenticate now?": 6, "Token received, you can close this window now.": 12, - "Transferring %s from %s to %s...": 42, + "Transferring %s from %s to %s...": 40, "Unique playlist identifier": 23, "Updated service %v using backend %v\n": 10, "User name": 18, - "Visit the URL for authorization: %v": 31, - "Yes": 37, + "Visit the URL for authorization: %v": 53, + "Yes": 35, "a service with this name already exists": 4, "backend %s does not implement %s": 13, - "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, + "done": 34, + "exporting": 32, + "importing": 33, + "invalid timestamp string \"%v\"": 46, + "key must only consist of A-Za-z0-9_-": 49, + "no configuration file defined, cannot write config": 48, + "no existing service configurations": 37, + "no service configuration \"%v\"": 50, "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, 0x000002ed, 0x00000321, 0x00000342, - 0x00000352, 0x00000378, 0x0000039a, 0x000003d8, + 0x000002b8, 0x000002ed, 0x00000321, 0x00000343, + 0x00000353, 0x00000379, 0x000003b7, 0x000003e1, // Entry 20 - 3F - 0x000003fe, 0x00000428, 0x00000468, 0x00000473, - 0x0000047e, 0x00000485, 0x00000488, 0x0000048d, - 0x000004b6, 0x000004be, 0x000004c6, 0x000004ef, - 0x0000050d, 0x0000054a, 0x00000575, 0x00000580, - 0x0000058d, 0x000005b1, 0x000005d4, 0x00000625, - 0x0000065c, 0x00000683, 0x00000683, + 0x00000421, 0x0000042c, 0x00000437, 0x0000043e, + 0x00000441, 0x00000446, 0x0000046f, 0x00000477, + 0x0000047f, 0x000004a8, 0x000004c6, 0x00000503, + 0x0000052e, 0x00000539, 0x00000546, 0x0000056a, + 0x0000058d, 0x000005de, 0x00000615, 0x0000063c, + 0x0000063c, 0x0000063c, 0x0000063c, } // Size: 244 bytes -const deData string = "" + // Size: 1667 bytes +const deData string = "" + // Size: 1596 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" + @@ -128,10 +128,9 @@ const deData string = "" + // Size: 1667 bytes "\x02Benutzername\x02Zugriffstoken\x02Dateipfad\x02An Datei anhängen\x02T" + "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" + + "ivieren\x02Übersprungene Listens ignorieren\x02Verzeichnispfad\x02Listen" + + "s im Inkognito-Modus ignorieren\x02Minimale Wiedergabedauer für überspru" + + "ngene Titel (Sekunden)\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" + @@ -151,18 +150,18 @@ var enIndex = []uint32{ // 55 elements 0x00000170, 0x0000019f, 0x000001c6, 0x000001de, 0x000001e8, 0x000001f6, 0x00000201, 0x0000020b, 0x00000218, 0x00000222, 0x00000231, 0x00000240, - 0x0000025b, 0x0000028a, 0x000002b7, 0x000002cf, - 0x000002de, 0x000002ff, 0x00000316, 0x0000034d, + 0x0000025b, 0x0000028a, 0x000002b7, 0x000002ce, + 0x000002dd, 0x000002fe, 0x00000335, 0x00000351, // Entry 20 - 3F - 0x00000374, 0x00000390, 0x000003c3, 0x000003cd, - 0x000003d7, 0x000003dc, 0x000003e0, 0x000003e3, - 0x00000406, 0x0000040e, 0x00000416, 0x00000440, - 0x0000045e, 0x00000497, 0x000004c1, 0x000004cd, - 0x000004da, 0x000004fb, 0x0000051b, 0x0000054e, - 0x00000573, 0x00000594, 0x000005cd, + 0x00000384, 0x0000038e, 0x00000398, 0x0000039d, + 0x000003a1, 0x000003a4, 0x000003c7, 0x000003cf, + 0x000003d7, 0x00000401, 0x0000041f, 0x00000458, + 0x00000482, 0x0000048e, 0x0000049b, 0x000004bc, + 0x000004dc, 0x0000050f, 0x00000534, 0x00000555, + 0x0000058e, 0x000005bc, 0x000005e3, } // Size: 244 bytes -const enData string = "" + // Size: 1485 bytes +const enData string = "" + // Size: 1507 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" + @@ -175,18 +174,18 @@ const enData string = "" + // Size: 1485 bytes "\x02Server URL\x02User name\x02Access token\x02File path\x02Append to fi" + "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)" + "tens\x02Ignore skipped listens\x02Directory path\x02Ignore listens in in" + + "cognito mode\x02Minimum playback duration for skipped tracks (seconds)" + + "\x02Error: OAuth state mismatch\x04\x00\x01\x0a.\x02Access token receive" + + "d, you can use %[1]v now.\x02exporting\x02importing\x02done\x02Yes\x02No" + + "\x02no existing service configurations\x02Service\x02Backend\x02Transfer" + + "ring %[1]s from %[2]s to %[3]s...\x02From timestamp: %[1]v (%[2]v)\x02Im" + + "port 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 ti" + + "mestamp string \x22%[1]v\x22\x02Latest timestamp: %[1]v (%[2]v)\x02no co" + + "nfiguration file defined, cannot write config\x02key must only consist o" + + "f A-Za-z0-9_-\x02no service configuration \x22%[1]v\x22\x02Ignored dupli" + + "cate listen %[1]v: \x22%[2]v\x22 by %[3]v (%[4]v)\x02Specify a time zone" + + " for the listen timestamps\x02Visit the URL for authorization: %[1]v" - // Total table size 3640 bytes (3KiB); checksum: 719A868A + // Total table size 3591 bytes (3KiB); checksum: 6C862242 diff --git a/internal/translations/locales/de/out.gotext.json b/internal/translations/locales/de/out.gotext.json index 18333c6..65abfdd 100644 --- a/internal/translations/locales/de/out.gotext.json +++ b/internal/translations/locales/de/out.gotext.json @@ -305,9 +305,14 @@ "translation": "Autokorrektur für übermittelte Titel deaktivieren" }, { - "id": "Include skipped listens", - "message": "Include skipped listens", - "translation": "Übersprungene Titel einbeziehen" + "id": "Ignore skipped listens", + "message": "Ignore skipped listens", + "translation": "Übersprungene Listens ignorieren" + }, + { + "id": "Specify a time zone for the listen timestamps", + "message": "Specify a time zone for the listen timestamps", + "translation": "" }, { "id": "Directory path", @@ -319,28 +324,23 @@ "message": "Ignore listens in incognito mode", "translation": "Listens im Inkognito-Modus ignorieren" }, - { - "id": "Ignore skipped listens", - "message": "Ignore skipped listens", - "translation": "Übersprungene Listens ignorieren" - }, { "id": "Minimum playback duration for skipped tracks (seconds)", "message": "Minimum playback duration for skipped tracks (seconds)", "translation": "Minimale Wiedergabedauer für übersprungene Titel (Sekunden)" }, { - "id": "Visit the URL for authorization: {Url}", - "message": "Visit the URL for authorization: {Url}", - "translation": "URL für Autorisierung öffnen: {Url}", + "id": "Visit the URL for authorization: {URL}", + "message": "Visit the URL for authorization: {URL}", + "translation": "", "placeholders": [ { - "id": "Url", + "id": "URL", "string": "%[1]v", "type": "string", "underlyingType": "string", "argNum": 1, - "expr": "authUrl.Url" + "expr": "authURL.URL" } ] }, diff --git a/internal/translations/locales/en/out.gotext.json b/internal/translations/locales/en/out.gotext.json index 54c6719..de7d4d9 100644 --- a/internal/translations/locales/en/out.gotext.json +++ b/internal/translations/locales/en/out.gotext.json @@ -359,9 +359,16 @@ "fuzzy": true }, { - "id": "Include skipped listens", - "message": "Include skipped listens", - "translation": "Include skipped listens", + "id": "Ignore skipped listens", + "message": "Ignore skipped listens", + "translation": "Ignore skipped listens", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Specify a time zone for the listen timestamps", + "message": "Specify a time zone for the listen timestamps", + "translation": "Specify a time zone for the listen timestamps", "translatorComment": "Copied from source.", "fuzzy": true }, @@ -379,13 +386,6 @@ "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)", @@ -394,18 +394,18 @@ "fuzzy": true }, { - "id": "Visit the URL for authorization: {Url}", - "message": "Visit the URL for authorization: {Url}", - "translation": "Visit the URL for authorization: {Url}", + "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", + "id": "URL", "string": "%[1]v", "type": "string", "underlyingType": "string", "argNum": 1, - "expr": "authUrl.Url" + "expr": "authURL.URL" } ], "fuzzy": true From 90e101080f707311bd539474cb2c7d2cea6920fa Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 29 Apr 2025 13:51:09 +0000 Subject: [PATCH 145/150] 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 --- internal/translations/locales/de/messages.gotext.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/translations/locales/de/messages.gotext.json b/internal/translations/locales/de/messages.gotext.json index afa4129..19a7a25 100644 --- a/internal/translations/locales/de/messages.gotext.json +++ b/internal/translations/locales/de/messages.gotext.json @@ -175,7 +175,7 @@ { "id": "backend {Backend} does not implement {InterfaceName}", "message": "backend {Backend} does not implement {InterfaceName}", - "translation": "das backend {Backend} implementiert {InterfaceName} nicht", + "translation": "das Backend {Backend} implementiert {InterfaceName} nicht", "placeholders": [ { "id": "Backend", @@ -413,7 +413,7 @@ { "id": "Transferring {Entity} from {SourceName} to {TargetName}...", "message": "Transferring {Entity} from {SourceName} to {TargetName}...", - "translation": "Übertrage {Entity} von {SourceName} nach {TargetName}...", + "translation": "Übertrage {Entity} von {SourceName} nach {TargetName}…", "placeholders": [ { "id": "Entity", From a5442b477e3669ca8c3dda7808d8c308d08368e9 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 29 Apr 2025 16:06:13 +0200 Subject: [PATCH 146/150] Sync translations with new strings --- internal/cli/transfer.go | 2 +- internal/translations/catalog.go | 36 ++++++------- .../locales/de/messages.gotext.json | 54 +++++++++---------- .../translations/locales/de/out.gotext.json | 8 +-- .../locales/en/messages.gotext.json | 50 ++++++++--------- .../translations/locales/en/out.gotext.json | 6 +-- 6 files changed, 78 insertions(+), 78 deletions(-) diff --git a/internal/cli/transfer.go b/internal/cli/transfer.go index 427af06..ade7ece 100644 --- a/internal/cli/transfer.go +++ b/internal/cli/transfer.go @@ -88,7 +88,7 @@ func (c *TransferCmd[E, I, R]) resolveBackends(source string, target string) err } func (c *TransferCmd[E, I, R]) Transfer(exp backends.ExportProcessor[R], imp backends.ImportProcessor[R]) error { - fmt.Println(i18n.Tr("Transferring %s from %s to %s...", c.entity, c.sourceName, c.targetName)) + fmt.Println(i18n.Tr("Transferring %s from %s to %s…", c.entity, c.sourceName, c.targetName)) // Authenticate backends, if needed config := viper.GetViper() diff --git a/internal/translations/catalog.go b/internal/translations/catalog.go index c4a7937..374179f 100644 --- a/internal/translations/catalog.go +++ b/internal/translations/catalog.go @@ -76,7 +76,7 @@ var messageKeyToIndex = map[string]int{ "Specify a time zone for the listen timestamps": 52, "The backend %v requires authentication. Authenticate now?": 6, "Token received, you can close this window now.": 12, - "Transferring %s from %s to %s...": 40, + "Transferring %s from %s to %s…": 40, "Unique playlist identifier": 23, "Updated service %v using backend %v\n": 10, "User name": 18, @@ -123,7 +123,7 @@ const deData string = "" + // Size: 1596 bytes "\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" + + " 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\x02Beim Import auf Listen-Du" + @@ -134,13 +134,13 @@ const deData string = "" + // Size: 1596 bytes "\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“" + " %[2]s nach %[3]s…\x02Ab Zeitstempel: %[1]v (%[2]v)\x02Import fehlgeschl" + + "agen, letzter Zeitstempel war %[1]v (%[2]s)\x02%[1]v von %[2]v %[3]s in " + + "%[4]v importiert.\x02Importlog:\x02%[1]v: %[2]v\x02ungültiger Zeitstempe" + + "l „%[1]v“\x02Letzter Zeitstempel: %[1]v (%[2]v)\x02keine Konfigurationsd" + + "atei definiert, Konfiguration kann nicht geschrieben werden\x02Schlüssel" + + " darf nur die Zeichen A-Za-z0-9_- beinhalten\x02keine Servicekonfigurati" + + "on „%[1]v“" var enIndex = []uint32{ // 55 elements // Entry 0 - 1F @@ -179,13 +179,13 @@ const enData string = "" + // Size: 1507 bytes "\x02Error: OAuth state mismatch\x04\x00\x01\x0a.\x02Access token receive" + "d, you can use %[1]v now.\x02exporting\x02importing\x02done\x02Yes\x02No" + "\x02no existing service configurations\x02Service\x02Backend\x02Transfer" + - "ring %[1]s from %[2]s to %[3]s...\x02From timestamp: %[1]v (%[2]v)\x02Im" + - "port 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 ti" + - "mestamp string \x22%[1]v\x22\x02Latest timestamp: %[1]v (%[2]v)\x02no co" + - "nfiguration file defined, cannot write config\x02key must only consist o" + - "f A-Za-z0-9_-\x02no service configuration \x22%[1]v\x22\x02Ignored dupli" + - "cate listen %[1]v: \x22%[2]v\x22 by %[3]v (%[4]v)\x02Specify a time zone" + - " for the listen timestamps\x02Visit the URL for authorization: %[1]v" + "ring %[1]s from %[2]s to %[3]s…\x02From timestamp: %[1]v (%[2]v)\x02Impo" + + "rt failed, last reported timestamp was %[1]v (%[2]s)\x02Imported %[1]v o" + + "f %[2]v %[3]s into %[4]v.\x02Import log:\x02%[1]v: %[2]v\x02invalid time" + + "stamp string \x22%[1]v\x22\x02Latest timestamp: %[1]v (%[2]v)\x02no conf" + + "iguration file defined, cannot write config\x02key must only consist of " + + "A-Za-z0-9_-\x02no service configuration \x22%[1]v\x22\x02Ignored duplica" + + "te listen %[1]v: \x22%[2]v\x22 by %[3]v (%[4]v)\x02Specify a time zone f" + + "or the listen timestamps\x02Visit the URL for authorization: %[1]v" - // Total table size 3591 bytes (3KiB); checksum: 6C862242 + // Total table size 3591 bytes (3KiB); checksum: 2A4B9572 diff --git a/internal/translations/locales/de/messages.gotext.json b/internal/translations/locales/de/messages.gotext.json index 19a7a25..dfa808b 100644 --- a/internal/translations/locales/de/messages.gotext.json +++ b/internal/translations/locales/de/messages.gotext.json @@ -261,9 +261,9 @@ "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": "Listen-Duplikat ignoriert {ListenedAt}: „{TrackName}“ von {ArtistName} ({RecordingMbid})", + "id": "Ignored duplicate listen {ListenedAt}: \"{TrackName}\" by {ArtistName} ({RecordingMBID})", + "message": "Ignored duplicate listen {ListenedAt}: \"{TrackName}\" by {ArtistName} ({RecordingMBID})", + "translation": "", "placeholders": [ { "id": "ListenedAt", @@ -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" } ] }, @@ -305,9 +305,14 @@ "translation": "Autokorrektur für übermittelte Titel deaktivieren" }, { - "id": "Include skipped listens", - "message": "Include skipped listens", - "translation": "Übersprungene Titel einbeziehen" + "id": "Ignore skipped listens", + "message": "Ignore skipped listens", + "translation": "Übersprungene Listens ignorieren" + }, + { + "id": "Specify a time zone for the listen timestamps", + "message": "Specify a time zone for the listen timestamps", + "translation": "" }, { "id": "Directory path", @@ -319,28 +324,23 @@ "message": "Ignore listens in incognito mode", "translation": "Listens im Inkognito-Modus ignorieren" }, - { - "id": "Ignore skipped listens", - "message": "Ignore skipped listens", - "translation": "Übersprungene Listens ignorieren" - }, { "id": "Minimum playback duration for skipped tracks (seconds)", "message": "Minimum playback duration for skipped tracks (seconds)", "translation": "Minimale Wiedergabedauer für übersprungene Titel (Sekunden)" }, { - "id": "Visit the URL for authorization: {Url}", - "message": "Visit the URL for authorization: {Url}", - "translation": "URL für Autorisierung öffnen: {Url}", + "id": "Visit the URL for authorization: {URL}", + "message": "Visit the URL for authorization: {URL}", + "translation": "", "placeholders": [ { - "id": "Url", + "id": "URL", "string": "%[1]v", "type": "string", "underlyingType": "string", "argNum": 1, - "expr": "authUrl.Url" + "expr": "authURL.URL" } ] }, @@ -367,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", @@ -411,8 +411,8 @@ "translation": "Backend" }, { - "id": "Transferring {Entity} from {SourceName} to {TargetName}...", - "message": "Transferring {Entity} from {SourceName} to {TargetName}...", + "id": "Transferring {Entity} from {SourceName} to {TargetName}…", + "message": "Transferring {Entity} from {SourceName} to {TargetName}…", "translation": "Übertrage {Entity} von {SourceName} nach {TargetName}…", "placeholders": [ { diff --git a/internal/translations/locales/de/out.gotext.json b/internal/translations/locales/de/out.gotext.json index 65abfdd..7a13af8 100644 --- a/internal/translations/locales/de/out.gotext.json +++ b/internal/translations/locales/de/out.gotext.json @@ -175,7 +175,7 @@ { "id": "backend {Backend} does not implement {InterfaceName}", "message": "backend {Backend} does not implement {InterfaceName}", - "translation": "das backend {Backend} implementiert {InterfaceName} nicht", + "translation": "das Backend {Backend} implementiert {InterfaceName} nicht", "placeholders": [ { "id": "Backend", @@ -411,9 +411,9 @@ "translation": "Backend" }, { - "id": "Transferring {Entity} from {SourceName} to {TargetName}...", - "message": "Transferring {Entity} from {SourceName} to {TargetName}...", - "translation": "Übertrage {Entity} von {SourceName} nach {TargetName}...", + "id": "Transferring {Entity} from {SourceName} to {TargetName}…", + "message": "Transferring {Entity} from {SourceName} to {TargetName}…", + "translation": "Übertrage {Entity} von {SourceName} nach {TargetName}…", "placeholders": [ { "id": "Entity", diff --git a/internal/translations/locales/en/messages.gotext.json b/internal/translations/locales/en/messages.gotext.json index 7687276..ed62636 100644 --- a/internal/translations/locales/en/messages.gotext.json +++ b/internal/translations/locales/en/messages.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 @@ -359,9 +359,16 @@ "fuzzy": true }, { - "id": "Include skipped listens", - "message": "Include skipped listens", - "translation": "Include skipped listens", + "id": "Ignore skipped listens", + "message": "Ignore skipped listens", + "translation": "Ignore skipped listens", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Specify a time zone for the listen timestamps", + "message": "Specify a time zone for the listen timestamps", + "translation": "Specify a time zone for the listen timestamps", "translatorComment": "Copied from source.", "fuzzy": true }, @@ -379,13 +386,6 @@ "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)", @@ -394,18 +394,18 @@ "fuzzy": true }, { - "id": "Visit the URL for authorization: {Url}", - "message": "Visit the URL for authorization: {Url}", - "translation": "Visit the URL for authorization: {Url}", + "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", + "id": "URL", "string": "%[1]v", "type": "string", "underlyingType": "string", "argNum": 1, - "expr": "authUrl.Url" + "expr": "authURL.URL" } ], "fuzzy": true @@ -491,9 +491,9 @@ "fuzzy": true }, { - "id": "Transferring {Entity} from {SourceName} to {TargetName}...", - "message": "Transferring {Entity} from {SourceName} to {TargetName}...", - "translation": "Transferring {Entity} from {SourceName} to {TargetName}...", + "id": "Transferring {Entity} from {SourceName} to {TargetName}…", + "message": "Transferring {Entity} from {SourceName} to {TargetName}…", + "translation": "Transferring {Entity} from {SourceName} to {TargetName}…", "translatorComment": "Copied from source.", "placeholders": [ { @@ -714,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 de7d4d9..eecf359 100644 --- a/internal/translations/locales/en/out.gotext.json +++ b/internal/translations/locales/en/out.gotext.json @@ -491,9 +491,9 @@ "fuzzy": true }, { - "id": "Transferring {Entity} from {SourceName} to {TargetName}...", - "message": "Transferring {Entity} from {SourceName} to {TargetName}...", - "translation": "Transferring {Entity} from {SourceName} to {TargetName}...", + "id": "Transferring {Entity} from {SourceName} to {TargetName}…", + "message": "Transferring {Entity} from {SourceName} to {TargetName}…", + "translation": "Transferring {Entity} from {SourceName} to {TargetName}…", "translatorComment": "Copied from source.", "placeholders": [ { From a6cc8d49ac278694e840d41091bab82ba056e0f3 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 29 Apr 2025 14:11:31 +0000 Subject: [PATCH 147/150] 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 | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/internal/translations/locales/de/messages.gotext.json b/internal/translations/locales/de/messages.gotext.json index dfa808b..8cbe44a 100644 --- a/internal/translations/locales/de/messages.gotext.json +++ b/internal/translations/locales/de/messages.gotext.json @@ -263,7 +263,7 @@ { "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,7 +312,7 @@ { "id": "Specify a time zone for the listen timestamps", "message": "Specify a time zone for the listen timestamps", - "translation": "" + "translation": "Zeitzone für den Abspiel-Zeitstempel" }, { "id": "Directory path", @@ -332,7 +332,7 @@ { "id": "Visit the URL for authorization: {URL}", "message": "Visit the URL for authorization: {URL}", - "translation": "", + "translation": "Zur Anmeldung folgende URL aufrufen: {URL}", "placeholders": [ { "id": "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", From 19852be68b6003c03cd45b1d80db54251954b82b Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 29 Apr 2025 16:12:42 +0200 Subject: [PATCH 148/150] Updated translations --- internal/translations/catalog.go | 152 +++++++++--------- .../translations/locales/de/out.gotext.json | 6 +- 2 files changed, 80 insertions(+), 78 deletions(-) diff --git a/internal/translations/catalog.go b/internal/translations/catalog.go index 374179f..3eb2f7e 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": 45, + "%v: %v": 48, "Aborted": 8, "Access token": 19, - "Access token received, you can use %v now.\n": 31, + "Access token received, you can use %v now.\n": 34, "Append to file": 21, - "Backend": 39, + "Backend": 42, "Check for duplicate listens on import (slower)": 24, "Client ID": 15, "Client secret": 16, "Delete the service configuration \"%v\"?": 7, - "Directory path": 27, - "Disable auto correction of submitted listens": 25, - "Error: OAuth state mismatch": 30, + "Directory path": 29, + "Disable auto correction of submitted listens": 26, + "Error: OAuth state mismatch": 33, "Failed reading config: %v": 2, "File path": 20, - "From timestamp: %v (%v)": 41, - "Ignore listens in incognito mode": 28, - "Ignore skipped listens": 26, - "Ignored duplicate listen %v: \"%v\" by %v (%v)": 51, - "Import failed, last reported timestamp was %v (%s)": 42, - "Import log:": 44, - "Imported %v of %v %s into %v.": 43, - "Latest timestamp: %v (%v)": 47, - "Minimum playback duration for skipped tracks (seconds)": 29, - "No": 36, + "From timestamp: %v (%v)": 44, + "Ignore listens in incognito mode": 30, + "Ignore skipped listens": 27, + "Ignored duplicate listen %v: \"%v\" by %v (%v)": 25, + "Import failed, last reported timestamp was %v (%s)": 45, + "Import log:": 47, + "Imported %v of %v %s into %v.": 46, + "Latest timestamp: %v (%v)": 50, + "Minimum playback duration for skipped tracks (seconds)": 31, + "No": 39, "Playlist title": 22, "Saved service %v using backend %v": 5, "Server URL": 17, - "Service": 38, + "Service": 41, "Service \"%v\" deleted\n": 9, "Service name": 3, - "Specify a time zone for the listen timestamps": 52, + "Specify a time zone for the listen timestamps": 28, "The backend %v requires authentication. Authenticate now?": 6, "Token received, you can close this window now.": 12, - "Transferring %s from %s to %s…": 40, + "Transferring %s from %s to %s…": 43, "Unique playlist identifier": 23, "Updated service %v using backend %v\n": 10, "User name": 18, - "Visit the URL for authorization: %v": 53, - "Yes": 35, + "Visit the URL for authorization: %v": 32, + "Yes": 38, "a service with this name already exists": 4, "backend %s does not implement %s": 13, - "done": 34, - "exporting": 32, - "importing": 33, - "invalid timestamp string \"%v\"": 46, - "key must only consist of A-Za-z0-9_-": 49, - "no configuration file defined, cannot write config": 48, - "no existing service configurations": 37, - "no service configuration \"%v\"": 50, + "done": 37, + "exporting": 35, + "importing": 36, + "invalid timestamp string \"%v\"": 49, + "key must only consist of A-Za-z0-9_-": 52, + "no configuration file defined, cannot write config": 51, + "no existing service configurations": 40, + "no service configuration \"%v\"": 53, "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, 0x000002ed, 0x00000321, 0x00000343, - 0x00000353, 0x00000379, 0x000003b7, 0x000003e1, + 0x000002b8, 0x000002ed, 0x00000328, 0x0000035c, + 0x0000037e, 0x000003a4, 0x000003b4, 0x000003da, // Entry 20 - 3F - 0x00000421, 0x0000042c, 0x00000437, 0x0000043e, - 0x00000441, 0x00000446, 0x0000046f, 0x00000477, - 0x0000047f, 0x000004a8, 0x000004c6, 0x00000503, - 0x0000052e, 0x00000539, 0x00000546, 0x0000056a, - 0x0000058d, 0x000005de, 0x00000615, 0x0000063c, - 0x0000063c, 0x0000063c, 0x0000063c, + 0x00000418, 0x00000443, 0x0000046d, 0x000004ad, + 0x000004b8, 0x000004c3, 0x000004ca, 0x000004cd, + 0x000004d2, 0x000004fb, 0x00000503, 0x0000050b, + 0x00000534, 0x00000552, 0x0000058f, 0x000005ba, + 0x000005c5, 0x000005d2, 0x000005f6, 0x00000619, + 0x0000066a, 0x000006a1, 0x000006c8, } // Size: 244 bytes -const deData string = "" + // Size: 1596 bytes +const deData string = "" + // Size: 1736 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" + @@ -127,20 +127,22 @@ const deData string = "" + // Size: 1596 bytes "nbekanntes Backend „%[1]s“\x02Client-ID\x02Client-Secret\x02Server-URL" + "\x02Benutzername\x02Zugriffstoken\x02Dateipfad\x02An Datei anhängen\x02T" + "itel der Playlist\x02Eindeutige Playlist-ID\x02Beim Import auf Listen-Du" + - "plikate prüfen (langsamer)\x02Autokorrektur für übermittelte Titel deakt" + - "ivieren\x02Übersprungene Listens ignorieren\x02Verzeichnispfad\x02Listen" + - "s im Inkognito-Modus ignorieren\x02Minimale Wiedergabedauer für überspru" + - "ngene Titel (Sekunden)\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 fehlgeschl" + - "agen, letzter Zeitstempel war %[1]v (%[2]s)\x02%[1]v von %[2]v %[3]s in " + - "%[4]v importiert.\x02Importlog:\x02%[1]v: %[2]v\x02ungültiger Zeitstempe" + - "l „%[1]v“\x02Letzter Zeitstempel: %[1]v (%[2]v)\x02keine Konfigurationsd" + - "atei definiert, Konfiguration kann nicht geschrieben werden\x02Schlüssel" + - " darf nur die Zeichen A-Za-z0-9_- beinhalten\x02keine Servicekonfigurati" + - "on „%[1]v“" + "plikate prüfen (langsamer)\x02Listen-Duplikat ignoriert %[1]v: \x22%[2]v" + + "\x22 von %[3]v (%[4]v)\x02Autokorrektur für übermittelte Titel deaktivie" + + "ren\x02Übersprungene Listens ignorieren\x02Zeitzone für den Abspiel-Zeit" + + "stempel\x02Verzeichnispfad\x02Listens im Inkognito-Modus ignorieren\x02M" + + "inimale Wiedergabedauer für übersprungene Titel (Sekunden)\x02Zur Anmeld" + + "ung folgende URL aufrufen: %[1]v\x02Fehler: OAuth-State stimmt nicht übe" + + "rein\x04\x00\x01\x0a;\x02Zugriffstoken erhalten, %[1]v kann jetzt verwen" + + "det werden.\x02exportiere\x02importiere\x02fertig\x02Ja\x02Nein\x02keine" + + " bestehenden Servicekonfigurationen\x02Service\x02Backend\x02Übertrage %" + + "[1]s von %[2]s nach %[3]s…\x02Ab Zeitstempel: %[1]v (%[2]v)\x02Import fe" + + "hlgeschlagen, letzter Zeitstempel war %[1]v (%[2]s)\x02%[1]v von %[2]v %" + + "[3]s in %[4]v importiert.\x02Importlog:\x02%[1]v: %[2]v\x02ungültiger Ze" + + "itstempel „%[1]v“\x02Letzter Zeitstempel: %[1]v (%[2]v)\x02keine Konfigu" + + "rationsdatei definiert, Konfiguration kann nicht geschrieben werden\x02S" + + "chlüssel darf nur die Zeichen A-Za-z0-9_- beinhalten\x02keine Servicekon" + + "figuration „%[1]v“" var enIndex = []uint32{ // 55 elements // Entry 0 - 1F @@ -150,15 +152,15 @@ var enIndex = []uint32{ // 55 elements 0x00000170, 0x0000019f, 0x000001c6, 0x000001de, 0x000001e8, 0x000001f6, 0x00000201, 0x0000020b, 0x00000218, 0x00000222, 0x00000231, 0x00000240, - 0x0000025b, 0x0000028a, 0x000002b7, 0x000002ce, - 0x000002dd, 0x000002fe, 0x00000335, 0x00000351, + 0x0000025b, 0x0000028a, 0x000002c3, 0x000002f0, + 0x00000307, 0x00000335, 0x00000344, 0x00000365, // Entry 20 - 3F - 0x00000384, 0x0000038e, 0x00000398, 0x0000039d, - 0x000003a1, 0x000003a4, 0x000003c7, 0x000003cf, - 0x000003d7, 0x00000401, 0x0000041f, 0x00000458, - 0x00000482, 0x0000048e, 0x0000049b, 0x000004bc, - 0x000004dc, 0x0000050f, 0x00000534, 0x00000555, - 0x0000058e, 0x000005bc, 0x000005e3, + 0x0000039c, 0x000003c3, 0x000003df, 0x00000412, + 0x0000041c, 0x00000426, 0x0000042b, 0x0000042f, + 0x00000432, 0x00000455, 0x0000045d, 0x00000465, + 0x0000048f, 0x000004ad, 0x000004e6, 0x00000510, + 0x0000051c, 0x00000529, 0x0000054a, 0x0000056a, + 0x0000059d, 0x000005c2, 0x000005e3, } // Size: 244 bytes const enData string = "" + // Size: 1507 bytes @@ -173,19 +175,19 @@ const enData string = "" + // Size: 1507 bytes "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\x02Check for duplicat" + - "e listens on import (slower)\x02Disable auto correction of submitted lis" + - "tens\x02Ignore skipped listens\x02Directory path\x02Ignore listens in in" + - "cognito mode\x02Minimum playback duration for skipped tracks (seconds)" + - "\x02Error: OAuth state mismatch\x04\x00\x01\x0a.\x02Access token receive" + - "d, you can use %[1]v now.\x02exporting\x02importing\x02done\x02Yes\x02No" + - "\x02no existing service configurations\x02Service\x02Backend\x02Transfer" + - "ring %[1]s from %[2]s to %[3]s…\x02From timestamp: %[1]v (%[2]v)\x02Impo" + - "rt failed, last reported timestamp was %[1]v (%[2]s)\x02Imported %[1]v o" + - "f %[2]v %[3]s into %[4]v.\x02Import log:\x02%[1]v: %[2]v\x02invalid time" + - "stamp string \x22%[1]v\x22\x02Latest timestamp: %[1]v (%[2]v)\x02no conf" + - "iguration file defined, cannot write config\x02key must only consist of " + - "A-Za-z0-9_-\x02no service configuration \x22%[1]v\x22\x02Ignored duplica" + - "te listen %[1]v: \x22%[2]v\x22 by %[3]v (%[4]v)\x02Specify a time zone f" + - "or the listen timestamps\x02Visit the URL for authorization: %[1]v" + "e listens on import (slower)\x02Ignored duplicate listen %[1]v: \x22%[2]" + + "v\x22 by %[3]v (%[4]v)\x02Disable auto correction of submitted listens" + + "\x02Ignore skipped listens\x02Specify a time zone for the listen timesta" + + "mps\x02Directory path\x02Ignore listens in incognito mode\x02Minimum pla" + + "yback duration for skipped tracks (seconds)\x02Visit the URL for authori" + + "zation: %[1]v\x02Error: OAuth state mismatch\x04\x00\x01\x0a.\x02Access " + + "token received, you can use %[1]v now.\x02exporting\x02importing\x02done" + + "\x02Yes\x02No\x02no existing service configurations\x02Service\x02Backen" + + "d\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)\x02Imp" + + "orted %[1]v of %[2]v %[3]s into %[4]v.\x02Import log:\x02%[1]v: %[2]v" + + "\x02invalid timestamp string \x22%[1]v\x22\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" - // Total table size 3591 bytes (3KiB); checksum: 2A4B9572 + // Total table size 3731 bytes (3KiB); checksum: F7951710 diff --git a/internal/translations/locales/de/out.gotext.json b/internal/translations/locales/de/out.gotext.json index 7a13af8..680505e 100644 --- a/internal/translations/locales/de/out.gotext.json +++ b/internal/translations/locales/de/out.gotext.json @@ -263,7 +263,7 @@ { "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,7 +312,7 @@ { "id": "Specify a time zone for the listen timestamps", "message": "Specify a time zone for the listen timestamps", - "translation": "" + "translation": "Zeitzone für den Abspiel-Zeitstempel" }, { "id": "Directory path", @@ -332,7 +332,7 @@ { "id": "Visit the URL for authorization: {URL}", "message": "Visit the URL for authorization: {URL}", - "translation": "", + "translation": "Zur Anmeldung folgende URL aufrufen: {URL}", "placeholders": [ { "id": "URL", From 1e91b684cb547946d9f47a7830fbea0368142f12 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 29 Apr 2025 16:16:43 +0200 Subject: [PATCH 149/150] Release 0.5.0 --- CHANGES.md | 2 +- internal/version/version.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index cda0d79..7324f77 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,6 @@ # Scotty Changelog -## 0.5.0 - (not yet released) +## 0.5.0 - 2025-04-29 - ListenBrainz: handle missing loves metadata in case of merged recordings - ListenBrainz: fix loves import loading all existing loves - ListenBrainz: fixed progress for loves import diff --git a/internal/version/version.go b/internal/version/version.go index 818bec1..07d1569 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -1,5 +1,5 @@ /* -Copyright © 2023-2024 Philipp Wolfer +Copyright © 2023-2025 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.4.1" + AppVersion = "0.5.0" AppURL = "https://git.sr.ht/~phw/scotty/" ) From 0a411fe2fa90a0db8edc82316f7d2534c88c0c9f Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 29 Apr 2025 17:25:10 +0200 Subject: [PATCH 150/150] If locale detection fails fall back to English --- internal/i18n/i18n.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/internal/i18n/i18n.go b/internal/i18n/i18n.go index cbfd516..a910ca0 100644 --- a/internal/i18n/i18n.go +++ b/internal/i18n/i18n.go @@ -16,11 +16,10 @@ Scotty. If not, see . package i18n import ( - "log" - "github.com/Xuanwo/go-locale" _ "go.uploadedlobster.com/scotty/internal/translations" + "golang.org/x/text/language" "golang.org/x/text/message" ) @@ -29,7 +28,7 @@ var localizer Localizer func init() { tag, err := locale.Detect() if err != nil { - log.Fatal(err) + tag = language.English } localizer = New(tag) }