12 Commits

10 changed files with 147 additions and 42 deletions

1
.gitignore vendored
View File

@@ -2,3 +2,4 @@ gropple
release release
dist dist
.env .env
dist/

View File

@@ -1,13 +1,3 @@
# 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. # You may remove this if you don't use go modules.
@@ -27,7 +17,7 @@ archives:
# 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 }}_
{{- title .Os }}_ {{- .Os }}_
{{- if eq .Arch "amd64" }}x86_64 {{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386 {{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }} {{- else }}{{ .Arch }}{{ end }}

View File

@@ -2,7 +2,11 @@
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.1] - 2023-11-26 ## [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 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 - Fix for portable mode and using executable in the current working directory

View File

@@ -189,9 +189,10 @@ 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("Could not find %s on the path", newConfig.DownloadProfiles[i].Command) return fmt.Errorf("problem with command '%s': %s", newConfig.DownloadProfiles[i].Command, err)
} }
} }
@@ -318,7 +319,8 @@ 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 {
panic(err) log.Printf("error writing config: %s", err)
os.Exit(1)
} }
path := cs.ConfigPath path := cs.ConfigPath
@@ -337,3 +339,28 @@ 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

@@ -2,6 +2,8 @@ package config
import ( import (
"os" "os"
"os/exec"
"path/filepath"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -172,3 +174,41 @@ func configServiceFromString(configString string) *ConfigService {
} }
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)
cmd = "./sleep"
path, err = AbsPathToExecutable(cmd)
if assert.NoError(t, err) {
assert.Equal(t, cmdPath, path)
}
}

View File

@@ -281,16 +281,21 @@ func (dl *Download) Begin() {
cmdSlice = append(cmdSlice, dl.Url) cmdSlice = append(cmdSlice, dl.Url)
} }
dl.Log = append(dl.Log, fmt.Sprintf("executing: %s with args: %s", dl.DownloadProfile.Command, strings.Join(cmdSlice, " "))) cmdPath, err := config.AbsPathToExecutable(dl.DownloadProfile.Command)
execAbsolutePath, err := filepath.Abs(dl.DownloadProfile.Command)
if err != nil { if err != nil {
panic(err) 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
} }
cmd := exec.Command(execAbsolutePath, cmdSlice...) 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: %v (executable: %s) in %s", cmd, execAbsolutePath, 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 {
@@ -299,7 +304,6 @@ 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
} }
@@ -371,7 +375,6 @@ 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
@@ -426,8 +429,6 @@ 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

@@ -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
[download] Destination: The Greatest Shot In Television [2WoDQBhJCVQ].f137.mp4 [debug] Invoking hlsnative downloader on "https://example.org/urls/1.2.3.4%
[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

View File

@@ -15,7 +15,7 @@ import (
func main() { func main() {
versionInfo := &version.Manager{ versionInfo := &version.Manager{
VersionInfo: version.Info{CurrentVersion: "v1.1.1-alpha.1"}, VersionInfo: version.Info{CurrentVersion: "v1.1.2"},
} }
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,7 +84,10 @@
<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> <span class="pure-form-message">Which command to run. Your path will be searched, or you can specify the full path here.
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,7 +92,10 @@ 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 {
panic(err) log.Printf("error: %s", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
} }
type Info struct { type Info struct {
@@ -113,7 +116,10 @@ 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 {
panic(err) log.Printf("error: %s", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
} }
} }
} }
@@ -149,12 +155,18 @@ 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 {
panic(err) log.Printf("error: %s", 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 {
panic(err) log.Printf("error: %s", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
} }
} }
} }
@@ -167,7 +179,10 @@ 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 {
panic(err) log.Printf("error: %s", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
} }
err = cs.Config.UpdateFromJSON(b) err = cs.Config.UpdateFromJSON(b)
@@ -221,7 +236,10 @@ 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 {
panic(err) log.Printf("error: %s", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
} }
err = json.Unmarshal(b, &thisReq) err = json.Unmarshal(b, &thisReq)
@@ -271,7 +289,10 @@ 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 {
panic(err) log.Printf("error: %s", 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 {
@@ -307,14 +328,20 @@ 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 {
panic(err) log.Printf("error: %s", 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 {
panic(err) log.Printf("error: %s", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
} }
return return
} else if method == "POST" { } else if method == "POST" {
@@ -389,13 +416,19 @@ func fetchHandler(cs *config.ConfigService, vm *version.Manager, dm *download.Ma
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 {
panic(err) log.Printf("error: %s", 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 {
panic(err) log.Printf("error: %s", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
} }
} }
@@ -412,13 +445,19 @@ func bulkHandler(cs *config.ConfigService, vm *version.Manager, dm *download.Man
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 {
panic(err) log.Printf("error: %s", 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 {
panic(err) log.Printf("error: %s", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
} }
return return