From 58a47a43e702cfabeaa20d4e784ad00fceee0ed6 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Fri, 8 Dec 2023 08:38:17 +0100 Subject: [PATCH] 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 {