26 Commits

Author SHA1 Message Date
3fda737af2 Merge branch 'main' into speedLimiter 2024-12-30 16:47:21 +01:00
8cf3d4dda8 Show limit 2024-12-30 16:46:30 +01:00
2e18921a27 Show limit 2024-12-30 16:45:56 +01:00
f1efb1d67c impl speed limit 2024-12-30 16:32:12 +01:00
457ede5b62 Speed 2024-12-30 16:20:48 +01:00
7eb724d01f Speed 2024-12-30 16:16:51 +01:00
189bbb0874 Speed 2024-12-30 16:16:37 +01:00
68da5f9658 Speed 2024-12-30 16:16:21 +01:00
83cd0b722b style 2024-12-30 16:04:37 +01:00
ca176e1a76 Update README.md 2024-10-07 13:02:40 +02:00
54656f2630 Update README.md 2024-10-07 13:02:17 +02:00
f38b0c69d9 Merge pull request 'Poller' (#9) from Poller into main
Reviewed-on: #9
2024-10-07 12:59:18 +02:00
b1ba08933a Console should also beable to be controlled by env var 2024-10-07 12:48:37 +02:00
a049610291 Implement polling, update readme 2024-10-07 12:46:38 +02:00
c46538a55f Change the config paths according to new layout 2024-10-07 12:46:26 +02:00
fe6b7c78f6 Add options for polling, path validation, env variables 2024-10-07 12:45:49 +02:00
f9c2ac64d7 Merge pull request 'watchFolder' (#8) from watchFolder into main
Reviewed-on: #8
2024-10-06 22:45:52 +02:00
1f42b2a877 Update README.md 2024-10-06 22:45:33 +02:00
e03226a7ee Make sure the file is fully written 2024-10-06 00:37:47 +02:00
f1015ab62e Change watched file name 2024-10-06 00:14:46 +02:00
acf172933d Watcher 2024-10-06 00:09:43 +02:00
99f75f1cd1 Change the wss if https 2024-09-25 15:47:39 +02:00
c7712982f3 Merge pull request 'Implement a live console of the downloader on the progress page' (#6) from implConsole into main
Reviewed-on: #6
2024-09-24 20:40:17 +02:00
bf78384fa8 Update readme 2024-09-24 20:33:20 +02:00
7445627f7e Add option to disable the console broadcasts 2024-09-24 15:17:58 +02:00
cc28f0f3c2 Correctly split console for each process 2024-09-24 15:01:09 +02:00
12 changed files with 573 additions and 38 deletions

View File

@ -7,15 +7,51 @@ drmdtool is a utility for processing .drmd files using N_m3u8DL-RE.
Create a `config.toml` file in the same directory as the drmdtool executable: Create a `config.toml` file in the same directory as the drmdtool executable:
```toml ```toml
[General]
BaseDir = "/path/to/save/downloads" BaseDir = "/path/to/save/downloads"
Format = "mkv" Format = "mkv"
TempBaseDir = "/tmp/nre" TempBaseDir = "/tmp/nre"
EnableConsole = true
[N_m3u8DL-RE] [WatchFolder]
Path = "/path/to/watched/folder"
PollingInterval = 10
UsePolling = true
UseInotify = false
[N_m3u8DLRE]
Path = "/path/to/N_m3u8DL-RE" Path = "/path/to/N_m3u8DL-RE"
``` ```
Adjust the paths and format as needed. (mkv, mp4) ### Configuration Options
- **General**
- `BaseDir`: Directory where downloaded files will be saved.
- `Format`: Output format for the downloaded files (e.g., `mkv`, `mp4`).
- `TempBaseDir`: Temporary directory for intermediate files.
- `EnableConsole`: Boolean to enable or disable console output.
- **WatchFolder**
- `Path`: Directory to watch for new `.drmd` files.
- `PollingInterval`: Interval in seconds for polling the watch folder.
- `UsePolling`: Boolean to enable or disable folder polling.
- `UseInotify`: Boolean to enable or disable inotify for file watching.
- **N_m3u8DLRE**
- `Path`: Path to the N_m3u8DL-RE executable.
### Environment Variable Overrides
You can override the configuration options using environment variables. The following environment variables are supported:
- `BASE_DIR`: Overrides `General.BaseDir`
- `FORMAT`: Overrides `General.Format`
- `TEMP_BASE_DIR`: Overrides `General.TempBaseDir`
- `ENABLE_CONSOLE`: Overrides `General.EnableConsole` (set to `true` or `false`)
- `WATCHED_FOLDER`: Overrides `WatchFolder.Path`
- `USE_POLLING`: Overrides `WatchFolder.UsePolling` (set to `true` or `false`)
- `USE_INOTIFY`: Overrides `WatchFolder.UseInotify` (set to `true` or `false`)
- `POLLING_INTERVAL`: Overrides `WatchFolder.PollingInterval`
## Web UI Usage ## Web UI Usage
@ -28,7 +64,6 @@ Adjust the paths and format as needed. (mkv, mp4)
3. Use the interface to upload .drmd files and monitor download progress 3. Use the interface to upload .drmd files and monitor download progress
## CLI Usage ## CLI Usage
To process a file directly from the command line: To process a file directly from the command line:
@ -39,7 +74,6 @@ 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.
# Previews # Previews
## Index Page ## Index Page

View File

@ -1,6 +1,14 @@
BaseDir = "/mnt/media" [General]
Format = "mkv" BaseDir = "/mnt/Media"
Format = "mkv"
TempBaseDir = "/tmp/nre" TempBaseDir = "/tmp/nre"
EnableConsole = true
[WatchFolder]
Path = "/mnt/Watched"
PollingInterval = 10
UsePolling = false
UseInotify = false
[N_m3u8DLRE] [N_m3u8DLRE]
Path = "nre" Path = "nre"

View File

@ -4,15 +4,26 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"strconv"
"strings"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
) )
type Config struct { type Config struct {
BaseDir string General struct {
Format string BaseDir string
TempBaseDir string Format string
N_m3u8DLRE struct { TempBaseDir string
EnableConsole bool
}
WatchFolder struct {
Path string
UsePolling bool
UseInotify bool
PollingInterval int
}
N_m3u8DLRE struct {
Path string Path string
} }
} }
@ -22,21 +33,112 @@ var config Config
func loadConfig() { func loadConfig() {
configFile, err := os.Open("config.toml") configFile, err := os.Open("config.toml")
if err != nil { if err != nil {
fmt.Println("Error opening config file:", err) logger.LogError("Config", fmt.Sprintf("Error opening config file: %v", err))
return os.Exit(1)
} }
defer configFile.Close() defer configFile.Close()
byteValue, _ := io.ReadAll(configFile) byteValue, _ := io.ReadAll(configFile)
if _, err := toml.Decode(string(byteValue), &config); err != nil { if _, err := toml.Decode(string(byteValue), &config); err != nil {
fmt.Println("Error decoding config file:", err) logger.LogError("Config", fmt.Sprintf("Error decoding config file: %v", err))
return os.Exit(1)
} }
if config.N_m3u8DLRE.Path == "" { overrideConfigWithEnv()
fmt.Println("Error: N_m3u8DL-RE path is not specified in the config file")
return if err := validatePaths(); err != nil {
logger.LogError("Config", fmt.Sprintf("Configuration error: %v", err))
os.Exit(1)
} }
if config.WatchFolder.PollingInterval <= 0 {
config.WatchFolder.PollingInterval = 10
}
logConfig()
}
func overrideConfigWithEnv() {
if envBaseDir := os.Getenv("BASE_DIR"); envBaseDir != "" {
config.General.BaseDir = envBaseDir
}
if envFormat := os.Getenv("FORMAT"); envFormat != "" {
config.General.Format = envFormat
}
if envTempBaseDir := os.Getenv("TEMP_BASE_DIR"); envTempBaseDir != "" {
config.General.TempBaseDir = envTempBaseDir
}
if envEnableConsole := os.Getenv("ENABLE_CONSOLE"); envEnableConsole != "" {
config.General.EnableConsole = strings.ToLower(envEnableConsole) == "true"
}
if envWatchedFolder := os.Getenv("WATCHED_FOLDER"); envWatchedFolder != "" {
config.WatchFolder.Path = envWatchedFolder
}
if envUsePolling := os.Getenv("USE_POLLING"); envUsePolling != "" {
config.WatchFolder.UsePolling = strings.ToLower(envUsePolling) == "true"
}
if envUseInotify := os.Getenv("USE_INOTIFY"); envUseInotify != "" {
config.WatchFolder.UseInotify = strings.ToLower(envUseInotify) == "true"
}
if envPollingInterval := os.Getenv("POLLING_INTERVAL"); envPollingInterval != "" {
if interval, err := strconv.Atoi(envPollingInterval); err == nil {
config.WatchFolder.PollingInterval = interval
}
}
}
func validatePaths() error {
paths := []struct {
name string
path string
}{
{"BaseDir", config.General.BaseDir},
}
for _, p := range paths {
if p.path == "" {
return fmt.Errorf("%s is not specified", p.name)
}
if _, err := os.Stat(p.path); os.IsNotExist(err) {
return fmt.Errorf("%s does not exist: %s", p.name, p.path)
} else if err != nil {
return fmt.Errorf("error accessing %s: %v", p.name, err)
}
}
if config.WatchFolder.UsePolling || config.WatchFolder.UseInotify {
if config.WatchFolder.Path == "" {
return fmt.Errorf("WatchedFolder is not specified")
}
if _, err := os.Stat(config.WatchFolder.Path); os.IsNotExist(err) {
return fmt.Errorf("WatchedFolder does not exist: %s", config.WatchFolder.Path)
} else if err != nil {
return fmt.Errorf("error accessing WatchedFolder: %v", err)
}
}
return nil
}
func logConfig() {
configInfo := fmt.Sprintf(`
Configuration Loaded:
General:
BaseDir: %s
Format: %s
TempBaseDir: %s
EnableConsole: %t
WatchFolder:
Path: %s
UsePolling: %t
UseInotify: %t
PollingInterval: %d
N_m3u8DLRE:
Path: %s
`, config.General.BaseDir, config.General.Format, config.General.TempBaseDir, config.General.EnableConsole,
config.WatchFolder.Path, config.WatchFolder.UsePolling, config.WatchFolder.UseInotify, config.WatchFolder.PollingInterval,
config.N_m3u8DLRE.Path)
logger.LogInfo("Config", configInfo)
} }

View File

@ -20,10 +20,10 @@ func removeBOM(input []byte) []byte {
return input return input
} }
func downloadFile(item Item, jobInfo *JobInfo) error { func downloadFile(drmdFilename string, item Item, jobInfo *JobInfo) error {
logger.LogInfo("Download File", fmt.Sprintf("Starting download for: %s", item.Filename)) logger.LogInfo("Download File", fmt.Sprintf("Starting download for: %s", item.Filename))
tempDir := filepath.Join(config.TempBaseDir, sanitizeFilename(item.Filename)) tempDir := filepath.Join(config.General.TempBaseDir, sanitizeFilename(item.Filename))
err := os.MkdirAll(tempDir, 0755) err := os.MkdirAll(tempDir, 0755)
if err != nil { if err != nil {
logger.LogError("Download File", fmt.Sprintf("Error creating temporary directory: %v", err)) logger.LogError("Download File", fmt.Sprintf("Error creating temporary directory: %v", err))
@ -131,7 +131,10 @@ func downloadFile(item Item, jobInfo *JobInfo) error {
go func() { go func() {
for { for {
if outputBuffer.Len() > 0 { if outputBuffer.Len() > 0 {
broadcast(outputBuffer.Bytes()) message := outputBuffer.Bytes()
if config.General.EnableConsole {
broadcast(drmdFilename, message)
}
outputBuffer.Reset() outputBuffer.Reset()
} }
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
@ -180,9 +183,9 @@ func getDownloadCommand(item Item, mpdPath string, tempDir string) string {
filename := fmt.Sprintf("\"%s\"", sanitizedFilename) filename := fmt.Sprintf("\"%s\"", sanitizedFilename)
command += fmt.Sprintf(" --save-name %s", filename) command += fmt.Sprintf(" --save-name %s", filename)
command += fmt.Sprintf(" --mux-after-done format=%s", config.Format) command += fmt.Sprintf(" --mux-after-done format=%s", config.General.Format)
saveDir := config.BaseDir saveDir := config.General.BaseDir
if metadata.Type == "serie" { if metadata.Type == "serie" {
saveDir = filepath.Join(saveDir, "Series", metadata.Title, metadata.Season) saveDir = filepath.Join(saveDir, "Series", metadata.Title, metadata.Season)
} else { } else {
@ -192,6 +195,10 @@ func getDownloadCommand(item Item, mpdPath string, tempDir string) string {
command += fmt.Sprintf(" --tmp-dir \"%s\"", tempDir) command += fmt.Sprintf(" --tmp-dir \"%s\"", tempDir)
if globalSpeedLimit != "" {
command += fmt.Sprintf(" -R %s", globalSpeedLimit)
}
fmt.Println(command) fmt.Println(command)
return command return command

View File

@ -8,9 +8,12 @@ require (
github.com/beevik/etree v1.4.1 github.com/beevik/etree v1.4.1
) )
require golang.org/x/sys v0.4.0 // indirect
require ( require (
github.com/asticode/go-astikit v0.20.0 // indirect github.com/asticode/go-astikit v0.20.0 // indirect
github.com/asticode/go-astits v1.8.0 // indirect github.com/asticode/go-astits v1.8.0 // indirect
github.com/fsnotify/fsnotify v1.7.0
github.com/gorilla/websocket v1.5.3 github.com/gorilla/websocket v1.5.3
golang.org/x/net v0.0.0-20200904194848-62affa334b73 // indirect golang.org/x/net v0.0.0-20200904194848-62affa334b73 // indirect
golang.org/x/text v0.3.2 // indirect golang.org/x/text v0.3.2 // indirect

View File

@ -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/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 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/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 h1:EN123kAtAAE2pg/+TvBsUBZfHCWNNFyL2ZBPPfNWAc0=
@ -28,6 +30,8 @@ golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=

View File

@ -48,7 +48,11 @@ func handleRoot(w http.ResponseWriter, r *http.Request) {
CurrentFile string CurrentFile string
Paused bool Paused bool
} }
}{jobsInfo}) GlobalSpeedLimit string
}{
Jobs: jobsInfo,
GlobalSpeedLimit: globalSpeedLimit,
})
if err != nil { if err != nil {
logger.LogError("Handle Root", fmt.Sprintf("Error executing template: %v", err)) logger.LogError("Handle Root", fmt.Sprintf("Error executing template: %v", err))
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -382,19 +386,36 @@ func updateProgress(filename string, value float64, currentFile string) {
} }
var upgrader = websocket.Upgrader{} var upgrader = websocket.Upgrader{}
var clients = make(map[*websocket.Conn]bool) var clients = make(map[string]map[*websocket.Conn]bool)
var mu sync.Mutex var mu sync.Mutex
func handleWebSocket(w http.ResponseWriter, r *http.Request) { func handleWebSocket(w http.ResponseWriter, r *http.Request) {
fmt.Println(config.General.EnableConsole)
if !config.General.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) conn, err := upgrader.Upgrade(w, r, nil)
if err != nil { if err != nil {
fmt.Println("Error while upgrading connection:", err) logger.LogError("WebSocket", fmt.Sprintf("Error while upgrading connection: %v", err))
return return
} }
defer conn.Close() defer conn.Close()
logger.LogInfo("WebSocket", fmt.Sprintf("WebSocket connection established for filename: %s", filename))
mu.Lock() mu.Lock()
clients[conn] = true if clients[filename] == nil {
clients[filename] = make(map[*websocket.Conn]bool)
}
clients[filename][conn] = true
mu.Unlock() mu.Unlock()
for { for {
@ -404,18 +425,54 @@ func handleWebSocket(w http.ResponseWriter, r *http.Request) {
} }
mu.Lock() mu.Lock()
delete(clients, conn) delete(clients[filename], conn)
mu.Unlock() mu.Unlock()
logger.LogInfo("WebSocket", fmt.Sprintf("WebSocket connection closed for filename: %s", filename))
} }
func broadcast(message []byte) { func broadcast(filename string, message []byte) {
if !config.General.EnableConsole {
return
}
mu.Lock() mu.Lock()
defer mu.Unlock() defer mu.Unlock()
for client := range clients { for client := range clients[filename] {
if err := client.WriteMessage(websocket.TextMessage, message); err != nil { if err := client.WriteMessage(websocket.TextMessage, message); err != nil {
client.Close() client.Close()
delete(clients, client) delete(clients[filename], client)
logger.LogError("Broadcast", fmt.Sprintf("Error writing message to client: %v", err))
} }
} }
} }
func handleSetSpeedLimit(w http.ResponseWriter, r *http.Request) {
logger.LogInfo("Set Speed Limit", "Received request to set speed limit")
if r.Method != http.MethodPost {
logger.LogError("Set Speed Limit", "Invalid method")
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var requestData struct {
SpeedLimit string `json:"speedLimit"`
}
if err := json.NewDecoder(r.Body).Decode(&requestData); err != nil {
logger.LogError("Set Speed Limit", "Invalid request body")
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
if requestData.SpeedLimit == "unlimited" {
globalSpeedLimit = ""
} else {
globalSpeedLimit = requestData.SpeedLimit
}
logger.LogInfo("Set Speed Limit", fmt.Sprintf("Global speed limit set to: %s", globalSpeedLimit))
w.WriteHeader(http.StatusOK)
}

View File

@ -44,6 +44,8 @@ var templates *template.Template
//go:embed templates //go:embed templates
var templateFS embed.FS var templateFS embed.FS
var globalSpeedLimit string
func init() { func init() {
if err := os.MkdirAll(uploadDir, 0755); err != nil { if err := os.MkdirAll(uploadDir, 0755); err != nil {
fmt.Printf("Error creating upload directory: %v\n", err) fmt.Printf("Error creating upload directory: %v\n", err)
@ -60,6 +62,7 @@ func main() {
flag.Parse() flag.Parse()
if *inputFile == "" { if *inputFile == "" {
go watchFolder()
startWebServer() startWebServer()
} else { } else {
items, err := parseInputFile(*inputFile) items, err := parseInputFile(*inputFile)
@ -69,6 +72,8 @@ func main() {
} }
processItems(*inputFile, items) processItems(*inputFile, items)
} }
http.HandleFunc("/set-speed-limit", handleSetSpeedLimit)
} }
func startWebServer() { func startWebServer() {
@ -82,8 +87,9 @@ func startWebServer() {
http.HandleFunc("/resume", handleResume) http.HandleFunc("/resume", handleResume)
http.HandleFunc("/clear-completed", handleClearCompleted) http.HandleFunc("/clear-completed", handleClearCompleted)
http.HandleFunc("/ws", handleWebSocket) http.HandleFunc("/ws", handleWebSocket)
http.HandleFunc("/set-speed-limit", handleSetSpeedLimit)
fmt.Println("Starting web server on http://0.0.0.0:8080") logger.LogInfo("Main", "Starting web server on http://0.0.0.0:8080")
http.ListenAndServe(":8080", nil) http.ListenAndServe(":8080", nil)
} }

View File

@ -28,7 +28,6 @@
input[type="file"], input[type="submit"] { input[type="file"], input[type="submit"] {
background-color: #2d2d2d; background-color: #2d2d2d;
color: #d4d4d4; color: #d4d4d4;
border: 1px solid #444;
padding: 8px 12px; padding: 8px 12px;
border-radius: 4px; border-radius: 4px;
margin-bottom: 10px; margin-bottom: 10px;
@ -77,6 +76,11 @@
.paused { .paused {
color: #ffa500; color: #ffa500;
} }
.speed-limit {
font-size: 1em;
color: #a0a0a0;
margin-top: 10px;
}
@media (max-width: 600px) { @media (max-width: 600px) {
body { body {
padding: 10px; padding: 10px;
@ -107,6 +111,94 @@
#clear-completed:hover { #clear-completed:hover {
background-color: #d32f2f; background-color: #d32f2f;
} }
/* New CSS for speed limit form */
.settings-section {
margin-top: 30px;
}
.speed-limit-form {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 20px;
}
.speed-limit-form .form-group {
display: flex;
align-items: center;
gap: 10px;
}
.speed-limit-form input[type="number"],
.speed-limit-form select,
.speed-limit-form button {
background-color: #2d2d2d;
color: #d4d4d4;
border: 1px solid #444;
padding: 8px 12px;
border-radius: 4px;
}
.speed-limit-form button {
cursor: pointer;
background-color: #4CAF50;
color: white;
}
.speed-limit-form button:hover {
background-color: #45a049;
}
.speed-limit-container {
display: flex;
align-items: center;
margin-bottom: 20px;
background-color: #2d2d2d;
padding: 8px 12px;
border-radius: 4px;
}
.speed-limit-container .form-group {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
}
.speed-limit-container input[type="number"] {
background-color: #2d2d2d;
color: #d4d4d4;
border: 1px solid #444;
padding: 8px 12px;
border-radius: 4px;
height: 40px;
box-sizing: border-box;
flex-grow: 1;
}
.speed-limit-container select,
.speed-limit-container button {
background-color: #2d2d2d;
color: #d4d4d4;
border: 1px solid #444;
padding: 8px 12px;
border-radius: 4px;
height: 40px;
box-sizing: border-box;
}
.speed-limit-container button {
cursor: pointer;
background-color: #4CAF50;
color: white;
}
.speed-limit-container button:hover {
background-color: #45a049;
}
.speed-limit-container .speed-limit {
color: #d4d4d4;
margin-left: auto;
display: flex;
align-items: center;
}
.speed-limit-container .speed-limit span {
margin-left: 5px;
}
.current-speed-limit {
color: #d4d4d4;
margin-top: 10px;
}
</style> </style>
</head> </head>
<body> <body>
@ -135,6 +227,23 @@
{{end}} {{end}}
</ul> </ul>
<button id="clear-completed" onclick="clearCompleted()">Clear Completed Jobs</button> <button id="clear-completed" onclick="clearCompleted()">Clear Completed Jobs</button>
<div class="settings-section">
<h2>Settings</h2>
<div class="speed-limit-container">
<div class="form-group">
<label for="speedLimitValue">Speed Limit:</label>
<input type="number" id="speedLimitValue" name="speedLimitValue" min="0" step="0.01" required>
<select id="speedLimitUnit" name="speedLimitUnit">
<option value="GBps">GBps</option>
<option value="MBps" selected>MBps</option>
<option value="KBps">KBps</option>
</select>
<button type="button" onclick="updateSpeedLimit(event)">Set Limit</button>
</div>
</div>
</div>
<script> <script>
function clearCompleted() { function clearCompleted() {
fetch('/clear-completed', { method: 'POST' }) fetch('/clear-completed', { method: 'POST' })
@ -147,6 +256,54 @@
} }
}); });
} }
function updateSpeedLimit(event) {
event.preventDefault();
const speedLimitValue = document.getElementById('speedLimitValue').value;
const speedLimitUnit = document.getElementById('speedLimitUnit').value;
const speedLimit = speedLimitValue === "0" ? "unlimited" : speedLimitValue + speedLimitUnit;
if (!validateSpeedLimit(speedLimitValue)) {
alert('Please enter a valid speed limit.');
return;
}
fetch('/set-speed-limit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ speedLimit }),
}).then(response => {
if (response.ok) {
alert('Speed limit updated successfully');
document.getElementById('currentSpeedLimit').textContent = speedLimit;
} else {
alert('Failed to update speed limit');
}
});
}
function validateSpeedLimit(value) {
const number = parseFloat(value);
return !isNaN(number) && number >= 0;
}
document.addEventListener('DOMContentLoaded', function() {
const currentSpeedLimit = "{{if .GlobalSpeedLimit}}{{.GlobalSpeedLimit}}{{else}}0{{end}}";
const speedLimitValueInput = document.getElementById('speedLimitValue');
const speedLimitUnitSelect = document.getElementById('speedLimitUnit');
const match = currentSpeedLimit.match(/(\d+(\.\d+)?)([A-Za-z]+)/);
if (match) {
speedLimitValueInput.value = match[1];
speedLimitUnitSelect.value = match[3];
} else {
speedLimitValueInput.value = "0";
speedLimitUnitSelect.value = "MBps";
}
});
</script> </script>
</body> </body>
</html> </html>

View File

@ -145,9 +145,10 @@
<div style="display: none;" id="console"></div> <div style="display: none;" id="console"></div>
<script> <script>
let isPaused = false; let isPaused = false;
const filename = "{{.Filename}}";
function updateProgress() { function updateProgress() {
fetch('/progress?filename={{.Filename}}', { fetch(`/progress?filename=${filename}`, {
headers: { headers: {
'Accept': 'application/json' 'Accept': 'application/json'
} }
@ -179,7 +180,7 @@
} }
function abortDownload() { function abortDownload() {
fetch('/abort?filename={{.Filename}}', { method: 'POST' }) fetch(`/abort?filename=${filename}`, { method: 'POST' })
.then(response => { .then(response => {
if (response.ok) { if (response.ok) {
console.log('Abort signal sent. The download will stop soon.'); console.log('Abort signal sent. The download will stop soon.');
@ -190,7 +191,7 @@
} }
function pauseDownload() { function pauseDownload() {
fetch('/pause?filename={{.Filename}}', { method: 'POST' }) fetch(`/pause?filename=${filename}`, { method: 'POST' })
.then(response => { .then(response => {
if (response.ok) { if (response.ok) {
console.log('Pause signal sent. The download will pause soon.'); console.log('Pause signal sent. The download will pause soon.');
@ -203,7 +204,7 @@
} }
function resumeDownload() { function resumeDownload() {
fetch('/resume?filename={{.Filename}}', { method: 'POST' }) fetch(`/resume?filename=${filename}`, { method: 'POST' })
.then(response => { .then(response => {
if (response.ok) { if (response.ok) {
console.log('Resume signal sent. The download will resume soon.'); console.log('Resume signal sent. The download will resume soon.');
@ -217,13 +218,22 @@
} }
const consoleDiv = document.getElementById('console'); const consoleDiv = document.getElementById('console');
const ws = new WebSocket(`ws://${window.location.host}/ws`); const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const ws = new WebSocket(`${protocol}//${window.location.host}/ws?filename=${filename}`);
ws.onmessage = function(event) { ws.onmessage = function(event) {
consoleDiv.textContent += event.data; consoleDiv.textContent += event.data;
consoleDiv.scrollTop = consoleDiv.scrollHeight; consoleDiv.scrollTop = consoleDiv.scrollHeight;
}; };
ws.onclose = function() {
console.log('WebSocket connection closed');
};
ws.onerror = function(error) {
console.error('WebSocket error:', error);
};
document.getElementById('toggle-console').onclick = function() { document.getElementById('toggle-console').onclick = function() {
if (consoleDiv.style.display === "none") { if (consoleDiv.style.display === "none") {
consoleDiv.style.display = "block"; consoleDiv.style.display = "block";

View File

@ -259,21 +259,25 @@ func processItems(filename string, items []Item) error {
select { select {
case <-jobInfo.AbortChan: case <-jobInfo.AbortChan:
updateProgress(filename, 100, "Aborted") updateProgress(filename, 100, "Aborted")
logger.LogJobState(filename, "aborted")
return fmt.Errorf("download aborted") return fmt.Errorf("download aborted")
default: default:
if jobInfo.Paused { if jobInfo.Paused {
select { select {
case <-jobInfo.ResumeChan: case <-jobInfo.ResumeChan:
jobInfo.Paused = false jobInfo.Paused = false
logger.LogJobState(filename, "resumed")
case <-jobInfo.AbortChan: case <-jobInfo.AbortChan:
updateProgress(filename, 100, "Aborted") updateProgress(filename, 100, "Aborted")
logger.LogJobState(filename, "aborted")
return fmt.Errorf("download aborted") return fmt.Errorf("download aborted")
} }
} }
updateProgress(filename, float64(i)/float64(len(items))*100, items[i].Filename) 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 != nil {
if err.Error() == "download paused" { if err.Error() == "download paused" {
logger.LogJobState(filename, "paused")
removeCompletedEpisodes(filename, items[:i]) removeCompletedEpisodes(filename, items[:i])
i-- i--
continue continue
@ -282,6 +286,7 @@ func processItems(filename string, items []Item) error {
} }
} }
updateProgress(filename, 100, "") updateProgress(filename, 100, "")
logger.LogJobState(filename, "completed successfully")
return nil return nil
} }

142
src/watcher.go Normal file
View File

@ -0,0 +1,142 @@
package main
import (
"fmt"
"io"
"log"
"os"
"path/filepath"
"strings"
"time"
"github.com/fsnotify/fsnotify"
)
func watchFolder() {
if config.WatchFolder.UsePolling {
go pollFolder()
}
if config.WatchFolder.UseInotify {
go inotifyWatch()
}
}
func inotifyWatch() {
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
defer watcher.Close()
done := make(chan bool)
go func() {
for {
select {
case event, ok := <-watcher.Events:
if !ok {
return
}
if event.Op&fsnotify.Create == fsnotify.Create {
if strings.HasSuffix(event.Name, ".drmd") {
fmt.Println("New .drmd detected:", event.Name)
processWatchedFile(event.Name)
}
}
case err, ok := <-watcher.Errors:
if !ok {
return
}
log.Println("Error:", err)
}
}
}()
err = watcher.Add(config.WatchFolder.Path)
if err != nil {
log.Fatal(err)
}
<-done
}
func pollFolder() {
ticker := time.NewTicker(time.Duration(config.WatchFolder.PollingInterval) * time.Second)
defer ticker.Stop()
for range ticker.C {
files, err := filepath.Glob(filepath.Join(config.WatchFolder.Path, "*.drmd"))
if err != nil {
log.Println("Error polling folder:", err)
continue
}
for _, file := range files {
fmt.Println("New .drmd detected via polling:", file)
go processWatchedFile(file)
}
}
}
func processWatchedFile(filePath string) {
for {
initialSize, err := getFileSize(filePath)
if err != nil {
logger.LogError("Watcher", fmt.Sprintf("Error getting file size: %v", err))
return
}
time.Sleep(1 * time.Second)
currentSize, err := getFileSize(filePath)
if err != nil {
logger.LogError("Watcher", fmt.Sprintf("Error getting file size: %v", err))
return
}
if initialSize == currentSize {
break
}
}
file, err := os.Open(filePath)
if err != nil {
logger.LogError("Watcher", fmt.Sprintf("Error opening file: %v", err))
return
}
defer file.Close()
originalFilename := filepath.Base(filePath)
tempFile, err := os.CreateTemp(uploadDir, originalFilename)
if err != nil {
logger.LogError("Watcher", fmt.Sprintf("Error creating temporary file: %v", err))
return
}
defer tempFile.Close()
_, err = io.Copy(tempFile, file)
if err != nil {
logger.LogError("Watcher", fmt.Sprintf("Error copying file: %v", err))
return
}
if err := os.Remove(filePath); err != nil {
logger.LogError("Watcher", fmt.Sprintf("Error deleting original file: %v", err))
}
items, err := parseInputFile(tempFile.Name())
if err != nil {
logger.LogError("Watcher", fmt.Sprintf("Error parsing input file: %v", err))
return
}
go processItems(filepath.Base(tempFile.Name()), items)
}
func getFileSize(filePath string) (int64, error) {
fileInfo, err := os.Stat(filePath)
if err != nil {
return 0, err
}
return fileInfo.Size(), nil
}