19 Commits

Author SHA1 Message Date
5d1f4ffadb Update changelog, skip docker for now until it is ready 2023-03-15 05:18:56 +10:30
385de634f6 Fix Stop button 2023-03-15 05:15:49 +10:30
431ef985bc Cleanup spruious logs 2023-03-15 05:15:25 +10:30
a5e201c290 Restore cleanup 2023-03-15 05:04:02 +10:30
d650725523 Clean up documentation 2023-03-15 05:00:56 +10:30
7f0a51d659 New alpha 2023-03-15 04:46:14 +10:30
4909f63c93 Appease the linter 2023-03-15 04:45:10 +10:30
c8f10e01c7 Cleanup for release 2023-03-15 04:33:48 +10:30
cf7efa70ee Adjust log display to always show all entries. Fixes #22. 2023-03-13 11:43:08 +10:30
ea70f47f76 Update Changelog 2023-03-13 10:54:47 +10:30
2e3156ef65 New alpha release 2023-03-13 10:50:25 +10:30
08e2c1c377 Use constants and constructors in the test 2023-03-13 10:48:38 +10:30
b40dd218f1 Add move to destination functionality 2023-03-13 10:32:20 +10:30
ba87b943ea Add some stress-test data 2023-03-13 10:25:59 +10:30
3e7a3a2f3b Fix some more races 2023-03-10 00:07:29 +10:30
f2c05d0144 Restore test 2023-03-09 23:24:03 +10:30
3d72b8b16a Remove debugging test URLs 2023-03-09 21:46:28 +10:30
9944cb9104 Appease linter 2023-03-09 21:32:24 +10:30
9719449d01 Update deps 2023-03-09 21:32:13 +10:30
14 changed files with 427 additions and 182 deletions

View File

@@ -4,16 +4,19 @@ All notable changes to this project will be documented in this file.
## [Unreleased] ## [Unreleased]
## [v0.6.0] - 2023-03-09 ## [v0.6.0] - 2023-03-15
- Configurable destinations for downloads - Configurable destinations for downloads
- Multiple destination directories can be configured
- When queueing a download, an alternate destination can be selected
- When downloading from a playlist, show the total number of videos and how many have been downloaded - When downloading from a playlist, show the total number of videos and how many have been downloaded
- Show version in web UI - Show version in web UI
- Improve index page (show URL of queued downloads instead of nothing) - Improve index page (show URL of queued downloads instead of nothing)
- Add docker support
- Fixes and improvements to capturing output info and showing it in the UI - Fixes and improvements to capturing output info and showing it in the UI
- Show all log output in the popup
- Fixes to handling of queued downloads - Fixes to handling of queued downloads
- Fix portable mode to look in binary directory, not current directory - Fix portable mode to look in binary directory, not current directory
- Automatically cleanup download list, removing old entries automatically
## [v0.5.5] - 2022-04-09 ## [v0.5.5] - 2022-04-09

View File

@@ -16,9 +16,10 @@ A frontend to youtube-dl (or compatible forks, like yt-dlp) to download videos w
## Binaries ## Binaries
Binaries are available at https://github.com/tardisx/gropple/releases Binaries are available at <https://github.com/tardisx/gropple/releases>
Gropple will automatically check for available updates and prompt you to upgrade. Gropple will automatically check for available updates and prompt you to
upgrade.
## Running ## Running
@@ -32,66 +33,75 @@ interface. The address will be printed after startup:
## Using ## Using
Bring up `http://localhost:6283` (or your configured address) in your browser. You Bring up `http://localhost:6283` (or your configured address) in your browser.
should see a link to the bookmarklet at the top of the screen, and the list of You should see a link to the bookmarklet at the top of the screen, and the list
downloads (currently empty). of downloads (currently empty).
Drag the bookmarklet to your favourites bar, or otherwise bookmark it as you Drag the bookmarklet to your favourites bar, or otherwise bookmark it as you see
see fit. Any kind of browser bookmark should work. The bookmarklet contains fit. Any kind of browser bookmark should work. The bookmarklet contains embedded
embedded javascript to pass the URL of whatever page you are currently on back javascript to pass the URL of whatever page you are currently on back to
to gropple. gropple.
So, whenever you are on a page with a video you would like to download just Whenever you are on a page with a video you would like to download just click
click the bookmarklet. the bookmarklet.
A popup window will appear. Choose a download profile and the download will start. A popup window will appear. Choose a download profile and the download will
The status will be shown in the window, updating in real time. start. The status will be shown in the window, updating in real time.
You may close this window at any time without stopping the download, the status You may close this window at any time without stopping the download, the status
of all downloads is available on the index page. of all downloads is available on the index page.
## Configuration ## Configuration
Click the "config" link on the index page to configure gropple. The default options Click the "config" link on the index page to configure gropple. The default
are fine if you are running on your local machine. If you are running it remotely options are fine if you are running on your local machine. If you are running it
you will need to set the "server address" to ensure the bookmarklet has the correct remotely you will need to set the "server address" to ensure the bookmarklet has
URL in it. the correct URL in it.
### Configuring Downloaders ### Configuring Downloaders
Gropple's default configuration uses the original youtube-dl and has two profiles set Gropple's default configuration uses `yt-dlp` and has two profiles set up, one
up, one for downloading video, the other for downloading audio (mp3). for downloading video, the other for downloading audio (mp3).
Note that gropple does not include any downloaders, you have to install them separately. Note that gropple does not include any downloaders, you have to install them
separately.
If you would like to use a youtube-dl fork (like [yt-dlp](https://github.com/yt-dlp/yt-dlp)) If you would like to use a youtube-dl compatible fork or change the options you
or change the options, you can do so on the right hand side. Create as many profiles as you can do so on the right hand side. Create as many profiles as you wish, whenever
wish, whenever you start a download you can choose the appropriate profile. you start a download you can choose the appropriate profile.
Note that the command arguments must each be specified separately - see the default configuration Note that the command arguments must each be specified separately - see the
for an example. default configuration for an example.
While gropple will use your `PATH` to find the executable, you can also specify a full path While gropple will use your `PATH` to find the executable, you can also specify
instead. Note that any tools that the downloader calls itself (for instance, ffmpeg) will a full path instead. Note that any tools that the downloader calls itself (for
probably need to be available on your path. instance, `ffmpeg`) will need to be available on your path.
### Alternate destinations
Gropple supports adding additional optional destinations. By default, all
downloads will be stored in the main download path specified in the config. You
can also add one or more destinations, and you can choose one of these
destinations when queueing a new download, or while it is still downloading from
the popup.
The file will be moved after downloading is complete.
## Portable mode ## Portable mode
If you'd like to use gropple from a USB stick or similar, copy the config file from If you'd like to use gropple from a USB stick or similar, copy the config file
it's default location (shown when you start gropple) to the same location as the binary, and rename it to `gropple.yml`. from its default location (shown when you start gropple) to the same location as
the binary, and rename it to `gropple.yml`.
If that file is present in the same directory as the binary, it will be used instead.
## Problems ## Problems
Most download problems are probably diagnosable via the log - check in the popup window and scroll Many download problems are diagnosable via the log - check in the popup window
the log down to the bottom. The most common problem is that youtube-dl cannot be found, or its and scroll the log down to the bottom. The most common problem is that `yt-dlp`
dependency (like ffmpeg) cannot be found on your path. cannot be found, or its dependency (like `ffmpeg`) cannot be found on your path.
For other problems, please file an issue on github. For other problems, please file an issue on github.
## TODO ## TODO
Many things. Please raise an issue after checking the [currently open issues](https://github.com/tardisx/gropple/issues). Many things. Please raise an issue after checking the [currently open
issues](https://github.com/tardisx/gropple/issues).

View File

@@ -40,6 +40,7 @@ foreach my $type (keys %build) {
} }
# now docker # now docker
exit 0;
$ENV{VERSION}="$version"; $ENV{VERSION}="$version";
system "docker-compose", "-f", "docker-compose.build.yml", "build"; system "docker-compose", "-f", "docker-compose.build.yml", "build";
system "docker", "tag", "tardisx/gropple:$version", "tardisx/gropple:latest"; system "docker", "tag", "tardisx/gropple:$version", "tardisx/gropple:latest";

View File

@@ -57,6 +57,7 @@ type ConfigService struct {
func (cs *ConfigService) LoadTestConfig() { func (cs *ConfigService) LoadTestConfig() {
cs.LoadDefaultConfig() 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: "sleep", Args: []string{"5"}}}
} }
@@ -93,7 +94,6 @@ func (cs *ConfigService) LoadDefaultConfig() {
cs.Config = &defaultConfig cs.Config = &defaultConfig
return
} }
func (c *Config) ProfileCalled(name string) *DownloadProfile { func (c *Config) ProfileCalled(name string) *DownloadProfile {
@@ -318,6 +318,9 @@ func (cs *ConfigService) WriteConfig() {
} }
defer file.Close() defer file.Close()
file.Write(s) _, err = file.Write(s)
if err != nil {
log.Fatalf("could not write config file %s: %s", path, err)
}
file.Close() file.Close()
} }

View File

@@ -1,12 +1,14 @@
package download package download
import ( import (
"encoding/json"
"fmt" "fmt"
"io" "io"
"log" "log"
"net/url" "net/url"
"os" "os"
"os/exec" "os/exec"
"path/filepath"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
@@ -23,7 +25,7 @@ type Download struct {
PopupUrl string `json:"popup_url"` PopupUrl string `json:"popup_url"`
Process *os.Process `json:"-"` Process *os.Process `json:"-"`
ExitCode int `json:"exit_code"` ExitCode int `json:"exit_code"`
State string `json:"state"` State State `json:"state"`
DownloadProfile config.DownloadProfile `json:"download_profile"` DownloadProfile config.DownloadProfile `json:"download_profile"`
Destination *config.Destination `json:"destination"` Destination *config.Destination `json:"destination"`
Finished bool `json:"finished"` Finished bool `json:"finished"`
@@ -45,6 +47,32 @@ type Manager struct {
Lock sync.Mutex Lock sync.Mutex
} }
func (m *Manager) String() string {
m.Lock.Lock()
defer m.Lock.Unlock()
out := fmt.Sprintf("Max per domain: %d, downloads: %d\n", m.MaxPerDomain, len(m.Downloads))
for _, dl := range m.Downloads {
out = out + fmt.Sprintf("%3d: (%10s) %30s\n", dl.Id, dl.State, dl.Url)
}
return out
}
type State string
const (
STATE_PREPARING State = "Preparing to start"
STATE_CHOOSE_PROFILE State = "Choose Profile"
STATE_QUEUED State = "Queued"
STATE_DOWNLOADING State = "Downloading"
STATE_DOWNLOADING_METADATA State = "Downloading metadata"
STATE_FAILED State = "Failed"
STATE_COMPLETE State = "Complete"
STATE_MOVED State = "Moved"
)
var CanStopDownload = false var CanStopDownload = false
var downloadId int32 = 0 var downloadId int32 = 0
@@ -54,13 +82,52 @@ func (m *Manager) ManageQueue() {
m.Lock.Lock() m.Lock.Lock()
m.startQueued(m.MaxPerDomain) m.startQueued(m.MaxPerDomain)
// m.cleanup() m.moveToDest()
m.cleanup()
m.Lock.Unlock() m.Lock.Unlock()
time.Sleep(time.Second) time.Sleep(time.Second)
} }
} }
func (m *Manager) DownloadsAsJSON() ([]byte, error) {
m.Lock.Lock()
defer m.Lock.Unlock()
for _, dl := range m.Downloads {
dl.Lock.Lock()
defer dl.Lock.Unlock()
}
b, err := json.Marshal(m.Downloads)
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) {
@@ -70,7 +137,7 @@ func (m *Manager) startQueued(maxRunning int) {
for _, dl := range m.Downloads { for _, dl := range m.Downloads {
dl.Lock.Lock() dl.Lock.Lock()
if dl.State == "downloading" || dl.State == "preparing to start" { if dl.State == STATE_DOWNLOADING || dl.State == STATE_PREPARING {
active[dl.domain()]++ active[dl.domain()]++
} }
dl.Lock.Unlock() dl.Lock.Unlock()
@@ -81,8 +148,8 @@ func (m *Manager) startQueued(maxRunning int) {
dl.Lock.Lock() dl.Lock.Lock()
if dl.State == "queued" && (maxRunning == 0 || active[dl.domain()] < maxRunning) { if dl.State == STATE_QUEUED && (maxRunning == 0 || active[dl.domain()] < maxRunning) {
dl.State = "preparing to start" dl.State = STATE_PREPARING
active[dl.domain()]++ active[dl.domain()]++
log.Printf("Starting download for id:%d (%s)", dl.Id, dl.Url) log.Printf("Starting download for id:%d (%s)", dl.Id, dl.Url)
@@ -100,16 +167,17 @@ func (m *Manager) startQueued(maxRunning int) {
} }
// cleanup removes old downloads from the list. Hardcoded to remove them one hour // cleanup removes old downloads from the list. Hardcoded to remove them one hour
// completion. // completion. Expects the Manager to be locked.
func (m *Manager) XXXcleanup() { func (m *Manager) cleanup() {
newDLs := []*Download{} newDLs := []*Download{}
for _, dl := range m.Downloads { for _, dl := range m.Downloads {
dl.Lock.Lock()
if dl.Finished && time.Since(dl.FinishedTS) > time.Duration(time.Hour) { if dl.Finished && time.Since(dl.FinishedTS) > time.Duration(time.Hour) {
// do nothing // do nothing
} else { } else {
newDLs = append(newDLs, dl) newDLs = append(newDLs, dl)
} }
dl.Lock.Unlock()
} }
m.Downloads = newDLs m.Downloads = newDLs
@@ -131,7 +199,18 @@ func (m *Manager) GetDlById(id int) (*Download, error) {
func (m *Manager) Queue(dl *Download) { func (m *Manager) Queue(dl *Download) {
dl.Lock.Lock() dl.Lock.Lock()
defer dl.Lock.Unlock() defer dl.Lock.Unlock()
dl.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 {
@@ -140,7 +219,7 @@ func NewDownload(url string, conf *config.Config) *Download {
Id: int(downloadId), Id: int(downloadId),
Url: url, Url: url,
PopupUrl: fmt.Sprintf("/fetch/%d", int(downloadId)), PopupUrl: fmt.Sprintf("/fetch/%d", int(downloadId)),
State: "choose profile", State: STATE_CHOOSE_PROFILE,
Files: []string{}, Files: []string{},
Log: []string{}, Log: []string{},
Config: conf, Config: conf,
@@ -153,7 +232,6 @@ func (m *Manager) AddDownload(dl *Download) {
m.Lock.Lock() m.Lock.Lock()
defer m.Lock.Unlock() defer m.Lock.Unlock()
m.Downloads = append(m.Downloads, dl) m.Downloads = append(m.Downloads, dl)
return
} }
// func (dl *Download) AppendLog(text string) { // func (dl *Download) AppendLog(text string) {
@@ -173,7 +251,10 @@ func (dl *Download) Stop() {
dl.Lock.Lock() dl.Lock.Lock()
defer dl.Lock.Unlock() defer dl.Lock.Unlock()
dl.Log = append(dl.Log, "aborted by user") dl.Log = append(dl.Log, "aborted by user")
dl.Process.Kill() err := dl.Process.Kill()
if err != nil {
log.Printf("could not send kill to process: %s", err)
}
} }
// domain returns a domain for this Download. Download should be locked. // domain returns a domain for this Download. Download should be locked.
@@ -194,7 +275,7 @@ func (dl *Download) domain() string {
func (dl *Download) Begin() { func (dl *Download) Begin() {
dl.Lock.Lock() dl.Lock.Lock()
dl.State = "downloading" dl.State = STATE_DOWNLOADING
cmdSlice := []string{} cmdSlice := []string{}
cmdSlice = append(cmdSlice, dl.DownloadProfile.Args...) cmdSlice = append(cmdSlice, dl.DownloadProfile.Args...)
@@ -208,7 +289,7 @@ func (dl *Download) Begin() {
stdout, err := cmd.StdoutPipe() stdout, err := cmd.StdoutPipe()
if err != nil { if err != nil {
dl.State = "failed" dl.State = STATE_FAILED
dl.Finished = true dl.Finished = true
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))
@@ -219,7 +300,7 @@ func (dl *Download) Begin() {
stderr, err := cmd.StderrPipe() stderr, err := cmd.StderrPipe()
if err != nil { if err != nil {
dl.State = "failed" dl.State = STATE_FAILED
dl.Finished = true dl.Finished = true
dl.FinishedTS = time.Now() dl.FinishedTS = time.Now()
dl.Log = append(dl.Log, fmt.Sprintf("error setting up stderr pipe: %v", err)) dl.Log = append(dl.Log, fmt.Sprintf("error setting up stderr pipe: %v", err))
@@ -231,7 +312,7 @@ func (dl *Download) Begin() {
log.Printf("Executing command: %v", cmd) log.Printf("Executing command: %v", cmd)
err = cmd.Start() err = cmd.Start()
if err != nil { if err != nil {
dl.State = "failed" dl.State = STATE_FAILED
dl.Finished = true dl.Finished = true
dl.FinishedTS = time.Now() dl.FinishedTS = time.Now()
dl.Log = append(dl.Log, fmt.Sprintf("error starting command '%s': %v", dl.DownloadProfile.Command, err)) dl.Log = append(dl.Log, fmt.Sprintf("error starting command '%s': %v", dl.DownloadProfile.Command, err))
@@ -258,19 +339,30 @@ func (dl *Download) Begin() {
}() }()
wg.Wait() wg.Wait()
cmd.Wait()
err = cmd.Wait()
dl.Lock.Lock() dl.Lock.Lock()
log.Printf("Process finished for id: %d (%v)", dl.Id, cmd) if err != nil {
log.Printf("process failed for id: %d: %s", dl.Id, err)
dl.State = "complete" dl.State = STATE_FAILED
dl.Finished = true dl.Finished = true
dl.FinishedTS = time.Now() dl.FinishedTS = time.Now()
dl.ExitCode = cmd.ProcessState.ExitCode() dl.ExitCode = cmd.ProcessState.ExitCode()
if dl.ExitCode != 0 { } else {
dl.State = "failed"
log.Printf("process finished for id: %d (%v)", dl.Id, cmd)
dl.State = STATE_COMPLETE
dl.Finished = true
dl.FinishedTS = time.Now()
dl.ExitCode = cmd.ProcessState.ExitCode()
if dl.ExitCode != 0 {
dl.State = STATE_FAILED
}
} }
dl.Lock.Unlock() dl.Lock.Unlock()
@@ -318,7 +410,7 @@ func (dl *Download) updateMetadata(s string) {
matches := etaRE.FindStringSubmatch(s) matches := etaRE.FindStringSubmatch(s)
if len(matches) == 2 { if len(matches) == 2 {
dl.Eta = matches[1] dl.Eta = matches[1]
dl.State = "downloading" dl.State = STATE_DOWNLOADING
} }
@@ -379,7 +471,7 @@ func (dl *Download) updateMetadata(s string) {
metadataDL := regexp.MustCompile(`Downloading JSON metadata page (\d+)`) metadataDL := regexp.MustCompile(`Downloading JSON metadata page (\d+)`)
matches = metadataDL.FindStringSubmatch(s) matches = metadataDL.FindStringSubmatch(s)
if len(matches) == 2 { if len(matches) == 2 {
dl.State = "Downloading metadata, page " + matches[1] dl.State = STATE_DOWNLOADING_METADATA
} }
// [FixupM3u8] Fixing MPEG-TS in MP4 container of "file [-168849776_456239489].mp4" // [FixupM3u8] Fixing MPEG-TS in MP4 container of "file [-168849776_456239489].mp4"

View File

@@ -0,0 +1,8 @@
//go:build !testdata
package download
import "github.com/tardisx/gropple/config"
func (m *Manager) AddStressTestData(c *config.ConfigService) {
}

View File

@@ -2,7 +2,11 @@ package download
import ( import (
"strings" "strings"
"sync"
"testing" "testing"
"time"
"github.com/tardisx/gropple/config"
) )
func TestUpdateMetadata(t *testing.T) { func TestUpdateMetadata(t *testing.T) {
@@ -76,79 +80,158 @@ func TestUpdateMetadata(t *testing.T) {
// [download] 100% of 4.64MiB in 00:00 // [download] 100% of 4.64MiB in 00:00
// [ffmpeg] Merging formats into "Halo Infinite Flight 4K Gameplay-wi7Agv1M6PY.mp4" // [ffmpeg] Merging formats into "Halo Infinite Flight 4K Gameplay-wi7Agv1M6PY.mp4"
// func TestQueue(t *testing.T) { func TestQueue(t *testing.T) {
// cs := config.ConfigService{} cs := config.ConfigService{}
// cs.LoadTestConfig() cs.LoadTestConfig()
// conf := cs.Config conf := cs.Config
// new1 := Download{Id: 1, Url: "http://sub.example.org/foo1", State: "queued", DownloadProfile: conf.DownloadProfiles[0], Config: conf} new1 := NewDownload("http://sub.example.org/foo1", conf)
// new2 := Download{Id: 2, Url: "http://sub.example.org/foo2", State: "queued", DownloadProfile: conf.DownloadProfiles[0], Config: conf} new2 := NewDownload("http://sub.example.org/foo2", conf)
// new3 := Download{Id: 3, Url: "http://sub.example.org/foo3", State: "queued", DownloadProfile: conf.DownloadProfiles[0], Config: conf} new3 := NewDownload("http://sub.example.org/foo3", conf)
// new4 := Download{Id: 4, Url: "http://example.org/", State: "queued", DownloadProfile: conf.DownloadProfiles[0], Config: conf} new4 := NewDownload("http://example.org/", conf)
// dls := Downloads{&new1, &new2, &new3, &new4} // pretend the user chose a profile for each
// dls.StartQueued(1) new1.DownloadProfile = *conf.ProfileCalled("test profile")
// time.Sleep(time.Millisecond * 100) new2.DownloadProfile = *conf.ProfileCalled("test profile")
// if dls[0].State == "queued" { new3.DownloadProfile = *conf.ProfileCalled("test profile")
// t.Error("#1 was not started") new4.DownloadProfile = *conf.ProfileCalled("test profile")
// } new1.State = STATE_QUEUED
// if dls[1].State != "queued" { new2.State = STATE_QUEUED
// t.Error("#2 is not queued") new3.State = STATE_QUEUED
// } new4.State = STATE_QUEUED
// if dls[3].State == "queued" {
// t.Error("#4 is not started")
// }
// // this should start no more, as one is still going q := Manager{
// dls.StartQueued(1) Downloads: []*Download{},
// time.Sleep(time.Millisecond * 100) MaxPerDomain: 2,
// if dls[1].State != "queued" { Lock: sync.Mutex{},
// t.Error("#2 was started when it should not be") }
// }
// dls.StartQueued(2) q.AddDownload(new1)
// time.Sleep(time.Millisecond * 100) q.AddDownload(new2)
// if dls[1].State == "queued" { q.AddDownload(new3)
// t.Error("#2 was not started but it should be") q.AddDownload(new4)
// } q.startQueued(1)
// dls.StartQueued(2) // two should start, one from each of the two domains
// time.Sleep(time.Millisecond * 100) time.Sleep(time.Millisecond * 100)
// if dls[3].State == "queued" { if q.Downloads[0].State != STATE_DOWNLOADING {
// t.Error("#4 was not started but it should be") t.Errorf("#1 was not downloading - %s instead ", q.Downloads[0].State)
// } t.Log(q.String())
}
if q.Downloads[1].State != STATE_QUEUED {
t.Errorf("#2 is not queued - %s instead", q.Downloads[1].State)
t.Log(q.String())
}
if q.Downloads[2].State != STATE_QUEUED {
t.Errorf("#3 is not queued - %s instead", q.Downloads[2].State)
t.Log(q.String())
}
if q.Downloads[3].State != STATE_DOWNLOADING {
t.Errorf("#4 is not downloading - %s instead", q.Downloads[3].State)
t.Log(q.String())
}
// // reset them all // this should start no more, as one is still going
// dls[0].State = "queued" q.startQueued(1)
// dls[1].State = "queued" time.Sleep(time.Millisecond * 100)
// dls[2].State = "queued" if q.Downloads[0].State != STATE_DOWNLOADING {
// dls[3].State = "queued" t.Errorf("#1 was not downloading - %s instead ", q.Downloads[0].State)
t.Log(q.String())
}
if q.Downloads[1].State != STATE_QUEUED {
t.Errorf("#2 is not queued - %s instead", q.Downloads[1].State)
t.Log(q.String())
}
if q.Downloads[2].State != STATE_QUEUED {
t.Errorf("#3 is not queued - %s instead", q.Downloads[2].State)
t.Log(q.String())
}
if q.Downloads[3].State != STATE_DOWNLOADING {
t.Errorf("#4 is not downloading - %s instead", q.Downloads[3].State)
t.Log(q.String())
}
// dls.StartQueued(0) // wait until the two finish, check
// time.Sleep(time.Millisecond * 100) time.Sleep(time.Second * 5.0)
if q.Downloads[0].State != STATE_COMPLETE {
t.Errorf("#1 was not complete - %s instead ", q.Downloads[0].State)
t.Log(q.String())
}
if q.Downloads[1].State != STATE_QUEUED {
t.Errorf("#2 is not queued - %s instead", q.Downloads[1].State)
t.Log(q.String())
}
if q.Downloads[2].State != STATE_QUEUED {
t.Errorf("#3 is not queued - %s instead", q.Downloads[2].State)
t.Log(q.String())
}
if q.Downloads[3].State != STATE_COMPLETE {
t.Errorf("#4 is not complete - %s instead", q.Downloads[3].State)
t.Log(q.String())
}
// // they should all be going // this should start one more, as one is still going
// if dls[0].State == "queued" || dls[1].State == "queued" || dls[2].State == "queued" || dls[3].State == "queued" { q.startQueued(1)
// t.Error("none should be queued") time.Sleep(time.Millisecond * 100)
// } if q.Downloads[0].State != STATE_COMPLETE {
t.Errorf("#1 was not complete - %s instead ", q.Downloads[0].State)
t.Log(q.String())
}
if q.Downloads[1].State != STATE_DOWNLOADING {
t.Errorf("#2 is not downloading - %s instead", q.Downloads[1].State)
t.Log(q.String())
}
if q.Downloads[2].State != STATE_QUEUED {
t.Errorf("#3 is not queued - %s instead", q.Downloads[2].State)
t.Log(q.String())
}
if q.Downloads[3].State != STATE_COMPLETE {
t.Errorf("#4 is not complete - %s instead", q.Downloads[3].State)
t.Log(q.String())
}
// // reset them all // this should start no more, as one is still going
// dls[0].State = "queued" q.startQueued(1)
// dls[1].State = "queued" time.Sleep(time.Millisecond * 100)
// dls[2].State = "queued" if q.Downloads[0].State != STATE_COMPLETE {
// dls[3].State = "queued" t.Errorf("#1 was not complete - %s instead ", q.Downloads[0].State)
t.Log(q.String())
}
if q.Downloads[1].State != STATE_DOWNLOADING {
t.Errorf("#2 is not downloading - %s instead", q.Downloads[1].State)
t.Log(q.String())
}
if q.Downloads[2].State != STATE_QUEUED {
t.Errorf("#3 is not queued - %s instead", q.Downloads[2].State)
t.Log(q.String())
}
if q.Downloads[3].State != STATE_COMPLETE {
t.Errorf("#4 is not complete - %s instead", q.Downloads[3].State)
t.Log(q.String())
}
// dls.StartQueued(2) // but if we allow two per domain, the other queued one will start
// time.Sleep(time.Millisecond * 100) q.startQueued(2)
time.Sleep(time.Millisecond * 100)
if q.Downloads[0].State != STATE_COMPLETE {
t.Errorf("#1 was not complete - %s instead ", q.Downloads[0].State)
t.Log(q.String())
}
if q.Downloads[1].State != STATE_DOWNLOADING {
t.Errorf("#2 is not downloading - %s instead", q.Downloads[1].State)
t.Log(q.String())
}
if q.Downloads[2].State != STATE_DOWNLOADING {
t.Errorf("#3 is not downloading - %s instead", q.Downloads[2].State)
t.Log(q.String())
}
if q.Downloads[3].State != STATE_COMPLETE {
t.Errorf("#4 is not complete - %s instead", q.Downloads[3].State)
t.Log(q.String())
}
// // first two should be running, third not (same domain) and 4th running (different domain) }
// if dls[0].State == "queued" || dls[1].State == "queued" || dls[2].State != "queued" || dls[3].State == "queued" {
// t.Error("incorrect queued")
// }
// }
func TestUpdateMetadataPlaylist(t *testing.T) { func TestUpdateMetadataPlaylist(t *testing.T) {

View File

@@ -0,0 +1,28 @@
//go:build testdata
package download
import "github.com/tardisx/gropple/config"
func (m *Manager) AddStressTestData(c *config.ConfigService) {
urls := []string{
"https://www.youtube.com/watch?v=qG_rRkuGBW8",
"https://www.youtube.com/watch?v=ZUzhZpQAU40",
"https://www.youtube.com/watch?v=kVxM3eRWGak",
"https://www.youtube.com/watch?v=pl-y9869y0w",
"https://vimeo.com/783453809",
"https://www.youtube.com/watch?v=Uw4NEPE4l3A",
"https://www.youtube.com/watch?v=2RF0lcTuuYE",
"https://www.youtube.com/watch?v=lymwNQY0dus",
"https://www.youtube.com/watch?v=NTc-I4Z_duc",
"https://www.youtube.com/watch?v=wNSm1TJ84Ac",
"https://vimeo.com/786570322",
}
for _, u := range urls {
d := NewDownload(u, c.Config)
d.DownloadProfile = *c.Config.ProfileCalled("standard video")
m.AddDownload(d)
m.Queue(d)
}
}

2
go.mod
View File

@@ -4,6 +4,6 @@ go 1.20
require ( require (
github.com/gorilla/mux v1.8.0 github.com/gorilla/mux v1.8.0
golang.org/x/mod v0.5.1 golang.org/x/mod v0.9.0
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0
) )

4
go.sum
View File

@@ -1,7 +1,7 @@
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38= golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
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=

101
main.go
View File

@@ -21,11 +21,10 @@ import (
) )
var dm *download.Manager var dm *download.Manager
var downloadId = 0
var configService *config.ConfigService var configService *config.ConfigService
var versionInfo = version.Manager{ var versionInfo = version.Manager{
VersionInfo: version.Info{CurrentVersion: "v0.6.0-alpha.1"}, VersionInfo: version.Info{CurrentVersion: "v0.6.0-alpha.4"},
} }
//go:embed web //go:embed web
@@ -102,7 +101,10 @@ func main() {
// check for a new version every 4 hours // check for a new version every 4 hours
go func() { go func() {
for { for {
versionInfo.UpdateGitHubVersion() err := versionInfo.UpdateGitHubVersion()
if err != nil {
log.Printf("could not get version info: %s", err)
}
time.Sleep(time.Hour * 4) time.Sleep(time.Hour * 4)
} }
}() }()
@@ -110,25 +112,7 @@ func main() {
// start downloading queued downloads when slots available, and clean up // start downloading queued downloads when slots available, and clean up
// old entries // old entries
go dm.ManageQueue() go dm.ManageQueue()
dm.AddStressTestData(configService)
urls := []string{
"https://www.youtube.com/watch?v=qG_rRkuGBW8",
"https://www.youtube.com/watch?v=ZUzhZpQAU40",
// "https://www.youtube.com/watch?v=kVxM3eRWGak",
// "https://www.youtube.com/watch?v=pl-y9869y0w",
// "https://www.youtube.com/watch?v=Uw4NEPE4l3A",
// "https://www.youtube.com/watch?v=6tIsT57_nS0",
// "https://www.youtube.com/watch?v=2RF0lcTuuYE",
// "https://www.youtube.com/watch?v=lymwNQY0dus",
// "https://www.youtube.com/watch?v=NTc-I4Z_duc",
// "https://www.youtube.com/watch?v=wNSm1TJ84Ac",
}
for _, u := range urls {
d := download.NewDownload(u, configService.Config)
d.DownloadProfile = *configService.Config.ProfileCalled("standard video")
dm.AddDownload(d)
dm.Queue(d)
}
log.Printf("Visit %s for details on installing the bookmarklet and to check status", configService.Config.Server.Address) log.Printf("Visit %s for details on installing the bookmarklet and to check status", configService.Config.Server.Address)
log.Fatal(srv.ListenAndServe()) log.Fatal(srv.ListenAndServe())
@@ -139,7 +123,10 @@ func main() {
func versionRESTHandler(w http.ResponseWriter, r *http.Request) { func versionRESTHandler(w http.ResponseWriter, r *http.Request) {
if versionInfo.GetInfo().GithubVersionFetched { if versionInfo.GetInfo().GithubVersionFetched {
b, _ := json.Marshal(versionInfo.GetInfo()) b, _ := json.Marshal(versionInfo.GetInfo())
w.Write(b) _, err := w.Write(b)
if err != nil {
log.Printf("could not write to client: %s", err)
}
} else { } else {
w.WriteHeader(400) w.WriteHeader(400)
} }
@@ -191,7 +178,10 @@ func staticHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
io.Copy(w, f) _, err = io.Copy(w, f)
if err != nil {
log.Printf("could not write to client: %s", err)
}
return return
} }
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
@@ -227,13 +217,19 @@ func configRESTHandler(w http.ResponseWriter, r *http.Request) {
errorRes := errorResponse{Success: false, Error: err.Error()} errorRes := errorResponse{Success: false, Error: err.Error()}
errorResB, _ := json.Marshal(errorRes) errorResB, _ := json.Marshal(errorRes)
w.WriteHeader(400) w.WriteHeader(400)
w.Write(errorResB) _, err = w.Write(errorResB)
if err != nil {
log.Printf("could not write to client: %s", err)
}
return return
} }
configService.WriteConfig() configService.WriteConfig()
} }
b, _ := json.Marshal(configService.Config) b, _ := json.Marshal(configService.Config)
w.Write(b) _, err := w.Write(b)
if err != nil {
log.Printf("could not write config to client: %s", err)
}
} }
func fetchInfoOneRESTHandler(w http.ResponseWriter, r *http.Request) { func fetchInfoOneRESTHandler(w http.ResponseWriter, r *http.Request) {
@@ -275,7 +271,10 @@ func fetchInfoOneRESTHandler(w http.ResponseWriter, r *http.Request) {
errorRes := errorResponse{Success: false, Error: err.Error()} errorRes := errorResponse{Success: false, Error: err.Error()}
errorResB, _ := json.Marshal(errorRes) errorResB, _ := json.Marshal(errorRes)
w.WriteHeader(400) w.WriteHeader(400)
w.Write(errorResB) _, err = w.Write(errorResB)
if err != nil {
log.Printf("could not write to client: %s", err)
}
return return
} }
@@ -294,7 +293,10 @@ func fetchInfoOneRESTHandler(w http.ResponseWriter, r *http.Request) {
succRes := successResponse{Success: true, Message: "download started"} succRes := successResponse{Success: true, Message: "download started"}
succResB, _ := json.Marshal(succRes) succResB, _ := json.Marshal(succRes)
w.Write(succResB) _, err = w.Write(succResB)
if err != nil {
log.Printf("could not write to client: %s", err)
}
return return
} }
@@ -303,16 +305,16 @@ func fetchInfoOneRESTHandler(w http.ResponseWriter, r *http.Request) {
// nil means (probably) that they chose "don't move" - which is fine, // nil means (probably) that they chose "don't move" - which is fine,
// and maps to nil on the Download (the default state). // and maps to nil on the Download (the default state).
destination := configService.Config.DestinationCalled(thisReq.Destination) destination := configService.Config.DestinationCalled(thisReq.Destination)
dm.ChangeDestination(thisDownload, destination)
thisDownload.Lock.Lock() // log.Printf("%#v", thisDownload)
thisDownload.Destination = destination
thisDownload.Lock.Unlock()
log.Printf("%#v", thisDownload)
succRes := successResponse{Success: true, Message: "destination changed"} succRes := successResponse{Success: true, Message: "destination changed"}
succResB, _ := json.Marshal(succRes) succResB, _ := json.Marshal(succRes)
w.Write(succResB) _, err = w.Write(succResB)
if err != nil {
log.Printf("could not write to client: %s", err)
}
return return
} }
@@ -321,14 +323,24 @@ func fetchInfoOneRESTHandler(w http.ResponseWriter, r *http.Request) {
thisDownload.Stop() thisDownload.Stop()
succRes := successResponse{Success: true, Message: "download stopped"} succRes := successResponse{Success: true, Message: "download stopped"}
succResB, _ := json.Marshal(succRes) succResB, _ := json.Marshal(succRes)
w.Write(succResB) _, err = w.Write(succResB)
if err != nil {
log.Printf("could not write to client: %s", err)
}
return return
} }
} }
// just a get, return the object // just a get, return the object
thisDownload.Lock.Lock()
defer thisDownload.Lock.Unlock()
b, _ := json.Marshal(thisDownload) b, _ := json.Marshal(thisDownload)
w.Write(b)
_, err = w.Write(b)
if err != nil {
log.Printf("could not write to client: %s", err)
}
return return
} else { } else {
http.NotFound(w, r) http.NotFound(w, r)
@@ -337,10 +349,14 @@ func fetchInfoOneRESTHandler(w http.ResponseWriter, r *http.Request) {
func fetchInfoRESTHandler(w http.ResponseWriter, r *http.Request) { func fetchInfoRESTHandler(w http.ResponseWriter, r *http.Request) {
dm.Lock.Lock() b, err := dm.DownloadsAsJSON()
defer dm.Lock.Unlock() if err != nil {
b, _ := json.Marshal(dm.Downloads) panic(err)
w.Write(b) }
_, err = w.Write(b)
if err != nil {
log.Printf("could not write to client: %s", err)
}
} }
func fetchHandler(w http.ResponseWriter, r *http.Request) { func fetchHandler(w http.ResponseWriter, r *http.Request) {
@@ -394,18 +410,14 @@ func fetchHandler(w http.ResponseWriter, r *http.Request) {
} }
// create the new download // create the new download
log.Print("creating")
newDL := download.NewDownload(url[0], configService.Config) newDL := download.NewDownload(url[0], configService.Config)
log.Print("adding")
dm.AddDownload(newDL) dm.AddDownload(newDL)
log.Print("done")
t, err := template.ParseFS(webFS, "web/layout.tmpl", "web/popup.html") t, err := template.ParseFS(webFS, "web/layout.tmpl", "web/popup.html")
if err != nil { if err != nil {
panic(err) panic(err)
} }
log.Print("lock dl")
newDL.Lock.Lock() newDL.Lock.Lock()
defer newDL.Lock.Unlock() defer newDL.Lock.Unlock()
@@ -415,6 +427,5 @@ func fetchHandler(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
panic(err) panic(err)
} }
log.Print("unlock dl because rendered")
} }
} }

View File

@@ -26,7 +26,6 @@ type Manager struct {
} }
func (m *Manager) GetInfo() Info { func (m *Manager) GetInfo() Info {
// log.Print("getting info... b4 lock")
m.lock.Lock() m.lock.Lock()
defer m.lock.Unlock() defer m.lock.Unlock()

View File

@@ -33,6 +33,9 @@
.state-downloading { .state-downloading {
color: blue; color: blue;
} }
.state-moved {
color: green;
}
.state-complete { .state-complete {
color: green; color: green;
} }

View File

@@ -34,11 +34,11 @@
</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="pure-button" @click="stop()">stop</button>
{{ end }} {{ end }}
<div> <div>
<h4>Logs</h4> <h4>Logs</h4>
<pre x-text="log"> <pre x-text="log" style="height: auto;">
</pre> </pre>
</div> </div>
</div> </div>
@@ -101,7 +101,11 @@
this.state = info.state; this.state = info.state;
this.playlist_current = info.playlist_current; this.playlist_current = info.playlist_current;
this.playlist_total = info.playlist_total; this.playlist_total = info.playlist_total;
if (this.state != 'choose profile') { this.destination_chosen = null;
if (info.destination) {
this.destination_chosen = info.destination.name;
}
if (this.state != 'Choose Profile') {
this.profile_chosen = true; this.profile_chosen = true;
} }
this.finished = info.finished; this.finished = info.finished;