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,7 +11,7 @@
<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>
@ -78,7 +78,7 @@
</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" /> <input type="text" x-bind:id="'config-download-option-'+i+'-name'" class="input-long" placeholder="name" x-model="download_option.name" />
<span class="pure-form-message">The name of this destination. For your information only.</span> <span class="pure-form-message">The name of this option. For your information only.</span>
<label x-bind:for="'config-destinations-'+i+'-command'">Path</label> <label>Arguments</label>
<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> <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>
@ -158,7 +163,7 @@
<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,8 +53,7 @@
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>

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

@ -319,7 +319,7 @@ func fetchHandler(cs *config.ConfigService, vm *version.Manager, dm *download.Ma
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)