336 lines
7.4 KiB
Go
336 lines
7.4 KiB
Go
|
package main
|
||
|
|
||
|
import (
|
||
|
"encoding/base64"
|
||
|
"encoding/json"
|
||
|
"flag"
|
||
|
"fmt"
|
||
|
"html/template"
|
||
|
"io"
|
||
|
"net/http"
|
||
|
"net/url"
|
||
|
"os"
|
||
|
"os/exec"
|
||
|
"path/filepath"
|
||
|
"strings"
|
||
|
"sync"
|
||
|
"time"
|
||
|
|
||
|
"github.com/BurntSushi/toml"
|
||
|
)
|
||
|
|
||
|
type Item struct {
|
||
|
MPD string
|
||
|
Keys string
|
||
|
Filename string
|
||
|
Description string
|
||
|
Subtitles string
|
||
|
Poster string
|
||
|
Metadata string
|
||
|
}
|
||
|
|
||
|
type Items struct {
|
||
|
Items []Item
|
||
|
}
|
||
|
|
||
|
type Metadata struct {
|
||
|
Title string
|
||
|
Type string
|
||
|
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)
|
||
|
|
||
|
const uploadDir = "uploads"
|
||
|
|
||
|
type ProgressInfo struct {
|
||
|
Percentage float64
|
||
|
CurrentFile string
|
||
|
}
|
||
|
|
||
|
var templates *template.Template
|
||
|
|
||
|
func init() {
|
||
|
if err := os.MkdirAll(uploadDir, 0755); err != nil {
|
||
|
fmt.Printf("Error creating upload directory: %v\n", err)
|
||
|
}
|
||
|
|
||
|
templates = template.Must(template.ParseGlob("templates/*"))
|
||
|
}
|
||
|
|
||
|
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
|
||
|
}
|
||
|
|
||
|
inputFile := flag.String("f", "", "Path to the input JSON file")
|
||
|
flag.Parse()
|
||
|
|
||
|
if *inputFile == "" {
|
||
|
startWebServer()
|
||
|
} else {
|
||
|
processInputFile(*inputFile)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func startWebServer() {
|
||
|
http.HandleFunc("/", handleRoot)
|
||
|
http.HandleFunc("/upload", handleUpload)
|
||
|
http.HandleFunc("/progress", handleProgress)
|
||
|
|
||
|
fmt.Println("Starting web server on http://0.0.0.0:8080")
|
||
|
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()
|
||
|
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 {
|
||
|
progressMutex.Lock()
|
||
|
defer progressMutex.Unlock()
|
||
|
return progress[filename]
|
||
|
}
|
||
|
|
||
|
func getKeys(keys string) []string {
|
||
|
return strings.Split(keys, ",")
|
||
|
}
|
||
|
|
||
|
func parseMetadata(metadata string) Metadata {
|
||
|
parts := strings.Split(metadata, ";")
|
||
|
if len(parts) != 3 {
|
||
|
return Metadata{}
|
||
|
}
|
||
|
return Metadata{
|
||
|
Title: strings.TrimSpace(parts[0]),
|
||
|
Type: strings.TrimSpace(parts[1]),
|
||
|
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"
|
||
|
|
||
|
filename := fmt.Sprintf("\"%s\"", item.Filename)
|
||
|
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 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)
|
||
|
|
||
|
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 isValidURL(toTest string) bool {
|
||
|
_, err := url.ParseRequestURI(toTest)
|
||
|
return err == nil
|
||
|
}
|