diff --git a/config.go b/config.go index b481b97..b7253bb 100644 --- a/config.go +++ b/config.go @@ -9,9 +9,10 @@ import ( ) type Config struct { - BaseDir string - Format string - N_m3u8DLRE struct { + BaseDir string + Format string + TempBaseDir string + N_m3u8DLRE struct { Path string } } diff --git a/config.toml b/config.toml index 90f1faa..7fe8fdb 100644 --- a/config.toml +++ b/config.toml @@ -1,5 +1,6 @@ BaseDir = "/mnt/media" Format = "mkv" +TempBaseDir = "/tmp/nre" [N_m3u8DLRE] Path = "nre" diff --git a/downloaders.go b/downloaders.go index dd77c35..3681151 100644 --- a/downloaders.go +++ b/downloaders.go @@ -2,7 +2,6 @@ package main import ( "encoding/base64" - "encoding/json" "fmt" "io" "net/http" @@ -12,38 +11,6 @@ import ( "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 { if len(input) >= 3 && input[0] == 0xEF && input[1] == 0xBB && input[2] == 0xBF { return input[3:] @@ -51,9 +18,17 @@ func removeBOM(input []byte) []byte { return input } -func downloadFile(item Item) error { +func downloadFile(item Item, jobInfo *JobInfo) error { 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 if !isValidURL(item.MPD) { decodedMPD, err := base64.StdEncoding.DecodeString(item.MPD) @@ -108,7 +83,7 @@ func downloadFile(item Item) error { mpdPath = tempFile.Name() } - command := getDownloadCommand(item, mpdPath) + command := getDownloadCommand(item, mpdPath, tempDir) if item.Subtitles != "" { subtitlePaths, err := downloadAndConvertSubtitles(item.Subtitles) @@ -124,19 +99,45 @@ func downloadFile(item Item) error { cmd := exec.Command("bash", "-c", command) + jobsMutex.Lock() + jobInfo.Cmd = cmd + jobsMutex.Unlock() + cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - err := cmd.Run() + err = cmd.Start() 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") + os.RemoveAll(tempDir) return nil } -func getDownloadCommand(item Item, mpdPath string) string { +func getDownloadCommand(item Item, mpdPath string, tempDir string) string { metadata := parseMetadata(item.Metadata) 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(" --tmp-dir \"%s\"", tempDir) + fmt.Println(command) return command diff --git a/go.sum b/go.sum index 92cc1b6..97fe73a 100644 --- a/go.sum +++ b/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/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/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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/handlers.go b/handlers.go index d7df463..76b9701 100644 --- a/handlers.go +++ b/handlers.go @@ -51,16 +51,59 @@ func handleUpload(w http.ResponseWriter, r *http.Request) { tempFilename := filepath.Base(tempFile.Name()) + _, err = parseInputFile(tempFile.Name()) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/select?filename="+tempFilename, http.StatusSeeOther) +} + +func handleSelect(w http.ResponseWriter, r *http.Request) { + filename := r.URL.Query().Get("filename") + items, err := parseInputFile(filepath.Join(uploadDir, filename)) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + groupedItems := groupItemsBySeason(items) + + err = templates.ExecuteTemplate(w, "select", struct { + Filename string + Items map[string][]Item + }{ + Filename: filename, + Items: groupedItems, + }) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func handleProcess(w http.ResponseWriter, r *http.Request) { + filename := r.FormValue("filename") + selectedItems := r.Form["items"] + + items, err := parseInputFile(filepath.Join(uploadDir, filename)) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + filteredItems := filterSelectedItems(items, selectedItems) + go func() { - err := processInputFile(tempFile.Name()) + err := processItems(filename, filteredItems) if err != nil { fmt.Printf("Error processing file: %v\n", err) } - os.Remove(tempFile.Name()) + os.Remove(filepath.Join(uploadDir, filename)) }() - http.Redirect(w, r, "/progress?filename="+tempFilename, http.StatusSeeOther) + http.Redirect(w, r, "/progress?filename="+filename, http.StatusSeeOther) } func handleProgress(w http.ResponseWriter, r *http.Request) { @@ -92,3 +135,100 @@ func handleProgress(w http.ResponseWriter, r *http.Request) { 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() + } + + 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{}{} + + 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) + } + } +} diff --git a/main.go b/main.go index 74fcf39..1b66682 100644 --- a/main.go +++ b/main.go @@ -63,14 +63,25 @@ func main() { if *inputFile == "" { startWebServer() } 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() { http.HandleFunc("/", handleRoot) http.HandleFunc("/upload", handleUpload) + http.HandleFunc("/select", handleSelect) + http.HandleFunc("/process", handleProcess) 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") http.ListenAndServe(":8080", nil) diff --git a/templates/index b/templates/index index b0e7cc1..bc57763 100644 --- a/templates/index +++ b/templates/index @@ -45,6 +45,7 @@ ul { list-style-type: none; padding: 0; + margin-bottom: 10px; } li { background-color: #2d2d2d; @@ -83,6 +84,25 @@ input[type="file"], input[type="submit"] { 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; } @@ -107,5 +127,19 @@