mirror of
https://git.sr.ht/~phw/scotty
synced 2025-04-16 10:09:28 +02:00
Implemented service edit command
This commit is contained in:
parent
c6c0723e27
commit
58a47a43e7
15 changed files with 213 additions and 57 deletions
39
cmd/add.go
39
cmd/add.go
|
@ -27,7 +27,6 @@ import (
|
||||||
|
|
||||||
"github.com/manifoldco/promptui"
|
"github.com/manifoldco/promptui"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"go.uploadedlobster.com/scotty/internal/backends"
|
|
||||||
"go.uploadedlobster.com/scotty/internal/cli"
|
"go.uploadedlobster.com/scotty/internal/cli"
|
||||||
"go.uploadedlobster.com/scotty/internal/config"
|
"go.uploadedlobster.com/scotty/internal/config"
|
||||||
)
|
)
|
||||||
|
@ -39,12 +38,7 @@ var addCmd = &cobra.Command{
|
||||||
Long: `Add a service configuration.`,
|
Long: `Add a service configuration.`,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
// Select backend
|
// Select backend
|
||||||
sel := promptui.Select{
|
backend, err := cli.SelectBackend("")
|
||||||
Label: "Backend",
|
|
||||||
Items: backends.GetBackends(),
|
|
||||||
Size: 10,
|
|
||||||
}
|
|
||||||
_, backend, err := sel.Run()
|
|
||||||
cobra.CheckErr(err)
|
cobra.CheckErr(err)
|
||||||
|
|
||||||
// Set service name
|
// Set service name
|
||||||
|
@ -64,12 +58,13 @@ var addCmd = &cobra.Command{
|
||||||
|
|
||||||
// Prepare service config
|
// Prepare service config
|
||||||
service := config.ServiceConfig{
|
service := config.ServiceConfig{
|
||||||
Name: name,
|
Name: name,
|
||||||
Backend: backend,
|
Backend: backend,
|
||||||
|
ConfigValues: make(map[string]any),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Additional options
|
// Additional options
|
||||||
err = extraOptions(&service)
|
service, err = cli.PromptExtraOptions(service)
|
||||||
cobra.CheckErr(err)
|
cobra.CheckErr(err)
|
||||||
|
|
||||||
// Save the service config
|
// Save the service config
|
||||||
|
@ -92,27 +87,3 @@ func init() {
|
||||||
// is called directly, e.g.:
|
// is called directly, e.g.:
|
||||||
// addCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
// 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
|
|
||||||
}
|
|
||||||
|
|
68
cmd/edit.go
Normal file
68
cmd/edit.go
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
/*
|
||||||
|
Copyright © 2023 Philipp Wolfer <phw@uploadedlobster.com>
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
|
@ -36,8 +36,8 @@ type DeezerApiBackend struct {
|
||||||
|
|
||||||
func (b *DeezerApiBackend) Name() string { return "deezer" }
|
func (b *DeezerApiBackend) Name() string { return "deezer" }
|
||||||
|
|
||||||
func (b *DeezerApiBackend) Options() *[]models.BackendOption {
|
func (b *DeezerApiBackend) Options() []models.BackendOption {
|
||||||
return &[]models.BackendOption{{
|
return []models.BackendOption{{
|
||||||
Name: "client-id",
|
Name: "client-id",
|
||||||
Label: "Client ID",
|
Label: "Client ID",
|
||||||
Type: models.String,
|
Type: models.String,
|
||||||
|
|
|
@ -25,7 +25,7 @@ type DumpBackend struct{}
|
||||||
|
|
||||||
func (b *DumpBackend) Name() string { return "dump" }
|
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 {
|
func (b *DumpBackend) FromConfig(config *config.ServiceConfig) models.Backend {
|
||||||
return b
|
return b
|
||||||
|
|
|
@ -33,8 +33,8 @@ type FunkwhaleApiBackend struct {
|
||||||
|
|
||||||
func (b *FunkwhaleApiBackend) Name() string { return "funkwhale" }
|
func (b *FunkwhaleApiBackend) Name() string { return "funkwhale" }
|
||||||
|
|
||||||
func (b *FunkwhaleApiBackend) Options() *[]models.BackendOption {
|
func (b *FunkwhaleApiBackend) Options() []models.BackendOption {
|
||||||
return &[]models.BackendOption{{
|
return []models.BackendOption{{
|
||||||
Name: "server-url",
|
Name: "server-url",
|
||||||
Label: "Server URL",
|
Label: "Server URL",
|
||||||
Type: models.String,
|
Type: models.String,
|
||||||
|
|
|
@ -36,8 +36,8 @@ type JSPFBackend struct {
|
||||||
|
|
||||||
func (b *JSPFBackend) Name() string { return "jspf" }
|
func (b *JSPFBackend) Name() string { return "jspf" }
|
||||||
|
|
||||||
func (b *JSPFBackend) Options() *[]models.BackendOption {
|
func (b *JSPFBackend) Options() []models.BackendOption {
|
||||||
return &[]models.BackendOption{{
|
return []models.BackendOption{{
|
||||||
Name: "file-path",
|
Name: "file-path",
|
||||||
Label: "File path",
|
Label: "File path",
|
||||||
Type: models.String,
|
Type: models.String,
|
||||||
|
|
|
@ -43,8 +43,8 @@ type LastfmApiBackend struct {
|
||||||
|
|
||||||
func (b *LastfmApiBackend) Name() string { return "lastfm" }
|
func (b *LastfmApiBackend) Name() string { return "lastfm" }
|
||||||
|
|
||||||
func (b *LastfmApiBackend) Options() *[]models.BackendOption {
|
func (b *LastfmApiBackend) Options() []models.BackendOption {
|
||||||
return &[]models.BackendOption{{
|
return []models.BackendOption{{
|
||||||
Name: "username",
|
Name: "username",
|
||||||
Label: "User name",
|
Label: "User name",
|
||||||
Type: models.String,
|
Type: models.String,
|
||||||
|
|
|
@ -34,8 +34,8 @@ type ListenBrainzApiBackend struct {
|
||||||
|
|
||||||
func (b *ListenBrainzApiBackend) Name() string { return "listenbrainz" }
|
func (b *ListenBrainzApiBackend) Name() string { return "listenbrainz" }
|
||||||
|
|
||||||
func (b *ListenBrainzApiBackend) Options() *[]models.BackendOption {
|
func (b *ListenBrainzApiBackend) Options() []models.BackendOption {
|
||||||
return &[]models.BackendOption{{
|
return []models.BackendOption{{
|
||||||
Name: "username",
|
Name: "username",
|
||||||
Label: "User name",
|
Label: "User name",
|
||||||
Type: models.String,
|
Type: models.String,
|
||||||
|
|
|
@ -33,8 +33,8 @@ type MalojaApiBackend struct {
|
||||||
|
|
||||||
func (b *MalojaApiBackend) Name() string { return "maloja" }
|
func (b *MalojaApiBackend) Name() string { return "maloja" }
|
||||||
|
|
||||||
func (b *MalojaApiBackend) Options() *[]models.BackendOption {
|
func (b *MalojaApiBackend) Options() []models.BackendOption {
|
||||||
return &[]models.BackendOption{{
|
return []models.BackendOption{{
|
||||||
Name: "server-url",
|
Name: "server-url",
|
||||||
Label: "Server URL",
|
Label: "Server URL",
|
||||||
Type: models.String,
|
Type: models.String,
|
||||||
|
|
|
@ -36,8 +36,8 @@ type ScrobblerLogBackend struct {
|
||||||
|
|
||||||
func (b *ScrobblerLogBackend) Name() string { return "scrobbler-log" }
|
func (b *ScrobblerLogBackend) Name() string { return "scrobbler-log" }
|
||||||
|
|
||||||
func (b *ScrobblerLogBackend) Options() *[]models.BackendOption {
|
func (b *ScrobblerLogBackend) Options() []models.BackendOption {
|
||||||
return &[]models.BackendOption{{
|
return []models.BackendOption{{
|
||||||
Name: "file-path",
|
Name: "file-path",
|
||||||
Label: "File path",
|
Label: "File path",
|
||||||
Type: models.String,
|
Type: models.String,
|
||||||
|
|
|
@ -39,8 +39,8 @@ type SpotifyApiBackend struct {
|
||||||
|
|
||||||
func (b *SpotifyApiBackend) Name() string { return "spotify" }
|
func (b *SpotifyApiBackend) Name() string { return "spotify" }
|
||||||
|
|
||||||
func (b *SpotifyApiBackend) Options() *[]models.BackendOption {
|
func (b *SpotifyApiBackend) Options() []models.BackendOption {
|
||||||
return &[]models.BackendOption{{
|
return []models.BackendOption{{
|
||||||
Name: "client-id",
|
Name: "client-id",
|
||||||
Label: "Client ID",
|
Label: "Client ID",
|
||||||
Type: models.String,
|
Type: models.String,
|
||||||
|
|
|
@ -34,8 +34,8 @@ type SubsonicApiBackend struct {
|
||||||
|
|
||||||
func (b *SubsonicApiBackend) Name() string { return "subsonic" }
|
func (b *SubsonicApiBackend) Name() string { return "subsonic" }
|
||||||
|
|
||||||
func (b *SubsonicApiBackend) Options() *[]models.BackendOption {
|
func (b *SubsonicApiBackend) Options() []models.BackendOption {
|
||||||
return &[]models.BackendOption{{
|
return []models.BackendOption{{
|
||||||
Name: "server-url",
|
Name: "server-url",
|
||||||
Label: "Server URL",
|
Label: "Server URL",
|
||||||
Type: models.String,
|
Type: models.String,
|
||||||
|
|
88
internal/cli/services.go
Normal file
88
internal/cli/services.go
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
/*
|
||||||
|
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 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
|
||||||
|
}
|
|
@ -17,6 +17,7 @@ package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
|
||||||
"github.com/spf13/cast"
|
"github.com/spf13/cast"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
@ -44,6 +45,10 @@ func NewServiceConfig(name string, config *viper.Viper) ServiceConfig {
|
||||||
return service
|
return service
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c ServiceConfig) String() string {
|
||||||
|
return c.Name
|
||||||
|
}
|
||||||
|
|
||||||
func (c *ServiceConfig) GetString(key string) string {
|
func (c *ServiceConfig) GetString(key string) string {
|
||||||
return cast.ToString(c.ConfigValues[key])
|
return cast.ToString(c.ConfigValues[key])
|
||||||
}
|
}
|
||||||
|
@ -66,13 +71,27 @@ func (c *ServiceConfig) Save() error {
|
||||||
return viper.WriteConfig()
|
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 {
|
func AllServices() map[string]ServiceConfig {
|
||||||
services := make(map[string]ServiceConfig)
|
services := make(map[string]ServiceConfig)
|
||||||
config := viper.Sub("service")
|
config := viper.Sub("service")
|
||||||
if config != nil {
|
if config != nil {
|
||||||
for k, v := range config.AllSettings() {
|
for k := range config.AllSettings() {
|
||||||
s, ok := v.(*viper.Viper)
|
s := config.Sub(k)
|
||||||
if ok {
|
if s != nil {
|
||||||
services[k] = NewServiceConfig(k, s)
|
services[k] = NewServiceConfig(k, s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -80,6 +99,16 @@ func AllServices() map[string]ServiceConfig {
|
||||||
return services
|
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) {
|
func GetService(name string) (*ServiceConfig, error) {
|
||||||
key := "service." + name
|
key := "service." + name
|
||||||
config := viper.Sub(key)
|
config := viper.Sub(key)
|
||||||
|
|
|
@ -35,7 +35,7 @@ type Backend interface {
|
||||||
FromConfig(config *config.ServiceConfig) Backend
|
FromConfig(config *config.ServiceConfig) Backend
|
||||||
|
|
||||||
// Return configuration options
|
// Return configuration options
|
||||||
Options() *[]BackendOption
|
Options() []BackendOption
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImportBackend interface {
|
type ImportBackend interface {
|
||||||
|
|
Loading…
Add table
Reference in a new issue