7 Commits

11 changed files with 219 additions and 57 deletions

View File

@@ -12,6 +12,7 @@ before:
hooks:
# You may remove this if you don't use go modules.
- go mod tidy
- go test ./...
builds:
- env:

View File

@@ -2,6 +2,15 @@
All notable changes to this project will be documented in this file.
## [v1.1.1] - 2023-11-26
- Fix bug where a brand-new config was created with an out-of-date version
- Fix for portable mode and using executable in the current working directory
## [v1.1.0] - 2023-11-25
- Add feature to bulk add URL's for downloading
## [v1.0.1] - 2023-11-24
- Fix crash on migrating a config that had > 1 destinations

View File

@@ -164,6 +164,14 @@ Note that this also means that `yt-dlp` can resume partially downloaded files, a
also automatically 'backfill', downloading only files that have not been
downloaded yet from that playlist.
## Downloading a list of URL's in bulk
From main index page you can click the "Bulk" link in the menu to bring up the
bulk queue page.
In all respects this acts the same as the usual bookmarklet, but it has a
textbox for pasting many URLs at once. All downloads will be queued immediately.
## Portable mode
If you'd like to use gropple from a USB stick or similar, copy the config file

View File

@@ -1,49 +0,0 @@
#!/usr/bin/env perl
use strict;
use warnings;
open my $fh, "<", "main.go" || die $!;
my $version;
while (<$fh>) {
# CurrentVersion: "v0.04"
$version = $1 if /CurrentVersion:\s*"(v.*?)"/;
}
close $fh;
die "no version?" unless defined $version;
# quit if tests fail
system("go test ./...") && die "not building release with failing tests";
# so lazy
system "rm", "-rf", "release", "dist";
system "mkdir", "release";
system "mkdir", "dist";
my %build = (
win => { env => { GOOS => 'windows', GOARCH => 'amd64' }, filename => 'gropple.exe' },
linux => { env => { GOOS => 'linux', GOARCH => 'amd64' }, filename => 'gropple' },
mac => { env => { GOOS => 'darwin', GOARCH => 'amd64' }, filename => 'gropple' },
);
foreach my $type (keys %build) {
mkdir "release/$type";
}
foreach my $type (keys %build) {
local $ENV{GOOS} = $build{$type}->{env}->{GOOS};
local $ENV{GOARCH} = $build{$type}->{env}->{GOARCH};
system "go", "build", "-o", "release/$type/" . $build{$type}->{filename};
system "zip", "-j", "dist/gropple-$type-$version.zip", ( glob "release/$type/*" );
}
# now docker
exit 0;
$ENV{VERSION}="$version";
system "docker-compose", "-f", "docker-compose.build.yml", "build";
system "docker", "tag", "tardisx/gropple:$version", "tardisx/gropple:latest";
system "docker", "push", "tardisx/gropple:$version";
system "docker", "push", "tardisx/gropple:latest";

View File

@@ -65,7 +65,7 @@ type ConfigService struct {
func (cs *ConfigService) LoadTestConfig() {
cs.LoadDefaultConfig()
cs.Config.Server.DownloadPath = "/tmp"
cs.Config.DownloadProfiles = []DownloadProfile{{Name: "test profile", Command: "sleep", Args: []string{"5"}}}
cs.Config.DownloadProfiles = []DownloadProfile{{Name: "test profile", Command: "/bin/sleep", Args: []string{"5"}}}
}
func (cs *ConfigService) LoadDefaultConfig() {
@@ -98,7 +98,7 @@ func (cs *ConfigService) LoadDefaultConfig() {
defaultConfig.Destinations = nil
defaultConfig.DownloadOptions = make([]DownloadOption, 0)
defaultConfig.ConfigVersion = 3
defaultConfig.ConfigVersion = 4
cs.Config = &defaultConfig

View File

@@ -282,8 +282,15 @@ func (dl *Download) Begin() {
}
dl.Log = append(dl.Log, fmt.Sprintf("executing: %s with args: %s", dl.DownloadProfile.Command, strings.Join(cmdSlice, " ")))
cmd := exec.Command(dl.DownloadProfile.Command, cmdSlice...)
execAbsolutePath, err := filepath.Abs(dl.DownloadProfile.Command)
if err != nil {
panic(err)
}
cmd := exec.Command(execAbsolutePath, cmdSlice...)
cmd.Dir = dl.Config.Server.DownloadPath
log.Printf("Executing command: %v (executable: %s) in %s", cmd, execAbsolutePath, dl.Config.Server.DownloadPath)
stdout, err := cmd.StdoutPipe()
if err != nil {
@@ -307,9 +314,10 @@ func (dl *Download) Begin() {
return
}
log.Printf("Executing command: %v", cmd)
err = cmd.Start()
if err != nil {
log.Printf("Executing command failed: %s", err.Error())
dl.State = STATE_FAILED
dl.Finished = true
dl.FinishedTS = time.Now()

View File

@@ -15,7 +15,7 @@ import (
func main() {
versionInfo := &version.Manager{
VersionInfo: version.Info{CurrentVersion: "v1.0.1"},
VersionInfo: version.Info{CurrentVersion: "v1.1.1-alpha.1"},
}
log.Printf("Starting gropple %s - https://github.com/tardisx/gropple", versionInfo.GetInfo().CurrentVersion)

View File

@@ -0,0 +1,91 @@
{{ define "content" }}
{{ template "menu.tmpl" . }}
<div id="layout" class="pure-g pure-u-1" x-data="bulk_create()" >
<h1>Bulk upload</h1>
<p class="error" x-show="error_message" x-transition.duration.500ms x-text="error_message"></p>
<p class="success" x-show="success_message" x-transition.duration.500ms x-text="success_message"></p>
<p>Paste URLs here, one per line:</p>
<textarea x-model="urls" rows="20" cols="80">
</textarea>
<br><br>
<table class="pure-table" >
<tr>
<th>profile</th>
<td>
<select class="pure-input-1-2" x-model="profile_chosen">
<option value="">choose a profile</option>
{{ range $i := .config.DownloadProfiles }}
<option name="{{$i.Name}}">{{ $i.Name }}</option>
{{ end }}
</select>
</td>
</tr>
<tr>
<th>download option</th>
<td>
<select class="pure-input-1-2" x-model="download_option_chosen">
<option value="">no option</option>
{{ range $i := .config.DownloadOptions }}
<option name="{{$i.Name}}">{{ $i.Name }}</option>
{{ end }}
</select>
</td>
</tr>
<tr>
<th>&nbsp;</th>
<td>
<button class="button-small pure-button" @click="start()">add to queue</button>
</td>
</tr>
</table>
</div>
{{ end }}
{{ define "js" }}
<script>
function bulk_create() {
return {
profile_chosen: "",
download_option_chosen: "",
urls: "",
error_message: "",
success_message: "",
start() {
let op = {
method: 'POST',
body: JSON.stringify({action: 'start', urls: this.urls, profile: this.profile_chosen, download_option: this.download_option_chosen}),
headers: { 'Content-Type': 'application/json' }
};
fetch('/bulk', op)
.then(response => response.json())
.then(response => {
console.log(response)
if (response.error) {
this.error_message = response.error;
this.success_message = '';
document.body.scrollTop = document.documentElement.scrollTop = 0;
} else {
this.error_message = '';
this.success_message = response.message;
this.urls = '';
}
})
}
}
}
</script>
{{ end }}

View File

@@ -7,9 +7,11 @@
<li class="pure-menu-item">
<a href="/config" class="pure-menu-link">Config</a>
</li>
<li class="pure-menu-item">
<a href="/bulk" class="pure-menu-link">Bulk</a>
</li>
<li class="pure-menu-item">
<a href="https://github.com/tardisx/gropple" class="pure-menu-link">Github</a>
</li>
</ul>
</div>

View File

@@ -13,7 +13,7 @@
<select class="pure-input-1-2" x-model="profile_chosen">
<option value="">choose a profile</option>
{{ range $i := .config.DownloadProfiles }}
<option>{{ $i.Name }}</option>
<option name="{{$i.Name}}">{{ $i.Name }}</option>
{{ end }}
</select>
</td>
@@ -24,7 +24,7 @@
<select class="pure-input-1-2" x-model="download_option_chosen">
<option value="">no option</option>
{{ range $i := .config.DownloadOptions }}
<option>{{ $i.Name }}</option>
<option name="{{$i.Name}}">{{ $i.Name }}</option>
{{ end }}
</select>
</td>

View File

@@ -55,6 +55,9 @@ func CreateRoutes(cs *config.ConfigService, dm *download.Manager, vm *version.Ma
r.HandleFunc("/fetch", fetchHandler(cs, vm, dm))
r.HandleFunc("/fetch/{id}", fetchHandler(cs, vm, dm))
// handle the bulk uploader
r.HandleFunc("/bulk", bulkHandler(cs, vm, dm))
// get/update info on a download
r.HandleFunc("/rest/fetch/{id}", fetchInfoOneRESTHandler(cs, dm))
@@ -399,3 +402,92 @@ func fetchHandler(cs *config.ConfigService, vm *version.Manager, dm *download.Ma
}
}
func bulkHandler(cs *config.ConfigService, vm *version.Manager, dm *download.Manager) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
log.Printf("bulkHandler")
method := r.Method
if method == "GET" {
t, err := template.ParseFS(webFS, "data/templates/layout.tmpl", "data/templates/menu.tmpl", "data/templates/bulk.tmpl")
if err != nil {
panic(err)
}
templateData := map[string]interface{}{"config": cs.Config, "Version": vm.GetInfo()}
err = t.ExecuteTemplate(w, "layout", templateData)
if err != nil {
panic(err)
}
return
} else if method == "POST" {
type reqBulkType struct {
URLs string `json:"urls"`
ProfileChosen string `json:"profile"`
DownloadOptionChosen string `json:"download_option"`
}
req := reqBulkType{}
json.NewDecoder(r.Body).Decode(&req)
log.Printf("bulk POST request: %#v", req)
if req.URLs == "" {
w.WriteHeader(400)
json.NewEncoder(w).Encode(errorResponse{
Success: false,
Error: "No URLs supplied",
})
return
}
if req.ProfileChosen == "" {
w.WriteHeader(400)
json.NewEncoder(w).Encode(errorResponse{
Success: false,
Error: "you must choose a profile",
})
return
}
profile := cs.Config.ProfileCalled(req.ProfileChosen)
if profile == nil {
w.WriteHeader(400)
json.NewEncoder(w).Encode(errorResponse{
Success: false,
Error: fmt.Sprintf("no such profile: '%s'", req.ProfileChosen),
})
return
}
option := cs.Config.DownloadOptionCalled(req.DownloadOptionChosen)
// create the new downloads
urls := strings.Split(req.URLs, "\n")
count := 0
for _, thisURL := range urls {
thisURL = strings.TrimSpace(thisURL)
if thisURL != "" {
newDL := download.NewDownload(thisURL, cs.Config)
newDL.DownloadOption = option
newDL.DownloadProfile = *profile
dm.AddDownload(newDL)
dm.Queue(newDL)
log.Printf("queued %s", thisURL)
count++
}
}
w.WriteHeader(200)
json.NewEncoder(w).Encode(successResponse{
Success: true,
Message: fmt.Sprintf("queued %d downloads", count),
})
return
}
}
}