From c9fa21be73f539654e3124170b3145c2db53b11c Mon Sep 17 00:00:00 2001 From: Philipp Wolfer <ph.wolfer@gmail.com> Date: Tue, 5 Dec 2023 23:25:15 +0100 Subject: [PATCH] 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 <phw@uploadedlobster.com> + +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 <https://www.gnu.org/licenses/>. +*/ + +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 +}