diff --git a/config.go b/config.go new file mode 100644 index 0000000..b481b97 --- /dev/null +++ b/config.go @@ -0,0 +1,41 @@ +package main + +import ( + "fmt" + "io" + "os" + + "github.com/BurntSushi/toml" +) + +type Config struct { + BaseDir string + Format string + N_m3u8DLRE struct { + Path string + } +} + +var config Config + +func loadConfig() { + configFile, err := os.Open("config.toml") + if err != nil { + fmt.Println("Error opening config file:", err) + return + } + defer configFile.Close() + + byteValue, _ := io.ReadAll(configFile) + + if _, err := toml.Decode(string(byteValue), &config); err != nil { + fmt.Println("Error decoding config file:", err) + return + } + + if config.N_m3u8DLRE.Path == "" { + fmt.Println("Error: N_m3u8DL-RE path is not specified in the config file") + return + } + +} diff --git a/downloaders.go b/downloaders.go new file mode 100644 index 0000000..0aa2284 --- /dev/null +++ b/downloaders.go @@ -0,0 +1,130 @@ +package main + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" +) + +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) + } + + 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 downloadFile(item Item) error { + fmt.Println("Downloading:", item.Filename) + + mpdPath := item.MPD + if !isValidURL(item.MPD) { + + decodedMPD, err := base64.StdEncoding.DecodeString(item.MPD) + if err != nil { + return fmt.Errorf("error decoding base64 MPD: %v", err) + } + + tempFile, err := os.CreateTemp("", "temp_mpd_*.mpd") + if err != nil { + return fmt.Errorf("error creating temporary MPD file: %v", err) + } + defer os.Remove(tempFile.Name()) + + if _, err := tempFile.Write(decodedMPD); err != nil { + return fmt.Errorf("error writing to temporary MPD file: %v", err) + } + if err := tempFile.Close(); err != nil { + return fmt.Errorf("error closing temporary MPD file: %v", err) + } + + mpdPath = tempFile.Name() + } + + command := getDownloadCommand(item, mpdPath) + + if item.Subtitles != "" { + subtitlePaths, err := downloadAndConvertSubtitles(item.Subtitles) + if err != nil { + fmt.Printf("Error processing subtitles: %v\n", err) + } else { + for _, path := range subtitlePaths { + fmt.Println("Adding subtitle:", path) + command += fmt.Sprintf(" --mux-import \"path=%s:lang=nl:name=Nederlands\"", path) + } + } + } + + cmd := exec.Command("bash", "-c", command) + + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + err := cmd.Run() + if err != nil { + return fmt.Errorf("error executing download command: %v", err) + } + + fmt.Println("Download completed successfully") + return nil +} + +func getDownloadCommand(item Item, mpdPath string) string { + metadata := parseMetadata(item.Metadata) + keys := getKeys(item.Keys) + + command := fmt.Sprintf("%s %s", config.N_m3u8DLRE.Path, mpdPath) + + for _, key := range keys { + if key != "" { + command += fmt.Sprintf(" --key %s", key) + } + } + + command += " --auto-select" + + sanitizedFilename := sanitizeFilename(item.Filename) + + filename := fmt.Sprintf("\"%s\"", sanitizedFilename) + command += fmt.Sprintf(" --save-name %s", filename) + + command += fmt.Sprintf(" --mux-after-done format=%s", config.Format) + + saveDir := config.BaseDir + if metadata.Type == "serie" { + saveDir = filepath.Join(saveDir, "Series", metadata.Title, metadata.Season) + } else { + saveDir = filepath.Join(saveDir, "Movies", metadata.Title) + } + command += fmt.Sprintf(" --save-dir \"%s\"", saveDir) + + fmt.Println(command) + + return command +} diff --git a/handlers.go b/handlers.go new file mode 100644 index 0000000..04ad95d --- /dev/null +++ b/handlers.go @@ -0,0 +1,96 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "time" +) + +func handleRoot(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + http.NotFound(w, r) + return + } + + progressMutex.Lock() + jobs := make(map[string]*ProgressInfo) + for k, v := range progress { + jobs[k] = v + } + progressMutex.Unlock() + + err := templates.ExecuteTemplate(w, "index", struct{ Jobs map[string]*ProgressInfo }{jobs}) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func handleUpload(w http.ResponseWriter, r *http.Request) { + file, header, err := r.FormFile("file") + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + defer file.Close() + + filename := fmt.Sprintf("%d_%s", time.Now().UnixNano(), header.Filename) + filepath := filepath.Join(uploadDir, filename) + + newFile, err := os.Create(filepath) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer newFile.Close() + + _, err = io.Copy(newFile, file) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + go func() { + err := processInputFile(filepath) + if err != nil { + fmt.Printf("Error processing file: %v\n", err) + } + + os.Remove(filepath) + }() + + http.Redirect(w, r, "/progress?filename="+filename, http.StatusSeeOther) +} + +func handleProgress(w http.ResponseWriter, r *http.Request) { + filename := r.URL.Query().Get("filename") + fmt.Printf("Handling progress request for filename: %s\n", filename) + + if r.Header.Get("Accept") == "application/json" { + progressInfo := getProgress(filename) + fmt.Printf("Progress info for %s: %+v\n", filename, progressInfo) + + if progressInfo == nil { + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(map[string]string{"error": "No progress information found"}) + return + } + + w.Header().Set("Content-Type", "application/json") + err := json.NewEncoder(w).Encode(progressInfo) + if err != nil { + fmt.Printf("Error encoding progress info: %v\n", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + return + } + + err := templates.ExecuteTemplate(w, "progress", struct{ Filename string }{filename}) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} diff --git a/main.go b/main.go index 5b1643d..74fcf39 100644 --- a/main.go +++ b/main.go @@ -1,26 +1,15 @@ package main import ( - "encoding/base64" - "encoding/json" "flag" "fmt" "html/template" - "io" "net/http" - "net/url" "os" - "os/exec" - "path/filepath" "strings" "sync" - "time" "embed" - "regexp" - - "github.com/BurntSushi/toml" - "github.com/asticode/go-astisub" ) type Item struct { @@ -43,15 +32,6 @@ type Metadata struct { Season string } -type Config struct { - BaseDir string - Format string - N_m3u8DLRE struct { - Path string - } -} - -var config Config var progressMutex sync.Mutex var progress = make(map[string]*ProgressInfo) @@ -76,25 +56,7 @@ func init() { } func main() { - configFile, err := os.Open("config.toml") - if err != nil { - fmt.Println("Error opening config file:", err) - return - } - defer configFile.Close() - - byteValue, _ := io.ReadAll(configFile) - - if _, err := toml.Decode(string(byteValue), &config); err != nil { - fmt.Println("Error decoding config file:", err) - return - } - - if config.N_m3u8DLRE.Path == "" { - fmt.Println("Error: N_m3u8DL-RE path is not specified in the config file") - return - } - + loadConfig() inputFile := flag.String("f", "", "Path to the input JSON file") flag.Parse() @@ -114,121 +76,6 @@ func startWebServer() { http.ListenAndServe(":8080", nil) } -func handleRoot(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/" { - http.NotFound(w, r) - return - } - - progressMutex.Lock() - jobs := make(map[string]*ProgressInfo) - for k, v := range progress { - jobs[k] = v - } - progressMutex.Unlock() - - err := templates.ExecuteTemplate(w, "index", struct{ Jobs map[string]*ProgressInfo }{jobs}) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } -} - -func handleUpload(w http.ResponseWriter, r *http.Request) { - file, header, err := r.FormFile("file") - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - defer file.Close() - - filename := fmt.Sprintf("%d_%s", time.Now().UnixNano(), header.Filename) - filepath := filepath.Join(uploadDir, filename) - - newFile, err := os.Create(filepath) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - defer newFile.Close() - - _, err = io.Copy(newFile, file) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - go func() { - err := processInputFile(filepath) - if err != nil { - fmt.Printf("Error processing file: %v\n", err) - } - - os.Remove(filepath) - }() - - http.Redirect(w, r, "/progress?filename="+filename, http.StatusSeeOther) -} - -func handleProgress(w http.ResponseWriter, r *http.Request) { - filename := r.URL.Query().Get("filename") - fmt.Printf("Handling progress request for filename: %s\n", filename) - - if r.Header.Get("Accept") == "application/json" { - progressInfo := getProgress(filename) - fmt.Printf("Progress info for %s: %+v\n", filename, progressInfo) - - if progressInfo == nil { - w.WriteHeader(http.StatusNotFound) - json.NewEncoder(w).Encode(map[string]string{"error": "No progress information found"}) - return - } - - w.Header().Set("Content-Type", "application/json") - err := json.NewEncoder(w).Encode(progressInfo) - if err != nil { - fmt.Printf("Error encoding progress info: %v\n", err) - http.Error(w, "Internal Server Error", http.StatusInternalServerError) - return - } - return - } - - err := templates.ExecuteTemplate(w, "progress", struct{ Filename string }{filename}) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } -} - -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) - } - - 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 updateProgress(filename string, value float64, currentFile string) { progressMutex.Lock() defer progressMutex.Unlock() @@ -260,152 +107,3 @@ func parseMetadata(metadata string) Metadata { Season: "S" + strings.TrimSpace(parts[2]), } } - -func getDownloadCommand(item Item, mpdPath string) string { - metadata := parseMetadata(item.Metadata) - keys := getKeys(item.Keys) - - command := fmt.Sprintf("%s %s", config.N_m3u8DLRE.Path, mpdPath) - - for _, key := range keys { - if key != "" { - command += fmt.Sprintf(" --key %s", key) - } - } - - command += " --auto-select" - - sanitizedFilename := sanitizeFilename(item.Filename) - - filename := fmt.Sprintf("\"%s\"", sanitizedFilename) - command += fmt.Sprintf(" --save-name %s", filename) - - command += fmt.Sprintf(" --mux-after-done format=%s", config.Format) - - saveDir := config.BaseDir - if metadata.Type == "serie" { - saveDir = filepath.Join(saveDir, "Series", metadata.Title, metadata.Season) - } else { - saveDir = filepath.Join(saveDir, "Movies", metadata.Title) - } - command += fmt.Sprintf(" --save-dir \"%s\"", saveDir) - - fmt.Println(command) - - return command -} - -func sanitizeFilename(filename string) string { - filename = regexp.MustCompile(`[<>:"/\\|?*]`).ReplaceAllString(filename, "_") - - filename = strings.Trim(filename, ".") - - return filename -} - -func downloadFile(item Item) error { - fmt.Println("Downloading:", item.Filename) - - mpdPath := item.MPD - if !isValidURL(item.MPD) { - - decodedMPD, err := base64.StdEncoding.DecodeString(item.MPD) - if err != nil { - return fmt.Errorf("error decoding base64 MPD: %v", err) - } - - tempFile, err := os.CreateTemp("", "temp_mpd_*.mpd") - if err != nil { - return fmt.Errorf("error creating temporary MPD file: %v", err) - } - defer os.Remove(tempFile.Name()) - - if _, err := tempFile.Write(decodedMPD); err != nil { - return fmt.Errorf("error writing to temporary MPD file: %v", err) - } - if err := tempFile.Close(); err != nil { - return fmt.Errorf("error closing temporary MPD file: %v", err) - } - - mpdPath = tempFile.Name() - } - - command := getDownloadCommand(item, mpdPath) - - subtitlePaths, err := downloadAndConvertSubtitles(item.Subtitles) - if err != nil { - fmt.Printf("Error processing subtitles: %v\n", err) - } - - for _, path := range subtitlePaths { - fmt.Println("Adding subtitle:", path) - command += fmt.Sprintf(" --mux-import \"path=%s:lang=nl:name=Nederlands\"", path) - } - - cmd := exec.Command("bash", "-c", command) - - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - err = cmd.Run() - if err != nil { - return fmt.Errorf("error executing download command: %v", err) - } - - fmt.Println("Download completed successfully") - return nil -} - -func downloadAndConvertSubtitles(subtitlesURLs string) ([]string, error) { - var subtitlePaths []string - urls := strings.Split(subtitlesURLs, ",") - - for _, url := range urls { - vttPath, err := downloadSubtitle(url) - if err != nil { - return nil, fmt.Errorf("error downloading subtitle: %v", err) - } - - srtPath, err := convertVTTtoSRT(vttPath) - if err != nil { - return nil, fmt.Errorf("error converting subtitle: %v", err) - } - - subtitlePaths = append(subtitlePaths, srtPath) - } - - return subtitlePaths, nil -} - -func downloadSubtitle(url string) (string, error) { - resp, err := http.Get(url) - if err != nil { - return "", err - } - defer resp.Body.Close() - - tempFile, err := os.CreateTemp("", "subtitle_*.vtt") - if err != nil { - return "", err - } - defer tempFile.Close() - - _, err = io.Copy(tempFile, resp.Body) - if err != nil { - return "", err - } - - return tempFile.Name(), nil -} - -func convertVTTtoSRT(vttPath string) (string, error) { - srtPath := strings.TrimSuffix(vttPath, ".vtt") + ".srt" - s1, _ := astisub.OpenFile(vttPath) - s1.Write(srtPath) - return srtPath, nil -} - -func isValidURL(toTest string) bool { - _, err := url.ParseRequestURI(toTest) - return err == nil -} diff --git a/subtitles.go b/subtitles.go new file mode 100644 index 0000000..c30bd81 --- /dev/null +++ b/subtitles.go @@ -0,0 +1,60 @@ +package main + +import ( + "fmt" + "io" + "net/http" + "os" + "strings" + + "github.com/asticode/go-astisub" +) + +func downloadAndConvertSubtitles(subtitlesURLs string) ([]string, error) { + var subtitlePaths []string + urls := strings.Split(subtitlesURLs, ",") + + for _, url := range urls { + vttPath, err := downloadSubtitle(url) + if err != nil { + return nil, fmt.Errorf("error downloading subtitle: %v", err) + } + + srtPath, err := convertVTTtoSRT(vttPath) + if err != nil { + return nil, fmt.Errorf("error converting subtitle: %v", err) + } + + subtitlePaths = append(subtitlePaths, srtPath) + } + + return subtitlePaths, nil +} + +func downloadSubtitle(url string) (string, error) { + resp, err := http.Get(url) + if err != nil { + return "", err + } + defer resp.Body.Close() + + tempFile, err := os.CreateTemp("", "subtitle_*.vtt") + if err != nil { + return "", err + } + defer tempFile.Close() + + _, err = io.Copy(tempFile, resp.Body) + if err != nil { + return "", err + } + + return tempFile.Name(), nil +} + +func convertVTTtoSRT(vttPath string) (string, error) { + srtPath := strings.TrimSuffix(vttPath, ".vtt") + ".srt" + s1, _ := astisub.OpenFile(vttPath) + s1.Write(srtPath) + return srtPath, nil +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..f6c0c80 --- /dev/null +++ b/utils.go @@ -0,0 +1,20 @@ +package main + +import ( + "net/url" + "regexp" + "strings" +) + +func sanitizeFilename(filename string) string { + filename = regexp.MustCompile(`[<>:"/\\|?*]`).ReplaceAllString(filename, "_") + + filename = strings.Trim(filename, ".") + + return filename +} + +func isValidURL(toTest string) bool { + _, err := url.ParseRequestURI(toTest) + return err == nil +}