Compare commits

...

20 Commits
v1.1.1 ... main

Author SHA1 Message Date
be963c7c54 Be specific about version 2025-04-25 17:24:13 +09:30
661e676fd2 Fix version passing 2025-04-25 17:10:59 +09:30
08b56d1c77 Update version 2025-04-25 16:50:42 +09:30
2c42204f90 Version from goreleaser 2025-04-25 16:48:36 +09:30
dc31122c05 Bump version 2025-04-25 11:56:47 +09:30
08d73d72e1 Fix goreleaser config 2025-04-25 11:56:38 +09:30
b559f15eb1 New goreleaser config and docker setup (with arm64) 2025-04-25 11:44:59 +09:30
d864a8c486 Do some de-linting, but disable for now in goreleaser 2025-04-25 11:13:48 +09:30
0655700d15 Upgrade dependencies 2025-04-25 11:04:12 +09:30
4cff36e54f Update .gitignore 2025-04-25 11:00:57 +09:30
54e5dbab60 Update changelog 2024-03-17 09:45:35 +10:30
c45e261396 Fix workflow 2024-03-17 09:38:08 +10:30
192479819d Bump version for next release 2024-03-17 09:37:03 +10:30
92c4cc6284 Upgrade deps 2024-03-17 09:36:53 +10:30
dd211f6077 Lint 2024-03-17 09:33:09 +10:30
3b23ff356c Fix template 2024-03-16 23:42:42 +10:30
94b57fc327 Update gitignore 2024-03-16 21:21:31 +10:30
36607b43ab Fix goreleaser 2024-03-16 21:21:05 +10:30
b466157cd0 Bump version 2024-03-16 21:16:49 +10:30
d9a979b782 Remove (almost) all the panics 2024-03-16 21:14:46 +10:30
14 changed files with 156 additions and 74 deletions

View File

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

4
.gitignore vendored
View File

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

View File

@ -1,16 +1,7 @@
# 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
version: 2
before:
hooks:
# You may remove this if you don't use go modules.
- go mod tidy
- go test ./...
@ -23,7 +14,7 @@ builds:
- darwin
archives:
- format: tar.gz
- formats: [tar.gz]
# this name template makes the OS and Arch compatible with the results of `uname`.
name_template: >-
{{ .ProjectName }}_
@ -35,16 +26,28 @@ archives:
# use zip for windows archives
format_overrides:
- goos: windows
format: zip
formats: [zip]
changelog:
skip: true
disable: true
dockers:
- image_templates:
- "tardisx/gropple:{{ .Tag }}"
- "tardisx/gropple:v{{ .Major }}"
- "tardisx/gropple:v{{ .Major }}.{{ .Minor }}"
- "tardisx/gropple:latest"
- "tardisx/gropple:{{ .Tag }}-amd64"
use: buildx
build_flag_templates:
- "--pull"
- "--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,6 +2,20 @@
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

View File

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

View File

@ -266,14 +266,14 @@ func (cs *ConfigService) LoadConfig() error {
path := cs.ConfigPath
b, err := os.ReadFile(path)
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{}
cs.Config = &c
err = yaml.Unmarshal(b, &c)
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
@ -319,7 +319,8 @@ func (cs *ConfigService) LoadConfig() error {
func (cs *ConfigService) WriteConfig() {
s, err := yaml.Marshal(cs.Config)
if err != nil {
panic(err)
log.Printf("error writing config: %s", err)
os.Exit(1)
}
path := cs.ConfigPath

View File

@ -1,6 +1,7 @@
package config
import (
"errors"
"os"
"os/exec"
"path/filepath"
@ -166,8 +167,13 @@ profiles:
func configServiceFromString(configString string) *ConfigService {
tmpFile, _ := os.CreateTemp("", "gropple_test_*.yml")
tmpFile.Write([]byte(configString))
tmpFile.Close()
_, err1 := tmpFile.Write([]byte(configString))
err2 := tmpFile.Close()
if errors.Join(err1, err2) != nil {
panic("got unexpected error")
}
cs := ConfigService{
Config: &Config{},
ConfigPath: tmpFile.Name(),
@ -204,7 +210,7 @@ func TestLookForExecutable(t *testing.T) {
_, err = AbsPathToExecutable(cmd)
assert.Error(t, err)
os.Chdir(cmdDir)
os.Chdir(cmdDir) //nolint
cmd = "./sleep"
path, err = AbsPathToExecutable(cmd)
if assert.NoError(t, err) {

View File

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

View File

@ -277,7 +277,7 @@ func (dl *Download) Begin() {
}
// 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)
}
@ -429,8 +429,6 @@ func (dl *Download) updateMetadata(s string) {
p, err := strconv.ParseFloat(matches[1], 32)
if err == nil {
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
newD.updateMetadata("[download] 0.0% of 504.09MiB at 135.71KiB/s ETA 01:03:36")
if newD.Eta != "01:03:36" {
t.Fatalf("bad long eta in dl\n%#v", newD)
t.Fatalf("bad long eta in dl\n%#v", newD) //nolint
}
newD.updateMetadata("[download] 0.0% of 504.09MiB at 397.98KiB/s ETA 21:38")
if newD.Eta != "21:38" {
t.Fatalf("bad short eta in dl\n%#v", newD)
t.Fatalf("bad short eta in dl\n%#v", newD) //nolint
}
// added a new file, now we are tracking two
@ -44,7 +44,7 @@ func TestUpdateMetadata(t *testing.T) {
// different download
newD.updateMetadata("[download] 99.3% of ~1.42GiB at 320.87KiB/s ETA 00:07 (frag 212/214)")
if newD.Eta != "00:07" {
t.Fatalf("bad short eta in dl with frag\n%v", newD)
t.Fatalf("bad short eta in dl with frag\n%v", newD) //nolint
}
// [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
[info] 2WoDQBhJCVQ: Downloading 1 format(s): 137+140
[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 966.50KiB/s ETA 00:13
[download] 0.1% of 12.82MiB at 1.54MiB/s ETA 00:08

8
go.mod
View File

@ -1,11 +1,13 @@
module github.com/tardisx/gropple
go 1.20
go 1.23.0
toolchain go1.24.1
require (
github.com/gorilla/mux v1.8.1
github.com/stretchr/testify v1.8.4
golang.org/x/mod v0.14.0
github.com/stretchr/testify v1.9.0
golang.org/x/mod v0.24.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/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/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
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/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=

11
main.go
View File

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

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