diff --git a/cmd/zurg/main.go b/cmd/zurg/main.go index 77f91aa..57652b2 100644 --- a/cmd/zurg/main.go +++ b/cmd/zurg/main.go @@ -7,7 +7,7 @@ import ( "time" "github.com/debridmediamanager.com/zurg/internal/config" - "github.com/debridmediamanager.com/zurg/internal/dav" + "github.com/debridmediamanager.com/zurg/internal/net" "github.com/debridmediamanager.com/zurg/internal/torrent" "github.com/hashicorp/golang-lru/v2/expirable" ) @@ -23,7 +23,7 @@ func main() { t := torrent.NewTorrentManager(config, cache) mux := http.NewServeMux() - dav.Router(mux, config, t, cache) + net.Router(mux, config, t, cache) addr := fmt.Sprintf(":%s", config.GetPort()) log.Printf("Starting server on %s\n", addr) diff --git a/internal/dav/propfind.go b/internal/dav/propfind.go index b7586c6..76b7bf5 100644 --- a/internal/dav/propfind.go +++ b/internal/dav/propfind.go @@ -97,7 +97,7 @@ func handleSingleTorrent(requestPath string, w http.ResponseWriter, r *http.Requ return nil, fmt.Errorf("cannot find directory when generating single torrent: %s", requestPath) } - resp, err := createSingleTorrentResponse("/"+directory, sameNameTorrents, t) + resp, err := createSingleTorrentResponse("/"+directory, sameNameTorrents) if err != nil { return nil, fmt.Errorf("cannot read directory (%s): %w", requestPath, err) } diff --git a/internal/dav/response.go b/internal/dav/response.go index 1745acd..a87ec58 100644 --- a/internal/dav/response.go +++ b/internal/dav/response.go @@ -37,7 +37,7 @@ func createMultiTorrentResponse(basePath string, torrents []torrent.Torrent) (*d // createTorrentResponse creates a WebDAV response for a single torrent // but it also handles the case where there are many torrents with the same name -func createSingleTorrentResponse(basePath string, torrents []torrent.Torrent, t *torrent.TorrentManager) (*dav.MultiStatus, error) { +func createSingleTorrentResponse(basePath string, torrents []torrent.Torrent) (*dav.MultiStatus, error) { var responses []dav.Response // initial response is the directory itself diff --git a/internal/http/get.go b/internal/http/get.go new file mode 100644 index 0000000..5c3466e --- /dev/null +++ b/internal/http/get.go @@ -0,0 +1,211 @@ +package http + +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/debridmediamanager.com/zurg/pkg/realdebrid" + "github.com/hashicorp/golang-lru/v2/expirable" +) + +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 := findAllTorrentsWithName(t, baseDirectory, torrentName) + if torrents == nil { + log.Println("Cannot find torrent", torrentName) + http.Error(w, "Cannot find file", http.StatusNotFound) + return + } + + filenameV2, linkFragment := davextra.ExtractLinkFragment(filename) + torrent, file := getFile(torrents, filenameV2, linkFragment) + if file == nil { + log.Println("Cannot find file", filename) + http.Error(w, "Cannot find file", http.StatusNotFound) + return + } + if file.Link == "" { + log.Println("Link not found", 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 { + // TODO: Readd the file + // when unrestricting fails, it means the file is not available anymore, but still in their database + // if it's the only file, tough luck + // if it's the only file, try to readd it + // delete the old one, add a new one + log.Println("Cannot unrestrict link", link, filenameV2) + t.MarkFileAsDeleted(torrent, file) + http.Error(w, "Cannot find file", http.StatusNotFound) + return + } + if resp.Filename != filenameV2 { + // TODO: Redo the logic to handle mismatch + // [SRS] Pokemon S22E01-35 1080p WEBRip AAC 2.0 x264 CC.rar + // Pokemon.S22E24.The.Secret.Princess.DUBBED.1080p.WEBRip.AAC.2.0.x264-SRS.mkv + // Action: schedule a "cleanup" job for the parent torrent + // If the file extension changed, that means it's a different file + 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.HasPrefix(file.Link, fmt.Sprintf("https://real-debrid.com/d/%s", fragment)) { + return &torrents[t], &torrents[t].SelectedFiles[f] + } + } + } + return nil, nil +} + +func findAllTorrentsWithName(t *torrent.TorrentManager, directory, torrentName string) []torrent.Torrent { + matchingTorrents := make([]torrent.Torrent, 0, 10) + torrents := t.GetByDirectory(directory) + for i := range torrents { + if torrents[i].Name == torrentName || strings.HasPrefix(torrents[i].Name, torrentName) { + matchingTorrents = append(matchingTorrents, torrents[i]) + } + } + return matchingTorrents +} + +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 := "