17 Commits
0.02 ... v0.4.0

10 changed files with 300 additions and 41 deletions

11
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "gomod" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "daily"

View File

@@ -2,11 +2,13 @@
A web service and bookmarklet to download videos with a single click.
![Screencast](/screencast.gif)
## Pre-requisites
* a passing familiarity with the command line
* youtube-dl (plus any of its required dependencies, like ffmpeg)
* golang compiler
* golang compiler (if you'd like to build from source)
## Build
@@ -23,20 +25,55 @@ Binaries are available at https://github.com/tardisx/gropple/releases
With no arguments, it will listen on port 6283 and use an address of 'http://localhost:6283'.
The address must be specified so that the bookmarklet can refer to the correct
host when it is not running on your local machine. You may also need to specify
host if it is not running on your local machine. You may also need to specify
a different address if you are running it behind a proxy server or similar.
## Using
Bring up `http://localhost:6283` (or your chosen address) in your browser. You should see a link to the bookmarklet at the top of the screen, and the list of downloads (currently empty).
Bring up `http://localhost:6283` (or your chosen address) in your browser. You
should see a link to the bookmarklet at the top of the screen, and the list of
downloads (currently empty).
Drag the bookmarklet to your favourites bar, or otherwise bookmark it as you see fit.
Drag the bookmarklet to your favourites bar, or otherwise bookmark it as you
see fit.
Whenever you are on a page with a video you would like to download, simply click the bookmarklet.
Whenever you are on a page with a video you would like to download, simply
click the bookmarklet.
A popup window will appear, the download will start on the your gropple server and the status will be shown in the window.
A popup window will appear, the download will start on the your gropple server
and the status will be shown in the window.
You may close this window at any time without stopping the download, the status of all downloads is available on the index page.
You may close this window at any time without stopping the download, the status
of all downloads is available on the index page.
## Using an alternative downloader
The default downloader is youtube-dl. It is possible to use a different downloader
via the `-dl-cmd` command line option.
While `gropple` will use your `PATH` to find the executable, you may also want
to specify a full path instead.o
So, for instance, to use `youtube-dlc` instead of `youtube-dl` and specify the
full path:
`gropple -dl-cmd /home/username/bin/youtube-dlc`
Note that this is only the path to the executable. If you need to change the
command arguments, see below.
## Changing the youtube-dl arguments
The default arguments passed to `youtube-dl` are:
* `--newline` (needed to allow gropple to properly parse the output)
* `--write-info-json` (optional, but provides information on the download in the corresponding .json file)
* `-f` and `bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best` (choose the type of video `youtube-dl` will download)
These are customisable on the command line for `gropple`. For example, to duplicate these default options, you would
do:
`gropple -dl-args '--newline' -dl-args '--write-info-json' -dl-args '-f' -dl-args 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best`
## TODO

View File

@@ -7,7 +7,8 @@ open my $fh, "<", "main.go" || die $!;
my $version;
while (<$fh>) {
$version = $1 if /^const\s+currentVersion.*?"(v[\d\.]+)"/;
# CurrentVersion: "v0.04"
$version = $1 if /CurrentVersion:\s*"(v[\d\.]+)"/;
}
close $fh;

5
go.mod
View File

@@ -2,4 +2,7 @@ module github.com/tardisx/gropple
go 1.16
require github.com/gorilla/mux v1.8.0
require (
github.com/gorilla/mux v1.8.0
golang.org/x/mod v0.5.1
)

13
go.sum
View File

@@ -1,2 +1,15 @@
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

87
main.go
View File

@@ -18,6 +18,7 @@ import (
"strconv"
"github.com/gorilla/mux"
"github.com/tardisx/gropple/version"
)
type download struct {
@@ -39,22 +40,51 @@ var downloadPath = "./"
var address string
const currentVersion = "v0.02"
var dlCmd = "youtube-dl"
type args []string
var dlArgs = args{}
var defaultArgs = args{
"--write-info-json",
"-f", "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best",
"--newline",
}
var versionInfo = version.Info{CurrentVersion: "v0.4.0"}
//go:embed web
var webFS embed.FS
func (i *args) Set(value string) error {
*i = append(*i, strings.TrimSpace(value))
return nil
}
func (i *args) String() string {
return strings.Join(*i, ",")
}
func main() {
var port int
flag.IntVar(&port, "port", 6283, "port to listen on")
flag.StringVar(&address, "address", "http://localhost:6283", "address for the service")
flag.StringVar(&downloadPath, "path", "", "path for downloaded files - defaults to current directory")
flag.StringVar(&dlCmd, "dl-cmd", "youtube-dl", "downloader to use")
flag.Var(&dlArgs, "dl-args", "arguments to the downloader")
flag.Parse()
if len(dlArgs) == 0 {
dlArgs = defaultArgs
}
r := mux.NewRouter()
r.HandleFunc("/", HomeHandler)
r.HandleFunc("/fetch", FetchHandler)
r.HandleFunc("/fetch/info/{id}", FetchInfoHandler)
r.HandleFunc("/fetch/info", FetchInfoHandler)
r.HandleFunc("/fetch/info/{id}", FetchInfoOneHandler)
r.HandleFunc("/version", VersionHandler)
http.Handle("/", r)
@@ -66,10 +96,26 @@ func main() {
ReadTimeout: 5 * time.Second,
}
log.Printf("starting gropple %s - https://github.com/tardisx/gropple", currentVersion)
// check for a new version every 4 hours
go func() {
for {
versionInfo.UpdateGitHubVersion()
time.Sleep(time.Hour * 4)
}
}()
log.Printf("starting gropple %s - https://github.com/tardisx/gropple", versionInfo.CurrentVersion)
log.Printf("go to %s for details on installing the bookmarklet and to check status", address)
log.Fatal(srv.ListenAndServe())
}
func VersionHandler(w http.ResponseWriter, r *http.Request) {
if versionInfo.GithubVersionFetched {
b, _ := json.Marshal(versionInfo)
w.Write(b)
} else {
w.WriteHeader(400)
}
}
func HomeHandler(w http.ResponseWriter, r *http.Request) {
@@ -99,7 +145,7 @@ func HomeHandler(w http.ResponseWriter, r *http.Request) {
}
func FetchInfoHandler(w http.ResponseWriter, r *http.Request) {
func FetchInfoOneHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
idString := vars["id"]
if idString != "" {
@@ -121,15 +167,29 @@ func FetchInfoHandler(w http.ResponseWriter, r *http.Request) {
}
}
func FetchInfoHandler(w http.ResponseWriter, r *http.Request) {
b, _ := json.Marshal(downloads)
w.Write(b)
}
func FetchHandler(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
url, present := query["url"] //filters=["color", "price", "brand"]
url, present := query["url"]
if !present {
fmt.Fprint(w, "something")
w.WriteHeader(400)
fmt.Fprint(w, "No url supplied")
return
} else {
// check the URL for a sudden but inevitable betrayal
if strings.Contains(url[0], address) {
w.WriteHeader(400)
fmt.Fprint(w, "you musn't gropple your gropple :-)")
return
}
// create the record
// XXX should be atomic!
downloadId++
@@ -150,6 +210,7 @@ func FetchHandler(w http.ResponseWriter, r *http.Request) {
go func() {
queue(&newDownload)
}()
t, err := template.ParseFS(webFS, "web/layout.tmpl", "web/popup.html")
if err != nil {
panic(err)
@@ -158,19 +219,15 @@ func FetchHandler(w http.ResponseWriter, r *http.Request) {
if err != nil {
panic(err)
}
// fmt.Fprintf(w, "Started DL %d!", downloadId)
}
}
func queue(dl *download) {
cmdSlice := []string{}
cmdSlice = append(cmdSlice, dlArgs...)
cmdSlice = append(cmdSlice, dl.Url)
cmd := exec.Command(
"youtube-dl",
"--write-info-json",
"-f", "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best",
"--newline", dl.Url,
)
cmd := exec.Command(dlCmd, cmdSlice...)
cmd.Dir = downloadPath
stdout, err := cmd.StdoutPipe()
@@ -284,7 +341,7 @@ func updateMetadata(dl *download, s string) {
// This means a file has been "created" by merging others
// [ffmpeg] Merging formats into "Toto - Africa (Official HD Video)-FTQbiNvZqaY.mp4"
mergedFilename := regexp.MustCompile(`Merging formats into "(.+)$`)
mergedFilename := regexp.MustCompile(`Merging formats into "(.+)"$`)
matches = mergedFilename.FindStringSubmatch(s)
if len(matches) == 2 {
dl.Files = append(dl.Files, matches[1])

BIN
screencast.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 MiB

79
version/version.go Normal file
View File

@@ -0,0 +1,79 @@
// Package version deals with versioning of the software
package version
import (
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"golang.org/x/mod/semver"
)
type Info struct {
CurrentVersion string `json:"current_version"`
GithubVersion string `json:"github_version"`
UpgradeAvailable bool `json:"upgrade_available"`
GithubVersionFetched bool `json:"-"`
}
func (i *Info) UpdateGitHubVersion() error {
i.GithubVersionFetched = false
versionUrl := "https://api.github.com/repos/tardisx/gropple/releases"
resp, err := http.Get(versionUrl)
if err != nil {
log.Fatal("Error getting response. ", err)
}
defer resp.Body.Close()
b, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read body: %v", err)
}
type release struct {
HTMLUrl string `json:"html_url"`
TagName string `json:"tag_name"`
Name string `json:"name"`
}
var releases []release
err = json.Unmarshal(b, &releases)
if err != nil {
return fmt.Errorf("failed to read unmarshal: %v", err)
}
if len(releases) == 0 {
log.Printf("found no releases on github?")
return errors.New("no releases found")
}
i.GithubVersion = releases[0].Name
i.GithubVersionFetched = true
i.UpgradeAvailable = i.canUpgrade()
return nil
}
func (i *Info) canUpgrade() bool {
if !i.GithubVersionFetched {
return false
}
log.Printf("We are %s, github is %s", i.CurrentVersion, i.GithubVersion)
if !semver.IsValid(i.CurrentVersion) {
log.Fatalf("current version %s is invalid", i.CurrentVersion)
}
if !semver.IsValid(i.GithubVersion) {
log.Fatalf("github version %s is invalid", i.GithubVersion)
}
if semver.Compare(i.CurrentVersion, i.GithubVersion) == -1 {
return true
}
return false
}

View File

@@ -1,12 +1,24 @@
{{ define "content" }}
<div>
<p>
Drag this bookmarklet: <a href="{{ .BookmarkletURL }}">Gropple</a> to your bookmark bar, and click it
on any page you want to grab the video from.
</p>
</div>
<div>
<div x-data="index()" x-init="fetch_data(); fetch_version()">
<h2>gropple</h2>
<p x-show="version && version.upgrade_available">
<a href="https://github.com/tardisx/gropple/releases">Upgrade is available</a> -
you have
<span x-text="version.current_version"></span> and
<span x-text="version.github_version"></span>
is available.</p>
<div>
<p>
Drag this bookmarklet: <a href="{{ .BookmarkletURL }}">Gropple</a> to your bookmark bar, and click it
on any page you want to grab the video from.
</p>
</div>
<table class="pure-table">
<thead>
<tr>
@@ -14,16 +26,21 @@
</tr>
</thead>
<tbody>
<template x-for="item in items">
<tr>
<td x-text="item.id"></td>
<td x-text="item.files"></td>
<td><a x-bind:href="item.url">link</a></td>
<td x-text="item.state"></td>
<td x-text="item.percent"></td>
<td x-text="item.eta"></td>
<td x-text="item.finished"></td>
</tr>
</template>
{{ range $k, $v := .Downloads }}
<tr>
<td>{{ $v.Id }}</td>
<td>{{ range $_, $f := $v.Files }}{{ $f }}<br>{{ end }}</td>
<td><a href="{{ $v.Url }}">link</a></td>
<td>{{ $v.State }}</td>
<td>{{ $v.Percent }}</td>
<td>{{ $v.Eta }}</td>
<td>{{ $v.Finished }}</td>
</tr>
{{ end }}
</tbody>
</table>
@@ -31,4 +48,34 @@
{{ end }}
{{ define "js" }}
<script>
function index() {
return {
items: [], version: {},
fetch_version() {
fetch('/version')
.then(response => response.json())
.then(info => {
this.version = info;
setTimeout(() => { this.fetch_version() }, 1000 * 60 );
})
.catch(error => {
console.log('failed to fetch version info - will retry');
setTimeout(() => { this.fetch_version() }, 1000 );
});
},
fetch_data() {
fetch('/fetch/info')
.then(response => response.json())
.then(info => {
// will be null if no downloads yet
if (info) {
this.items = info;
}
setTimeout(() => { this.fetch_data() }, 1000);
})
},
}
}
</script>
{{ end }}

View File

@@ -8,11 +8,22 @@
<link rel="stylesheet" href="https://unpkg.com/purecss@2.0.6/build/pure-min.css" integrity="sha384-Uu6IeWbM+gzNVXJcM9XV3SohHtmWE+3VGi496jvgX1jyvDTXfdK+rfZc8C1Aehk5" crossorigin="anonymous">
<link rel="stylesheet" href="https://unpkg.com/purecss@2.0.6/build/grids-responsive-min.css">
<style>
pre { font-size: 70%; }
pre {
font-size: 60%;
height: 100px;
overflow:auto;
}
footer {
padding-top: 50px;
font-size: 30%;
}
</style>
</head>
<body style="margin:4; padding:4">
{{ template "content" . }}
<footer>
Homepage: <a href="https://github.com/tardisx/gropple">https://github.com/tardisx/gropple</a>
</footer>
</body>
{{ template "js" . }}
</html>