Destinations are now DownloadOptions

This commit is contained in:
Justin Hawkins 2023-11-20 21:01:50 +10:30
parent fa978fecc2
commit a1e6421842
12 changed files with 200 additions and 139 deletions

View File

@ -27,6 +27,12 @@ type DownloadProfile struct {
Args []string `yaml:"args" json:"args"` Args []string `yaml:"args" json:"args"`
} }
// DownloadOption contains configuration for extra arguments to pass to the download command
type DownloadOption struct {
Name string `yaml:"name" json:"name"`
Args []string `yaml:"args" json:"args"`
}
// UI holds the configuration for the user interface // UI holds the configuration for the user interface
type UI struct { type UI struct {
PopupWidth int `yaml:"popup_width" json:"popup_width"` PopupWidth int `yaml:"popup_width" json:"popup_width"`
@ -44,8 +50,9 @@ type Config struct {
ConfigVersion int `yaml:"config_version" json:"config_version"` ConfigVersion int `yaml:"config_version" json:"config_version"`
Server Server `yaml:"server" json:"server"` Server Server `yaml:"server" json:"server"`
UI UI `yaml:"ui" json:"ui"` UI UI `yaml:"ui" json:"ui"`
Destinations []Destination `yaml:"destinations" json:"destinations"` Destinations []Destination `yaml:"destinations" json:"destinations"` // no longer in use, see DownloadOptions
DownloadProfiles []DownloadProfile `yaml:"profiles" json:"profiles"` DownloadProfiles []DownloadProfile `yaml:"profiles" json:"profiles"`
DownloadOptions []DownloadOption `yaml:"download_options" json:"download_options"`
} }
// ConfigService is a struct to handle configuration requests, allowing for the // ConfigService is a struct to handle configuration requests, allowing for the
@ -88,7 +95,8 @@ func (cs *ConfigService) LoadDefaultConfig() {
defaultConfig.Server.MaximumActiveDownloads = 2 defaultConfig.Server.MaximumActiveDownloads = 2
defaultConfig.Destinations = make([]Destination, 0) defaultConfig.Destinations = nil
defaultConfig.DownloadOptions = make([]DownloadOption, 0)
defaultConfig.ConfigVersion = 3 defaultConfig.ConfigVersion = 3
@ -96,7 +104,7 @@ func (cs *ConfigService) LoadDefaultConfig() {
} }
// ProfileCalled returns the corresponding profile, or nil if it does not exist // ProfileCalled returns the corresponding DownloadProfile, or nil if it does not exist
func (c *Config) ProfileCalled(name string) *DownloadProfile { func (c *Config) ProfileCalled(name string) *DownloadProfile {
for _, p := range c.DownloadProfiles { for _, p := range c.DownloadProfiles {
if p.Name == name { if p.Name == name {
@ -106,11 +114,11 @@ func (c *Config) ProfileCalled(name string) *DownloadProfile {
return nil return nil
} }
// DestinationCalled returns the corresponding destination, or nil if it does not exist // DownloadOptionCalled returns the corresponding DownloadOption, or nil if it does not exist
func (c *Config) DestinationCalled(name string) *Destination { func (c *Config) DownloadOptionCalled(name string) *DownloadOption {
for _, p := range c.Destinations { for _, o := range c.DownloadOptions {
if p.Name == name { if o.Name == name {
return &p return &o
} }
} }
return nil return nil
@ -187,17 +195,6 @@ func (c *Config) UpdateFromJSON(j []byte) error {
} }
} }
// check destinations
for _, dest := range newConfig.Destinations {
s, err := os.Stat(dest.Path)
if err != nil {
return fmt.Errorf("destination '%s' (%s) is bad: %s", dest.Name, dest.Path, err)
}
if !s.IsDir() {
return fmt.Errorf("destination '%s' (%s) is not a directory", dest.Name, dest.Path)
}
}
*c = newConfig *c = newConfig
return nil return nil
} }
@ -295,6 +292,20 @@ func (cs *ConfigService) LoadConfig() error {
log.Print("migrated config from version 2 => 3") log.Print("migrated config from version 2 => 3")
} }
if c.ConfigVersion == 3 {
c.ConfigVersion = 4
for i := range c.Destinations {
newDownloadOption := DownloadOption{
Name: c.Destinations[i].Name,
Args: []string{"-o", fmt.Sprintf("%s/%%(title)s [%%(id)s].%%(ext)s", c.Destinations[i].Path)},
}
c.DownloadOptions = append(c.DownloadOptions, newDownloadOption)
c.Destinations = nil
}
configMigrated = true
log.Print("migrated config from version 3 => 4")
}
if configMigrated { if configMigrated {
log.Print("Writing new config after version migration") log.Print("Writing new config after version migration")
cs.WriteConfig() cs.WriteConfig()

View File

@ -3,10 +3,12 @@ package config
import ( import (
"os" "os"
"testing" "testing"
"github.com/stretchr/testify/assert"
) )
func TestMigrationV1toV3(t *testing.T) { func TestMigrationV1toV4(t *testing.T) {
v2Config := `config_version: 1 v1Config := `config_version: 1
server: server:
port: 6123 port: 6123
address: http://localhost:6123 address: http://localhost:6123
@ -31,12 +33,12 @@ profiles:
- --audio-format - --audio-format
- mp3 - mp3
` `
cs := configServiceFromString(v2Config) cs := configServiceFromString(v1Config)
err := cs.LoadConfig() err := cs.LoadConfig()
if err != nil { if err != nil {
t.Errorf("got error when loading config: %s", err) t.Errorf("got error when loading config: %s", err)
} }
if cs.Config.ConfigVersion != 3 { if cs.Config.ConfigVersion != 4 {
t.Errorf("did not migrate version (it is '%d')", cs.Config.ConfigVersion) t.Errorf("did not migrate version (it is '%d')", cs.Config.ConfigVersion)
} }
if cs.Config.Server.MaximumActiveDownloads != 2 { if cs.Config.Server.MaximumActiveDownloads != 2 {
@ -48,6 +50,58 @@ profiles:
os.Remove(cs.ConfigPath) os.Remove(cs.ConfigPath)
} }
func TestMigrateV3toV4(t *testing.T) {
v3Config := `config_version: 3
server:
port: 6123
address: http://localhost:6123
download_path: /tmp/Downloads
maximum_active_downloads_per_domain: 2
ui:
popup_width: 900
popup_height: 900
destinations:
- name: cool destination
path: /tmp/coolness
profiles:
- name: standard video
command: yt-dlp
args:
- --newline
- --write-info-json
- -f
- bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best
- name: standard mp3
command: yt-dlp
args:
- --newline
- --write-info-json
- --extract-audio
- --audio-format
- mp3`
cs := configServiceFromString(v3Config)
err := cs.LoadConfig()
if err != nil {
t.Errorf("got error when loading config: %s", err)
}
if cs.Config.ConfigVersion != 4 {
t.Errorf("did not migrate version (it is '%d')", cs.Config.ConfigVersion)
}
if cs.Config.Server.MaximumActiveDownloads != 2 {
t.Error("did not add MaximumActiveDownloads")
}
if len(cs.Config.Destinations) != 0 {
t.Error("incorrect number of destinations from migrated file")
}
if assert.Len(t, cs.Config.DownloadOptions, 1) {
if assert.Len(t, cs.Config.DownloadOptions[0].Args, 2) {
assert.Equal(t, "-o", cs.Config.DownloadOptions[0].Args[0])
assert.Equal(t, "/tmp/coolness/%(title)s [%(id)s].%(ext)s", cs.Config.DownloadOptions[0].Args[1])
}
}
os.Remove(cs.ConfigPath)
}
func configServiceFromString(configString string) *ConfigService { func configServiceFromString(configString string) *ConfigService {
tmpFile, _ := os.CreateTemp("", "gropple_test_*.yml") tmpFile, _ := os.CreateTemp("", "gropple_test_*.yml")
tmpFile.Write([]byte(configString)) tmpFile.Write([]byte(configString))

View File

@ -8,7 +8,6 @@ import (
"net/url" "net/url"
"os" "os"
"os/exec" "os/exec"
"path/filepath"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
@ -27,7 +26,7 @@ type Download struct {
ExitCode int `json:"exit_code"` ExitCode int `json:"exit_code"`
State State `json:"state"` State State `json:"state"`
DownloadProfile config.DownloadProfile `json:"download_profile"` DownloadProfile config.DownloadProfile `json:"download_profile"`
Destination *config.Destination `json:"destination"` DownloadOption *config.DownloadOption `json:"download_option"`
Finished bool `json:"finished"` Finished bool `json:"finished"`
FinishedTS time.Time `json:"finished_ts"` FinishedTS time.Time `json:"finished_ts"`
Files []string `json:"files"` Files []string `json:"files"`
@ -82,7 +81,6 @@ func (m *Manager) ManageQueue() {
m.Lock.Lock() m.Lock.Lock()
m.startQueued(m.MaxPerDomain) m.startQueued(m.MaxPerDomain)
m.moveToDest()
m.cleanup() m.cleanup()
m.Lock.Unlock() m.Lock.Unlock()
@ -102,32 +100,6 @@ func (m *Manager) DownloadsAsJSON() ([]byte, error) {
return b, err return b, err
} }
func (m *Manager) moveToDest() {
// move any downloads that are complete and have a dest
for _, dl := range m.Downloads {
dl.Lock.Lock()
if dl.Destination != nil && dl.State == STATE_COMPLETE {
dl.State = STATE_MOVED
for _, fn := range dl.Files {
src := filepath.Join(dl.Config.Server.DownloadPath, fn)
dst := filepath.Join(dl.Destination.Path, fn)
err := os.Rename(src, dst)
if err != nil {
log.Printf("%s", err)
dl.Log = append(dl.Log, fmt.Sprintf("Could not move %s to %s - %s", fn, dl.Destination.Path, err))
break
} else {
dl.Log = append(dl.Log, fmt.Sprintf("Moved %s to %s", fn, dl.Destination.Path))
}
}
}
dl.Lock.Unlock()
}
}
// startQueued starts any downloads that have been queued, we would not exceed // startQueued starts any downloads that have been queued, we would not exceed
// maxRunning. If maxRunning is 0, there is no limit. // maxRunning. If maxRunning is 0, there is no limit.
func (m *Manager) startQueued(maxRunning int) { func (m *Manager) startQueued(maxRunning int) {
@ -202,17 +174,6 @@ func (m *Manager) Queue(dl *Download) {
dl.State = STATE_QUEUED dl.State = STATE_QUEUED
} }
func (m *Manager) ChangeDestination(dl *Download, dest *config.Destination) {
dl.Lock.Lock()
// we can only change destination is certain cases...
if dl.State != STATE_FAILED && dl.State != STATE_MOVED {
dl.Destination = dest
}
dl.Lock.Unlock()
}
func NewDownload(url string, conf *config.Config) *Download { func NewDownload(url string, conf *config.Config) *Download {
atomic.AddInt32(&downloadId, 1) atomic.AddInt32(&downloadId, 1)
dl := Download{ dl := Download{

View File

@ -237,15 +237,15 @@ func TestUpdateMetadataPlaylist(t *testing.T) {
output := ` output := `
start of log... start of log...
[download] Downloading playlist: niceuser [download] Downloading playlist: nice_user
[RedGifsUser] niceuser: Downloading JSON metadata page 1 [RedGifsUser] nice_user: Downloading JSON metadata page 1
[RedGifsUser] niceuser: Downloading JSON metadata page 2 [RedGifsUser] nice_user: Downloading JSON metadata page 2
[RedGifsUser] niceuser: Downloading JSON metadata page 3 [RedGifsUser] nice_user: Downloading JSON metadata page 3
[RedGifsUser] niceuser: Downloading JSON metadata page 4 [RedGifsUser] nice_user: Downloading JSON metadata page 4
[RedGifsUser] niceuser: Downloading JSON metadata page 5 [RedGifsUser] nice_user: Downloading JSON metadata page 5
[RedGifsUser] niceuser: Downloading JSON metadata page 6 [RedGifsUser] nice_user: Downloading JSON metadata page 6
[info] Writing playlist metadata as JSON to: niceuser [niceuser].info.json [info] Writing playlist metadata as JSON to: nice_user [nice_user].info.json
[RedGifsUser] playlist niceuser: Downloading 3 videos [RedGifsUser] playlist nice_user: Downloading 3 videos
[download] Downloading video 1 of 3 [download] Downloading video 1 of 3
[info] wrongpreciouschrysomelid: Downloading 1 format(s): hd [info] wrongpreciouschrysomelid: Downloading 1 format(s): hd
[info] Writing video metadata as JSON to: Splendid Wonderful Speaker Power Chocolate Drop [wrongpreciouschrysomelid].info.json [info] Writing video metadata as JSON to: Splendid Wonderful Speaker Power Chocolate Drop [wrongpreciouschrysomelid].info.json
@ -279,8 +279,8 @@ start of log...
[download] 69.1% of 2.89MiB at 11.63MiB/s ETA 00:00 [download] 69.1% of 2.89MiB at 11.63MiB/s ETA 00:00
[download] 100% of 2.89MiB at 14.25MiB/s ETA 00:00 [download] 100% of 2.89MiB at 14.25MiB/s ETA 00:00
[download] 100% of 2.89MiB in 00:00 [download] 100% of 2.89MiB in 00:00
[info] Writing updated playlist metadata as JSON to: niceuser [niceuser].info.json [info] Writing updated playlist metadata as JSON to: nice_user [nice_user].info.json
[download] Finished downloading playlist: niceuser [download] Finished downloading playlist: nice_user
` `
newD := Download{} newD := Download{}

7
go.mod
View File

@ -4,6 +4,13 @@ go 1.20
require ( require (
github.com/gorilla/mux v1.8.1 github.com/gorilla/mux v1.8.1
github.com/stretchr/testify v1.8.4
golang.org/x/mod v0.14.0 golang.org/x/mod v0.14.0
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0
) )
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

8
go.sum
View File

@ -1,8 +1,16 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -11,14 +11,14 @@
<div class="pure-g"> <div class="pure-g">
<div class="pure-u-1"> <div class="pure-u-1">
<button class="pure-button pure-button-primary" @click="save_config();" href="#">Save Config</button> <button class="button-small pure-button button-small pure-button-primary" @click="save_config();" href="#">Save Config</button>
</div> </div>
</div> </div>
<div class="pure-g"> <div class="pure-g">
<div class="pure-u-lg-1-3 pure-u-1 l-box"> <div class="pure-u-lg-1-3 pure-u-1 l-box">
<form class="pure-form pure-form-stacked gropple-config"> <form class="pure-form pure-form-stacked gropple-config">
<fieldset> <fieldset>
@ -65,8 +65,8 @@
<legend>Download Profiles</legend> <legend>Download Profiles</legend>
<p>Gropple supports multiple download profiles. Each profile specifies a different youtube-dl <p>Gropple supports multiple download profiles. Each profile specifies a different youtube-dl
compatible command, and arguments. When starting a download, you may choose which profile compatible command, and arguments. When starting a download, you may choose which profile
to use. The URL will be appended to the argument list at the end. to use. The URL will be appended to the argument list at the end.
</p> </p>
@ -75,10 +75,10 @@
<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 x-bind:for="'config-profiles-'+i+'-name'">Name of profile <span x-text="i+1"></span>
</label> </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> <button class="button-small 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>
@ -92,11 +92,11 @@
<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);;">delete arg</button> <button class="button-small 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="button-small pure-button button-add" href="#" @click.prevent="profile.args.push('');">add arg</button>
<span class="pure-form-message">Arguments for the command. Note that the shell is not used, so there is no need to quote or escape arguments, including those with spaces.</span> <span class="pure-form-message">Arguments for the command. Note that the shell is not used, so there is no need to quote or escape arguments, including those with spaces.</span>
<hr> <hr>
@ -104,7 +104,7 @@
</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> <button class="button-small pure-button button-add" href="#" @click.prevent="config.profiles.push({name: 'new profile', command: 'youtube-dl', args: []});">add profile</button>
</fieldset> </fieldset>
</form> </form>
@ -113,39 +113,44 @@
<div class="pure-u-lg-1-3 pure-u-1 l-box"> <div class="pure-u-lg-1-3 pure-u-1 l-box">
<form class="pure-form gropple-config"> <form class="pure-form gropple-config">
<fieldset> <fieldset>
<legend>Destinations</legend> <legend>Download Options</legend>
<p>You can specify custom destinations (directories) here. Downloads can be <p>You can specify custom download options here. These are (optionally) selectable in addition
moved to one of these directories after completion from the index page, to the profile when starting a download. They append extra arguments to the downloader command.
if you do not want them to be left in the download path above.</p> The most common use is to specify a particular <tt>-o</tt> argument to <tt>yt-dlp</tt> to allow files to be downloaded
to a custom path.</p>
</p> </p>
<template x-for="(dest, i) in config.destinations"> <template x-for="(download_option, i) in config.download_options">
<div> <div>
<label x-bind:for="'config-destinations-'+i+'-name'">Name of destination <span x-text="i+1"></span> <label x-bind:for="'config-download-option-'+i+'-name'">Name of option <span x-text="i+1"></span>
</label> </label>
<input type="text" x-bind:id="'config-destinations-'+i+'-name'" class="input-long" placeholder="name" x-model="dest.name" />
<span class="pure-form-message">The name of this destination. For your information only.</span> <input type="text" x-bind:id="'config-download-option-'+i+'-name'" class="input-long" placeholder="name" x-model="download_option.name" />
<label x-bind:for="'config-destinations-'+i+'-command'">Path</label> <span class="pure-form-message">The name of this option. For your information only.</span>
<input type="text" x-bind:id="'config-destinations-'+i+'-command'" class="input-long" placeholder="name" x-model="dest.path" />
<span class="pure-form-message">Path to move completed downloads to.</span>
<button class="pure-button button-del" href="#" @click.prevent="config.destinations.splice(i, 1);">delete destination</button> <label>Arguments</label>
<template x-for="(arg, j) in download_option.args">
<div>
<input type="text" x-bind:id="'config-download-option-'+i+'-arg-'+j" placeholder="arg" x-model="download_option.args[j]" />
<button class="button-small pure-button button-del" href="#" @click.prevent="download_option.args.splice(j, 1);;">delete arg</button>
</div>
</template>
<button class="button-small pure-button button-del" href="#" @click.prevent="config.download_options.splice(i, 1);">delete option</button>
<hr> <hr>
</div> </div>
</template> </template>
<button class="pure-button button-add" href="#" @click.prevent="config.destinations.push({name: 'new destination', path: '/tmp'});">add destination</button> <button class="button-small pure-button button-add" href="#" @click.prevent="config.download_options.push({name: 'new option', args: ['-o', 'someting']});">add option</button>
</fieldset> </fieldset>
</form> </form>
</div> </div>
<div class="pure-g"> <div class="pure-g">
<div class="pure-u-1"> <div class="pure-u-1">
<button class="pure-button pure-button-primary" @click="save_config();" href="#">Save Config</button> <button class="button-small pure-button button-small pure-button-primary" @click="save_config();" href="#">Save Config</button>
</div> </div>
</div> </div>
@ -157,8 +162,8 @@
{{ define "js" }} {{ define "js" }}
<script> <script>
function config() { function config() {
return { return {
config: { server : {}, ui : {}, profiles: [], destinations: []}, config: { server : {}, ui : {}, profiles: [], download_options: []},
error_message: '', error_message: '',
success_message: '', success_message: '',

View File

@ -24,13 +24,23 @@
<table class="pure-table"> <table class="pure-table">
<thead> <thead>
<tr> <tr>
<th>id</th><th>filename</th><th>url</th><th>show</th><th>state</th><th>percent</th><th>eta</th><th>finished</th> <th>id</th>
<th>filename</th>
<th>url</th>
<th>state</th>
<th>percent</th>
<th>eta</th>
<th>finished</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<template x-for="item in items"> <template x-for="item in items">
<tr> <tr>
<td x-text="item.id"></td> <td>
<a class="int-link" @click="show_popup(item)" href="#">
<span x-text="item.id">
</a>
</td>
<td> <td>
<span x-show="item.files && item.files.length > 0"> <span x-show="item.files && item.files.length > 0">
<ul> <ul>
@ -43,18 +53,17 @@
x-text="item.url"> x-text="item.url">
</span> </span>
</td> </td>
<td><a class="int-link" x-bind:href="item.url">&#x2197;</a></td> <td><a class="int-link" x-bind:href="item.url">&#x1F517;</a></td>
<td><a class="int-link" @click="show_popup(item)" href="#">&#x1F4C4;</a></td>
<td :class="'state-'+item.state" x-text="item.state"></td> <td :class="'state-'+item.state" x-text="item.state"></td>
<td x-text="item.percent"></td> <td x-text="item.percent"></td>
<td x-text="item.eta"></td> <td x-text="item.eta"></td>
<td x-text="item.finished ? '&#x2714;' : '-'"></td> <td x-text="item.finished ? '&#x2714;' : '-'"></td>
</tr> </tr>
</template>
</tbody> </template>
</tbody>
</table> </table>
</div> </div>
{{ end }} {{ end }}
@ -62,7 +71,7 @@
{{ define "js" }} {{ define "js" }}
<script> <script>
function index() { function index() {
return { return {
items: [], version: {}, popups: {}, items: [], version: {}, popups: {},
fetch_version() { fetch_version() {
fetch('/rest/version') fetch('/rest/version')

View File

@ -5,9 +5,24 @@
<title>gropple</title> <title>gropple</title>
<script src="/static/alpine.min.js" defer></script> <script src="/static/alpine.min.js" defer></script>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://unpkg.com/purecss@2.0.6/build/pure-min.css" integrity="sha384-Uu6IeWbM+gzNVXJcM9XV3SohHtmWE+3VGi496jvgX1jyvDTXfdK+rfZc8C1Aehk5" crossorigin="anonymous"> <link rel="preconnect" href="https://rsms.me/">
<link rel="stylesheet" href="https://unpkg.com/purecss@2.0.6/build/grids-responsive-min.css"> <link rel="stylesheet" href="https://rsms.me/inter/inter.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/purecss@3.0.0/build/pure-min.css" integrity="sha384-X38yfunGUhNzHpBaEBsWLO+A0HDYOQi8ufWDkZ0k9e0eXz/tH3II7uKZ9msv++Ls" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/purecss@3.0.0/build/grids-responsive-min.css">
<style> <style>
:root {
font-family: Inter, sans-serif;
font-feature-settings: 'liga' 1, 'calt' 1; /* fix for Chrome */
}
@supports (font-variation-settings: normal) {
:root { font-family: InterVariable, sans-serif; }
}
.button-small {
font-size: 85%;
}
.pure-g > div { .pure-g > div {
box-sizing: border-box; box-sizing: border-box;
} }
@ -45,9 +60,6 @@
.gropple-config input.input-long { .gropple-config input.input-long {
width: 27em; width: 27em;
} }
.gropple-config button {
border-radius: 12px;
}
.gropple-config button.button-del { .gropple-config button.button-del {
background: rgb(202, 60, 60); background: rgb(202, 60, 60);
} }

View File

@ -2,6 +2,7 @@
<div id="layout" class="pure-g pure-u-1" x-data="popup()" x-init="fetch_data()"> <div id="layout" class="pure-g pure-u-1" x-data="popup()" x-init="fetch_data()">
<h2>Download started</h2> <h2>Download started</h2>
<p>Fetching <tt>{{ .dl.Url }}</tt></p> <p>Fetching <tt>{{ .dl.Url }}</tt></p>
<form class="pure-form">
<table class="pure-table" > <table class="pure-table" >
<tr> <tr>
<th>profile</th> <th>profile</th>
@ -9,9 +10,9 @@
</tr> </tr>
<tr><th>current filename</th><td x-text="filename"></td></tr> <tr><th>current filename</th><td x-text="filename"></td></tr>
<tr> <tr>
<th>destination</th> <th>option</th>
<td> <td>
{{ if .dl.Destination }} {{ .dl.Destination.Name }} {{ else }} leave in {{ .config.Server.DownloadPath }} {{ end }} {{ if .dl.DownloadOption }} {{ .dl.DownloadOption.Name }} {{ else }} n/a {{ end }}
</td> </td>
</tr> </tr>
<tr><th>state</th><td x-text="state"></td></tr> <tr><th>state</th><td x-text="state"></td></tr>
@ -22,8 +23,9 @@
</table> </table>
<p>You can close this window and your download will continue. Check the <a href="/" target="_gropple_status">Status page</a> to see all downloads in progress.</p> <p>You can close this window and your download will continue. Check the <a href="/" target="_gropple_status">Status page</a> to see all downloads in progress.</p>
{{ if .canStop }} {{ if .canStop }}
<button x-show="state=='Downloading'" class="pure-button" @click="stop()">stop</button> <button x-show="state=='Downloading'" class="button-small pure-button" @click="stop()">stop</button>
{{ end }} {{ end }}
</form>
<div> <div>
<h4>Logs</h4> <h4>Logs</h4>
<pre x-text="log" style="height: auto;"> <pre x-text="log" style="height: auto;">

View File

@ -19,11 +19,11 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<th>destination</th> <th>download option</th>
<td> <td>
<select class="pure-input-1-2" x-model="destination_chosen"> <select class="pure-input-1-2" x-model="download_option_chosen">
<option value="">leave in {{ .config.Server.DownloadPath }}</option> <option value="">no option</option>
{{ range $i := .config.Destinations }} {{ range $i := .config.DownloadOptions }}
<option>{{ $i.Name }}</option> <option>{{ $i.Name }}</option>
{{ end }} {{ end }}
</select> </select>
@ -32,7 +32,7 @@
<tr> <tr>
<th>&nbsp;</th> <th>&nbsp;</th>
<td> <td>
<button class="pure-button" @click="start()">start download</button> <button class="button-small pure-button" @click="start()">start download</button>
</td> </td>
</tr> </tr>
@ -44,12 +44,12 @@
function popup_create() { function popup_create() {
return { return {
profile_chosen: "", profile_chosen: "",
destination_chosen: "", download_option_chosen: "",
error_message: "", error_message: "",
start() { start() {
let op = { let op = {
method: 'POST', method: 'POST',
body: JSON.stringify({action: 'start', url: '{{ .url }}', profile: this.profile_chosen, destination: this.destination_chosen}), body: JSON.stringify({action: 'start', url: '{{ .url }}', profile: this.profile_chosen, download_option: this.download_option_chosen}),
headers: { 'Content-Type': 'application/json' } headers: { 'Content-Type': 'application/json' }
}; };
fetch('/fetch', op) fetch('/fetch', op)

View File

@ -317,9 +317,9 @@ func fetchHandler(cs *config.ConfigService, vm *version.Manager, dm *download.Ma
} else if method == "POST" { } else if method == "POST" {
// creating a new one // creating a new one
type reqType struct { type reqType struct {
URL string `json:"url"` URL string `json:"url"`
ProfileChosen string `json:"profile"` ProfileChosen string `json:"profile"`
DestinationChosen string `json:"destination"` DownloadOptionChosen string `json:"download_option"`
} }
req := reqType{} req := reqType{}
@ -356,20 +356,12 @@ func fetchHandler(cs *config.ConfigService, vm *version.Manager, dm *download.Ma
return return
} }
destination := cs.Config.DestinationCalled(req.DestinationChosen) option := cs.Config.DownloadOptionCalled(req.DownloadOptionChosen)
if req.DestinationChosen != "" && destination == nil {
w.WriteHeader(400)
json.NewEncoder(w).Encode(errorResponse{
Success: false,
Error: fmt.Sprintf("no such destination: '%s'", req.DestinationChosen),
})
return
}
// create the new download // create the new download
newDL := download.NewDownload(req.URL, cs.Config) newDL := download.NewDownload(req.URL, cs.Config)
id := newDL.Id id := newDL.Id
newDL.Destination = destination newDL.DownloadOption = option
newDL.DownloadProfile = *profile newDL.DownloadProfile = *profile
dm.AddDownload(newDL) dm.AddDownload(newDL)
dm.Queue(newDL) dm.Queue(newDL)