diff --git a/README.md b/README.md index 6b2b17b..1628f8c 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Create a `config.toml` file in the same directory as the drmdtool executable: BaseDir = "/path/to/save/downloads" Format = "mkv" TempBaseDir = "/tmp/nre" +EnableConsole = true [N_m3u8DL-RE] Path = "/path/to/N_m3u8DL-RE" diff --git a/config.toml b/config.toml index 7fe8fdb..58e198c 100644 --- a/config.toml +++ b/config.toml @@ -1,6 +1,7 @@ BaseDir = "/mnt/media" Format = "mkv" TempBaseDir = "/tmp/nre" +EnableConsole = true [N_m3u8DLRE] -Path = "nre" +Path = "nre" \ No newline at end of file diff --git a/src/config.go b/src/config.go index b7253bb..fc36b81 100644 --- a/src/config.go +++ b/src/config.go @@ -15,6 +15,7 @@ type Config struct { N_m3u8DLRE struct { Path string } + EnableConsole bool } var config Config diff --git a/src/downloaders.go b/src/downloaders.go index eef7fdb..c027fb7 100644 --- a/src/downloaders.go +++ b/src/downloaders.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "encoding/base64" "fmt" "io" @@ -9,6 +10,7 @@ import ( "os/exec" "path/filepath" "strings" + "time" ) func removeBOM(input []byte) []byte { @@ -18,12 +20,13 @@ func removeBOM(input []byte) []byte { return input } -func downloadFile(item Item, jobInfo *JobInfo) error { - fmt.Println("Downloading:", item.Filename) +func downloadFile(drmdFilename string, item Item, jobInfo *JobInfo) error { + logger.LogInfo("Download File", fmt.Sprintf("Starting download for: %s", item.Filename)) tempDir := filepath.Join(config.TempBaseDir, sanitizeFilename(item.Filename)) err := os.MkdirAll(tempDir, 0755) if err != nil { + logger.LogError("Download File", fmt.Sprintf("Error creating temporary directory: %v", err)) return fmt.Errorf("error creating temporary directory: %v", err) } @@ -33,19 +36,23 @@ func downloadFile(item Item, jobInfo *JobInfo) error { if !isValidURL(item.MPD) { decodedMPD, err := base64.StdEncoding.DecodeString(item.MPD) if err != nil { + logger.LogError("Download File", fmt.Sprintf("Error decoding base64 MPD: %v", err)) return fmt.Errorf("error decoding base64 MPD: %v", err) } tempFile, err := os.CreateTemp("", "temp_mpd_*.mpd") if err != nil { + logger.LogError("Download File", fmt.Sprintf("Error creating temporary MPD file: %v", err)) return fmt.Errorf("error creating temporary MPD file: %v", err) } defer os.Remove(tempFile.Name()) if _, err := tempFile.Write(decodedMPD); err != nil { + logger.LogError("Download File", fmt.Sprintf("Error writing to temporary MPD file: %v", err)) return fmt.Errorf("error writing to temporary MPD file: %v", err) } if err := tempFile.Close(); err != nil { + logger.LogError("Download File", fmt.Sprintf("Error closing temporary MPD file: %v", err)) return fmt.Errorf("error closing temporary MPD file: %v", err) } @@ -53,30 +60,36 @@ func downloadFile(item Item, jobInfo *JobInfo) error { } else if strings.HasPrefix(item.MPD, "https://pubads.g.doubleclick.net") { resp, err := http.Get(item.MPD) if err != nil { + logger.LogError("Download File", fmt.Sprintf("Error downloading MPD: %v", err)) return fmt.Errorf("error downloading MPD: %v", err) } defer resp.Body.Close() mpdContent, err := io.ReadAll(resp.Body) if err != nil { + logger.LogError("Download File", fmt.Sprintf("Error reading MPD content: %v", err)) return fmt.Errorf("error reading MPD content: %v", err) } fixedMPDContent, err := fixGoPlay(string(mpdContent)) if err != nil { + logger.LogError("Download File", fmt.Sprintf("Error fixing MPD content: %v", err)) return fmt.Errorf("error fixing MPD content: %v", err) } tempFile, err := os.CreateTemp("", "fixed_mpd_*.mpd") if err != nil { + logger.LogError("Download File", fmt.Sprintf("Error creating temporary MPD file: %v", err)) return fmt.Errorf("error creating temporary MPD file: %v", err) } defer os.Remove(tempFile.Name()) if _, err := tempFile.WriteString(fixedMPDContent); err != nil { + logger.LogError("Download File", fmt.Sprintf("Error writing to temporary MPD file: %v", err)) return fmt.Errorf("error writing to temporary MPD file: %v", err) } if err := tempFile.Close(); err != nil { + logger.LogError("Download File", fmt.Sprintf("Error closing temporary MPD file: %v", err)) return fmt.Errorf("error closing temporary MPD file: %v", err) } @@ -88,26 +101,25 @@ func downloadFile(item Item, jobInfo *JobInfo) error { if item.Subtitles != "" { subtitlePaths, err := downloadAndConvertSubtitles(item.Subtitles) if err != nil { - fmt.Printf("Error processing subtitles: %v\n", err) + logger.LogError("Download File", fmt.Sprintf("Error processing subtitles: %v", err)) } else { for _, path := range subtitlePaths { - fmt.Println("Adding subtitle:", path) + logger.LogInfo("Download File", fmt.Sprintf("Adding subtitle: %s", path)) command += fmt.Sprintf(" --mux-import \"path=%s:lang=nl:name=Nederlands\"", path) } } } cmd := exec.Command("bash", "-c", command) - - jobsMutex.Lock() jobInfo.Cmd = cmd - jobsMutex.Unlock() - cmd.Stdout = os.Stdout + var outputBuffer bytes.Buffer + cmd.Stdout = io.MultiWriter(&outputBuffer) cmd.Stderr = os.Stderr err = cmd.Start() if err != nil { + logger.LogError("Download File", fmt.Sprintf("Error starting download command: %v", err)) return fmt.Errorf("error starting download command: %v", err) } @@ -116,23 +128,39 @@ func downloadFile(item Item, jobInfo *JobInfo) error { done <- cmd.Wait() }() + go func() { + for { + if outputBuffer.Len() > 0 { + message := outputBuffer.Bytes() + if config.EnableConsole { + broadcast(drmdFilename, message) + } + outputBuffer.Reset() + } + time.Sleep(1 * time.Second) + } + }() + select { case <-jobInfo.AbortChan: if cmd.Process != nil { cmd.Process.Kill() } os.RemoveAll(tempDir) + logger.LogInfo("Download File", "Download aborted") return fmt.Errorf("download aborted") case err := <-done: if jobInfo.Paused { + logger.LogInfo("Download File", "Download paused") return fmt.Errorf("download paused") } if err != nil { + logger.LogError("Download File", fmt.Sprintf("Error executing download command: %v", err)) return fmt.Errorf("error executing download command: %v", err) } } - fmt.Println("Download completed successfully") + logger.LogInfo("Download File", "Download completed successfully") return nil } diff --git a/src/go.mod b/src/go.mod index bc9873f..38e8b9d 100644 --- a/src/go.mod +++ b/src/go.mod @@ -11,6 +11,7 @@ require ( require ( github.com/asticode/go-astikit v0.20.0 // indirect github.com/asticode/go-astits v1.8.0 // indirect + github.com/gorilla/websocket v1.5.3 golang.org/x/net v0.0.0-20200904194848-62affa334b73 // indirect golang.org/x/text v0.3.2 // indirect ) diff --git a/src/go.sum b/src/go.sum index 97fe73a..68a64b8 100644 --- a/src/go.sum +++ b/src/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/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 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= diff --git a/src/handlers.go b/src/handlers.go index c5e2e5f..309c4ae 100644 --- a/src/handlers.go +++ b/src/handlers.go @@ -9,6 +9,9 @@ import ( "os" "path/filepath" "strings" + "sync" + + "github.com/gorilla/websocket" ) type ProgressInfo struct { @@ -47,19 +50,23 @@ func handleRoot(w http.ResponseWriter, r *http.Request) { } }{jobsInfo}) if err != nil { + logger.LogError("Handle Root", fmt.Sprintf("Error executing template: %v", err)) http.Error(w, err.Error(), http.StatusInternalServerError) } } func handleUpload(w http.ResponseWriter, r *http.Request) { + logger.LogInfo("Handle Upload", "Starting file upload") err := r.ParseMultipartForm(32 << 20) if err != nil { + logger.LogError("Handle Upload", fmt.Sprintf("Error parsing multipart form: %v", err)) http.Error(w, err.Error(), http.StatusBadRequest) return } files := r.MultipartForm.File["files"] if len(files) == 0 { + logger.LogError("Handle Upload", "No files uploaded") http.Error(w, "No files uploaded", http.StatusBadRequest) return } @@ -69,6 +76,7 @@ func handleUpload(w http.ResponseWriter, r *http.Request) { for _, fileHeader := range files { file, err := fileHeader.Open() if err != nil { + logger.LogError("Handle Upload", fmt.Sprintf("Error opening file: %v", err)) http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -76,6 +84,7 @@ func handleUpload(w http.ResponseWriter, r *http.Request) { tempFile, err := os.CreateTemp(uploadDir, fileHeader.Filename) if err != nil { + logger.LogError("Handle Upload", fmt.Sprintf("Error creating temporary file: %v", err)) http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -83,6 +92,7 @@ func handleUpload(w http.ResponseWriter, r *http.Request) { _, err = io.Copy(tempFile, file) if err != nil { + logger.LogError("Handle Upload", fmt.Sprintf("Error copying file: %v", err)) http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -91,6 +101,7 @@ func handleUpload(w http.ResponseWriter, r *http.Request) { _, err = parseInputFile(tempFile.Name()) if err != nil { + logger.LogError("Handle Upload", fmt.Sprintf("Error parsing input file: %v", err)) http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -104,10 +115,12 @@ func handleUpload(w http.ResponseWriter, r *http.Request) { } if len(validFiles) == 0 { + logger.LogError("Handle Upload", "No valid files were uploaded") http.Error(w, "No valid files were uploaded", http.StatusBadRequest) return } + logger.LogInfo("Handle Upload", fmt.Sprintf("Redirecting to select with files: %v", validFiles)) http.Redirect(w, r, "/select?files="+url.QueryEscape(strings.Join(validFiles, ",")), http.StatusSeeOther) } @@ -124,11 +137,13 @@ func handleSelect(w http.ResponseWriter, r *http.Request) { fullPath := filepath.Join(uploadDir, filename) if _, err := os.Stat(fullPath); os.IsNotExist(err) { + logger.LogError("Handle Select", fmt.Sprintf("File does not exist: %s", fullPath)) continue } items, err := parseInputFile(fullPath) if err != nil { + logger.LogError("Handle Select", fmt.Sprintf("Error parsing input file: %v", err)) continue } @@ -139,6 +154,7 @@ func handleSelect(w http.ResponseWriter, r *http.Request) { } if len(allItems) == 0 { + logger.LogError("Handle Select", "No valid files were processed") http.Error(w, "No valid files were processed", http.StatusBadRequest) return } @@ -151,18 +167,22 @@ func handleSelect(w http.ResponseWriter, r *http.Request) { AllItems: allItems, }) if err != nil { + logger.LogError("Handle Select", fmt.Sprintf("Error executing template: %v", err)) http.Error(w, err.Error(), http.StatusInternalServerError) } } func handleProcess(w http.ResponseWriter, r *http.Request) { + logger.LogInfo("Handle Process", "Starting process") if err := r.ParseForm(); err != nil { + logger.LogError("Handle Process", fmt.Sprintf("Error parsing form: %v", err)) http.Error(w, err.Error(), http.StatusBadRequest) return } selectedItems := r.Form["items"] if len(selectedItems) == 0 { + logger.LogError("Handle Process", "No items selected") http.Error(w, "No items selected", http.StatusBadRequest) return } @@ -171,6 +191,7 @@ func handleProcess(w http.ResponseWriter, r *http.Request) { for _, item := range selectedItems { parts := strings.SplitN(item, ":", 2) if len(parts) != 2 { + logger.LogError("Handle Process", "Invalid item format") continue } filename, itemName := parts[0], parts[1] @@ -178,9 +199,11 @@ func handleProcess(w http.ResponseWriter, r *http.Request) { } for filename, items := range itemsByFile { + logger.LogInfo("Handle Process", fmt.Sprintf("Processing file: %s", filename)) fullPath := filepath.Join(uploadDir, filename) allItems, err := parseInputFile(fullPath) if err != nil { + logger.LogError("Handle Process", fmt.Sprintf("Error parsing input file: %v", err)) http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -223,6 +246,7 @@ func handleProgress(w http.ResponseWriter, r *http.Request) { func handlePause(w http.ResponseWriter, r *http.Request) { filename := r.URL.Query().Get("filename") if filename == "" { + logger.LogError("Pause Handler", "Filename is required") http.Error(w, "Filename is required", http.StatusBadRequest) return } @@ -232,12 +256,14 @@ func handlePause(w http.ResponseWriter, r *http.Request) { jobsMutex.Unlock() if !exists { + logger.LogError("Pause Handler", "Job not found") http.Error(w, "Job not found", http.StatusNotFound) return } jobInfo.Paused = true if jobInfo.Cmd != nil && jobInfo.Cmd.Process != nil { + logger.LogJobState(filename, "pausing") jobInfo.Cmd.Process.Kill() } @@ -354,3 +380,66 @@ func updateProgress(filename string, value float64, currentFile string) { } } } + +var upgrader = websocket.Upgrader{} +var clients = make(map[string]map[*websocket.Conn]bool) +var mu sync.Mutex + +func handleWebSocket(w http.ResponseWriter, r *http.Request) { + fmt.Println(config.EnableConsole) + if !config.EnableConsole { + http.Error(w, "Console output is disabled", http.StatusForbidden) + return + } + + filename := r.URL.Query().Get("filename") + if filename == "" { + http.Error(w, "Filename is required", http.StatusBadRequest) + return + } + + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + logger.LogError("WebSocket", fmt.Sprintf("Error while upgrading connection: %v", err)) + return + } + defer conn.Close() + + logger.LogInfo("WebSocket", fmt.Sprintf("WebSocket connection established for filename: %s", filename)) + + mu.Lock() + if clients[filename] == nil { + clients[filename] = make(map[*websocket.Conn]bool) + } + clients[filename][conn] = true + mu.Unlock() + + for { + if _, _, err := conn.NextReader(); err != nil { + break + } + } + + mu.Lock() + delete(clients[filename], conn) + mu.Unlock() + + logger.LogInfo("WebSocket", fmt.Sprintf("WebSocket connection closed for filename: %s", filename)) +} + +func broadcast(filename string, message []byte) { + if !config.EnableConsole { + return + } + + mu.Lock() + defer mu.Unlock() + + for client := range clients[filename] { + if err := client.WriteMessage(websocket.TextMessage, message); err != nil { + client.Close() + delete(clients[filename], client) + logger.LogError("Broadcast", fmt.Sprintf("Error writing message to client: %v", err)) + } + } +} diff --git a/src/logger.go b/src/logger.go new file mode 100644 index 0000000..4375a0f --- /dev/null +++ b/src/logger.go @@ -0,0 +1,36 @@ +package main + +import ( + "log" + "os" +) + +type Logger struct { + *log.Logger +} + +const ( + Reset = "\033[0m" + Red = "\033[31m" + Green = "\033[32m" + Yellow = "\033[33m" + Blue = "\033[34m" +) + +func NewLogger(prefix string) *Logger { + return &Logger{ + Logger: log.New(os.Stdout, prefix, log.Ldate|log.Ltime|log.Lshortfile), + } +} + +func (l *Logger) LogInfo(jobName, message string) { + l.Printf("%s[INFO] [%s] %s%s", Green, jobName, message, Reset) +} + +func (l *Logger) LogError(jobName, message string) { + l.Printf("%s[ERROR] [%s] %s%s", Red, jobName, message, Reset) +} + +func (l *Logger) LogJobState(jobName, state string) { + l.Printf("%s[JOB STATE] [%s] %s%s", Yellow, jobName, state, Reset) +} diff --git a/src/main.go b/src/main.go index 7feea87..d9434f4 100644 --- a/src/main.go +++ b/src/main.go @@ -12,6 +12,8 @@ import ( "embed" ) +var logger *Logger + type Item struct { MPD string Keys string @@ -48,6 +50,8 @@ func init() { } templates = template.Must(template.ParseFS(templateFS, "templates/*")) + + logger = NewLogger("") } func main() { @@ -60,7 +64,7 @@ func main() { } else { items, err := parseInputFile(*inputFile) if err != nil { - fmt.Printf("Error parsing input file: %v\n", err) + logger.LogError("Main", fmt.Sprintf("Error parsing input file: %v", err)) return } processItems(*inputFile, items) @@ -77,6 +81,7 @@ func startWebServer() { http.HandleFunc("/pause", handlePause) http.HandleFunc("/resume", handleResume) http.HandleFunc("/clear-completed", handleClearCompleted) + http.HandleFunc("/ws", handleWebSocket) fmt.Println("Starting web server on http://0.0.0.0:8080") http.ListenAndServe(":8080", nil) diff --git a/src/subtitles.go b/src/subtitles.go index c30bd81..91b22b8 100644 --- a/src/subtitles.go +++ b/src/subtitles.go @@ -15,13 +15,16 @@ func downloadAndConvertSubtitles(subtitlesURLs string) ([]string, error) { urls := strings.Split(subtitlesURLs, ",") for _, url := range urls { + logger.LogInfo("Subtitle Download", fmt.Sprintf("Downloading subtitle from %s", url)) vttPath, err := downloadSubtitle(url) if err != nil { + logger.LogError("Subtitle Download", fmt.Sprintf("Error downloading subtitle: %v", err)) return nil, fmt.Errorf("error downloading subtitle: %v", err) } srtPath, err := convertVTTtoSRT(vttPath) if err != nil { + logger.LogError("Subtitle Download", fmt.Sprintf("Error converting subtitle: %v", err)) return nil, fmt.Errorf("error converting subtitle: %v", err) } @@ -32,23 +35,28 @@ func downloadAndConvertSubtitles(subtitlesURLs string) ([]string, error) { } func downloadSubtitle(url string) (string, error) { + logger.LogInfo("Download Subtitle", fmt.Sprintf("Starting download from %s", url)) resp, err := http.Get(url) if err != nil { + logger.LogError("Download Subtitle", fmt.Sprintf("Error getting subtitle URL: %v", err)) return "", err } defer resp.Body.Close() tempFile, err := os.CreateTemp("", "subtitle_*.vtt") if err != nil { + logger.LogError("Download Subtitle", fmt.Sprintf("Error creating temp file: %v", err)) return "", err } defer tempFile.Close() _, err = io.Copy(tempFile, resp.Body) if err != nil { + logger.LogError("Download Subtitle", fmt.Sprintf("Error copying to temp file: %v", err)) return "", err } + logger.LogInfo("Download Subtitle", "Subtitle downloaded successfully") return tempFile.Name(), nil } @@ -56,5 +64,6 @@ func convertVTTtoSRT(vttPath string) (string, error) { srtPath := strings.TrimSuffix(vttPath, ".vtt") + ".srt" s1, _ := astisub.OpenFile(vttPath) s1.Write(srtPath) + logger.LogInfo("Convert VTT to SRT", fmt.Sprintf("Converted %s to %s", vttPath, srtPath)) return srtPath, nil } diff --git a/src/templates/index b/src/templates/index index af57727..fff9d5e 100644 --- a/src/templates/index +++ b/src/templates/index @@ -11,7 +11,7 @@ color: #d4d4d4; line-height: 1.6; padding: 20px; - max-width: 800px; + max-width: 900px; margin: 0 auto; box-sizing: border-box; } diff --git a/src/templates/progress b/src/templates/progress index f3c710d..7b6162c 100644 --- a/src/templates/progress +++ b/src/templates/progress @@ -11,7 +11,7 @@ color: #d4d4d4; line-height: 1.6; padding: 20px; - max-width: 800px; + max-width: 900px; margin: 0 auto; box-sizing: border-box; } @@ -68,7 +68,7 @@ #abort-button:hover { background-color: #d32f2f; } - #pause-button, #resume-button { + #pause-button, #resume-button, #toggle-console { background-color: #4CAF50; color: white; border: none; @@ -77,7 +77,7 @@ border-radius: 4px; cursor: pointer; } - #pause-button:hover, #resume-button:hover { + #pause-button:hover, #resume-button:hover, #toggle-console:hover { background-color: #45a049; } #resume-button { @@ -96,6 +96,17 @@ #back-button:hover { background-color: #1976D2; } + #console { + display: none; /* Initially hidden */ + background-color: black; + color: white; + height: 300px; /* Adjust height as needed */ + overflow-y: scroll; + white-space: pre; /* Preserve whitespace */ + font-family: monospace; /* Use monospace font */ + margin-top: 10px; + border: 1px solid #ccc; + } @media (max-width: 600px) { body { padding: 10px; @@ -128,13 +139,16 @@ + + diff --git a/src/templates/select b/src/templates/select index 9568500..d3c6668 100644 --- a/src/templates/select +++ b/src/templates/select @@ -11,7 +11,7 @@ color: #d4d4d4; line-height: 1.6; padding: 20px; - max-width: 800px; + max-width: 900px; margin: 0 auto; box-sizing: border-box; } diff --git a/src/utils.go b/src/utils.go index 0282c06..0849fe4 100644 --- a/src/utils.go +++ b/src/utils.go @@ -259,21 +259,25 @@ func processItems(filename string, items []Item) error { select { case <-jobInfo.AbortChan: updateProgress(filename, 100, "Aborted") + logger.LogJobState(filename, "aborted") return fmt.Errorf("download aborted") default: if jobInfo.Paused { select { case <-jobInfo.ResumeChan: jobInfo.Paused = false + logger.LogJobState(filename, "resumed") case <-jobInfo.AbortChan: updateProgress(filename, 100, "Aborted") + logger.LogJobState(filename, "aborted") return fmt.Errorf("download aborted") } } updateProgress(filename, float64(i)/float64(len(items))*100, items[i].Filename) - err := downloadFile(items[i], jobInfo) + err := downloadFile(filename, items[i], jobInfo) if err != nil { if err.Error() == "download paused" { + logger.LogJobState(filename, "paused") removeCompletedEpisodes(filename, items[:i]) i-- continue @@ -282,6 +286,7 @@ func processItems(filename string, items []Item) error { } } updateProgress(filename, 100, "") + logger.LogJobState(filename, "completed successfully") return nil }