Compare commits

..

No commits in common. "main" and "v1.1.0" have entirely different histories.
main ... v1.1.0

17 changed files with 132 additions and 259 deletions

View File

@ -16,7 +16,7 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v2 uses: actions/setup-go@v2
with: with:
go-version: 1.22 go-version: 1.17
- name: Build - name: Build
run: go build -v ./... run: go build -v ./...

4
.gitignore vendored
View File

@ -1,6 +1,4 @@
gropple gropple
release release
dist dist
.env .env
dist/
.DS_Store

View File

@ -1,9 +1,17 @@
version: 2 # This is an example .goreleaser.yml file with some sensible defaults.
# Make sure to check the documentation at https://goreleaser.com
# The lines below are called `modelines`. See `:help modeline`
# Feel free to remove those if you don't want/need to use them.
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
version: 1
before: before:
hooks: hooks:
# You may remove this if you don't use go modules.
- go mod tidy - go mod tidy
- go test ./...
builds: builds:
- env: - env:
@ -14,7 +22,7 @@ builds:
- darwin - darwin
archives: archives:
- formats: [tar.gz] - format: tar.gz
# this name template makes the OS and Arch compatible with the results of `uname`. # this name template makes the OS and Arch compatible with the results of `uname`.
name_template: >- name_template: >-
{{ .ProjectName }}_ {{ .ProjectName }}_
@ -26,28 +34,16 @@ archives:
# use zip for windows archives # use zip for windows archives
format_overrides: format_overrides:
- goos: windows - goos: windows
formats: [zip] format: zip
changelog: changelog:
disable: true skip: true
dockers: dockers:
- image_templates: - image_templates:
- "tardisx/gropple:{{ .Tag }}-amd64" - "tardisx/gropple:{{ .Tag }}"
use: buildx - "tardisx/gropple:v{{ .Major }}"
build_flag_templates: - "tardisx/gropple:v{{ .Major }}.{{ .Minor }}"
- "--pull" - "tardisx/gropple:latest"
- "--platform=linux/amd64"
- image_templates:
- "tardisx/gropple:{{ .Tag }}-arm64"
use: buildx
build_flag_templates:
- "--pull"
- "--platform=linux/arm64"
goarch: arm64
docker_manifests:
- name_template: "tardisx/gropple:{{ .Tag }}"
image_templates:
- "tardisx/gropple:{{ .Tag }}-amd64"
- "tardisx/gropple:{{ .Tag }}-arm64"

View File

@ -2,25 +2,6 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [v1.1.4] - 2025-04-25
- Minor refactorings, upgrade dependencies
- Migrate to goreleaser v2 configuration
- Generate arm64 docker builds
## [v1.1.3] - 2024-03-17
- Code cleanups, better error checking
## [v1.1.2] - 2024-03-16
- Fix a crash for a certain pattern of log line
## [v1.1.1] - 2023-12-08
- 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 ## [v1.1.0] - 2023-11-25
- Add feature to bulk add URL's for downloading - Add feature to bulk add URL's for downloading

View File

@ -1,4 +1,4 @@
FROM ubuntu:noble FROM ubuntu:mantic
COPY gropple / COPY gropple /
RUN apt update && apt install -y curl python3 ffmpeg RUN apt update && apt install -y curl python3 ffmpeg

View File

@ -164,14 +164,6 @@ Note that this also means that `yt-dlp` can resume partially downloaded files, a
also automatically 'backfill', downloading only files that have not been also automatically 'backfill', downloading only files that have not been
downloaded yet from that playlist. 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 ## Portable mode
If you'd like to use gropple from a USB stick or similar, copy the config file If you'd like to use gropple from a USB stick or similar, copy the config file

49
build_release.pl Executable file
View File

@ -0,0 +1,49 @@
#!/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() { func (cs *ConfigService) LoadTestConfig() {
cs.LoadDefaultConfig() cs.LoadDefaultConfig()
cs.Config.Server.DownloadPath = "/tmp" cs.Config.Server.DownloadPath = "/tmp"
cs.Config.DownloadProfiles = []DownloadProfile{{Name: "test profile", Command: "/bin/sleep", Args: []string{"5"}}} cs.Config.DownloadProfiles = []DownloadProfile{{Name: "test profile", Command: "sleep", Args: []string{"5"}}}
} }
func (cs *ConfigService) LoadDefaultConfig() { func (cs *ConfigService) LoadDefaultConfig() {
@ -98,7 +98,7 @@ func (cs *ConfigService) LoadDefaultConfig() {
defaultConfig.Destinations = nil defaultConfig.Destinations = nil
defaultConfig.DownloadOptions = make([]DownloadOption, 0) defaultConfig.DownloadOptions = make([]DownloadOption, 0)
defaultConfig.ConfigVersion = 4 defaultConfig.ConfigVersion = 3
cs.Config = &defaultConfig cs.Config = &defaultConfig
@ -189,10 +189,9 @@ func (c *Config) UpdateFromJSON(j []byte) error {
} }
// check the command exists // check the command exists
_, err := exec.LookPath(newConfig.DownloadProfiles[i].Command)
_, err := AbsPathToExecutable(newConfig.DownloadProfiles[i].Command)
if err != nil { if err != nil {
return fmt.Errorf("problem with command '%s': %s", newConfig.DownloadProfiles[i].Command, err) return fmt.Errorf("Could not find %s on the path", newConfig.DownloadProfiles[i].Command)
} }
} }
@ -266,14 +265,14 @@ func (cs *ConfigService) LoadConfig() error {
path := cs.ConfigPath path := cs.ConfigPath
b, err := os.ReadFile(path) b, err := os.ReadFile(path)
if err != nil { if err != nil {
return fmt.Errorf("could not read config '%s': %v", path, err) return fmt.Errorf("Could not read config '%s': %v", path, err)
} }
c := Config{} c := Config{}
cs.Config = &c cs.Config = &c
err = yaml.Unmarshal(b, &c) err = yaml.Unmarshal(b, &c)
if err != nil { if err != nil {
return fmt.Errorf("could not parse YAML config '%s': %v", path, err) return fmt.Errorf("Could not parse YAML config '%s': %v", path, err)
} }
// do migrations // do migrations
@ -319,8 +318,7 @@ func (cs *ConfigService) LoadConfig() error {
func (cs *ConfigService) WriteConfig() { func (cs *ConfigService) WriteConfig() {
s, err := yaml.Marshal(cs.Config) s, err := yaml.Marshal(cs.Config)
if err != nil { if err != nil {
log.Printf("error writing config: %s", err) panic(err)
os.Exit(1)
} }
path := cs.ConfigPath path := cs.ConfigPath
@ -339,28 +337,3 @@ func (cs *ConfigService) WriteConfig() {
} }
file.Close() file.Close()
} }
// AbsPathToExecutable takes a command name, which may or may not be path-qualified,
// and returns the fully qualified path to it, or an error if could not be found, or
// if it does not appear to be a file.
func AbsPathToExecutable(cmd string) (string, error) {
pathCmd, err := exec.LookPath(cmd)
if err != nil {
return "", fmt.Errorf("could not LookPath '%s': %w", cmd, err)
}
execAbsolutePath, err := filepath.Abs(pathCmd)
if err != nil {
return "", fmt.Errorf("could not get absolute path to '%s': %w", cmd, err)
}
fi, err := os.Stat(execAbsolutePath)
if err != nil {
return "", fmt.Errorf("could not get stat '%s': %w", cmd, err)
}
if !fi.Mode().IsRegular() {
return "", fmt.Errorf("'%s' is not a regular file: %w", cmd, err)
}
return execAbsolutePath, nil
}

View File

@ -1,10 +1,7 @@
package config package config
import ( import (
"errors"
"os" "os"
"os/exec"
"path/filepath"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -167,54 +164,11 @@ profiles:
func configServiceFromString(configString string) *ConfigService { func configServiceFromString(configString string) *ConfigService {
tmpFile, _ := os.CreateTemp("", "gropple_test_*.yml") tmpFile, _ := os.CreateTemp("", "gropple_test_*.yml")
_, err1 := tmpFile.Write([]byte(configString)) tmpFile.Write([]byte(configString))
err2 := tmpFile.Close() tmpFile.Close()
if errors.Join(err1, err2) != nil {
panic("got unexpected error")
}
cs := ConfigService{ cs := ConfigService{
Config: &Config{}, Config: &Config{},
ConfigPath: tmpFile.Name(), ConfigPath: tmpFile.Name(),
} }
return &cs return &cs
} }
func TestLookForExecutable(t *testing.T) {
cmdPath, err := exec.LookPath("sleep")
if err != nil {
t.Errorf("cannot run this test without knowing about sleep: %s", err)
t.FailNow()
}
cmdDir := filepath.Dir(cmdPath)
cmd := "sleep"
path, err := AbsPathToExecutable(cmd)
if assert.NoError(t, err) {
assert.Equal(t, cmdPath, path)
}
cmd = cmdPath
path, err = AbsPathToExecutable(cmd)
if assert.NoError(t, err) {
assert.Equal(t, cmdPath, path)
}
cmd = "../../../../../../../../.." + cmdPath
path, err = AbsPathToExecutable(cmd)
if assert.NoError(t, err) {
assert.Equal(t, cmdPath, path)
}
cmd = "./sleep"
_, err = AbsPathToExecutable(cmd)
assert.Error(t, err)
os.Chdir(cmdDir) //nolint
cmd = "./sleep"
path, err = AbsPathToExecutable(cmd)
if assert.NoError(t, err) {
assert.Equal(t, cmdPath, path)
}
}

View File

@ -2,7 +2,7 @@ version: "3.9"
services: services:
gropple: gropple:
image: tardisx/gropple:v1.1.4 image: tardisx/gropple:latest
volumes: volumes:
- /tmp/gropple-config-dir/:/config - /tmp/gropple-config-dir/:/config
- /tmp/downloads/:/downloads/ - /tmp/downloads/:/downloads/

View File

@ -277,25 +277,13 @@ func (dl *Download) Begin() {
} }
// only add the url if it's not empty or an example URL. This helps us with testing // only add the url if it's not empty or an example URL. This helps us with testing
if dl.Url != "" && !strings.Contains(dl.domain(), "example.org") { if !(dl.Url == "" || strings.Contains(dl.domain(), "example.org")) {
cmdSlice = append(cmdSlice, dl.Url) cmdSlice = append(cmdSlice, dl.Url)
} }
cmdPath, err := config.AbsPathToExecutable(dl.DownloadProfile.Command) dl.Log = append(dl.Log, fmt.Sprintf("executing: %s with args: %s", dl.DownloadProfile.Command, strings.Join(cmdSlice, " ")))
if err != nil { cmd := exec.Command(dl.DownloadProfile.Command, cmdSlice...)
dl.State = STATE_FAILED
dl.Finished = true
dl.FinishedTS = time.Now()
dl.Log = append(dl.Log, fmt.Sprintf("error finding executable for downloader: %s", err.Error()))
dl.Lock.Unlock()
return
}
dl.Log = append(dl.Log, fmt.Sprintf("executing: %s (%s) with args: %s", dl.DownloadProfile.Command, cmdPath, strings.Join(cmdSlice, " ")))
cmd := exec.Command(cmdPath, cmdSlice...)
cmd.Dir = dl.Config.Server.DownloadPath cmd.Dir = dl.Config.Server.DownloadPath
log.Printf("Executing command executable: %s) in %s", cmdPath, dl.Config.Server.DownloadPath)
stdout, err := cmd.StdoutPipe() stdout, err := cmd.StdoutPipe()
if err != nil { if err != nil {
@ -304,6 +292,7 @@ func (dl *Download) Begin() {
dl.FinishedTS = time.Now() dl.FinishedTS = time.Now()
dl.Log = append(dl.Log, fmt.Sprintf("error setting up stdout pipe: %v", err)) dl.Log = append(dl.Log, fmt.Sprintf("error setting up stdout pipe: %v", err))
dl.Lock.Unlock() dl.Lock.Unlock()
return return
} }
@ -318,10 +307,9 @@ func (dl *Download) Begin() {
return return
} }
log.Printf("Executing command: %v", cmd)
err = cmd.Start() err = cmd.Start()
if err != nil { if err != nil {
log.Printf("Executing command failed: %s", err.Error())
dl.State = STATE_FAILED dl.State = STATE_FAILED
dl.Finished = true dl.Finished = true
dl.FinishedTS = time.Now() dl.FinishedTS = time.Now()
@ -375,6 +363,7 @@ func (dl *Download) Begin() {
} }
} }
dl.Lock.Unlock() dl.Lock.Unlock()
} }
// updateDownload updates the download based on data from the reader. Expects the // updateDownload updates the download based on data from the reader. Expects the
@ -429,6 +418,8 @@ func (dl *Download) updateMetadata(s string) {
p, err := strconv.ParseFloat(matches[1], 32) p, err := strconv.ParseFloat(matches[1], 32)
if err == nil { if err == nil {
dl.Percent = float32(p) dl.Percent = float32(p)
} else {
panic(err)
} }
} }

View File

@ -21,11 +21,11 @@ func TestUpdateMetadata(t *testing.T) {
// eta's might be xx:xx:xx or xx:xx // eta's might be xx:xx:xx or xx:xx
newD.updateMetadata("[download] 0.0% of 504.09MiB at 135.71KiB/s ETA 01:03:36") newD.updateMetadata("[download] 0.0% of 504.09MiB at 135.71KiB/s ETA 01:03:36")
if newD.Eta != "01:03:36" { if newD.Eta != "01:03:36" {
t.Fatalf("bad long eta in dl\n%#v", newD) //nolint t.Fatalf("bad long eta in dl\n%#v", newD)
} }
newD.updateMetadata("[download] 0.0% of 504.09MiB at 397.98KiB/s ETA 21:38") newD.updateMetadata("[download] 0.0% of 504.09MiB at 397.98KiB/s ETA 21:38")
if newD.Eta != "21:38" { if newD.Eta != "21:38" {
t.Fatalf("bad short eta in dl\n%#v", newD) //nolint t.Fatalf("bad short eta in dl\n%#v", newD)
} }
// added a new file, now we are tracking two // added a new file, now we are tracking two
@ -44,7 +44,7 @@ func TestUpdateMetadata(t *testing.T) {
// different download // different download
newD.updateMetadata("[download] 99.3% of ~1.42GiB at 320.87KiB/s ETA 00:07 (frag 212/214)") newD.updateMetadata("[download] 99.3% of ~1.42GiB at 320.87KiB/s ETA 00:07 (frag 212/214)")
if newD.Eta != "00:07" { if newD.Eta != "00:07" {
t.Fatalf("bad short eta in dl with frag\n%v", newD) //nolint t.Fatalf("bad short eta in dl with frag\n%v", newD)
} }
// [FixupM3u8] Fixing MPEG-TS in MP4 container of "file [-168849776_456239489].mp4" // [FixupM3u8] Fixing MPEG-TS in MP4 container of "file [-168849776_456239489].mp4"
@ -317,7 +317,7 @@ func TestUpdateMetadataSingle(t *testing.T) {
[youtube] 2WoDQBhJCVQ: Downloading android player API JSON [youtube] 2WoDQBhJCVQ: Downloading android player API JSON
[info] 2WoDQBhJCVQ: Downloading 1 format(s): 137+140 [info] 2WoDQBhJCVQ: Downloading 1 format(s): 137+140
[info] Writing video metadata as JSON to: The Greatest Shot In Television [2WoDQBhJCVQ].info.json [info] Writing video metadata as JSON to: The Greatest Shot In Television [2WoDQBhJCVQ].info.json
[debug] Invoking hlsnative downloader on "https://example.org/urls/1.2.3.4% [download] Destination: The Greatest Shot In Television [2WoDQBhJCVQ].f137.mp4
[download] 0.0% of 12.82MiB at 510.94KiB/s ETA 00:26 [download] 0.0% of 12.82MiB at 510.94KiB/s ETA 00:26
[download] 0.0% of 12.82MiB at 966.50KiB/s ETA 00:13 [download] 0.0% of 12.82MiB at 966.50KiB/s ETA 00:13
[download] 0.1% of 12.82MiB at 1.54MiB/s ETA 00:08 [download] 0.1% of 12.82MiB at 1.54MiB/s ETA 00:08

8
go.mod
View File

@ -1,13 +1,11 @@
module github.com/tardisx/gropple module github.com/tardisx/gropple
go 1.23.0 go 1.20
toolchain go1.24.1
require ( require (
github.com/gorilla/mux v1.8.1 github.com/gorilla/mux v1.8.1
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.8.4
golang.org/x/mod v0.24.0 golang.org/x/mod v0.14.0
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0
) )

8
go.sum
View File

@ -4,10 +4,10 @@ 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 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=

11
main.go
View File

@ -9,18 +9,13 @@ import (
"github.com/tardisx/gropple/config" "github.com/tardisx/gropple/config"
"github.com/tardisx/gropple/download" "github.com/tardisx/gropple/download"
v "github.com/tardisx/gropple/version" "github.com/tardisx/gropple/version"
"github.com/tardisx/gropple/web" "github.com/tardisx/gropple/web"
) )
var (
version = "dev"
)
func main() { func main() {
versionInfo := &v.Manager{ versionInfo := &version.Manager{
// version from goreleaser has no leading 'v', even if the tag does VersionInfo: version.Info{CurrentVersion: "v1.1.0"},
VersionInfo: v.Info{CurrentVersion: "v" + version},
} }
log.Printf("Starting gropple %s - https://github.com/tardisx/gropple", versionInfo.GetInfo().CurrentVersion) log.Printf("Starting gropple %s - https://github.com/tardisx/gropple", versionInfo.GetInfo().CurrentVersion)

View File

@ -84,10 +84,7 @@
<label x-bind:for="'config-profiles-'+i+'-command'">Command to run</label> <label x-bind:for="'config-profiles-'+i+'-command'">Command to run</label>
<input type="text" x-bind:id="'config-profiles-'+i+'-command'" class="input-long" placeholder="name" x-model="profile.command" /> <input type="text" x-bind:id="'config-profiles-'+i+'-command'" class="input-long" placeholder="name" x-model="profile.command" />
<span class="pure-form-message">Which command to run. Your path will be searched, or you can specify the full path here. <span class="pure-form-message">Which command to run. Your path will be searched, or you can specify the full path here.</span>
If you are using gropple in portable mode and store the executables with the gropple executable, use a prefix of
<tt>./</tt>, for instance <tt>yt-dlp.exe</tt>.
</span>
<label>Arguments</label> <label>Arguments</label>

View File

@ -92,10 +92,7 @@ func homeHandler(cs *config.ConfigService, vm *version.Manager, dm *download.Man
t, err := template.ParseFS(webFS, "data/templates/layout.tmpl", "data/templates/menu.tmpl", "data/templates/index.tmpl") t, err := template.ParseFS(webFS, "data/templates/layout.tmpl", "data/templates/menu.tmpl", "data/templates/index.tmpl")
if err != nil { if err != nil {
log.Printf("error: %s", err) panic(err)
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(err.Error()))
return
} }
type Info struct { type Info struct {
@ -116,10 +113,7 @@ func homeHandler(cs *config.ConfigService, vm *version.Manager, dm *download.Man
defer dm.Lock.Unlock() defer dm.Lock.Unlock()
err = t.ExecuteTemplate(w, "layout", info) err = t.ExecuteTemplate(w, "layout", info)
if err != nil { if err != nil {
log.Printf("error: %s", err) panic(err)
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(err.Error()))
return
} }
} }
} }
@ -155,18 +149,12 @@ func configHandler() func(w http.ResponseWriter, r *http.Request) {
t, err := template.ParseFS(webFS, "data/templates/layout.tmpl", "data/templates/menu.tmpl", "data/templates/config.tmpl") t, err := template.ParseFS(webFS, "data/templates/layout.tmpl", "data/templates/menu.tmpl", "data/templates/config.tmpl")
if err != nil { if err != nil {
log.Printf("error: %s", err) panic(err)
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(err.Error()))
return
} }
err = t.ExecuteTemplate(w, "layout", nil) err = t.ExecuteTemplate(w, "layout", nil)
if err != nil { if err != nil {
log.Printf("error: %s", err) panic(err)
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(err.Error()))
return
} }
} }
} }
@ -179,10 +167,7 @@ func configRESTHandler(cs *config.ConfigService) func(w http.ResponseWriter, r *
log.Printf("Updating config") log.Printf("Updating config")
b, err := io.ReadAll(r.Body) b, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
log.Printf("error: %s", err) panic(err)
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(err.Error()))
return
} }
err = cs.Config.UpdateFromJSON(b) err = cs.Config.UpdateFromJSON(b)
@ -236,10 +221,7 @@ func fetchInfoOneRESTHandler(cs *config.ConfigService, dm *download.Manager) fun
b, err := io.ReadAll(r.Body) b, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
log.Printf("error: %s", err) panic(err)
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(err.Error()))
return
} }
err = json.Unmarshal(b, &thisReq) err = json.Unmarshal(b, &thisReq)
@ -289,10 +271,7 @@ func fetchInfoRESTHandler(dm *download.Manager) func(w http.ResponseWriter, r *h
b, err := dm.DownloadsAsJSON() b, err := dm.DownloadsAsJSON()
if err != nil { if err != nil {
log.Printf("error: %s", err) panic(err)
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(err.Error()))
return
} }
_, err = w.Write(b) _, err = w.Write(b)
if err != nil { if err != nil {
@ -328,20 +307,14 @@ func fetchHandler(cs *config.ConfigService, vm *version.Manager, dm *download.Ma
t, err := template.ParseFS(webFS, "data/templates/layout.tmpl", "data/templates/popup.tmpl") t, err := template.ParseFS(webFS, "data/templates/layout.tmpl", "data/templates/popup.tmpl")
if err != nil { if err != nil {
log.Printf("error: %s", err) panic(err)
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(err.Error()))
return
} }
templateData := map[string]interface{}{"dl": dl, "config": cs.Config, "canStop": download.CanStopDownload, "Version": vm.GetInfo()} templateData := map[string]interface{}{"dl": dl, "config": cs.Config, "canStop": download.CanStopDownload, "Version": vm.GetInfo()}
err = t.ExecuteTemplate(w, "layout", templateData) err = t.ExecuteTemplate(w, "layout", templateData)
if err != nil { if err != nil {
log.Printf("error: %s", err) panic(err)
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(err.Error()))
return
} }
return return
} else if method == "POST" { } else if method == "POST" {
@ -353,29 +326,23 @@ func fetchHandler(cs *config.ConfigService, vm *version.Manager, dm *download.Ma
} }
req := reqType{} req := reqType{}
err := json.NewDecoder(r.Body).Decode(&req) json.NewDecoder(r.Body).Decode(&req)
if err != nil {
log.Printf("error decoding body of request: %s", err)
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(err.Error()))
return
}
log.Printf("popup POST request: %#v", req) log.Printf("popup POST request: %#v", req)
if req.URL == "" { if req.URL == "" {
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(400)
_ = json.NewEncoder(w).Encode(errorResponse{ json.NewEncoder(w).Encode(errorResponse{
Success: false, Success: false,
Error: "No URL supplied", Error: "No URL supplied",
}) })
return return
} else { } else {
if req.ProfileChosen == "" { if req.ProfileChosen == "" {
w.WriteHeader(400) w.WriteHeader(400)
_ = json.NewEncoder(w).Encode(errorResponse{ json.NewEncoder(w).Encode(errorResponse{
Success: false, Success: false,
Error: "you must choose a profile", Error: "you must choose a profile",
}) })
@ -385,7 +352,7 @@ func fetchHandler(cs *config.ConfigService, vm *version.Manager, dm *download.Ma
profile := cs.Config.ProfileCalled(req.ProfileChosen) profile := cs.Config.ProfileCalled(req.ProfileChosen)
if profile == nil { if profile == nil {
w.WriteHeader(400) w.WriteHeader(400)
_ = json.NewEncoder(w).Encode(errorResponse{ json.NewEncoder(w).Encode(errorResponse{
Success: false, Success: false,
Error: fmt.Sprintf("no such profile: '%s'", req.ProfileChosen), Error: fmt.Sprintf("no such profile: '%s'", req.ProfileChosen),
}) })
@ -403,7 +370,7 @@ func fetchHandler(cs *config.ConfigService, vm *version.Manager, dm *download.Ma
dm.Queue(newDL) dm.Queue(newDL)
w.WriteHeader(200) w.WriteHeader(200)
_ = json.NewEncoder(w).Encode(queuedResponse{ json.NewEncoder(w).Encode(queuedResponse{
Success: true, Success: true,
Location: fmt.Sprintf("/fetch/%d", id), Location: fmt.Sprintf("/fetch/%d", id),
}) })
@ -416,25 +383,19 @@ func fetchHandler(cs *config.ConfigService, vm *version.Manager, dm *download.Ma
if !present { if !present {
w.WriteHeader(400) w.WriteHeader(400)
_, _ = fmt.Fprint(w, "No url supplied") fmt.Fprint(w, "No url supplied")
return return
} }
t, err := template.ParseFS(webFS, "data/templates/layout.tmpl", "data/templates/popup_create.tmpl") t, err := template.ParseFS(webFS, "data/templates/layout.tmpl", "data/templates/popup_create.tmpl")
if err != nil { if err != nil {
log.Printf("error: %s", err) panic(err)
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(err.Error()))
return
} }
templateData := map[string]interface{}{"config": cs.Config, "url": url[0], "Version": vm.GetInfo()} templateData := map[string]interface{}{"config": cs.Config, "url": url[0], "Version": vm.GetInfo()}
err = t.ExecuteTemplate(w, "layout", templateData) err = t.ExecuteTemplate(w, "layout", templateData)
if err != nil { if err != nil {
log.Printf("error: %s", err) panic(err)
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(err.Error()))
return
} }
} }
@ -447,28 +408,22 @@ func bulkHandler(cs *config.ConfigService, vm *version.Manager, dm *download.Man
log.Printf("bulkHandler") log.Printf("bulkHandler")
method := r.Method method := r.Method
switch method { if method == "GET" {
case "GET":
t, err := template.ParseFS(webFS, "data/templates/layout.tmpl", "data/templates/menu.tmpl", "data/templates/bulk.tmpl") t, err := template.ParseFS(webFS, "data/templates/layout.tmpl", "data/templates/menu.tmpl", "data/templates/bulk.tmpl")
if err != nil { if err != nil {
log.Printf("error: %s", err) panic(err)
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(err.Error()))
return
} }
templateData := map[string]interface{}{"config": cs.Config, "Version": vm.GetInfo()} templateData := map[string]interface{}{"config": cs.Config, "Version": vm.GetInfo()}
err = t.ExecuteTemplate(w, "layout", templateData) err = t.ExecuteTemplate(w, "layout", templateData)
if err != nil { if err != nil {
log.Printf("error: %s", err) panic(err)
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(err.Error()))
return
} }
return return
case "POST":
} else if method == "POST" {
type reqBulkType struct { type reqBulkType struct {
URLs string `json:"urls"` URLs string `json:"urls"`
ProfileChosen string `json:"profile"` ProfileChosen string `json:"profile"`
@ -476,19 +431,13 @@ func bulkHandler(cs *config.ConfigService, vm *version.Manager, dm *download.Man
} }
req := reqBulkType{} req := reqBulkType{}
err := json.NewDecoder(r.Body).Decode(&req) json.NewDecoder(r.Body).Decode(&req)
if err != nil {
log.Printf("error decoding request body: %s", err)
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(err.Error()))
return
}
log.Printf("bulk POST request: %#v", req) log.Printf("bulk POST request: %#v", req)
if req.URLs == "" { if req.URLs == "" {
w.WriteHeader(400) w.WriteHeader(400)
_ = json.NewEncoder(w).Encode(errorResponse{ json.NewEncoder(w).Encode(errorResponse{
Success: false, Success: false,
Error: "No URLs supplied", Error: "No URLs supplied",
}) })
@ -498,7 +447,7 @@ func bulkHandler(cs *config.ConfigService, vm *version.Manager, dm *download.Man
if req.ProfileChosen == "" { if req.ProfileChosen == "" {
w.WriteHeader(400) w.WriteHeader(400)
_ = json.NewEncoder(w).Encode(errorResponse{ json.NewEncoder(w).Encode(errorResponse{
Success: false, Success: false,
Error: "you must choose a profile", Error: "you must choose a profile",
}) })
@ -508,7 +457,7 @@ func bulkHandler(cs *config.ConfigService, vm *version.Manager, dm *download.Man
profile := cs.Config.ProfileCalled(req.ProfileChosen) profile := cs.Config.ProfileCalled(req.ProfileChosen)
if profile == nil { if profile == nil {
w.WriteHeader(400) w.WriteHeader(400)
_ = json.NewEncoder(w).Encode(errorResponse{ json.NewEncoder(w).Encode(errorResponse{
Success: false, Success: false,
Error: fmt.Sprintf("no such profile: '%s'", req.ProfileChosen), Error: fmt.Sprintf("no such profile: '%s'", req.ProfileChosen),
}) })
@ -534,7 +483,7 @@ func bulkHandler(cs *config.ConfigService, vm *version.Manager, dm *download.Man
} }
w.WriteHeader(200) w.WriteHeader(200)
_ = json.NewEncoder(w).Encode(successResponse{ json.NewEncoder(w).Encode(successResponse{
Success: true, Success: true,
Message: fmt.Sprintf("queued %d downloads", count), Message: fmt.Sprintf("queued %d downloads", count),
}) })