Compare commits
7 Commits
v1.0.5
...
7159bae9f7
Author | SHA1 | Date | |
---|---|---|---|
7159bae9f7
|
|||
5b6e1e6b01 | |||
4b03c7c59b
|
|||
1dd8aa594d
|
|||
72889d3083
|
|||
bd87baa40a
|
|||
2f738413f3
|
40
Makefile
Normal file
40
Makefile
Normal 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
|
15
README.md
15
README.md
@ -38,3 +38,18 @@ 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.
|
||||
|
||||
|
||||
# Previews
|
||||
|
||||
## Index Page
|
||||
|
||||

|
||||
|
||||
## Select Page
|
||||
|
||||

|
||||
|
||||
## Progress Page
|
||||
|
||||

|
||||
|
BIN
images/index.png
Normal file
BIN
images/index.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 33 KiB |
BIN
images/progress.png
Normal file
BIN
images/progress.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 29 KiB |
BIN
images/select.png
Normal file
BIN
images/select.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 41 KiB |
@ -133,7 +133,6 @@ func downloadFile(item Item, jobInfo *JobInfo) error {
|
||||
}
|
||||
|
||||
fmt.Println("Download completed successfully")
|
||||
os.RemoveAll(tempDir)
|
||||
return nil
|
||||
}
|
||||
|
@ -132,6 +132,8 @@ func handleSelect(w http.ResponseWriter, r *http.Request) {
|
||||
continue
|
||||
}
|
||||
|
||||
sortItems(items)
|
||||
|
||||
groupedItems := groupItemsBySeason(items)
|
||||
allItems[filename] = groupedItems
|
||||
}
|
||||
@ -184,6 +186,7 @@ func handleProcess(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
selectedItems := filterSelectedItems(allItems, items)
|
||||
sortItems(selectedItems)
|
||||
go processItems(filename, selectedItems)
|
||||
}
|
||||
|
125
src/main_test.go
Normal file
125
src/main_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
@ -50,6 +50,18 @@
|
||||
button:hover, input[type="submit"]:hover {
|
||||
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>
|
||||
</head>
|
||||
<body>
|
||||
@ -59,11 +71,12 @@
|
||||
{{range $filename, $fileItems := .AllItems}}
|
||||
<h2>{{$filename}}</h2>
|
||||
{{range $season, $items := $fileItems}}
|
||||
<div class="season">
|
||||
<div class="season" id="season-{{$filename}}-{{$season}}">
|
||||
<div class="season-title">
|
||||
<input type="checkbox" class="season-checkbox" id="season-{{$filename}}-{{$season}}" checked onchange="toggleSeason('{{$filename}}-{{$season}}')">
|
||||
<label for="season-{{$filename}}-{{$season}}">{{$season}}</label>
|
||||
<input type="checkbox" class="season-checkbox" id="season-checkbox-{{$filename}}-{{$season}}" checked onchange="toggleSeason('{{$filename}}-{{$season}}')">
|
||||
<label for="season-checkbox-{{$filename}}-{{$season}}">{{$season}}</label>
|
||||
</div>
|
||||
<div class="season-items">
|
||||
{{range $item := $items}}
|
||||
<div class="item">
|
||||
<label>
|
||||
@ -73,6 +86,7 @@
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
<div>
|
||||
@ -94,7 +108,7 @@
|
||||
}
|
||||
|
||||
function toggleSeason(season) {
|
||||
var seasonCheckbox = document.getElementById('season-' + season);
|
||||
var seasonCheckbox = document.getElementById('season-checkbox-' + season);
|
||||
var episodeCheckboxes = document.getElementsByClassName('episode-' + season);
|
||||
for (var i = 0; i < episodeCheckboxes.length; i++) {
|
||||
episodeCheckboxes[i].checked = seasonCheckbox.checked;
|
@ -9,6 +9,7 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@ -196,6 +197,43 @@ func filterSelectedItems(items []Item, selectedItems []string) []Item {
|
||||
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 {
|
||||
jobsMutex.Lock()
|
||||
jobInfo := &JobInfo{
|
||||
@ -215,6 +253,8 @@ func processItems(filename string, items []Item) error {
|
||||
}
|
||||
}()
|
||||
|
||||
sortItems(items)
|
||||
|
||||
for i := 0; i < len(items); i++ {
|
||||
select {
|
||||
case <-jobInfo.AbortChan:
|
Reference in New Issue
Block a user