mirror of
https://git.sr.ht/~phw/scotty
synced 2025-04-15 17:49:29 +02:00
Dynamic per-backend configuration options
This commit is contained in:
parent
ae5f1c5f26
commit
c9fa21be73
14 changed files with 262 additions and 17 deletions
86
cmd/add.go
86
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
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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{},
|
||||
|
|
|
@ -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 {
|
||||
|
|
32
internal/models/options.go
Normal file
32
internal/models/options.go
Normal file
|
@ -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
|
||||
}
|
Loading…
Add table
Reference in a new issue