Allow for adding/deleting profiles, add a bunch of sanity checks for config changes.

This commit is contained in:
Justin Hawkins 2021-09-30 17:04:12 +09:30
parent bf127f6cc2
commit 89b142a150
5 changed files with 102 additions and 34 deletions

View File

@ -3,8 +3,10 @@ package config
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"log" "log"
"os" "os"
"strings"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
@ -35,15 +37,20 @@ type Config struct {
func DefaultConfig() *Config { func DefaultConfig() *Config {
defaultConfig := Config{} defaultConfig := Config{}
stdProfile := DownloadProfile{Name: "standard youtube-dl video", Command: "youtube-dl", Args: []string{ stdProfile := DownloadProfile{Name: "standard video", Command: "youtube-dl", Args: []string{
"--newline", "--newline",
"--write-info-json", "--write-info-json",
"-f", "-f",
"bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best", "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best",
}} }}
mp3Profile := DownloadProfile{Name: "standard mp3", Command: "youtube-dl", Args: []string{
"extract-audio",
"--audio-format", "mp3",
"--prefer-ffmpeg",
}}
defaultConfig.DownloadProfiles = append(defaultConfig.DownloadProfiles, stdProfile) defaultConfig.DownloadProfiles = append(defaultConfig.DownloadProfiles, stdProfile)
defaultConfig.DownloadProfiles = append(defaultConfig.DownloadProfiles, stdProfile) defaultConfig.DownloadProfiles = append(defaultConfig.DownloadProfiles, mp3Profile)
defaultConfig.Server.Port = 6123 defaultConfig.Server.Port = 6123
defaultConfig.Server.Address = "http://localhost:6123" defaultConfig.Server.Address = "http://localhost:6123"
@ -64,11 +71,59 @@ func (c *Config) UpdateFromJSON(j []byte) error {
log.Printf("Unmarshal error in config: %v", err) log.Printf("Unmarshal error in config: %v", err)
return err return err
} }
log.Printf("Config is unmarshalled ok")
// other checks // sanity checks
if newConfig.UI.PopupHeight < 100 || newConfig.UI.PopupHeight > 2000 { if newConfig.UI.PopupHeight < 100 || newConfig.UI.PopupHeight > 2000 {
return errors.New("bad popup height") return errors.New("invalid popup height - should be 100-2000")
}
if newConfig.UI.PopupWidth < 100 || newConfig.UI.PopupWidth > 2000 {
return errors.New("invalid popup width - should be 100-2000")
}
// check listen port
if newConfig.Server.Port < 1 || newConfig.Server.Port > 65535 {
return errors.New("invalid server listen port")
}
// check download path
fi, err := os.Stat(newConfig.Server.DownloadPath)
if os.IsNotExist(err) {
return fmt.Errorf("path '%s' does not exist", newConfig.Server.DownloadPath)
}
if !fi.IsDir() {
return fmt.Errorf("path '%s' is not a directory", newConfig.Server.DownloadPath)
}
// check profile name uniqueness
for i, p1 := range newConfig.DownloadProfiles {
for j, p2 := range newConfig.DownloadProfiles {
if i != j && p1.Name == p2.Name {
return fmt.Errorf("duplicate download profile name '%s'", p1.Name)
}
}
}
// remove leading/trailing spaces from args and commands and check for emptiness
for i := range newConfig.DownloadProfiles {
newConfig.DownloadProfiles[i].Name = strings.TrimSpace(newConfig.DownloadProfiles[i].Name)
if newConfig.DownloadProfiles[i].Name == "" {
return errors.New("profile name cannot be empty")
}
newConfig.DownloadProfiles[i].Command = strings.TrimSpace(newConfig.DownloadProfiles[i].Command)
if newConfig.DownloadProfiles[i].Command == "" {
return fmt.Errorf("command in profile %s cannot be empty", newConfig.DownloadProfiles[i].Name)
}
// check the args
for j := range newConfig.DownloadProfiles[i].Args {
newConfig.DownloadProfiles[i].Args[j] = strings.TrimSpace(newConfig.DownloadProfiles[i].Args[j])
if newConfig.DownloadProfiles[i].Args[j] == "" {
return fmt.Errorf("argument %d of profile %s is empty", j+1, newConfig.DownloadProfiles[i].Name)
}
}
} }
*c = newConfig *c = newConfig
@ -137,12 +192,14 @@ func (c *Config) WriteConfig() {
file, err := os.Create( file, err := os.Create(
path, path,
) )
if err != nil { if err != nil {
log.Fatalf("Could not open config file") log.Fatalf("Could not open config file")
} }
defer file.Close()
file.Write(s) file.Write(s)
file.Close() file.Close()
log.Printf("Stored in %s", path) log.Printf("Wrote configuration out to %s", path)
} }

View File

@ -161,8 +161,8 @@ func ConfigRESTHandler(w http.ResponseWriter, r *http.Request) {
w.Write(errorResB) w.Write(errorResB)
return return
} }
conf.WriteConfig()
} }
conf.WriteConfig()
b, _ := json.Marshal(conf) b, _ := json.Marshal(conf)
w.Write(b) w.Write(b)
} }

View File

@ -3,18 +3,13 @@
<div x-data="config()" x-init="fetch_config();"> <div x-data="config()" x-init="fetch_config();">
<h2>gropple config</h2> <p class="error" x-text="error_message"></p>
<p class="success" x-text="success_message"></p>
<p x-text="error_message"></p> <p>Changes are not saved until "Save Changes" is pressed at the bottom of the page.</p>
<p x-show="version && version.upgrade_available">
<a href="https://github.com/tardisx/gropple/releases">Upgrade is available</a> -
you have
<span x-text="version.current_version"></span> and
<span x-text="version.github_version"></span>
is available.</p>
<div class="pure-g"> <div class="pure-g">
<div class="pure-u-md-1-2 pure-u-1"> <div class="pure-u-md-1-2 pure-u-1">
<form class="pure-form pure-form-stacked gropple-config"> <form class="pure-form pure-form-stacked gropple-config">
@ -64,8 +59,12 @@
<template x-for="(profile, i) in config.profiles"> <template x-for="(profile, i) in config.profiles">
<div> <div>
<label x-bind:for="'config-profiles-'+i+'-name'">Name of profile <span x-text="i+1"></span></label> <label x-bind:for="'config-profiles-'+i+'-name'">Name of profile <span x-text="i+1"></span>
</label>
<input type="text" x-bind:id="'config-profiles-'+i+'-name'" class="input-long" placeholder="name" x-model="profile.name" /> <input type="text" x-bind:id="'config-profiles-'+i+'-name'" class="input-long" placeholder="name" x-model="profile.name" />
<button class="pure-button button-del" href="#" @click.prevent="config.profiles.splice(i, 1);;">delete profile</button>
<span class="pure-form-message">The name of this profile. For your information only.</span> <span class="pure-form-message">The name of this profile. For your information only.</span>
<label x-bind:for="'config-profiles-'+i+'-command'">Command to run</label> <label x-bind:for="'config-profiles-'+i+'-command'">Command to run</label>
@ -78,18 +77,19 @@
<template x-for="(arg, j) in profile.args"> <template x-for="(arg, j) in profile.args">
<div> <div>
<input type="text" x-bind:id="'config-profiles-'+i+'-arg-'+j" placeholder="arg" x-model="profile.args[j]" /> <input type="text" x-bind:id="'config-profiles-'+i+'-arg-'+j" placeholder="arg" x-model="profile.args[j]" />
<button class="pure-button button-del" href="#" @click.prevent="profile.args.splice(j, 1);;">del</button> <button class="pure-button button-del" href="#" @click.prevent="profile.args.splice(j, 1);;">delete arg</button>
</div> </div>
</template> </template>
<button class="pure-button button-add" href="#" @click.prevent="profile.args.push('');">add arg</button> <button class="pure-button button-add" href="#" @click.prevent="profile.args.push('');">add arg</button>
<hr> <hr>
</div> </div>
</template> </template>
<button class="pure-button button-add" href="#" @click.prevent="config.profiles.push({name: 'new profile', command: 'youtube-dl', args: []});">add profile</button>
</fieldset> </fieldset>
</form> </form>
</div> </div>
@ -110,8 +110,8 @@
function config() { function config() {
return { return {
config: { server : {}, ui : {}, profiles: [] }, config: { server : {}, ui : {}, profiles: [] },
version: {},
error_message: '', error_message: '',
success_message: '',
fetch_config() { fetch_config() {
fetch('/rest/config') fetch('/rest/config')
@ -124,7 +124,6 @@
}); });
}, },
save_config() { save_config() {
console.log(this.config);
let op = { let op = {
method: 'POST', method: 'POST',
body: JSON.stringify(this.config), body: JSON.stringify(this.config),
@ -132,18 +131,22 @@
} }
fetch('/rest/config', op) fetch('/rest/config', op)
.then(response => { .then(response => {
if (response.status >= 200 && response.status <= 299) { return response.json();
return response.json(); })
} else { .then(response => {
throw Error(response); if (response.error) {
}}) this.error_message = response.error;
.then(config => { this.success_message = '';
console.log('fixing object'); document.body.scrollTop = document.documentElement.scrollTop = 0;
this.config = config; } else {
this.error_message = '';
this.success_message = 'configuration saved';
document.body.scrollTop = document.documentElement.scrollTop = 0;
this.config = response;
}
}) })
.catch(error => { .catch(error => {
this.error_message = error.error; console.log('exception' ,error);
console.log('failed to update config', error);
}); });
} }
} }

View File

@ -3,9 +3,7 @@
<div x-data="index()" x-init="fetch_data(); fetch_version()"> <div x-data="index()" x-init="fetch_data(); fetch_version()">
<h2>gropple</h2> <p x-cloak x-show="version && version.upgrade_available">
<p x-show="version && version.upgrade_available">
<a href="https://github.com/tardisx/gropple/releases">Upgrade is available</a> - <a href="https://github.com/tardisx/gropple/releases">Upgrade is available</a> -
you have you have
<span x-text="version.current_version"></span> and <span x-text="version.current_version"></span> and

View File

@ -36,11 +36,21 @@
padding-top: .5em; padding-top: .5em;
padding-bottom: 1.5em; padding-bottom: 1.5em;
} }
.error {
color: red;
}
.success {
color: green;
}
[x-cloak] { display: none !important; }
</style> </style>
</head> </head>
<body style="margin:4; padding:4"> <body style="margin:4; padding:4">
<div class="pure-menu pure-menu-horizontal" style="height: 2em;"> <div class="pure-menu pure-menu-horizontal" style="height: 2em;">
<a href="#" class="pure-menu-heading pure-menu-link">gropple</a>
<ul class="pure-menu-list"> <ul class="pure-menu-list">
<li class="pure-menu-item"> <li class="pure-menu-item">
<a href="/" class="pure-menu-link">Home</a> <a href="/" class="pure-menu-link">Home</a>