8 Commits

7 changed files with 243 additions and 110 deletions

View File

@ -9,6 +9,7 @@ Create a `config.toml` file in the same directory as the drmdtool executable:
```toml ```toml
BaseDir = "/path/to/save/downloads" BaseDir = "/path/to/save/downloads"
Format = "mkv" Format = "mkv"
TempBaseDir = "/tmp/nre"
[N_m3u8DL-RE] [N_m3u8DL-RE]
Path = "/path/to/N_m3u8DL-RE" Path = "/path/to/N_m3u8DL-RE"
@ -37,9 +38,3 @@ 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.
## TODO
- ~~Filename Sanitation (Makes new directory on /... oops)~~
- ~~GoPlay Fix~~
- Windows?
- Proper UI?

View File

@ -5,38 +5,76 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"net/url"
"os" "os"
"path/filepath" "path/filepath"
"strings"
) )
type ProgressInfo struct {
Percentage float64
CurrentFile string
Paused bool
}
func handleRoot(w http.ResponseWriter, r *http.Request) { func handleRoot(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
progressMutex.Lock() progressMutex.Lock()
jobs := make(map[string]*ProgressInfo) defer progressMutex.Unlock()
for k, v := range progress {
jobs[k] = v
}
progressMutex.Unlock()
err := templates.ExecuteTemplate(w, "index", struct{ Jobs map[string]*ProgressInfo }{jobs}) jobsInfo := make(map[string]struct {
Percentage float64
CurrentFile string
Paused bool
})
for filename, info := range progress {
jobsInfo[filename] = struct {
Percentage float64
CurrentFile string
Paused bool
}{
Percentage: info.Percentage,
CurrentFile: info.CurrentFile,
Paused: info.Paused,
}
}
err := templates.ExecuteTemplate(w, "index", struct {
Jobs map[string]struct {
Percentage float64
CurrentFile string
Paused bool
}
}{jobsInfo})
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
} }
} }
func handleUpload(w http.ResponseWriter, r *http.Request) { func handleUpload(w http.ResponseWriter, r *http.Request) {
file, header, err := r.FormFile("file") err := r.ParseMultipartForm(32 << 20)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
files := r.MultipartForm.File["files"]
if len(files) == 0 {
http.Error(w, "No files uploaded", http.StatusBadRequest)
return
}
uploadedFiles := []string{}
for _, fileHeader := range files {
file, err := fileHeader.Open()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer file.Close() defer file.Close()
tempFile, err := os.CreateTemp(uploadDir, header.Filename) tempFile, err := os.CreateTemp(uploadDir, fileHeader.Filename)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
@ -49,33 +87,66 @@ func handleUpload(w http.ResponseWriter, r *http.Request) {
return return
} }
tempFilename := filepath.Base(tempFile.Name()) uploadedFiles = append(uploadedFiles, filepath.Base(tempFile.Name()))
_, err = parseInputFile(tempFile.Name()) _, err = parseInputFile(tempFile.Name())
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
http.Redirect(w, r, "/select?filename="+tempFilename, http.StatusSeeOther)
} }
func handleSelect(w http.ResponseWriter, r *http.Request) { validFiles := []string{}
filename := r.URL.Query().Get("filename") for _, file := range uploadedFiles {
items, err := parseInputFile(filepath.Join(uploadDir, filename)) if file != "" {
if err != nil { validFiles = append(validFiles, file)
http.Error(w, err.Error(), http.StatusInternalServerError) }
}
if len(validFiles) == 0 {
http.Error(w, "No valid files were uploaded", http.StatusBadRequest)
return return
} }
groupedItems := groupItemsBySeason(items) http.Redirect(w, r, "/select?files="+url.QueryEscape(strings.Join(validFiles, ",")), http.StatusSeeOther)
}
err = templates.ExecuteTemplate(w, "select", struct { func handleSelect(w http.ResponseWriter, r *http.Request) {
Filename string filesParam := r.URL.Query().Get("files")
Items map[string][]Item filenames := strings.Split(filesParam, ",")
allItems := make(map[string]map[string][]Item)
for _, filename := range filenames {
if filename == "" {
continue
}
fullPath := filepath.Join(uploadDir, filename)
if _, err := os.Stat(fullPath); os.IsNotExist(err) {
continue
}
items, err := parseInputFile(fullPath)
if err != nil {
continue
}
groupedItems := groupItemsBySeason(items)
allItems[filename] = groupedItems
}
if len(allItems) == 0 {
http.Error(w, "No valid files were processed", http.StatusBadRequest)
return
}
err := templates.ExecuteTemplate(w, "select", struct {
Filenames string
AllItems map[string]map[string][]Item
}{ }{
Filename: filename, Filenames: filesParam,
Items: groupedItems, AllItems: allItems,
}) })
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -83,36 +154,47 @@ func handleSelect(w http.ResponseWriter, r *http.Request) {
} }
func handleProcess(w http.ResponseWriter, r *http.Request) { func handleProcess(w http.ResponseWriter, r *http.Request) {
filename := r.FormValue("filename") if err := r.ParseForm(); err != nil {
selectedItems := r.Form["items"] http.Error(w, err.Error(), http.StatusBadRequest)
return
}
items, err := parseInputFile(filepath.Join(uploadDir, filename)) selectedItems := r.Form["items"]
if len(selectedItems) == 0 {
http.Error(w, "No items selected", http.StatusBadRequest)
return
}
itemsByFile := make(map[string][]string)
for _, item := range selectedItems {
parts := strings.SplitN(item, ":", 2)
if len(parts) != 2 {
continue
}
filename, itemName := parts[0], parts[1]
itemsByFile[filename] = append(itemsByFile[filename], itemName)
}
for filename, items := range itemsByFile {
fullPath := filepath.Join(uploadDir, filename)
allItems, err := parseInputFile(fullPath)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
filteredItems := filterSelectedItems(items, selectedItems) selectedItems := filterSelectedItems(allItems, items)
go processItems(filename, selectedItems)
go func() {
err := processItems(filename, filteredItems)
if err != nil {
fmt.Printf("Error processing file: %v\n", err)
} }
os.Remove(filepath.Join(uploadDir, filename)) http.Redirect(w, r, "/", http.StatusSeeOther)
}()
http.Redirect(w, r, "/progress?filename="+filename, http.StatusSeeOther)
} }
func handleProgress(w http.ResponseWriter, r *http.Request) { func handleProgress(w http.ResponseWriter, r *http.Request) {
filename := r.URL.Query().Get("filename") filename := r.URL.Query().Get("filename")
fmt.Printf("Handling progress request for filename: %s\n", filename)
if r.Header.Get("Accept") == "application/json" { if r.Header.Get("Accept") == "application/json" {
progressInfo := getProgress(filename) progressInfo := getProgress(filename)
fmt.Printf("Progress info for %s: %+v\n", filename, progressInfo)
if progressInfo == nil { if progressInfo == nil {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
@ -123,7 +205,6 @@ func handleProgress(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(progressInfo) err := json.NewEncoder(w).Encode(progressInfo)
if err != nil { if err != nil {
fmt.Printf("Error encoding progress info: %v\n", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError) http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return return
} }
@ -157,6 +238,12 @@ func handlePause(w http.ResponseWriter, r *http.Request) {
jobInfo.Cmd.Process.Kill() jobInfo.Cmd.Process.Kill()
} }
progressMutex.Lock()
if progressInfo, ok := progress[filename]; ok {
progressInfo.Paused = true
}
progressMutex.Unlock()
fmt.Fprintf(w, "Pause signal sent for %s", filename) fmt.Fprintf(w, "Pause signal sent for %s", filename)
} }
@ -179,6 +266,12 @@ func handleResume(w http.ResponseWriter, r *http.Request) {
jobInfo.Paused = false jobInfo.Paused = false
jobInfo.ResumeChan <- struct{}{} jobInfo.ResumeChan <- struct{}{}
progressMutex.Lock()
if progressInfo, ok := progress[filename]; ok {
progressInfo.Paused = false
}
progressMutex.Unlock()
fmt.Fprintf(w, "Resume signal sent for %s", filename) fmt.Fprintf(w, "Resume signal sent for %s", filename)
} }
@ -232,3 +325,29 @@ func clearCompletedJobs() {
} }
} }
} }
func updateProgress(filename string, value float64, currentFile string) {
progressMutex.Lock()
defer progressMutex.Unlock()
jobsMutex.Lock()
jobInfo, exists := jobs[filename]
jobsMutex.Unlock()
paused := false
if exists {
paused = jobInfo.Paused
}
if existingProgress, ok := progress[filename]; ok {
existingProgress.Percentage = value
existingProgress.CurrentFile = currentFile
existingProgress.Paused = paused
} else {
progress[filename] = &ProgressInfo{
Percentage: value,
CurrentFile: currentFile,
Paused: paused,
}
}
}

15
main.go
View File

@ -37,11 +37,6 @@ var progress = make(map[string]*ProgressInfo)
const uploadDir = "uploads" const uploadDir = "uploads"
type ProgressInfo struct {
Percentage float64
CurrentFile string
}
var templates *template.Template var templates *template.Template
//go:embed templates //go:embed templates
@ -87,16 +82,6 @@ func startWebServer() {
http.ListenAndServe(":8080", nil) http.ListenAndServe(":8080", 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 { func getProgress(filename string) *ProgressInfo {
progressMutex.Lock() progressMutex.Lock()
defer progressMutex.Unlock() defer progressMutex.Unlock()

View File

@ -74,6 +74,9 @@
display: inline-block; display: inline-block;
width: 5em; width: 5em;
} }
.paused {
color: #ffa500;
}
@media (max-width: 600px) { @media (max-width: 600px) {
body { body {
padding: 10px; padding: 10px;
@ -109,7 +112,7 @@
<body> <body>
<h1>Simple Downloader</h1> <h1>Simple Downloader</h1>
<form action="/upload" method="post" enctype="multipart/form-data"> <form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="file" accept=".drmd"> <input type="file" name="files" accept=".drmd" multiple>
<input type="submit" value="Upload and Process"> <input type="submit" value="Upload and Process">
</form> </form>
<h2>Currently Running Jobs</h2> <h2>Currently Running Jobs</h2>
@ -120,7 +123,11 @@
<a href="/progress?filename={{$filename}}">{{$filename}}</a> <a href="/progress?filename={{$filename}}">{{$filename}}</a>
</div> </div>
<div class="job-info"> <div class="job-info">
Progress: <span class="progress-text">{{printf "%5.1f%%" $info.Percentage}}</span> Current file: {{$info.CurrentFile}} Progress: <span class="progress-text">{{printf "%5.1f%%" $info.Percentage}}</span>
Current file: {{$info.CurrentFile}}
{{if $info.Paused}}
<span class="paused">(Paused)</span>
{{end}}
</div> </div>
</li> </li>
{{else}} {{else}}

View File

@ -127,10 +127,12 @@
<div> <div>
<button id="abort-button" onclick="abortDownload()">Abort Download</button> <button id="abort-button" onclick="abortDownload()">Abort Download</button>
<button id="pause-button" onclick="pauseDownload()">Pause Download</button> <button id="pause-button" onclick="pauseDownload()">Pause Download</button>
<button id="resume-button" onclick="resumeDownload()">Resume Download</button> <button id="resume-button" onclick="resumeDownload()" style="display: none;">Resume Download</button>
<button id="back-button" onclick="window.location.href='/'">Back to Index</button> <button id="back-button" onclick="window.location.href='/'">Back to Index</button>
</div> </div>
<script> <script>
let isPaused = false;
function updateProgress() { function updateProgress() {
fetch('/progress?filename={{.Filename}}', { fetch('/progress?filename={{.Filename}}', {
headers: { headers: {
@ -143,12 +145,25 @@
document.getElementById('progress-bar').style.width = progress + '%'; document.getElementById('progress-bar').style.width = progress + '%';
document.getElementById('progress-text').innerText = progress + '%'; document.getElementById('progress-text').innerText = progress + '%';
document.getElementById('currentFile').innerText = 'Current file: ' + (data.CurrentFile || 'None'); document.getElementById('currentFile').innerText = 'Current file: ' + (data.CurrentFile || 'None');
if (progress < 100) {
isPaused = data.Paused;
updatePauseResumeButtons();
if (progress < 100 && !isPaused) {
setTimeout(updateProgress, 1000); setTimeout(updateProgress, 1000);
} }
}); });
} }
updateProgress();
function updatePauseResumeButtons() {
if (isPaused) {
document.getElementById('pause-button').style.display = 'none';
document.getElementById('resume-button').style.display = 'inline-block';
} else {
document.getElementById('pause-button').style.display = 'inline-block';
document.getElementById('resume-button').style.display = 'none';
}
}
function abortDownload() { function abortDownload() {
fetch('/abort?filename={{.Filename}}', { method: 'POST' }) fetch('/abort?filename={{.Filename}}', { method: 'POST' })
@ -166,8 +181,8 @@
.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.');
document.getElementById('pause-button').style.display = 'none'; isPaused = true;
document.getElementById('resume-button').style.display = 'inline-block'; updatePauseResumeButtons();
} else { } else {
alert('Failed to pause the download.'); alert('Failed to pause the download.');
} }
@ -179,13 +194,16 @@
.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.');
document.getElementById('resume-button').style.display = 'none'; isPaused = false;
document.getElementById('pause-button').style.display = 'inline-block'; updatePauseResumeButtons();
updateProgress();
} else { } else {
alert('Failed to resume the download.'); alert('Failed to resume the download.');
} }
}); });
} }
updateProgress();
</script> </script>
</body> </body>
</html> </html>

View File

@ -55,23 +55,26 @@
<body> <body>
<h1>Select Items to Download</h1> <h1>Select Items to Download</h1>
<form action="/process" method="post"> <form action="/process" method="post">
<input type="hidden" name="filename" value="{{.Filename}}"> <input type="hidden" name="filenames" value="{{.Filenames}}">
{{range $season, $items := .Items}} {{range $filename, $fileItems := .AllItems}}
<h2>{{$filename}}</h2>
{{range $season, $items := $fileItems}}
<div class="season"> <div class="season">
<div class="season-title"> <div class="season-title">
<input type="checkbox" class="season-checkbox" id="season-{{$season}}" checked onchange="toggleSeason('{{$season}}')"> <input type="checkbox" class="season-checkbox" id="season-{{$filename}}-{{$season}}" checked onchange="toggleSeason('{{$filename}}-{{$season}}')">
<label for="season-{{$season}}">{{$season}}</label> <label for="season-{{$filename}}-{{$season}}">{{$season}}</label>
</div> </div>
{{range $item := $items}} {{range $item := $items}}
<div class="item"> <div class="item">
<label> <label>
<input type="checkbox" name="items" value="{{$item.Filename}}" checked class="episode-{{$season}}"> <input type="checkbox" name="items" value="{{$filename}}:{{$item.Filename}}" checked class="episode-{{$filename}}-{{$season}}">
{{$item.Filename}} {{$item.Filename}}
</label> </label>
</div> </div>
{{end}} {{end}}
</div> </div>
{{end}} {{end}}
{{end}}
<div> <div>
<button type="button" onclick="selectAll(true)">Select All</button> <button type="button" onclick="selectAll(true)">Select All</button>
<button type="button" onclick="selectAll(false)">Select None</button> <button type="button" onclick="selectAll(false)">Select None</button>

View File

@ -138,16 +138,24 @@ func findOrCreateSegmentTimeline(adaptationSet *etree.Element) *etree.Element {
return segmentTemplate.CreateElement("SegmentTimeline") return segmentTemplate.CreateElement("SegmentTimeline")
} }
func parseInputFile(inputFile string) ([]Item, error) { func parseInputFile(filename string) ([]Item, error) {
jsonFile, err := os.Open(inputFile) fileInfo, err := os.Stat(filename)
if err != nil { if err != nil {
return nil, fmt.Errorf("error opening file %s: %v", inputFile, err) return nil, err
}
if fileInfo.IsDir() {
return nil, fmt.Errorf("%s is a directory", filename)
} }
defer jsonFile.Close()
byteValue, err := io.ReadAll(jsonFile) file, err := os.Open(filename)
if err != nil { if err != nil {
return nil, fmt.Errorf("error reading file %s: %v", inputFile, err) return nil, err
}
defer file.Close()
byteValue, err := io.ReadAll(file)
if err != nil {
return nil, err
} }
byteValue = removeBOM(byteValue) byteValue = removeBOM(byteValue)
@ -155,7 +163,7 @@ func parseInputFile(inputFile string) ([]Item, error) {
var items Items var items Items
err = json.Unmarshal(byteValue, &items) err = json.Unmarshal(byteValue, &items)
if err != nil { if err != nil {
return nil, fmt.Errorf("error unmarshaling JSON: %v", err) return nil, err
} }
return items.Items, nil return items.Items, nil
@ -217,7 +225,6 @@ func processItems(filename string, items []Item) error {
select { select {
case <-jobInfo.ResumeChan: case <-jobInfo.ResumeChan:
jobInfo.Paused = false jobInfo.Paused = false
fmt.Printf("Resuming download for %s\n", filename)
case <-jobInfo.AbortChan: case <-jobInfo.AbortChan:
updateProgress(filename, 100, "Aborted") updateProgress(filename, 100, "Aborted")
return fmt.Errorf("download aborted") return fmt.Errorf("download aborted")
@ -231,7 +238,6 @@ func processItems(filename string, items []Item) error {
i-- i--
continue continue
} }
fmt.Printf("Error downloading file: %v\n", err)
} }
} }
} }