Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
f6a447d7f4
|
|||
551e53ad63 | |||
707de8fcf1
|
|||
64a6eb20a0 | |||
67b17c1df7
|
|||
142c09e624
|
|||
0b3797dc19 | |||
d4dae21d8f | |||
9f2677485e | |||
8aa915e6dc
|
|||
37c390f911
|
|||
2f9552e771
|
|||
dfe21445e5
|
|||
5397ba0907
|
|||
8c010665e1
|
|||
916d3004de
|
@ -9,6 +9,7 @@ Create a `config.toml` file in the same directory as the drmdtool executable:
|
|||||||
```toml
|
```toml
|
||||||
BaseDir = "/path/to/save/downloads"
|
BaseDir = "/path/to/save/downloads"
|
||||||
Format = "mkv"
|
Format = "mkv"
|
||||||
|
TempBaseDir = "/tmp/nre"
|
||||||
|
|
||||||
[N_m3u8DL-RE]
|
[N_m3u8DL-RE]
|
||||||
Path = "/path/to/N_m3u8DL-RE"
|
Path = "/path/to/N_m3u8DL-RE"
|
||||||
@ -37,9 +38,3 @@ To process a file directly from the command line:
|
|||||||
```
|
```
|
||||||
|
|
||||||
This will download the file and save it in the base directory specified in the config.
|
This will download the file and save it in the base directory specified in the config.
|
||||||
|
|
||||||
## TODO
|
|
||||||
- ~~Filename Sanitation (Makes new directory on /... oops)~~
|
|
||||||
- ~~GoPlay Fix~~
|
|
||||||
- Windows?
|
|
||||||
- Proper UI?
|
|
@ -9,9 +9,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
BaseDir string
|
BaseDir string
|
||||||
Format string
|
Format string
|
||||||
N_m3u8DLRE struct {
|
TempBaseDir string
|
||||||
|
N_m3u8DLRE struct {
|
||||||
Path string
|
Path string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
BaseDir = "/mnt/media"
|
BaseDir = "/mnt/media"
|
||||||
Format = "mkv"
|
Format = "mkv"
|
||||||
|
TempBaseDir = "/tmp/nre"
|
||||||
|
|
||||||
[N_m3u8DLRE]
|
[N_m3u8DLRE]
|
||||||
Path = "nre"
|
Path = "nre"
|
||||||
|
@ -2,7 +2,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -12,38 +11,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func processInputFile(inputFile string) error {
|
|
||||||
jsonFile, err := os.Open(inputFile)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error opening file %s: %v", inputFile, err)
|
|
||||||
}
|
|
||||||
defer jsonFile.Close()
|
|
||||||
|
|
||||||
byteValue, err := io.ReadAll(jsonFile)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error reading file %s: %v", inputFile, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
byteValue = removeBOM(byteValue)
|
|
||||||
|
|
||||||
var items Items
|
|
||||||
err = json.Unmarshal(byteValue, &items)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error unmarshaling JSON: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, item := range items.Items {
|
|
||||||
updateProgress(filepath.Base(inputFile), float64(i)/float64(len(items.Items))*100, item.Filename)
|
|
||||||
err := downloadFile(item)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error downloading file: %v\n", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
updateProgress(filepath.Base(inputFile), 100, "")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeBOM(input []byte) []byte {
|
func removeBOM(input []byte) []byte {
|
||||||
if len(input) >= 3 && input[0] == 0xEF && input[1] == 0xBB && input[2] == 0xBF {
|
if len(input) >= 3 && input[0] == 0xEF && input[1] == 0xBB && input[2] == 0xBF {
|
||||||
return input[3:]
|
return input[3:]
|
||||||
@ -51,9 +18,17 @@ func removeBOM(input []byte) []byte {
|
|||||||
return input
|
return input
|
||||||
}
|
}
|
||||||
|
|
||||||
func downloadFile(item Item) error {
|
func downloadFile(item Item, jobInfo *JobInfo) error {
|
||||||
fmt.Println("Downloading:", item.Filename)
|
fmt.Println("Downloading:", item.Filename)
|
||||||
|
|
||||||
|
tempDir := filepath.Join(config.TempBaseDir, sanitizeFilename(item.Filename))
|
||||||
|
err := os.MkdirAll(tempDir, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error creating temporary directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
jobInfo.TempDir = tempDir
|
||||||
|
|
||||||
mpdPath := item.MPD
|
mpdPath := item.MPD
|
||||||
if !isValidURL(item.MPD) {
|
if !isValidURL(item.MPD) {
|
||||||
decodedMPD, err := base64.StdEncoding.DecodeString(item.MPD)
|
decodedMPD, err := base64.StdEncoding.DecodeString(item.MPD)
|
||||||
@ -108,7 +83,7 @@ func downloadFile(item Item) error {
|
|||||||
mpdPath = tempFile.Name()
|
mpdPath = tempFile.Name()
|
||||||
}
|
}
|
||||||
|
|
||||||
command := getDownloadCommand(item, mpdPath)
|
command := getDownloadCommand(item, mpdPath, tempDir)
|
||||||
|
|
||||||
if item.Subtitles != "" {
|
if item.Subtitles != "" {
|
||||||
subtitlePaths, err := downloadAndConvertSubtitles(item.Subtitles)
|
subtitlePaths, err := downloadAndConvertSubtitles(item.Subtitles)
|
||||||
@ -124,19 +99,45 @@ func downloadFile(item Item) error {
|
|||||||
|
|
||||||
cmd := exec.Command("bash", "-c", command)
|
cmd := exec.Command("bash", "-c", command)
|
||||||
|
|
||||||
|
jobsMutex.Lock()
|
||||||
|
jobInfo.Cmd = cmd
|
||||||
|
jobsMutex.Unlock()
|
||||||
|
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
err := cmd.Run()
|
err = cmd.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error executing download command: %v", err)
|
return fmt.Errorf("error starting download command: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
done := make(chan error)
|
||||||
|
go func() {
|
||||||
|
done <- cmd.Wait()
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-jobInfo.AbortChan:
|
||||||
|
if cmd.Process != nil {
|
||||||
|
cmd.Process.Kill()
|
||||||
|
}
|
||||||
|
os.RemoveAll(tempDir)
|
||||||
|
return fmt.Errorf("download aborted")
|
||||||
|
case err := <-done:
|
||||||
|
if jobInfo.Paused {
|
||||||
|
return fmt.Errorf("download paused")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error executing download command: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Download completed successfully")
|
fmt.Println("Download completed successfully")
|
||||||
|
os.RemoveAll(tempDir)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDownloadCommand(item Item, mpdPath string) string {
|
func getDownloadCommand(item Item, mpdPath string, tempDir string) string {
|
||||||
metadata := parseMetadata(item.Metadata)
|
metadata := parseMetadata(item.Metadata)
|
||||||
keys := getKeys(item.Keys)
|
keys := getKeys(item.Keys)
|
||||||
|
|
||||||
@ -165,6 +166,8 @@ func getDownloadCommand(item Item, mpdPath string) string {
|
|||||||
}
|
}
|
||||||
command += fmt.Sprintf(" --save-dir \"%s\"", saveDir)
|
command += fmt.Sprintf(" --save-dir \"%s\"", saveDir)
|
||||||
|
|
||||||
|
command += fmt.Sprintf(" --tmp-dir \"%s\"", tempDir)
|
||||||
|
|
||||||
fmt.Println(command)
|
fmt.Println(command)
|
||||||
|
|
||||||
return command
|
return command
|
||||||
|
2
go.sum
2
go.sum
@ -10,6 +10,8 @@ github.com/beevik/etree v1.4.1 h1:PmQJDDYahBGNKDcpdX8uPy1xRCwoCGVUiW669MEirVI=
|
|||||||
github.com/beevik/etree v1.4.1/go.mod h1:gPNJNaBGVZ9AwsidazFZyygnd+0pAU38N4D+WemwKNs=
|
github.com/beevik/etree v1.4.1/go.mod h1:gPNJNaBGVZ9AwsidazFZyygnd+0pAU38N4D+WemwKNs=
|
||||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/pkg/exec v0.0.0-20150614095509-0bd164ad2a5a h1:EN123kAtAAE2pg/+TvBsUBZfHCWNNFyL2ZBPPfNWAc0=
|
||||||
|
github.com/pkg/exec v0.0.0-20150614095509-0bd164ad2a5a/go.mod h1:b95YoNrAnScjaWG+asr8lxqlrsPUcT2ZEBcjvVGshMo=
|
||||||
github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=
|
github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
325
handlers.go
325
handlers.go
@ -5,71 +5,196 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ProgressInfo struct {
|
||||||
|
Percentage float64
|
||||||
|
CurrentFile string
|
||||||
|
Paused bool
|
||||||
|
}
|
||||||
|
|
||||||
func handleRoot(w http.ResponseWriter, r *http.Request) {
|
func handleRoot(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.URL.Path != "/" {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
progressMutex.Lock()
|
progressMutex.Lock()
|
||||||
jobs := make(map[string]*ProgressInfo)
|
defer progressMutex.Unlock()
|
||||||
for k, v := range progress {
|
|
||||||
jobs[k] = v
|
|
||||||
}
|
|
||||||
progressMutex.Unlock()
|
|
||||||
|
|
||||||
err := templates.ExecuteTemplate(w, "index", struct{ Jobs map[string]*ProgressInfo }{jobs})
|
jobsInfo := make(map[string]struct {
|
||||||
|
Percentage float64
|
||||||
|
CurrentFile string
|
||||||
|
Paused bool
|
||||||
|
})
|
||||||
|
|
||||||
|
for filename, info := range progress {
|
||||||
|
jobsInfo[filename] = struct {
|
||||||
|
Percentage float64
|
||||||
|
CurrentFile string
|
||||||
|
Paused bool
|
||||||
|
}{
|
||||||
|
Percentage: info.Percentage,
|
||||||
|
CurrentFile: info.CurrentFile,
|
||||||
|
Paused: info.Paused,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := templates.ExecuteTemplate(w, "index", struct {
|
||||||
|
Jobs map[string]struct {
|
||||||
|
Percentage float64
|
||||||
|
CurrentFile string
|
||||||
|
Paused bool
|
||||||
|
}
|
||||||
|
}{jobsInfo})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleUpload(w http.ResponseWriter, r *http.Request) {
|
func handleUpload(w http.ResponseWriter, r *http.Request) {
|
||||||
file, header, err := r.FormFile("file")
|
err := r.ParseMultipartForm(32 << 20)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
tempFile, err := os.CreateTemp(uploadDir, header.Filename)
|
files := r.MultipartForm.File["files"]
|
||||||
if err != nil {
|
if len(files) == 0 {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, "No files uploaded", http.StatusBadRequest)
|
||||||
return
|
|
||||||
}
|
|
||||||
defer tempFile.Close()
|
|
||||||
|
|
||||||
_, err = io.Copy(tempFile, file)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tempFilename := filepath.Base(tempFile.Name())
|
uploadedFiles := []string{}
|
||||||
|
|
||||||
go func() {
|
for _, fileHeader := range files {
|
||||||
err := processInputFile(tempFile.Name())
|
file, err := fileHeader.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error processing file: %v\n", err)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
tempFile, err := os.CreateTemp(uploadDir, fileHeader.Filename)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer tempFile.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(tempFile, file)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
os.Remove(tempFile.Name())
|
uploadedFiles = append(uploadedFiles, filepath.Base(tempFile.Name()))
|
||||||
}()
|
|
||||||
|
|
||||||
http.Redirect(w, r, "/progress?filename="+tempFilename, http.StatusSeeOther)
|
_, err = parseInputFile(tempFile.Name())
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validFiles := []string{}
|
||||||
|
for _, file := range uploadedFiles {
|
||||||
|
if file != "" {
|
||||||
|
validFiles = append(validFiles, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(validFiles) == 0 {
|
||||||
|
http.Error(w, "No valid files were uploaded", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, "/select?files="+url.QueryEscape(strings.Join(validFiles, ",")), http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSelect(w http.ResponseWriter, r *http.Request) {
|
||||||
|
filesParam := r.URL.Query().Get("files")
|
||||||
|
filenames := strings.Split(filesParam, ",")
|
||||||
|
|
||||||
|
allItems := make(map[string]map[string][]Item)
|
||||||
|
|
||||||
|
for _, filename := range filenames {
|
||||||
|
if filename == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fullPath := filepath.Join(uploadDir, filename)
|
||||||
|
|
||||||
|
if _, err := os.Stat(fullPath); os.IsNotExist(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
items, err := parseInputFile(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
groupedItems := groupItemsBySeason(items)
|
||||||
|
allItems[filename] = groupedItems
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(allItems) == 0 {
|
||||||
|
http.Error(w, "No valid files were processed", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := templates.ExecuteTemplate(w, "select", struct {
|
||||||
|
Filenames string
|
||||||
|
AllItems map[string]map[string][]Item
|
||||||
|
}{
|
||||||
|
Filenames: filesParam,
|
||||||
|
AllItems: allItems,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleProcess(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedItems := r.Form["items"]
|
||||||
|
if len(selectedItems) == 0 {
|
||||||
|
http.Error(w, "No items selected", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsByFile := make(map[string][]string)
|
||||||
|
for _, item := range selectedItems {
|
||||||
|
parts := strings.SplitN(item, ":", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filename, itemName := parts[0], parts[1]
|
||||||
|
itemsByFile[filename] = append(itemsByFile[filename], itemName)
|
||||||
|
}
|
||||||
|
|
||||||
|
for filename, items := range itemsByFile {
|
||||||
|
fullPath := filepath.Join(uploadDir, filename)
|
||||||
|
allItems, err := parseInputFile(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedItems := filterSelectedItems(allItems, items)
|
||||||
|
go processItems(filename, selectedItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleProgress(w http.ResponseWriter, r *http.Request) {
|
func handleProgress(w http.ResponseWriter, r *http.Request) {
|
||||||
filename := r.URL.Query().Get("filename")
|
filename := r.URL.Query().Get("filename")
|
||||||
fmt.Printf("Handling progress request for filename: %s\n", filename)
|
|
||||||
|
|
||||||
if r.Header.Get("Accept") == "application/json" {
|
if r.Header.Get("Accept") == "application/json" {
|
||||||
progressInfo := getProgress(filename)
|
progressInfo := getProgress(filename)
|
||||||
fmt.Printf("Progress info for %s: %+v\n", filename, progressInfo)
|
|
||||||
|
|
||||||
if progressInfo == nil {
|
if progressInfo == nil {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
@ -80,7 +205,6 @@ func handleProgress(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
err := json.NewEncoder(w).Encode(progressInfo)
|
err := json.NewEncoder(w).Encode(progressInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error encoding progress info: %v\n", err)
|
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -92,3 +216,138 @@ func handleProgress(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handlePause(w http.ResponseWriter, r *http.Request) {
|
||||||
|
filename := r.URL.Query().Get("filename")
|
||||||
|
if filename == "" {
|
||||||
|
http.Error(w, "Filename is required", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jobsMutex.Lock()
|
||||||
|
jobInfo, exists := jobs[filename]
|
||||||
|
jobsMutex.Unlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
http.Error(w, "Job not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jobInfo.Paused = true
|
||||||
|
if jobInfo.Cmd != nil && jobInfo.Cmd.Process != nil {
|
||||||
|
jobInfo.Cmd.Process.Kill()
|
||||||
|
}
|
||||||
|
|
||||||
|
progressMutex.Lock()
|
||||||
|
if progressInfo, ok := progress[filename]; ok {
|
||||||
|
progressInfo.Paused = true
|
||||||
|
}
|
||||||
|
progressMutex.Unlock()
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "Pause signal sent for %s", filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleResume(w http.ResponseWriter, r *http.Request) {
|
||||||
|
filename := r.URL.Query().Get("filename")
|
||||||
|
if filename == "" {
|
||||||
|
http.Error(w, "Filename is required", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jobsMutex.Lock()
|
||||||
|
jobInfo, exists := jobs[filename]
|
||||||
|
jobsMutex.Unlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
http.Error(w, "Job not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jobInfo.Paused = false
|
||||||
|
jobInfo.ResumeChan <- struct{}{}
|
||||||
|
|
||||||
|
progressMutex.Lock()
|
||||||
|
if progressInfo, ok := progress[filename]; ok {
|
||||||
|
progressInfo.Paused = false
|
||||||
|
}
|
||||||
|
progressMutex.Unlock()
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "Resume signal sent for %s", filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAbort(w http.ResponseWriter, r *http.Request) {
|
||||||
|
filename := r.URL.Query().Get("filename")
|
||||||
|
if filename == "" {
|
||||||
|
http.Error(w, "Filename is required", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jobsMutex.Lock()
|
||||||
|
jobInfo, exists := jobs[filename]
|
||||||
|
jobsMutex.Unlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
http.Error(w, "Job not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
close(jobInfo.AbortChan)
|
||||||
|
if jobInfo.Cmd != nil && jobInfo.Cmd.Process != nil {
|
||||||
|
jobInfo.Cmd.Process.Kill()
|
||||||
|
}
|
||||||
|
|
||||||
|
if jobInfo.TempDir != "" {
|
||||||
|
os.RemoveAll(jobInfo.TempDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "Abort signal sent for %s", filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleClearCompleted(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
clearCompletedJobs()
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(map[string]bool{"success": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearCompletedJobs() {
|
||||||
|
progressMutex.Lock()
|
||||||
|
defer progressMutex.Unlock()
|
||||||
|
|
||||||
|
for filename, info := range progress {
|
||||||
|
if info.Percentage >= 100 {
|
||||||
|
delete(progress, filename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateProgress(filename string, value float64, currentFile string) {
|
||||||
|
progressMutex.Lock()
|
||||||
|
defer progressMutex.Unlock()
|
||||||
|
|
||||||
|
jobsMutex.Lock()
|
||||||
|
jobInfo, exists := jobs[filename]
|
||||||
|
jobsMutex.Unlock()
|
||||||
|
|
||||||
|
paused := false
|
||||||
|
if exists {
|
||||||
|
paused = jobInfo.Paused
|
||||||
|
}
|
||||||
|
|
||||||
|
if existingProgress, ok := progress[filename]; ok {
|
||||||
|
existingProgress.Percentage = value
|
||||||
|
existingProgress.CurrentFile = currentFile
|
||||||
|
existingProgress.Paused = paused
|
||||||
|
} else {
|
||||||
|
progress[filename] = &ProgressInfo{
|
||||||
|
Percentage: value,
|
||||||
|
CurrentFile: currentFile,
|
||||||
|
Paused: paused,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
28
main.go
28
main.go
@ -37,11 +37,6 @@ var progress = make(map[string]*ProgressInfo)
|
|||||||
|
|
||||||
const uploadDir = "uploads"
|
const uploadDir = "uploads"
|
||||||
|
|
||||||
type ProgressInfo struct {
|
|
||||||
Percentage float64
|
|
||||||
CurrentFile string
|
|
||||||
}
|
|
||||||
|
|
||||||
var templates *template.Template
|
var templates *template.Template
|
||||||
|
|
||||||
//go:embed templates
|
//go:embed templates
|
||||||
@ -63,29 +58,30 @@ func main() {
|
|||||||
if *inputFile == "" {
|
if *inputFile == "" {
|
||||||
startWebServer()
|
startWebServer()
|
||||||
} else {
|
} else {
|
||||||
processInputFile(*inputFile)
|
items, err := parseInputFile(*inputFile)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error parsing input file: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
processItems(*inputFile, items)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func startWebServer() {
|
func startWebServer() {
|
||||||
http.HandleFunc("/", handleRoot)
|
http.HandleFunc("/", handleRoot)
|
||||||
http.HandleFunc("/upload", handleUpload)
|
http.HandleFunc("/upload", handleUpload)
|
||||||
|
http.HandleFunc("/select", handleSelect)
|
||||||
|
http.HandleFunc("/process", handleProcess)
|
||||||
http.HandleFunc("/progress", handleProgress)
|
http.HandleFunc("/progress", handleProgress)
|
||||||
|
http.HandleFunc("/abort", handleAbort)
|
||||||
|
http.HandleFunc("/pause", handlePause)
|
||||||
|
http.HandleFunc("/resume", handleResume)
|
||||||
|
http.HandleFunc("/clear-completed", handleClearCompleted)
|
||||||
|
|
||||||
fmt.Println("Starting web server on http://0.0.0.0:8080")
|
fmt.Println("Starting web server on http://0.0.0.0:8080")
|
||||||
http.ListenAndServe(":8080", nil)
|
http.ListenAndServe(":8080", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateProgress(filename string, value float64, currentFile string) {
|
|
||||||
progressMutex.Lock()
|
|
||||||
defer progressMutex.Unlock()
|
|
||||||
progress[filename] = &ProgressInfo{
|
|
||||||
Percentage: value,
|
|
||||||
CurrentFile: currentFile,
|
|
||||||
}
|
|
||||||
fmt.Printf("Progress updated for %s: %.2f%%, Current file: %s\n", filename, value, currentFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getProgress(filename string) *ProgressInfo {
|
func getProgress(filename string) *ProgressInfo {
|
||||||
progressMutex.Lock()
|
progressMutex.Lock()
|
||||||
defer progressMutex.Unlock()
|
defer progressMutex.Unlock()
|
||||||
|
@ -45,6 +45,7 @@
|
|||||||
ul {
|
ul {
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
li {
|
li {
|
||||||
background-color: #2d2d2d;
|
background-color: #2d2d2d;
|
||||||
@ -73,6 +74,9 @@
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 5em;
|
width: 5em;
|
||||||
}
|
}
|
||||||
|
.paused {
|
||||||
|
color: #ffa500;
|
||||||
|
}
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
body {
|
body {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
@ -83,13 +87,32 @@
|
|||||||
input[type="file"], input[type="submit"] {
|
input[type="file"], input[type="submit"] {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
input[type="submit"], #clear-completed {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
input[type="submit"], #clear-completed {
|
||||||
|
cursor: pointer;
|
||||||
|
color: white;
|
||||||
|
border: 1px solid #444;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
max-width: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
#clear-completed {
|
||||||
|
background-color: #f44336;
|
||||||
|
}
|
||||||
|
#clear-completed:hover {
|
||||||
|
background-color: #d32f2f;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Simple Downloader</h1>
|
<h1>Simple Downloader</h1>
|
||||||
<form action="/upload" method="post" enctype="multipart/form-data">
|
<form action="/upload" method="post" enctype="multipart/form-data">
|
||||||
<input type="file" name="file" accept=".drmd">
|
<input type="file" name="files" accept=".drmd" multiple>
|
||||||
<input type="submit" value="Upload and Process">
|
<input type="submit" value="Upload and Process">
|
||||||
</form>
|
</form>
|
||||||
<h2>Currently Running Jobs</h2>
|
<h2>Currently Running Jobs</h2>
|
||||||
@ -100,12 +123,30 @@
|
|||||||
<a href="/progress?filename={{$filename}}">{{$filename}}</a>
|
<a href="/progress?filename={{$filename}}">{{$filename}}</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="job-info">
|
<div class="job-info">
|
||||||
Progress: <span class="progress-text">{{printf "%5.1f%%" $info.Percentage}}</span> Current file: {{$info.CurrentFile}}
|
Progress: <span class="progress-text">{{printf "%5.1f%%" $info.Percentage}}</span>
|
||||||
|
Current file: {{$info.CurrentFile}}
|
||||||
|
{{if $info.Paused}}
|
||||||
|
<span class="paused">(Paused)</span>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{{else}}
|
{{else}}
|
||||||
<li>No active jobs</li>
|
<li>No active jobs</li>
|
||||||
{{end}}
|
{{end}}
|
||||||
</ul>
|
</ul>
|
||||||
|
<button id="clear-completed" onclick="clearCompleted()">Clear Completed Jobs</button>
|
||||||
|
<script>
|
||||||
|
function clearCompleted() {
|
||||||
|
fetch('/clear-completed', { method: 'POST' })
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Failed to clear completed jobs');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -56,6 +56,46 @@
|
|||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
#abort-button {
|
||||||
|
background-color: #f44336;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 15px;
|
||||||
|
margin-top: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
#abort-button:hover {
|
||||||
|
background-color: #d32f2f;
|
||||||
|
}
|
||||||
|
#pause-button, #resume-button {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 15px;
|
||||||
|
margin-top: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
#pause-button:hover, #resume-button:hover {
|
||||||
|
background-color: #45a049;
|
||||||
|
}
|
||||||
|
#resume-button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
#back-button {
|
||||||
|
background-color: #2196F3;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 15px;
|
||||||
|
margin-top: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
#back-button:hover {
|
||||||
|
background-color: #1976D2;
|
||||||
|
}
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
body {
|
body {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
@ -84,7 +124,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="currentFile"></div>
|
<div id="currentFile"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<button id="abort-button" onclick="abortDownload()">Abort Download</button>
|
||||||
|
<button id="pause-button" onclick="pauseDownload()">Pause Download</button>
|
||||||
|
<button id="resume-button" onclick="resumeDownload()" style="display: none;">Resume Download</button>
|
||||||
|
<button id="back-button" onclick="window.location.href='/'">Back to Index</button>
|
||||||
|
</div>
|
||||||
<script>
|
<script>
|
||||||
|
let isPaused = false;
|
||||||
|
|
||||||
function updateProgress() {
|
function updateProgress() {
|
||||||
fetch('/progress?filename={{.Filename}}', {
|
fetch('/progress?filename={{.Filename}}', {
|
||||||
headers: {
|
headers: {
|
||||||
@ -97,11 +145,64 @@
|
|||||||
document.getElementById('progress-bar').style.width = progress + '%';
|
document.getElementById('progress-bar').style.width = progress + '%';
|
||||||
document.getElementById('progress-text').innerText = progress + '%';
|
document.getElementById('progress-text').innerText = progress + '%';
|
||||||
document.getElementById('currentFile').innerText = 'Current file: ' + (data.CurrentFile || 'None');
|
document.getElementById('currentFile').innerText = 'Current file: ' + (data.CurrentFile || 'None');
|
||||||
if (progress < 100) {
|
|
||||||
|
isPaused = data.Paused;
|
||||||
|
updatePauseResumeButtons();
|
||||||
|
|
||||||
|
if (progress < 100 && !isPaused) {
|
||||||
setTimeout(updateProgress, 1000);
|
setTimeout(updateProgress, 1000);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updatePauseResumeButtons() {
|
||||||
|
if (isPaused) {
|
||||||
|
document.getElementById('pause-button').style.display = 'none';
|
||||||
|
document.getElementById('resume-button').style.display = 'inline-block';
|
||||||
|
} else {
|
||||||
|
document.getElementById('pause-button').style.display = 'inline-block';
|
||||||
|
document.getElementById('resume-button').style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function abortDownload() {
|
||||||
|
fetch('/abort?filename={{.Filename}}', { method: 'POST' })
|
||||||
|
.then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
console.log('Abort signal sent. The download will stop soon.');
|
||||||
|
} else {
|
||||||
|
alert('Failed to abort the download.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function pauseDownload() {
|
||||||
|
fetch('/pause?filename={{.Filename}}', { method: 'POST' })
|
||||||
|
.then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
console.log('Pause signal sent. The download will pause soon.');
|
||||||
|
isPaused = true;
|
||||||
|
updatePauseResumeButtons();
|
||||||
|
} else {
|
||||||
|
alert('Failed to pause the download.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resumeDownload() {
|
||||||
|
fetch('/resume?filename={{.Filename}}', { method: 'POST' })
|
||||||
|
.then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
console.log('Resume signal sent. The download will resume soon.');
|
||||||
|
isPaused = false;
|
||||||
|
updatePauseResumeButtons();
|
||||||
|
updateProgress();
|
||||||
|
} else {
|
||||||
|
alert('Failed to resume the download.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
updateProgress();
|
updateProgress();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
105
templates/select
Normal file
105
templates/select
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Select Items to Download</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
color: #d4d4d4;
|
||||||
|
line-height: 1.6;
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
.season {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.season-title {
|
||||||
|
font-size: 1.2em;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.season-checkbox {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.item {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
button, input[type="submit"] {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 15px;
|
||||||
|
margin: 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
button:hover, input[type="submit"]:hover {
|
||||||
|
background-color: #45a049;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Select Items to Download</h1>
|
||||||
|
<form action="/process" method="post">
|
||||||
|
<input type="hidden" name="filenames" value="{{.Filenames}}">
|
||||||
|
{{range $filename, $fileItems := .AllItems}}
|
||||||
|
<h2>{{$filename}}</h2>
|
||||||
|
{{range $season, $items := $fileItems}}
|
||||||
|
<div class="season">
|
||||||
|
<div class="season-title">
|
||||||
|
<input type="checkbox" class="season-checkbox" id="season-{{$filename}}-{{$season}}" checked onchange="toggleSeason('{{$filename}}-{{$season}}')">
|
||||||
|
<label for="season-{{$filename}}-{{$season}}">{{$season}}</label>
|
||||||
|
</div>
|
||||||
|
{{range $item := $items}}
|
||||||
|
<div class="item">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="items" value="{{$filename}}:{{$item.Filename}}" checked class="episode-{{$filename}}-{{$season}}">
|
||||||
|
{{$item.Filename}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
<div>
|
||||||
|
<button type="button" onclick="selectAll(true)">Select All</button>
|
||||||
|
<button type="button" onclick="selectAll(false)">Select None</button>
|
||||||
|
<input type="submit" value="Start Download">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<script>
|
||||||
|
function selectAll(checked) {
|
||||||
|
var checkboxes = document.getElementsByName('items');
|
||||||
|
for (var i = 0; i < checkboxes.length; i++) {
|
||||||
|
checkboxes[i].checked = checked;
|
||||||
|
}
|
||||||
|
var seasonCheckboxes = document.getElementsByClassName('season-checkbox');
|
||||||
|
for (var i = 0; i < seasonCheckboxes.length; i++) {
|
||||||
|
seasonCheckboxes[i].checked = checked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSeason(season) {
|
||||||
|
var seasonCheckbox = document.getElementById('season-' + season);
|
||||||
|
var episodeCheckboxes = document.getElementsByClassName('episode-' + season);
|
||||||
|
for (var i = 0; i < episodeCheckboxes.length; i++) {
|
||||||
|
episodeCheckboxes[i].checked = seasonCheckbox.checked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
170
utils.go
170
utils.go
@ -1,15 +1,34 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/beevik/etree"
|
"github.com/beevik/etree"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type JobInfo struct {
|
||||||
|
AbortChan chan struct{}
|
||||||
|
ResumeChan chan struct{}
|
||||||
|
Cmd *exec.Cmd
|
||||||
|
Paused bool
|
||||||
|
TempDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
jobsMutex sync.Mutex
|
||||||
|
jobs = make(map[string]*JobInfo)
|
||||||
|
)
|
||||||
|
|
||||||
func sanitizeFilename(filename string) string {
|
func sanitizeFilename(filename string) string {
|
||||||
filename = regexp.MustCompile(`[<>:"/\\|?*]`).ReplaceAllString(filename, "_")
|
filename = regexp.MustCompile(`[<>:"/\\|?*]`).ReplaceAllString(filename, "_")
|
||||||
|
|
||||||
@ -118,3 +137,154 @@ func findOrCreateSegmentTimeline(adaptationSet *etree.Element) *etree.Element {
|
|||||||
segmentTemplate := representation.CreateElement("SegmentTemplate")
|
segmentTemplate := representation.CreateElement("SegmentTemplate")
|
||||||
return segmentTemplate.CreateElement("SegmentTimeline")
|
return segmentTemplate.CreateElement("SegmentTimeline")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseInputFile(filename string) ([]Item, error) {
|
||||||
|
fileInfo, err := os.Stat(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if fileInfo.IsDir() {
|
||||||
|
return nil, fmt.Errorf("%s is a directory", filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
byteValue, err := io.ReadAll(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
byteValue = removeBOM(byteValue)
|
||||||
|
|
||||||
|
var items Items
|
||||||
|
err = json.Unmarshal(byteValue, &items)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return items.Items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func groupItemsBySeason(items []Item) map[string][]Item {
|
||||||
|
grouped := make(map[string][]Item)
|
||||||
|
for _, item := range items {
|
||||||
|
metadata := parseMetadata(item.Metadata)
|
||||||
|
if metadata.Type == "serie" {
|
||||||
|
key := fmt.Sprintf("%s - %s", metadata.Title, metadata.Season)
|
||||||
|
grouped[key] = append(grouped[key], item)
|
||||||
|
} else {
|
||||||
|
grouped["Movies"] = append(grouped["Movies"], item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return grouped
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterSelectedItems(items []Item, selectedItems []string) []Item {
|
||||||
|
var filtered []Item
|
||||||
|
for _, item := range items {
|
||||||
|
for _, selected := range selectedItems {
|
||||||
|
if item.Filename == selected {
|
||||||
|
filtered = append(filtered, item)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
func processItems(filename string, items []Item) error {
|
||||||
|
jobsMutex.Lock()
|
||||||
|
jobInfo := &JobInfo{
|
||||||
|
AbortChan: make(chan struct{}),
|
||||||
|
ResumeChan: make(chan struct{}),
|
||||||
|
}
|
||||||
|
jobs[filename] = jobInfo
|
||||||
|
jobsMutex.Unlock()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
jobsMutex.Lock()
|
||||||
|
delete(jobs, filename)
|
||||||
|
jobsMutex.Unlock()
|
||||||
|
|
||||||
|
if jobInfo.TempDir != "" {
|
||||||
|
os.RemoveAll(jobInfo.TempDir)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for i := 0; i < len(items); i++ {
|
||||||
|
select {
|
||||||
|
case <-jobInfo.AbortChan:
|
||||||
|
updateProgress(filename, 100, "Aborted")
|
||||||
|
return fmt.Errorf("download aborted")
|
||||||
|
default:
|
||||||
|
if jobInfo.Paused {
|
||||||
|
select {
|
||||||
|
case <-jobInfo.ResumeChan:
|
||||||
|
jobInfo.Paused = false
|
||||||
|
case <-jobInfo.AbortChan:
|
||||||
|
updateProgress(filename, 100, "Aborted")
|
||||||
|
return fmt.Errorf("download aborted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateProgress(filename, float64(i)/float64(len(items))*100, items[i].Filename)
|
||||||
|
err := downloadFile(items[i], jobInfo)
|
||||||
|
if err != nil {
|
||||||
|
if err.Error() == "download paused" {
|
||||||
|
removeCompletedEpisodes(filename, items[:i])
|
||||||
|
i--
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateProgress(filename, 100, "")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeCompletedEpisodes(filename string, completedItems []Item) error {
|
||||||
|
inputFile := filepath.Join(uploadDir, filename)
|
||||||
|
items, err := parseInputFile(inputFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error parsing input file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
remainingItems := make([]Item, 0)
|
||||||
|
for _, item := range items {
|
||||||
|
if !isItemCompleted(item, completedItems) || isLastCompletedItem(item, completedItems) {
|
||||||
|
remainingItems = append(remainingItems, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedItems := Items{Items: remainingItems}
|
||||||
|
jsonData, err := json.MarshalIndent(updatedItems, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error marshaling updated items: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile(inputFile, jsonData, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error writing updated DRMD file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isItemCompleted(item Item, completedItems []Item) bool {
|
||||||
|
for _, completedItem := range completedItems {
|
||||||
|
if item.Filename == completedItem.Filename {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func isLastCompletedItem(item Item, completedItems []Item) bool {
|
||||||
|
if len(completedItems) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return item.Filename == completedItems[len(completedItems)-1].Filename
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user