gropple/main.go

422 lines
10 KiB
Go

package main
import (
"embed"
"encoding/json"
"flag"
"fmt"
"html/template"
"io"
"log"
"net/http"
"strings"
"time"
"strconv"
"github.com/gorilla/mux"
"github.com/tardisx/gropple/config"
"github.com/tardisx/gropple/download"
"github.com/tardisx/gropple/version"
)
var dm *download.Manager
var downloadId = 0
var configService *config.ConfigService
var versionInfo = version.Manager{
VersionInfo: version.Info{CurrentVersion: "v0.6.0"},
}
//go:embed web
var webFS embed.FS
type successResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
}
type errorResponse struct {
Success bool `json:"success"`
Error string `json:"error"`
}
func main() {
log.Printf("Starting gropple %s - https://github.com/tardisx/gropple", versionInfo.GetInfo().CurrentVersion)
var configPath string
flag.StringVar(&configPath, "config-path", "", "path to config file")
flag.Parse()
configService = &config.ConfigService{}
if configPath != "" {
configService.ConfigPath = configPath
} else {
configService.DetermineConfigDir()
}
exists, err := configService.ConfigFileExists()
if err != nil {
log.Fatal(err)
}
if !exists {
log.Print("No config file - creating default config")
configService.LoadDefaultConfig()
configService.WriteConfig()
log.Printf("Configuration written to %s", configService.ConfigPath)
} else {
err := configService.LoadConfig()
if err != nil {
log.Fatal(err)
}
log.Printf("Configuration loaded from %s", configService.ConfigPath)
}
// create the download manager
dm = &download.Manager{MaxPerDomain: configService.Config.Server.MaximumActiveDownloads}
r := mux.NewRouter()
r.HandleFunc("/", homeHandler)
r.HandleFunc("/static/{filename}", staticHandler)
r.HandleFunc("/config", configHandler)
r.HandleFunc("/fetch", fetchHandler)
r.HandleFunc("/fetch/{id}", fetchHandler)
// info for the list
r.HandleFunc("/rest/fetch", fetchInfoRESTHandler)
// info for one, including update
r.HandleFunc("/rest/fetch/{id}", fetchInfoOneRESTHandler)
r.HandleFunc("/rest/version", versionRESTHandler)
r.HandleFunc("/rest/config", configRESTHandler)
http.Handle("/", r)
srv := &http.Server{
Handler: r,
Addr: fmt.Sprintf(":%d", configService.Config.Server.Port),
// Good practice: enforce timeouts for servers you create!
WriteTimeout: 5 * time.Second,
ReadTimeout: 5 * time.Second,
}
// check for a new version every 4 hours
go func() {
for {
versionInfo.UpdateGitHubVersion()
time.Sleep(time.Hour * 4)
}
}()
// start downloading queued downloads when slots available, and clean up
// old entries
go dm.ManageQueue()
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.Fatal(srv.ListenAndServe())
}
// versionRESTHandler returns the version information, if we have up-to-date info from github
func versionRESTHandler(w http.ResponseWriter, r *http.Request) {
if versionInfo.GetInfo().GithubVersionFetched {
b, _ := json.Marshal(versionInfo.GetInfo())
w.Write(b)
} else {
w.WriteHeader(400)
}
}
// homeHandler returns the main index page
func homeHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
bookmarkletURL := fmt.Sprintf("javascript:(function(f,s,n,o){window.open(f+encodeURIComponent(s),n,o)}('%s/fetch?url=',window.location,'yourform','width=%d,height=%d'));", configService.Config.Server.Address, configService.Config.UI.PopupWidth, configService.Config.UI.PopupHeight)
t, err := template.ParseFS(webFS, "web/layout.tmpl", "web/menu.tmpl", "web/index.html")
if err != nil {
panic(err)
}
type Info struct {
Manager *download.Manager
BookmarkletURL template.URL
Config *config.Config
Version version.Info
}
info := Info{
Manager: dm,
BookmarkletURL: template.URL(bookmarkletURL),
Config: configService.Config,
Version: versionInfo.GetInfo(),
}
dm.Lock.Lock()
defer dm.Lock.Unlock()
err = t.ExecuteTemplate(w, "layout", info)
if err != nil {
panic(err)
}
}
// staticHandler handles requests for static files
func staticHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
filename := vars["filename"]
if strings.Index(filename, ".js") == len(filename)-3 {
f, err := webFS.Open("web/" + filename)
if err != nil {
log.Printf("error accessing %s - %v", filename, err)
w.WriteHeader(http.StatusNotFound)
return
}
w.WriteHeader(http.StatusOK)
io.Copy(w, f)
return
}
w.WriteHeader(http.StatusNotFound)
}
// configHandler returns the configuration page
func configHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
t, err := template.ParseFS(webFS, "web/layout.tmpl", "web/menu.tmpl", "web/config.html")
if err != nil {
panic(err)
}
err = t.ExecuteTemplate(w, "layout", nil)
if err != nil {
panic(err)
}
}
// configRESTHandler handles both reading and writing of the configuration
func configRESTHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" {
log.Printf("Updating config")
b, err := io.ReadAll(r.Body)
if err != nil {
panic(err)
}
err = configService.Config.UpdateFromJSON(b)
if err != nil {
errorRes := errorResponse{Success: false, Error: err.Error()}
errorResB, _ := json.Marshal(errorRes)
w.WriteHeader(400)
w.Write(errorResB)
return
}
configService.WriteConfig()
}
b, _ := json.Marshal(configService.Config)
w.Write(b)
}
//
func fetchInfoOneRESTHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
idString := vars["id"]
if idString != "" {
id, err := strconv.Atoi(idString)
if err != nil {
http.NotFound(w, r)
return
}
thisDownload, err := dm.GetDlById(id)
if err != nil {
http.NotFound(w, r)
return
}
if thisDownload == nil {
panic("should not happen")
}
if r.Method == "POST" {
type updateRequest struct {
Action string `json:"action"`
Profile string `json:"profile"`
Destination string `json:"destination"`
}
thisReq := updateRequest{}
b, err := io.ReadAll(r.Body)
if err != nil {
panic(err)
}
err = json.Unmarshal(b, &thisReq)
if err != nil {
errorRes := errorResponse{Success: false, Error: err.Error()}
errorResB, _ := json.Marshal(errorRes)
w.WriteHeader(400)
w.Write(errorResB)
return
}
if thisReq.Action == "start" {
// find the profile they asked for
profile := configService.Config.ProfileCalled(thisReq.Profile)
if profile == nil {
panic("bad profile name?")
}
// set the profile
thisDownload.Lock.Lock()
thisDownload.DownloadProfile = *profile
thisDownload.Lock.Unlock()
dm.Queue(thisDownload)
succRes := successResponse{Success: true, Message: "download started"}
succResB, _ := json.Marshal(succRes)
w.Write(succResB)
return
}
if thisReq.Action == "change_destination" {
// nil means (probably) that they chose "don't move" - which is fine,
// and maps to nil on the Download (the default state).
destination := configService.Config.DestinationCalled(thisReq.Destination)
thisDownload.Lock.Lock()
thisDownload.Destination = destination
thisDownload.Lock.Unlock()
log.Printf("%#v", thisDownload)
succRes := successResponse{Success: true, Message: "destination changed"}
succResB, _ := json.Marshal(succRes)
w.Write(succResB)
return
}
if thisReq.Action == "stop" {
thisDownload.Stop()
succRes := successResponse{Success: true, Message: "download stopped"}
succResB, _ := json.Marshal(succRes)
w.Write(succResB)
return
}
}
// just a get, return the object
b, _ := json.Marshal(thisDownload)
w.Write(b)
return
} else {
http.NotFound(w, r)
}
}
func fetchInfoRESTHandler(w http.ResponseWriter, r *http.Request) {
dm.Lock.Lock()
defer dm.Lock.Unlock()
b, _ := json.Marshal(dm.Downloads)
w.Write(b)
}
func fetchHandler(w http.ResponseWriter, r *http.Request) {
// if they refreshed the popup, just load the existing object, don't
// create a new one
vars := mux.Vars(r)
idString := vars["id"]
idInt, err := strconv.ParseInt(idString, 10, 32)
// existing, load it up
if err == nil && idInt > 0 {
dl, err := dm.GetDlById(int(idInt))
if err != nil {
log.Printf("not found")
w.WriteHeader(404)
return
}
t, err := template.ParseFS(webFS, "web/layout.tmpl", "web/popup.html")
if err != nil {
panic(err)
}
templateData := map[string]interface{}{"dl": dl, "config": configService.Config, "canStop": download.CanStopDownload}
err = t.ExecuteTemplate(w, "layout", templateData)
if err != nil {
panic(err)
}
return
}
query := r.URL.Query()
url, present := query["url"]
if !present {
w.WriteHeader(400)
fmt.Fprint(w, "No url supplied")
return
} else {
log.Printf("popup for %s", url)
// check the URL for a sudden but inevitable betrayal
if strings.Contains(url[0], configService.Config.Server.Address) {
w.WriteHeader(400)
fmt.Fprint(w, "you mustn't gropple your gropple :-)")
return
}
// create the new download
log.Print("creating")
newDL := download.NewDownload(url[0], configService.Config)
log.Print("adding")
dm.AddDownload(newDL)
log.Print("done")
t, err := template.ParseFS(webFS, "web/layout.tmpl", "web/popup.html")
if err != nil {
panic(err)
}
log.Print("lock dl")
newDL.Lock.Lock()
defer newDL.Lock.Unlock()
templateData := map[string]interface{}{"Version": versionInfo.GetInfo(), "dl": newDL, "config": configService.Config, "canStop": download.CanStopDownload}
err = t.ExecuteTemplate(w, "layout", templateData)
if err != nil {
panic(err)
}
log.Print("unlock dl because rendered")
}
}