diff --git a/Dockerfile b/Dockerfile
index cafb269..ed49449 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -6,7 +6,7 @@ ARG GOARCH=amd64
FROM golang:1-alpine AS builder
WORKDIR /app
COPY . .
-RUN CGO_ENABLED=0 GOOS=${GOOS} GOARCH=${GOARCH} go build -ldflags="-s -w" -o zurg cmd/zurg/main.go
+RUN CGO_ENABLED=0 GOOS=${GOOS} GOARCH=${GOARCH} go build -ldflags="-s -w" -o zurg cmd/zurg/zurg.go
# Obfuscation stage
FROM alpine:3 AS obfuscator
diff --git a/cmd/zurg/main.go b/cmd/zurg/main.go
deleted file mode 100644
index 57652b2..0000000
--- a/cmd/zurg/main.go
+++ /dev/null
@@ -1,34 +0,0 @@
-package main
-
-import (
- "fmt"
- "log"
- "net/http"
- "time"
-
- "github.com/debridmediamanager.com/zurg/internal/config"
- "github.com/debridmediamanager.com/zurg/internal/net"
- "github.com/debridmediamanager.com/zurg/internal/torrent"
- "github.com/hashicorp/golang-lru/v2/expirable"
-)
-
-func main() {
- config, configErr := config.LoadZurgConfig("./config.yml")
- if configErr != nil {
- log.Panicf("Config failed to load: %v", configErr)
- }
-
- cache := expirable.NewLRU[string, string](1e4, nil, time.Hour)
-
- t := torrent.NewTorrentManager(config, cache)
-
- mux := http.NewServeMux()
- net.Router(mux, config, t, cache)
-
- addr := fmt.Sprintf(":%s", config.GetPort())
- log.Printf("Starting server on %s\n", addr)
- err := http.ListenAndServe(addr, mux)
- if err != nil {
- log.Panicf("Failed to start server: %v", err)
- }
-}
diff --git a/internal/dav/propfind.go b/internal/dav/listing.go
similarity index 98%
rename from internal/dav/propfind.go
rename to internal/dav/listing.go
index f982f3b..5637b56 100644
--- a/internal/dav/propfind.go
+++ b/internal/dav/listing.go
@@ -38,6 +38,7 @@ func HandlePropfindRequest(w http.ResponseWriter, r *http.Request, t *torrent.To
case len(filteredSegments) == 2:
output, err = handleSingleTorrent(requestPath, w, r, t)
default:
+ log.Println("Not Found", r.Method, requestPath)
writeHTTPError(w, "Not Found", http.StatusNotFound)
return
}
@@ -105,6 +106,5 @@ func handleSingleTorrent(requestPath string, w http.ResponseWriter, r *http.Requ
}
func writeHTTPError(w http.ResponseWriter, errorMessage string, statusCode int) {
- log.Println(errorMessage)
http.Error(w, errorMessage, statusCode)
}
diff --git a/internal/http/get.go b/internal/http/get.go
deleted file mode 100644
index e066389..0000000
--- a/internal/http/get.go
+++ /dev/null
@@ -1,278 +0,0 @@
-package http
-
-import (
- "fmt"
- "log"
- "net/http"
- "net/url"
- "path"
- "path/filepath"
- "strings"
-
- "github.com/debridmediamanager.com/zurg/internal/config"
- "github.com/debridmediamanager.com/zurg/internal/torrent"
- "github.com/debridmediamanager.com/zurg/pkg/davextra"
- "github.com/debridmediamanager.com/zurg/pkg/realdebrid"
- "github.com/hashicorp/golang-lru/v2/expirable"
-)
-
-func HandleHeadRequest(w http.ResponseWriter, r *http.Request, t *torrent.TorrentManager, c config.ConfigInterface, cache *expirable.LRU[string, string]) {
- requestPath := path.Clean(r.URL.Path)
- requestPath = strings.Replace(requestPath, "/http", "", 1)
- if requestPath == "/favicon.ico" {
- return
- }
-
- segments := strings.Split(requestPath, "/")
- // If there are less than 3 segments, return an error or adjust as needed
- if len(segments) < 4 {
- log.Println("Method not implemented", r.Method, r.URL.Path)
- http.Error(w, "Method not implemented", http.StatusMethodNotAllowed)
- return
- }
-
- if data, exists := cache.Get("head:" + requestPath); exists {
- splits := strings.Split(data, " ")
- contentType := splits[0]
- contentLength := splits[1]
- w.Header().Set("Content-Type", contentType)
- w.Header().Set("Content-Length", contentLength)
- w.WriteHeader(http.StatusOK)
- return
- }
-
- baseDirectory := segments[len(segments)-3]
- torrentName := segments[len(segments)-2]
- filename := segments[len(segments)-1]
-
- torrents := t.FindAllTorrentsWithName(baseDirectory, torrentName)
- if torrents == nil {
- log.Println("Cannot find torrent", requestPath)
- http.Error(w, "Cannot find file", http.StatusNotFound)
- return
- }
-
- filenameV2, linkFragment := davextra.ExtractLinkFragment(filename)
- _, file := getFile(torrents, filenameV2, linkFragment)
- if file == nil {
- log.Println("Cannot find file (head)", requestPath)
- http.Error(w, "Cannot find file", http.StatusNotFound)
- return
- }
- if file.Link == "" {
- log.Println("Link not found (head)", filename)
- http.Error(w, "Cannot find file", http.StatusNotFound)
- return
- }
- contentType := getContentMimeType(filename)
- contentLength := fmt.Sprintf("%d", file.Bytes)
- w.Header().Set("Content-Type", contentType)
- w.Header().Set("Content-Length", contentLength)
- cache.Add("head:"+requestPath, contentType+" "+contentLength)
- w.WriteHeader(http.StatusOK)
-}
-
-func getContentMimeType(filePath string) string {
- switch filepath.Ext(filePath) {
- case ".mkv":
- return "video/x-matroska"
- case ".mp4":
- return "video/mp4"
- case ".avi":
- return "video/x-msvideo"
- case ".mp3":
- return "audio/mpeg"
- case ".rar":
- return "application/x-rar-compressed"
- case ".zip":
- return "application/zip"
- case ".7z":
- return "application/x-7z-compressed"
- case ".srt":
- return "text/plain"
- default:
- return "application/octet-stream"
- }
-}
-
-func HandleGetRequest(w http.ResponseWriter, r *http.Request, t *torrent.TorrentManager, c config.ConfigInterface, cache *expirable.LRU[string, string]) {
- requestPath := path.Clean(r.URL.Path)
- requestPath = strings.Replace(requestPath, "/http", "/", 1)
- if requestPath == "/favicon.ico" {
- return
- }
- segments := strings.Split(requestPath, "/")
- // If there are less than 3 segments, return an error or adjust as needed
- if len(segments) < 4 {
- HandleDirectoryListing(w, r, t, c, cache)
- return
- }
- if data, exists := cache.Get(requestPath); exists {
- http.Redirect(w, r, data, http.StatusFound)
- return
- }
-
- baseDirectory := segments[len(segments)-3]
- torrentName := segments[len(segments)-2]
- filename := segments[len(segments)-1]
-
- torrents := t.FindAllTorrentsWithName(baseDirectory, torrentName)
- if torrents == nil {
- log.Println("Cannot find torrent", requestPath)
- http.Error(w, "Cannot find file", http.StatusNotFound)
- return
- }
-
- filenameV2, linkFragment := davextra.ExtractLinkFragment(filename)
- _, file := getFile(torrents, filenameV2, linkFragment)
- if file == nil {
- log.Println("Cannot find file (get)", requestPath)
- http.Error(w, "Cannot find file", http.StatusNotFound)
- return
- }
- if file.Link == "" {
- log.Println("Link not found (get)", filename)
- http.Error(w, "Cannot find file", http.StatusNotFound)
- return
- }
- link := file.Link
-
- unrestrictFn := func() (*realdebrid.UnrestrictResponse, error) {
- return realdebrid.UnrestrictLink(c.GetToken(), link)
- }
- resp := realdebrid.RetryUntilOk(unrestrictFn)
- if resp == nil {
- log.Println("Cannot unrestrict link", link, filenameV2)
- // t.HideTheFile(torrent, file)
- // http.Error(w, "Cannot find file", http.StatusNotFound)
- http.Redirect(w, r, "https://send.nukes.wtf/tDeTd0", http.StatusFound)
- return
- }
- if resp.Filename != filenameV2 {
- actualExt := filepath.Ext(resp.Filename)
- expectedExt := filepath.Ext(filenameV2)
- if actualExt != expectedExt {
- log.Println("File extension mismatch", resp.Filename, filenameV2)
- } else {
- log.Println("Filename mismatch", resp.Filename, filenameV2)
- }
- }
- cache.Add(requestPath, resp.Download)
- http.Redirect(w, r, resp.Download, http.StatusFound)
-}
-
-// getFile finds a link by a fragment, it might be wrong
-func getFile(torrents []torrent.Torrent, filename, fragment string) (*torrent.Torrent, *torrent.File) {
- for t := range torrents {
- for f, file := range torrents[t].SelectedFiles {
- fname := filepath.Base(file.Path)
- if filename == fname && strings.Contains(file.Link, fragment) {
- return &torrents[t], &torrents[t].SelectedFiles[f]
- }
- }
- }
- return nil, nil
-}
-
-func HandleDirectoryListing(w http.ResponseWriter, r *http.Request, t *torrent.TorrentManager, c config.ConfigInterface, cache *expirable.LRU[string, string]) {
- requestPath := path.Clean(r.URL.Path)
-
- if data, exists := cache.Get(requestPath); exists {
- w.Header().Set("Content-Type", "text/html; charset=\"utf-8\"")
- w.WriteHeader(http.StatusOK)
- fmt.Fprint(w, data)
- return
- }
-
- var output *string
- var err error
-
- filteredSegments := removeEmptyStrings(strings.Split(requestPath, "/"))
- switch {
- case len(filteredSegments) == 1:
- output, err = handleRoot(w, r, c)
- case len(filteredSegments) == 2:
- output, err = handleListOfTorrents(requestPath, w, r, t, c)
- case len(filteredSegments) == 3:
- output, err = handleSingleTorrent(requestPath, w, r, t)
- case len(filteredSegments) > 3:
- HandleGetRequest(w, r, t, c, cache)
- default:
- writeHTTPError(w, "Not Found", http.StatusNotFound)
- return
- }
-
- if err != nil {
- log.Printf("Error processing request: %v\n", err)
- writeHTTPError(w, "Server error", http.StatusInternalServerError)
- return
- }
-
- if output != nil {
- cache.Add(requestPath, *output)
-
- w.Header().Set("Content-Type", "text/html; charset=\"utf-8\"")
- w.WriteHeader(http.StatusOK)
- fmt.Fprint(w, *output)
- }
-}
-
-func handleRoot(w http.ResponseWriter, r *http.Request, c config.ConfigInterface) (*string, error) {
- htmlDoc := "
"
-
- for _, directory := range c.GetDirectories() {
- directoryPath := url.PathEscape(directory)
- htmlDoc += fmt.Sprintf("- %s
", directoryPath, directory)
- }
-
- return &htmlDoc, nil
-}
-
-func handleListOfTorrents(requestPath string, w http.ResponseWriter, r *http.Request, t *torrent.TorrentManager, c config.ConfigInterface) (*string, error) {
- basePath := path.Base(requestPath)
-
- for _, directory := range c.GetDirectories() {
- if basePath == directory {
- torrents := t.GetByDirectory(basePath)
- resp, err := createMultiTorrentResponse(requestPath, torrents)
- if err != nil {
- return nil, fmt.Errorf("cannot read directory (%s): %w", basePath, err)
- }
- return &resp, nil
- }
- }
-
- return nil, fmt.Errorf("cannot find directory when generating list: %s", requestPath)
-}
-
-func handleSingleTorrent(requestPath string, w http.ResponseWriter, r *http.Request, t *torrent.TorrentManager) (*string, error) {
- fullDir := path.Dir(requestPath)
- directory := path.Base(fullDir)
- torrentName := path.Base(requestPath)
-
- sameNameTorrents := t.FindAllTorrentsWithName(directory, torrentName)
- if len(sameNameTorrents) == 0 {
- return nil, fmt.Errorf("cannot find directory when generating single torrent: %s", requestPath)
- }
-
- resp, err := createSingleTorrentResponse(requestPath, sameNameTorrents)
- if err != nil {
- return nil, fmt.Errorf("cannot read directory (%s): %w", requestPath, err)
- }
- return &resp, nil
-}
-
-func writeHTTPError(w http.ResponseWriter, errorMessage string, statusCode int) {
- log.Println(errorMessage)
- http.Error(w, errorMessage, statusCode)
-}
-
-func removeEmptyStrings(slice []string) []string {
- var result []string
- for _, s := range slice {
- if s != "" {
- result = append(result, s)
- }
- }
- return result
-}
diff --git a/internal/http/listing.go b/internal/http/listing.go
new file mode 100644
index 0000000..6013a4d
--- /dev/null
+++ b/internal/http/listing.go
@@ -0,0 +1,101 @@
+package http
+
+import (
+ "fmt"
+ "log"
+ "net/http"
+ "net/url"
+ "path"
+ "strings"
+
+ "github.com/debridmediamanager.com/zurg/internal/config"
+ "github.com/debridmediamanager.com/zurg/internal/torrent"
+ "github.com/hashicorp/golang-lru/v2/expirable"
+)
+
+func HandleDirectoryListing(w http.ResponseWriter, r *http.Request, t *torrent.TorrentManager, c config.ConfigInterface, cache *expirable.LRU[string, string]) {
+ requestPath := path.Clean(r.URL.Path)
+
+ if data, exists := cache.Get(requestPath); exists {
+ w.Header().Set("Content-Type", "text/html; charset=\"utf-8\"")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, data)
+ return
+ }
+
+ var output *string
+ var err error
+
+ filteredSegments := removeEmptySegments(strings.Split(requestPath, "/"))
+ switch {
+ case len(filteredSegments) == 1:
+ output, err = handleRoot(w, r, c)
+ case len(filteredSegments) == 2:
+ output, err = handleListOfTorrents(requestPath, w, r, t, c)
+ case len(filteredSegments) == 3:
+ output, err = handleSingleTorrent(requestPath, w, r, t)
+ default:
+ log.Println("Not Found (http)", r.Method, requestPath, len(filteredSegments))
+ writeHTTPError(w, "Not Found", http.StatusNotFound)
+ return
+ }
+
+ if err != nil {
+ log.Printf("Error processing request: %v\n", err)
+ writeHTTPError(w, "Server error", http.StatusInternalServerError)
+ return
+ }
+
+ if output != nil {
+ cache.Add(requestPath, *output)
+
+ w.Header().Set("Content-Type", "text/html; charset=\"utf-8\"")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, *output)
+ }
+}
+
+func handleRoot(w http.ResponseWriter, r *http.Request, c config.ConfigInterface) (*string, error) {
+ htmlDoc := ""
+
+ for _, directory := range c.GetDirectories() {
+ directoryPath := url.PathEscape(directory)
+ htmlDoc += fmt.Sprintf("- %s
", directoryPath, directory)
+ }
+
+ return &htmlDoc, nil
+}
+
+func handleListOfTorrents(requestPath string, w http.ResponseWriter, r *http.Request, t *torrent.TorrentManager, c config.ConfigInterface) (*string, error) {
+ basePath := path.Base(requestPath)
+
+ for _, directory := range c.GetDirectories() {
+ if basePath == directory {
+ torrents := t.GetByDirectory(basePath)
+ resp, err := createMultiTorrentResponse(requestPath, torrents)
+ if err != nil {
+ return nil, fmt.Errorf("cannot read directory (%s): %w", basePath, err)
+ }
+ return &resp, nil
+ }
+ }
+
+ return nil, fmt.Errorf("cannot find directory when generating list: %s", requestPath)
+}
+
+func handleSingleTorrent(requestPath string, w http.ResponseWriter, r *http.Request, t *torrent.TorrentManager) (*string, error) {
+ fullDir := path.Dir(requestPath)
+ directory := path.Base(fullDir)
+ torrentName := path.Base(requestPath)
+
+ sameNameTorrents := t.FindAllTorrentsWithName(directory, torrentName)
+ if len(sameNameTorrents) == 0 {
+ return nil, fmt.Errorf("cannot find directory when generating single torrent: %s", requestPath)
+ }
+
+ resp, err := createSingleTorrentResponse(requestPath, sameNameTorrents)
+ if err != nil {
+ return nil, fmt.Errorf("cannot read directory (%s): %w", requestPath, err)
+ }
+ return &resp, nil
+}
diff --git a/internal/http/util.go b/internal/http/util.go
new file mode 100644
index 0000000..8e71035
--- /dev/null
+++ b/internal/http/util.go
@@ -0,0 +1,19 @@
+package http
+
+import (
+ "net/http"
+)
+
+func writeHTTPError(w http.ResponseWriter, errorMessage string, statusCode int) {
+ http.Error(w, errorMessage, statusCode)
+}
+
+func removeEmptySegments(urlSegments []string) []string {
+ var result []string
+ for _, s := range urlSegments {
+ if s != "" {
+ result = append(result, s)
+ }
+ }
+ return result
+}
diff --git a/internal/net/router.go b/internal/net/router.go
index 130d307..18acffa 100644
--- a/internal/net/router.go
+++ b/internal/net/router.go
@@ -3,11 +3,14 @@ package net
import (
"log"
"net/http"
+ "path"
+ "strings"
"github.com/debridmediamanager.com/zurg/internal/config"
"github.com/debridmediamanager.com/zurg/internal/dav"
intHttp "github.com/debridmediamanager.com/zurg/internal/http"
"github.com/debridmediamanager.com/zurg/internal/torrent"
+ "github.com/debridmediamanager.com/zurg/internal/universal"
"github.com/hashicorp/golang-lru/v2/expirable"
)
@@ -16,10 +19,15 @@ func Router(mux *http.ServeMux, c config.ConfigInterface, t *torrent.TorrentMana
mux.HandleFunc("/http/", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
- intHttp.HandleDirectoryListing(w, r, t, c, cache)
+ requestPath := path.Clean(r.URL.Path)
+ if countNonEmptySegments(strings.Split(requestPath, "/")) > 3 {
+ universal.HandleGetRequest(w, r, t, c, cache)
+ } else {
+ intHttp.HandleDirectoryListing(w, r, t, c, cache)
+ }
case http.MethodHead:
- intHttp.HandleHeadRequest(w, r, t, c, cache)
+ universal.HandleHeadRequest(w, r, t, c, cache)
default:
log.Println("Method not implemented", r.Method)
@@ -33,7 +41,7 @@ func Router(mux *http.ServeMux, c config.ConfigInterface, t *torrent.TorrentMana
dav.HandlePropfindRequest(w, r, t, c, cache)
case http.MethodGet:
- dav.HandleGetRequest(w, r, t, c, cache)
+ universal.HandleGetRequest(w, r, t, c, cache)
case http.MethodOptions:
w.WriteHeader(http.StatusOK)
@@ -44,3 +52,13 @@ func Router(mux *http.ServeMux, c config.ConfigInterface, t *torrent.TorrentMana
}
})
}
+
+func countNonEmptySegments(urlSegments []string) int {
+ count := 0
+ for _, s := range urlSegments {
+ if s != "" {
+ count++
+ }
+ }
+ return count
+}
diff --git a/internal/torrent/manager.go b/internal/torrent/manager.go
index 673ae59..e65c607 100644
--- a/internal/torrent/manager.go
+++ b/internal/torrent/manager.go
@@ -32,7 +32,7 @@ type TorrentManager struct {
// and store them in-memory
func NewTorrentManager(config config.ConfigInterface, cache *expirable.LRU[string, string]) *TorrentManager {
t := &TorrentManager{
- requiredVersion: "24.10.2023",
+ requiredVersion: "26.10.2023",
config: config,
cache: cache,
workerPool: make(chan bool, config.GetNumOfWorkers()),
@@ -68,23 +68,6 @@ func NewTorrentManager(config config.ConfigInterface, cache *expirable.LRU[strin
return t
}
-func (t *TorrentManager) repairAll(wg *sync.WaitGroup) {
- wg.Wait()
- for _, torrent := range t.torrents {
- if torrent.ForRepair {
- log.Println("Issues detected on", torrent.Name, "; fixing...")
- t.repair(torrent.ID, torrent.SelectedFiles, true)
- }
- if len(torrent.Links) == 0 {
- // If the torrent has no links
- // and already processing repair
- // delete it!
- log.Println("Deleting", torrent.Name, "as it has no links")
- realdebrid.DeleteTorrent(t.config.GetToken(), torrent.ID)
- }
- }
-}
-
// GetByDirectory returns all torrents that have a file in the specified directory
func (t *TorrentManager) GetByDirectory(directory string) []Torrent {
var torrents []Torrent
@@ -99,11 +82,10 @@ func (t *TorrentManager) GetByDirectory(directory string) []Torrent {
}
// HideTheFile marks a file as deleted
-// func (t *TorrentManager) HideTheFile(torrent *Torrent, file *File) {
-// file.Link = ""
-// t.writeToFile(torrent)
-// t.repair(torrent.ID, []File{*file}, false)
-// }
+func (t *TorrentManager) HideTheFile(torrent *Torrent, file *File) {
+ file.Unavailable = true
+ t.repair(torrent.ID, torrent.SelectedFiles, false)
+}
// FindAllTorrentsWithName finds all torrents in a given directory with a given name
func (t *TorrentManager) FindAllTorrentsWithName(directory, torrentName string) []Torrent {
@@ -270,7 +252,7 @@ func (t *TorrentManager) addMoreInfo(torrent *Torrent) {
return
}
- log.Println("Getting info for", torrent.ID)
+ // log.Println("Getting info for", torrent.ID)
info, err := realdebrid.GetTorrentInfo(t.config.GetToken(), torrent.ID)
if err != nil {
log.Printf("Cannot get info: %v\n", err)
@@ -282,9 +264,16 @@ func (t *TorrentManager) addMoreInfo(torrent *Torrent) {
// if it is empty, it means the file is no longer available
// Files+Links together are the same as SelectedFiles
var selectedFiles []File
+ var streamableFiles []File
// if some Links are empty, we need to repair it
forRepair := false
for _, file := range info.Files {
+ if isStreamable(file.Path) {
+ streamableFiles = append(streamableFiles, File{
+ File: file,
+ Link: "",
+ })
+ }
if file.Selected == 0 {
continue
}
@@ -300,10 +289,14 @@ func (t *TorrentManager) addMoreInfo(torrent *Torrent) {
var isChaotic bool
selectedFiles, isChaotic = t.organizeChaos(info, selectedFiles)
if isChaotic {
- log.Println("This torrent is unfixable, ignoring", info.Name, info.ID)
+ log.Println("This torrent is unfixable, it's always returning an unstreamable link, ignoring", info.Name, info.ID)
} else {
- log.Println("Marking for repair", info.Name)
- forRepair = true
+ if len(streamableFiles) > 1 {
+ log.Println("Marking for repair", info.Name)
+ forRepair = true
+ } else {
+ log.Println("This torrent is unfixable, the lone streamable link has expired, ignoring", info.Name, info.ID)
+ }
}
} else {
// all links are still intact! good!
@@ -320,6 +313,64 @@ func (t *TorrentManager) addMoreInfo(torrent *Torrent) {
}
}
+// mapToDirectories maps torrents to directories
+func (t *TorrentManager) mapToDirectories() {
+ // Map torrents to directories
+ switch t.config.GetVersion() {
+ case "v1":
+ configV1 := t.config.(*config.ZurgConfigV1)
+ groupMap := configV1.GetGroupMap()
+ // for every group, iterate over every torrent
+ // and then sprinkle/distribute the torrents to the directories of the group
+ for group, directories := range groupMap {
+ log.Printf("Processing directory group '%s', sequence: %s\n", group, strings.Join(directories, " > "))
+ counter := make(map[string]int)
+ for i := range t.torrents {
+ // don't process torrents that are already mapped if it is not the first run
+ if _, exists := t.processedTorrents[t.torrents[i].ID]; exists {
+ continue
+ }
+
+ for _, directory := range directories {
+ var filenames []string
+ for _, file := range t.torrents[i].Files {
+ filenames = append(filenames, file.Path)
+ }
+ if configV1.MeetsConditions(directory, t.torrents[i].ID, t.torrents[i].Name, filenames) {
+ found := false
+ for _, dir := range t.TorrentDirectoriesMap[t.torrents[i].Name] {
+ if dir == directory {
+ found = true
+ break
+ }
+ }
+ if !found {
+ counter[directory]++
+ t.TorrentDirectoriesMap[t.torrents[i].Name] = append(t.TorrentDirectoriesMap[t.torrents[i].Name], directory)
+ break
+ }
+ }
+ }
+ }
+ sum := 0
+ for _, count := range counter {
+ sum += count
+ }
+ if sum > 0 {
+ log.Printf("Directory group processed: %s %v %d\n", group, counter, sum)
+ } else {
+ log.Println("No new additions to directory group", group)
+ }
+ }
+ default:
+ log.Println("Unknown config version")
+ }
+ for _, torrent := range t.torrents {
+ t.processedTorrents[torrent.ID] = true
+ }
+ log.Println("Finished mapping to directories")
+}
+
// getByID returns a torrent by its ID
func (t *TorrentManager) getByID(torrentID string) *Torrent {
for i := range t.torrents {
@@ -376,6 +427,116 @@ func (t *TorrentManager) readFromFile(torrentID string) *Torrent {
return &torrent
}
+func (t *TorrentManager) repairAll(wg *sync.WaitGroup) {
+ wg.Wait()
+ for _, torrent := range t.torrents {
+ if torrent.ForRepair {
+ log.Println("Issues were detected on", torrent.Name, "; fixing...")
+ t.repair(torrent.ID, torrent.SelectedFiles, true)
+ }
+ if len(torrent.Links) == 0 && torrent.Progress == 100 {
+ // If the torrent has no links
+ // and already processing repair
+ // delete it!
+ log.Println("Deleting", torrent.Name, "as it has no links")
+ realdebrid.DeleteTorrent(t.config.GetToken(), torrent.ID)
+ }
+ }
+}
+
+func (t *TorrentManager) repair(torrentID string, selectedFiles []File, tryReinsertionFirst bool) {
+ torrent := t.getByID(torrentID)
+ if torrent == nil {
+ return
+ }
+
+ // check if it is already "being" repaired
+ found := false
+ for _, hash := range t.inProgress {
+ if hash == torrent.Hash {
+ found = true
+ break
+ }
+ }
+ if found {
+ log.Println("Repair in progress, skipping", torrentID)
+ return
+ }
+
+ // check if it is already repaired
+ foundFiles := t.findAllDownloadedFilesFromHash(torrent.Hash)
+ var missingFiles []File
+ for _, sFile := range selectedFiles {
+ if sFile.Link == "" || sFile.Unavailable {
+ found := false
+ for _, fFile := range foundFiles {
+ // same file but different link, then yes it has been repaired
+ if sFile.Path == fFile.Path && sFile.Link != fFile.Link {
+ found = true
+ break
+ }
+ }
+ if !found {
+ missingFiles = append(missingFiles, sFile)
+ }
+ }
+ }
+ if len(missingFiles) == 0 {
+ log.Println(torrent.Name, "is already repaired")
+ return
+ }
+
+ // then we repair it!
+ log.Println("Repairing torrent", torrentID)
+ // check if we can still add more downloads
+ proceed := t.canCapacityHandle()
+ if !proceed {
+ log.Println("Cannot add more torrents, exiting")
+ return
+ }
+
+ // first solution: add the same selection, maybe it can be fixed by reinsertion?
+ success := false
+ if tryReinsertionFirst {
+ success = t.reinsertTorrent(torrent, "", true)
+ }
+ if !success {
+ // if all the selected files are missing but there are other streamable files
+ var otherStreamableFileIDs []int
+ for _, file := range torrent.Files {
+ found := false
+ for _, selectedFile := range selectedFiles {
+ if selectedFile.ID == file.ID {
+ found = true
+ break
+ }
+ }
+ if !found && isStreamable(file.Path) {
+ otherStreamableFileIDs = append(otherStreamableFileIDs, file.ID)
+ }
+ }
+ if (len(missingFiles) == len(selectedFiles) || len(missingFiles) == 1) && len(otherStreamableFileIDs) > 0 {
+ // we will download 1 extra streamable file to force a redownload of the missing files
+ // or if there's only 1 missing file, we will download 1 more to prevent a rename
+ missingFilesPlus1 := strings.Join(getFileIDs(missingFiles), ",")
+ missingFilesPlus1 += fmt.Sprintf(",%d", otherStreamableFileIDs[0])
+ t.reinsertTorrent(torrent, missingFilesPlus1, false)
+ } else {
+ // if not, last resort: add only the missing files but do it in 2 batches
+ half := len(missingFiles) / 2
+ missingFiles1 := strings.Join(getFileIDs(missingFiles[:half]), ",")
+ missingFiles2 := strings.Join(getFileIDs(missingFiles[half:]), ",")
+ if missingFiles1 != "" {
+ t.reinsertTorrent(torrent, missingFiles1, false)
+ }
+ if missingFiles2 != "" {
+ t.reinsertTorrent(torrent, missingFiles2, false)
+ }
+ }
+ log.Println("Waiting for downloads to finish")
+ }
+}
+
func (t *TorrentManager) reinsertTorrent(torrent *Torrent, missingFiles string, deleteIfFailed bool) bool {
// if missingFiles is not provided, look for missing files
if missingFiles == "" {
@@ -485,7 +646,7 @@ func (t *TorrentManager) organizeChaos(info *realdebrid.Torrent, selectedFiles [
}
if !found {
// "chaos" file, we don't know where it belongs
- isChaotic = true
+ isChaotic = !isStreamable(result.Response.Filename)
selectedFiles = append(selectedFiles, File{
File: realdebrid.File{
Path: result.Response.Filename,
@@ -500,133 +661,6 @@ func (t *TorrentManager) organizeChaos(info *realdebrid.Torrent, selectedFiles [
return selectedFiles, isChaotic
}
-func (t *TorrentManager) repair(torrentID string, selectedFiles []File, tryReinsertionFirst bool) {
- torrent := t.getByID(torrentID)
- if torrent == nil {
- return
- }
-
- // check if it is already "being" repaired
- found := false
- for _, hash := range t.inProgress {
- if hash == torrent.Hash {
- found = true
- break
- }
- }
- if found {
- log.Println("Repair in progress, skipping", torrentID)
- return
- }
-
- // check if it is already repaired
- foundFiles := t.findAllDownloadedFilesFromHash(torrent.Hash)
- var missingFiles []File
- for _, sFile := range selectedFiles {
- if sFile.Link == "" {
- found := false
- for _, fFile := range foundFiles {
- if sFile.Path == fFile.Path {
- found = true
- break
- }
- }
- if !found {
- missingFiles = append(missingFiles, sFile)
- }
- }
- }
- if len(missingFiles) == 0 {
- log.Println(torrent.Name, "is already repaired")
- return
- }
-
- // then we repair it!
- log.Println("Repairing torrent", torrentID)
- // check if we can still add more downloads
- proceed := t.canCapacityHandle()
- if !proceed {
- log.Println("Cannot add more torrents, exiting")
- return
- }
-
- // first solution: add the same selection, maybe it can be fixed by reinsertion?
- success := false
- if tryReinsertionFirst {
- success = t.reinsertTorrent(torrent, "", true)
- }
- if !success {
- // if not, last resort: add only the missing files and do it in 2 batches
- half := len(missingFiles) / 2
- missingFiles1 := getFileIDs(missingFiles[:half])
- missingFiles2 := getFileIDs(missingFiles[half:])
- if missingFiles1 != "" {
- t.reinsertTorrent(torrent, missingFiles1, false)
- }
- if missingFiles2 != "" {
- t.reinsertTorrent(torrent, missingFiles2, false)
- }
- log.Println("Waiting for downloads to finish")
- }
-}
-
-func (t *TorrentManager) mapToDirectories() {
- // Map torrents to directories
- switch t.config.GetVersion() {
- case "v1":
- configV1 := t.config.(*config.ZurgConfigV1)
- groupMap := configV1.GetGroupMap()
- // for every group, iterate over every torrent
- // and then sprinkle/distribute the torrents to the directories of the group
- for group, directories := range groupMap {
- log.Printf("Processing directory group '%s', sequence: %s\n", group, strings.Join(directories, " > "))
- counter := make(map[string]int)
- for i := range t.torrents {
- // don't process torrents that are already mapped if it is not the first run
- if _, exists := t.processedTorrents[t.torrents[i].ID]; exists {
- continue
- }
-
- for _, directory := range directories {
- var filenames []string
- for _, file := range t.torrents[i].Files {
- filenames = append(filenames, file.Path)
- }
- if configV1.MeetsConditions(directory, t.torrents[i].ID, t.torrents[i].Name, filenames) {
- found := false
- for _, dir := range t.TorrentDirectoriesMap[t.torrents[i].Name] {
- if dir == directory {
- found = true
- break
- }
- }
- if !found {
- counter[directory]++
- t.TorrentDirectoriesMap[t.torrents[i].Name] = append(t.TorrentDirectoriesMap[t.torrents[i].Name], directory)
- break
- }
- }
- }
- }
- sum := 0
- for _, count := range counter {
- sum += count
- }
- if sum > 0 {
- log.Printf("Directory group processed: %s %v %d\n", group, counter, sum)
- } else {
- log.Println("No new additions to directory group", group)
- }
- }
- default:
- log.Println("Unknown config version")
- }
- for _, torrent := range t.torrents {
- t.processedTorrents[torrent.ID] = true
- }
- log.Println("Finished mapping to directories")
-}
-
func (t *TorrentManager) canCapacityHandle() bool {
// max waiting time is 45 minutes
const maxRetries = 50
@@ -667,17 +701,3 @@ func (t *TorrentManager) canCapacityHandle() bool {
retryCount++
}
}
-
-func getFileIDs(files []File) string {
- var fileIDs string
- for _, file := range files {
- // this won't include the id=0 files that were "chaos"
- if file.File.Selected == 1 && file.ID != 0 && file.Link == "" {
- fileIDs += fmt.Sprintf("%d,", file.ID)
- }
- }
- if len(fileIDs) > 0 {
- fileIDs = fileIDs[:len(fileIDs)-1]
- }
- return fileIDs
-}
diff --git a/internal/torrent/types.go b/internal/torrent/types.go
index d98c422..1b73984 100644
--- a/internal/torrent/types.go
+++ b/internal/torrent/types.go
@@ -11,5 +11,6 @@ type Torrent struct {
type File struct {
realdebrid.File
- Link string
+ Link string
+ Unavailable bool
}
diff --git a/internal/torrent/util.go b/internal/torrent/util.go
new file mode 100644
index 0000000..a97218c
--- /dev/null
+++ b/internal/torrent/util.go
@@ -0,0 +1,24 @@
+package torrent
+
+import (
+ "fmt"
+ "strings"
+)
+
+func getFileIDs(files []File) []string {
+ var fileIDs []string
+ for _, file := range files {
+ if file.ID != 0 {
+ fileIDs = append(fileIDs, fmt.Sprintf("%d", file.ID))
+ }
+ }
+ return fileIDs
+}
+
+func isStreamable(filePath string) bool {
+ return strings.HasSuffix(filePath, ".mkv") ||
+ strings.HasSuffix(filePath, ".mp4") ||
+ strings.HasSuffix(filePath, ".avi") ||
+ strings.HasSuffix(filePath, ".wmv") ||
+ strings.HasSuffix(filePath, ".m4v")
+}
diff --git a/internal/dav/getfile.go b/internal/universal/get.go
similarity index 64%
rename from internal/dav/getfile.go
rename to internal/universal/get.go
index 1cf3651..c030177 100644
--- a/internal/dav/getfile.go
+++ b/internal/universal/get.go
@@ -1,4 +1,4 @@
-package dav
+package universal
import (
"log"
@@ -8,31 +8,40 @@ import (
"strings"
"github.com/debridmediamanager.com/zurg/internal/config"
+ "github.com/debridmediamanager.com/zurg/internal/dav"
+ intHttp "github.com/debridmediamanager.com/zurg/internal/http"
"github.com/debridmediamanager.com/zurg/internal/torrent"
"github.com/debridmediamanager.com/zurg/pkg/davextra"
"github.com/debridmediamanager.com/zurg/pkg/realdebrid"
"github.com/hashicorp/golang-lru/v2/expirable"
)
-// HandleGetRequest handles a GET request to a file
+// HandleGetRequest handles a GET request universally for both WebDAV and HTTP
func HandleGetRequest(w http.ResponseWriter, r *http.Request, t *torrent.TorrentManager, c config.ConfigInterface, cache *expirable.LRU[string, string]) {
requestPath := path.Clean(r.URL.Path)
+ isDav := true
+ if strings.Contains(requestPath, "/http") {
+ requestPath = strings.Replace(requestPath, "/http", "/", 1)
+ isDav = false
+ }
if requestPath == "/favicon.ico" {
return
}
segments := strings.Split(requestPath, "/")
// If there are less than 3 segments, return an error or adjust as needed
- if len(segments) < 4 {
- // log.Println("Invalid url", requestPath)
- // http.Error(w, "Cannot find file", http.StatusNotFound)
- HandlePropfindRequest(w, r, t, c, cache)
+ if len(segments) <= 3 {
+ if isDav {
+ dav.HandlePropfindRequest(w, r, t, c, cache)
+ } else {
+ intHttp.HandleDirectoryListing(w, r, t, c, cache)
+ }
return
}
if data, exists := cache.Get(requestPath); exists {
http.Redirect(w, r, data, http.StatusFound)
return
}
- // Get the last two segments
+
baseDirectory := segments[len(segments)-3]
torrentName := segments[len(segments)-2]
filename := segments[len(segments)-1]
@@ -45,14 +54,14 @@ func HandleGetRequest(w http.ResponseWriter, r *http.Request, t *torrent.Torrent
}
filenameV2, linkFragment := davextra.ExtractLinkFragment(filename)
- _, file := getFile(torrents, filenameV2, linkFragment)
+ torrent, file := getFile(torrents, filenameV2, linkFragment)
if file == nil {
- log.Println("Cannot find file", requestPath)
+ log.Println("Cannot find file (get)", requestPath)
http.Error(w, "Cannot find file", http.StatusNotFound)
return
}
if file.Link == "" {
- log.Println("Link not found", filename)
+ log.Println("Link not found (get)", filename)
http.Error(w, "Cannot find file", http.StatusNotFound)
return
}
@@ -63,12 +72,12 @@ func HandleGetRequest(w http.ResponseWriter, r *http.Request, t *torrent.Torrent
}
resp := realdebrid.RetryUntilOk(unrestrictFn)
if resp == nil {
- log.Println("Cannot unrestrict link", link, filenameV2)
- // t.HideTheFile(torrent, file)
+ if !file.Unavailable {
+ log.Println("Cannot unrestrict link", link, filenameV2)
+ t.HideTheFile(torrent, file)
+ }
http.Redirect(w, r, "https://send.nukes.wtf/tDeTd0", http.StatusFound)
- return
- }
- if resp.Filename != filenameV2 {
+ } else if resp.Filename != filenameV2 {
actualExt := filepath.Ext(resp.Filename)
expectedExt := filepath.Ext(filenameV2)
if actualExt != expectedExt {
@@ -76,20 +85,8 @@ func HandleGetRequest(w http.ResponseWriter, r *http.Request, t *torrent.Torrent
} else {
log.Println("Filename mismatch", resp.Filename, filenameV2)
}
+ } else {
+ cache.Add(requestPath, resp.Download)
+ http.Redirect(w, r, resp.Download, http.StatusFound)
}
- cache.Add(requestPath, resp.Download)
- http.Redirect(w, r, resp.Download, http.StatusFound)
-}
-
-// getFile finds a link by a fragment, it might be wrong
-func getFile(torrents []torrent.Torrent, filename, fragment string) (*torrent.Torrent, *torrent.File) {
- for t := range torrents {
- for f, file := range torrents[t].SelectedFiles {
- fname := filepath.Base(file.Path)
- if filename == fname && strings.Contains(file.Link, fragment) {
- return &torrents[t], &torrents[t].SelectedFiles[f]
- }
- }
- }
- return nil, nil
}
diff --git a/internal/universal/head.go b/internal/universal/head.go
new file mode 100644
index 0000000..c902b8b
--- /dev/null
+++ b/internal/universal/head.go
@@ -0,0 +1,94 @@
+package universal
+
+import (
+ "fmt"
+ "log"
+ "net/http"
+ "path"
+ "path/filepath"
+ "strings"
+
+ "github.com/debridmediamanager.com/zurg/internal/config"
+ "github.com/debridmediamanager.com/zurg/internal/torrent"
+ "github.com/debridmediamanager.com/zurg/pkg/davextra"
+ "github.com/hashicorp/golang-lru/v2/expirable"
+)
+
+func HandleHeadRequest(w http.ResponseWriter, r *http.Request, t *torrent.TorrentManager, c config.ConfigInterface, cache *expirable.LRU[string, string]) {
+ requestPath := path.Clean(r.URL.Path)
+ requestPath = strings.Replace(requestPath, "/http", "", 1)
+ if requestPath == "/favicon.ico" {
+ return
+ }
+
+ segments := strings.Split(requestPath, "/")
+ // If there are less than 3 segments, return an error or adjust as needed
+ if len(segments) < 4 {
+ log.Println("Method not implemented", r.Method, r.URL.Path)
+ http.Error(w, "Method not implemented", http.StatusMethodNotAllowed)
+ return
+ }
+
+ if data, exists := cache.Get("head:" + requestPath); exists {
+ splits := strings.Split(data, " ")
+ contentType := splits[0]
+ contentLength := splits[1]
+ w.Header().Set("Content-Type", contentType)
+ w.Header().Set("Content-Length", contentLength)
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+
+ baseDirectory := segments[len(segments)-3]
+ torrentName := segments[len(segments)-2]
+ filename := segments[len(segments)-1]
+
+ torrents := t.FindAllTorrentsWithName(baseDirectory, torrentName)
+ if torrents == nil {
+ log.Println("Cannot find torrent", requestPath)
+ http.Error(w, "Cannot find file", http.StatusNotFound)
+ return
+ }
+
+ filenameV2, linkFragment := davextra.ExtractLinkFragment(filename)
+ _, file := getFile(torrents, filenameV2, linkFragment)
+ if file == nil {
+ log.Println("Cannot find file (head)", requestPath)
+ http.Error(w, "Cannot find file", http.StatusNotFound)
+ return
+ }
+ if file.Link == "" {
+ log.Println("Link not found (head)", filename)
+ http.Error(w, "Cannot find file", http.StatusNotFound)
+ return
+ }
+ contentType := getContentMimeType(filename)
+ contentLength := fmt.Sprintf("%d", file.Bytes)
+ w.Header().Set("Content-Type", contentType)
+ w.Header().Set("Content-Length", contentLength)
+ cache.Add("head:"+requestPath, contentType+" "+contentLength)
+ w.WriteHeader(http.StatusOK)
+}
+
+func getContentMimeType(filePath string) string {
+ switch filepath.Ext(filePath) {
+ case ".mkv":
+ return "video/x-matroska"
+ case ".mp4":
+ return "video/mp4"
+ case ".avi":
+ return "video/x-msvideo"
+ case ".mp3":
+ return "audio/mpeg"
+ case ".rar":
+ return "application/x-rar-compressed"
+ case ".zip":
+ return "application/zip"
+ case ".7z":
+ return "application/x-7z-compressed"
+ case ".srt":
+ return "text/plain"
+ default:
+ return "application/octet-stream"
+ }
+}
diff --git a/internal/universal/util.go b/internal/universal/util.go
new file mode 100644
index 0000000..c8b4501
--- /dev/null
+++ b/internal/universal/util.go
@@ -0,0 +1,21 @@
+package universal
+
+import (
+ "path/filepath"
+ "strings"
+
+ "github.com/debridmediamanager.com/zurg/internal/torrent"
+)
+
+// getFile finds a link by a fragment, it might be wrong
+func getFile(torrents []torrent.Torrent, filename, fragment string) (*torrent.Torrent, *torrent.File) {
+ for t := range torrents {
+ for f, file := range torrents[t].SelectedFiles {
+ fname := filepath.Base(file.Path)
+ if filename == fname && strings.Contains(file.Link, fragment) {
+ return &torrents[t], &torrents[t].SelectedFiles[f]
+ }
+ }
+ }
+ return nil, nil
+}