7 Commits

Author SHA1 Message Date
7159bae9f7 Basic tests 2024-09-15 05:11:00 +02:00
5b6e1e6b01 Delete src/templates/stats 2024-09-15 05:02:02 +02:00
4b03c7c59b Change project structure 2024-09-15 05:00:02 +02:00
1dd8aa594d Add images for docxs 2024-09-15 04:40:14 +02:00
72889d3083 Sort episodes in a holy way 2024-09-15 04:29:48 +02:00
bd87baa40a Makefile 2024-09-15 00:34:25 +02:00
2f738413f3 H 2024-09-15 00:31:46 +02:00
17 changed files with 242 additions and 6 deletions

40
Makefile Normal file
View File

@ -0,0 +1,40 @@
GOCMD=go
GOBUILD=$(GOCMD) build
GOCLEAN=$(GOCMD) clean
GOTEST=$(GOCMD) test
GOGET=$(GOCMD) get
BINARY_NAME=drmdtool
SRC_DIR=src
all: test build
build:
cd $(SRC_DIR) && $(GOBUILD) -o ../$(BINARY_NAME) -v
test:
cd $(SRC_DIR) && $(GOTEST) -v ./...
clean:
$(GOCLEAN)
rm -f $(BINARY_NAME)
run:
cd $(SRC_DIR) && $(GOBUILD) -o ../$(BINARY_NAME) -v
./$(BINARY_NAME)
deps:
$(GOGET) github.com/BurntSushi/toml
$(GOGET) github.com/beevik/etree
$(GOGET) github.com/asticode/go-astisub
# Cross compilation
build-linux:
cd $(SRC_DIR) && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) -o ../$(BINARY_NAME)_linux -v
build-windows:
cd $(SRC_DIR) && CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GOBUILD) -o ../$(BINARY_NAME).exe -v
build-mac:
cd $(SRC_DIR) && CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(GOBUILD) -o ../$(BINARY_NAME)_mac -v
.PHONY: all build test clean run deps build-linux build-windows build-mac

View File

@ -37,4 +37,19 @@ To process a file directly from the command line:
./drmdtool -f /path/to/file.drmd ./drmdtool -f /path/to/file.drmd
``` ```
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
## Index Page
![Index Page](images/index.png)
## Select Page
![Select Page](images/select.png)
## Progress Page
![Progress Page](images/progress.png)

BIN
images/index.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
images/progress.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
images/select.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@ -133,7 +133,6 @@ func downloadFile(item Item, jobInfo *JobInfo) error {
} }
fmt.Println("Download completed successfully") fmt.Println("Download completed successfully")
os.RemoveAll(tempDir)
return nil return nil
} }

View File

View File

View File

@ -132,6 +132,8 @@ func handleSelect(w http.ResponseWriter, r *http.Request) {
continue continue
} }
sortItems(items)
groupedItems := groupItemsBySeason(items) groupedItems := groupItemsBySeason(items)
allItems[filename] = groupedItems allItems[filename] = groupedItems
} }
@ -184,6 +186,7 @@ func handleProcess(w http.ResponseWriter, r *http.Request) {
} }
selectedItems := filterSelectedItems(allItems, items) selectedItems := filterSelectedItems(allItems, items)
sortItems(selectedItems)
go processItems(filename, selectedItems) go processItems(filename, selectedItems)
} }

125
src/main_test.go Normal file
View File

@ -0,0 +1,125 @@
package main
import (
"encoding/json"
"os"
"reflect"
"testing"
)
func TestSanitizeFilename(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"file:name.mp4", "file_name.mp4"},
{"file/name.mp4", "file_name.mp4"},
{"file\\name.mp4", "file_name.mp4"},
{"file?name.mp4", "file_name.mp4"},
{"file*name.mp4", "file_name.mp4"},
{"file<name>.mp4", "file_name_.mp4"},
{".hidden", "hidden"},
}
for _, test := range tests {
result := sanitizeFilename(test.input)
if result != test.expected {
t.Errorf("sanitizeFilename(%q) = %q, want %q", test.input, result, test.expected)
}
}
}
func TestIsValidURL(t *testing.T) {
tests := []struct {
input string
expected bool
}{
{"https://example.com", true},
{"http://example.com", true},
{"ftp://example.com", true},
{"not a url", false},
}
for _, test := range tests {
result := isValidURL(test.input)
if result != test.expected {
t.Errorf("isValidURL(%q) = %v, want %v", test.input, result, test.expected)
}
}
}
func TestParseMetadata(t *testing.T) {
tests := []struct {
input string
expected Metadata
}{
{"Show Title; serie; 01", Metadata{Title: "Show Title", Type: "serie", Season: "S01"}},
{"Movie Title; movie; ", Metadata{Title: "Movie Title", Type: "movie", Season: "S"}},
{"Invalid Metadata", Metadata{}},
}
for _, test := range tests {
result := parseMetadata(test.input)
if !reflect.DeepEqual(result, test.expected) {
t.Errorf("parseMetadata(%q) = %v, want %v", test.input, result, test.expected)
}
}
}
func TestParseInputFile(t *testing.T) {
tempFile, err := os.CreateTemp("", "test_input_*.json")
if err != nil {
t.Fatalf("Failed to create temp file: %v", err)
}
defer os.Remove(tempFile.Name())
testData := Items{
Items: []Item{
{MPD: "http://example.com/video1.mpd", Filename: "video1.mp4"},
{MPD: "http://example.com/video2.mpd", Filename: "video2.mp4"},
},
}
jsonData, _ := json.Marshal(testData)
if _, err := tempFile.Write(jsonData); err != nil {
t.Fatalf("Failed to write to temp file: %v", err)
}
tempFile.Close()
items, err := parseInputFile(tempFile.Name())
if err != nil {
t.Fatalf("parseInputFile() returned an error: %v", err)
}
if len(items) != len(testData.Items) {
t.Errorf("parseInputFile() returned %d items, want %d", len(items), len(testData.Items))
}
for i, item := range items {
if !reflect.DeepEqual(item, testData.Items[i]) {
t.Errorf("parseInputFile() item %d = %v, want %v", i, item, testData.Items[i])
}
}
}
func TestGroupItemsBySeason(t *testing.T) {
items := []Item{
{Filename: "show1_s01e01.mp4", Metadata: "Show 1; serie; 01"},
{Filename: "show1_s01e02.mp4", Metadata: "Show 1; serie; 01"},
{Filename: "show2_s01e01.mp4", Metadata: "Show 2; serie; 01"},
{Filename: "movie1.mp4", Metadata: "Movie 1; movie; "},
}
grouped := groupItemsBySeason(items)
expectedGroups := map[string]int{
"Show 1 - S01": 2,
"Show 2 - S01": 1,
"Movies": 1,
}
for group, count := range expectedGroups {
if len(grouped[group]) != count {
t.Errorf("groupItemsBySeason() group %q has %d items, want %d", group, len(grouped[group]), count)
}
}
}

View File

@ -50,6 +50,18 @@
button:hover, input[type="submit"]:hover { button:hover, input[type="submit"]:hover {
background-color: #45a049; background-color: #45a049;
} }
#fix-order-button {
background-color: #2196F3;
color: white;
border: none;
padding: 10px 15px;
margin: 5px;
border-radius: 4px;
cursor: pointer;
}
#fix-order-button:hover {
background-color: #1976D2;
}
</style> </style>
</head> </head>
<body> <body>
@ -59,11 +71,12 @@
{{range $filename, $fileItems := .AllItems}} {{range $filename, $fileItems := .AllItems}}
<h2>{{$filename}}</h2> <h2>{{$filename}}</h2>
{{range $season, $items := $fileItems}} {{range $season, $items := $fileItems}}
<div class="season"> <div class="season" id="season-{{$filename}}-{{$season}}">
<div class="season-title"> <div class="season-title">
<input type="checkbox" class="season-checkbox" id="season-{{$filename}}-{{$season}}" checked onchange="toggleSeason('{{$filename}}-{{$season}}')"> <input type="checkbox" class="season-checkbox" id="season-checkbox-{{$filename}}-{{$season}}" checked onchange="toggleSeason('{{$filename}}-{{$season}}')">
<label for="season-{{$filename}}-{{$season}}">{{$season}}</label> <label for="season-checkbox-{{$filename}}-{{$season}}">{{$season}}</label>
</div> </div>
<div class="season-items">
{{range $item := $items}} {{range $item := $items}}
<div class="item"> <div class="item">
<label> <label>
@ -72,6 +85,7 @@
</label> </label>
</div> </div>
{{end}} {{end}}
</div>
</div> </div>
{{end}} {{end}}
{{end}} {{end}}
@ -94,7 +108,7 @@
} }
function toggleSeason(season) { function toggleSeason(season) {
var seasonCheckbox = document.getElementById('season-' + season); var seasonCheckbox = document.getElementById('season-checkbox-' + season);
var episodeCheckboxes = document.getElementsByClassName('episode-' + season); var episodeCheckboxes = document.getElementsByClassName('episode-' + season);
for (var i = 0; i < episodeCheckboxes.length; i++) { for (var i = 0; i < episodeCheckboxes.length; i++) {
episodeCheckboxes[i].checked = seasonCheckbox.checked; episodeCheckboxes[i].checked = seasonCheckbox.checked;

View File

@ -9,6 +9,7 @@ import (
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"regexp" "regexp"
"sort"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@ -196,6 +197,43 @@ func filterSelectedItems(items []Item, selectedItems []string) []Item {
return filtered return filtered
} }
func sortItems(items []Item) {
sort.Slice(items, func(i, j int) bool {
iMeta := parseMetadata(items[i].Metadata)
jMeta := parseMetadata(items[j].Metadata)
if iMeta.Title != jMeta.Title {
return iMeta.Title < jMeta.Title
}
iSeason := extractNumber(iMeta.Season)
jSeason := extractNumber(jMeta.Season)
if iSeason != jSeason {
return iSeason < jSeason
}
iEpisode := extractEpisodeNumber(items[i].Filename)
jEpisode := extractEpisodeNumber(items[j].Filename)
return iEpisode < jEpisode
})
}
func extractNumber(s string) int {
num, _ := strconv.Atoi(strings.TrimLeft(s, "S"))
return num
}
func extractEpisodeNumber(filename string) int {
parts := strings.Split(filename, "E")
if len(parts) > 1 {
num, _ := strconv.Atoi(parts[1])
return num
}
return 0
}
func processItems(filename string, items []Item) error { func processItems(filename string, items []Item) error {
jobsMutex.Lock() jobsMutex.Lock()
jobInfo := &JobInfo{ jobInfo := &JobInfo{
@ -215,6 +253,8 @@ func processItems(filename string, items []Item) error {
} }
}() }()
sortItems(items)
for i := 0; i < len(items); i++ { for i := 0; i < len(items); i++ {
select { select {
case <-jobInfo.AbortChan: case <-jobInfo.AbortChan: