Big commit

This commit is contained in:
Ben Sarmiento
2023-10-24 22:14:04 +02:00
parent 91472334b9
commit 2ce8273779
8 changed files with 105 additions and 71 deletions

View File

@@ -35,7 +35,10 @@ concurrent_workers: 10 # the higher the number the faster zurg runs through your
check_for_changes_every_secs: 15 # zurg polls real-debrid for changes in your library check_for_changes_every_secs: 15 # zurg polls real-debrid for changes in your library
info_cache_time_hours: 12 # how long do we want to check if a torrent is still alive or dead? 12 to 24 hours is good enough info_cache_time_hours: 12 # how long do we want to check if a torrent is still alive or dead? 12 to 24 hours is good enough
# repair fixes broken links, but it doesn't mean it will appear on the same location (especially if there's only 1 episode missing) # zurg can repair broken links, but it doesn't mean it will appear on the same location (especially if there's only 1 episode missing)
# e.g. i was missing e06 of Better.Call.Saul.S03.2160p.NF.WEBRip.DD5.1.x264-ViSUM
# after zurg re-added the file, it appeared on a different directory:
# Better.Call.Saul.S03E06.2160p.NF.WEBRip.DD5.1.x264-ViSUM.mkv
enable_repair: false # BEWARE! THERE CAN ONLY BE 1 INSTANCE OF ZURG THAT SHOULD REPAIR YOUR TORRENTS enable_repair: false # BEWARE! THERE CAN ONLY BE 1 INSTANCE OF ZURG THAT SHOULD REPAIR YOUR TORRENTS
# List of directory definitions and their filtering rules # List of directory definitions and their filtering rules

View File

@@ -1,7 +1,6 @@
package dav package dav
import ( import (
"fmt"
"log" "log"
"net/http" "net/http"
"path" "path"
@@ -40,15 +39,15 @@ func HandleGetRequest(w http.ResponseWriter, r *http.Request, t *torrent.Torrent
torrents := t.FindAllTorrentsWithName(baseDirectory, torrentName) torrents := t.FindAllTorrentsWithName(baseDirectory, torrentName)
if torrents == nil { if torrents == nil {
log.Println("Cannot find torrent", torrentName) log.Println("Cannot find torrent", requestPath)
http.Error(w, "Cannot find file", http.StatusNotFound) http.Error(w, "Cannot find file", http.StatusNotFound)
return return
} }
filenameV2, linkFragment := davextra.ExtractLinkFragment(filename) filenameV2, linkFragment := davextra.ExtractLinkFragment(filename)
torrent, file := getFile(torrents, filenameV2, linkFragment) _, file := getFile(torrents, filenameV2, linkFragment)
if file == nil { if file == nil {
log.Println("Cannot find file", filename) log.Println("Cannot find file", requestPath)
http.Error(w, "Cannot find file", http.StatusNotFound) http.Error(w, "Cannot find file", http.StatusNotFound)
return return
} }
@@ -65,8 +64,8 @@ func HandleGetRequest(w http.ResponseWriter, r *http.Request, t *torrent.Torrent
resp := realdebrid.RetryUntilOk(unrestrictFn) resp := realdebrid.RetryUntilOk(unrestrictFn)
if resp == nil { if resp == nil {
log.Println("Cannot unrestrict link", link, filenameV2) log.Println("Cannot unrestrict link", link, filenameV2)
t.MarkFileAsDeleted(torrent, file) // t.HideTheFile(torrent, file)
http.Error(w, "Cannot find file", http.StatusNotFound) http.Redirect(w, r, "https://send.nukes.wtf/tDeTd0", http.StatusFound)
return return
} }
if resp.Filename != filenameV2 { if resp.Filename != filenameV2 {
@@ -87,7 +86,7 @@ func getFile(torrents []torrent.Torrent, filename, fragment string) (*torrent.To
for t := range torrents { for t := range torrents {
for f, file := range torrents[t].SelectedFiles { for f, file := range torrents[t].SelectedFiles {
fname := filepath.Base(file.Path) fname := filepath.Base(file.Path)
if filename == fname && strings.HasPrefix(file.Link, fmt.Sprintf("https://real-debrid.com/d/%s", fragment)) { if filename == fname && strings.Contains(file.Link, fragment) {
return &torrents[t], &torrents[t].SelectedFiles[f] return &torrents[t], &torrents[t].SelectedFiles[f]
} }
} }

View File

@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
"net/url"
"path" "path"
"path/filepath" "path/filepath"
"strings" "strings"
@@ -17,7 +18,7 @@ import (
func HandleHeadRequest(w http.ResponseWriter, r *http.Request, t *torrent.TorrentManager, c config.ConfigInterface, cache *expirable.LRU[string, string]) { 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 := path.Clean(r.URL.Path)
requestPath = strings.Replace(requestPath, "/http", "/", 1) requestPath = strings.Replace(requestPath, "/http", "", 1)
if requestPath == "/favicon.ico" { if requestPath == "/favicon.ico" {
return return
} }
@@ -46,7 +47,7 @@ func HandleHeadRequest(w http.ResponseWriter, r *http.Request, t *torrent.Torren
torrents := t.FindAllTorrentsWithName(baseDirectory, torrentName) torrents := t.FindAllTorrentsWithName(baseDirectory, torrentName)
if torrents == nil { if torrents == nil {
log.Println("Cannot find torrent", torrentName, segments) log.Println("Cannot find torrent", requestPath)
http.Error(w, "Cannot find file", http.StatusNotFound) http.Error(w, "Cannot find file", http.StatusNotFound)
return return
} }
@@ -54,12 +55,12 @@ func HandleHeadRequest(w http.ResponseWriter, r *http.Request, t *torrent.Torren
filenameV2, linkFragment := davextra.ExtractLinkFragment(filename) filenameV2, linkFragment := davextra.ExtractLinkFragment(filename)
_, file := getFile(torrents, filenameV2, linkFragment) _, file := getFile(torrents, filenameV2, linkFragment)
if file == nil { if file == nil {
log.Println("Cannot find file", filename, segments) log.Println("Cannot find file (head)", requestPath)
http.Error(w, "Cannot find file", http.StatusNotFound) http.Error(w, "Cannot find file", http.StatusNotFound)
return return
} }
if file.Link == "" { if file.Link == "" {
log.Println("Link not found", filename) log.Println("Link not found 222", filename)
http.Error(w, "Cannot find file", http.StatusNotFound) http.Error(w, "Cannot find file", http.StatusNotFound)
return return
} }
@@ -117,20 +118,20 @@ func HandleGetRequest(w http.ResponseWriter, r *http.Request, t *torrent.Torrent
torrents := t.FindAllTorrentsWithName(baseDirectory, torrentName) torrents := t.FindAllTorrentsWithName(baseDirectory, torrentName)
if torrents == nil { if torrents == nil {
log.Println("Cannot find torrent", torrentName) log.Println("Cannot find torrent", requestPath)
http.Error(w, "Cannot find file", http.StatusNotFound) http.Error(w, "Cannot find file", http.StatusNotFound)
return return
} }
filenameV2, linkFragment := davextra.ExtractLinkFragment(filename) filenameV2, linkFragment := davextra.ExtractLinkFragment(filename)
torrent, file := getFile(torrents, filenameV2, linkFragment) _, file := getFile(torrents, filenameV2, linkFragment)
if file == nil { if file == nil {
log.Println("Cannot find file", filename) log.Println("Cannot find file (get)", requestPath)
http.Error(w, "Cannot find file", http.StatusNotFound) http.Error(w, "Cannot find file", http.StatusNotFound)
return return
} }
if file.Link == "" { if file.Link == "" {
log.Println("Link not found", filename) log.Println("Link not found 33", filename)
http.Error(w, "Cannot find file", http.StatusNotFound) http.Error(w, "Cannot find file", http.StatusNotFound)
return return
} }
@@ -142,8 +143,9 @@ func HandleGetRequest(w http.ResponseWriter, r *http.Request, t *torrent.Torrent
resp := realdebrid.RetryUntilOk(unrestrictFn) resp := realdebrid.RetryUntilOk(unrestrictFn)
if resp == nil { if resp == nil {
log.Println("Cannot unrestrict link", link, filenameV2) log.Println("Cannot unrestrict link", link, filenameV2)
t.MarkFileAsDeleted(torrent, file) // t.HideTheFile(torrent, file)
http.Error(w, "Cannot find file", http.StatusNotFound) // http.Error(w, "Cannot find file", http.StatusNotFound)
http.Redirect(w, r, "https://send.nukes.wtf/tDeTd0", http.StatusFound)
return return
} }
if resp.Filename != filenameV2 { if resp.Filename != filenameV2 {
@@ -163,8 +165,12 @@ func HandleGetRequest(w http.ResponseWriter, r *http.Request, t *torrent.Torrent
func getFile(torrents []torrent.Torrent, filename, fragment string) (*torrent.Torrent, *torrent.File) { func getFile(torrents []torrent.Torrent, filename, fragment string) (*torrent.Torrent, *torrent.File) {
for t := range torrents { for t := range torrents {
for f, file := range torrents[t].SelectedFiles { for f, file := range torrents[t].SelectedFiles {
// fmt.Println("~~~~~~~~~~~~~~", torrents[t].Version)
if torrents[t].ID == "ABUNEKZP3UPMU" || torrents[t].ID == "TAA5WUJ6BVEAE" {
fmt.Println("~~~~~~~~~~~~~~", torrents[t].ID, torrents[t].Version, len(torrents[t].Links), len(file.Link))
}
fname := filepath.Base(file.Path) fname := filepath.Base(file.Path)
if filename == fname && strings.HasPrefix(file.Link, fmt.Sprintf("https://real-debrid.com/d/%s", fragment)) { if filename == fname && strings.Contains(file.Link, fragment) {
return &torrents[t], &torrents[t].SelectedFiles[f] return &torrents[t], &torrents[t].SelectedFiles[f]
} }
} }
@@ -219,7 +225,8 @@ func handleRoot(w http.ResponseWriter, r *http.Request, c config.ConfigInterface
htmlDoc := "<ul>" htmlDoc := "<ul>"
for _, directory := range c.GetDirectories() { for _, directory := range c.GetDirectories() {
htmlDoc += fmt.Sprintf("<li><a href=\"/http/%s/\">%s</a></li>", directory, directory) directoryPath := url.PathEscape(directory)
htmlDoc += fmt.Sprintf("<li><a href=\"/http/%s/\">%s</a></li>", directoryPath, directory)
} }
return &htmlDoc, nil return &htmlDoc, nil

View File

@@ -2,6 +2,7 @@ package http
import ( import (
"fmt" "fmt"
"net/url"
"path/filepath" "path/filepath"
"github.com/debridmediamanager.com/zurg/internal/torrent" "github.com/debridmediamanager.com/zurg/internal/torrent"
@@ -23,7 +24,7 @@ func createMultiTorrentResponse(basePath string, torrents []torrent.Torrent) (st
} }
seen[item.Name] = true seen[item.Name] = true
path := filepath.Join(basePath, item.Name) path := filepath.Join(basePath, url.PathEscape(item.Name))
htmlDoc += fmt.Sprintf("<li><a href=\"%s/\">%s</a></li>", path, item.Name) htmlDoc += fmt.Sprintf("<li><a href=\"%s/\">%s</a></li>", path, item.Name)
} }
@@ -59,7 +60,7 @@ func createSingleTorrentResponse(basePath string, torrents []torrent.Torrent) (s
} }
finalName[filename] = true finalName[filename] = true
filePath := filepath.Join(currentPath, filename) filePath := filepath.Join(currentPath, url.PathEscape(filename))
htmlDoc += fmt.Sprintf("<li><a href=\"%s\">%s</a></li>", filePath, filename) htmlDoc += fmt.Sprintf("<li><a href=\"%s\">%s</a></li>", filePath, filename)
} }
} }

View File

@@ -16,6 +16,7 @@ import (
) )
type TorrentManager struct { type TorrentManager struct {
requiredVersion string
torrents []Torrent torrents []Torrent
inProgress []string inProgress []string
checksum string checksum string
@@ -29,6 +30,7 @@ type TorrentManager struct {
// and store them in-memory // and store them in-memory
func NewTorrentManager(config config.ConfigInterface, cache *expirable.LRU[string, string]) *TorrentManager { func NewTorrentManager(config config.ConfigInterface, cache *expirable.LRU[string, string]) *TorrentManager {
t := &TorrentManager{ t := &TorrentManager{
requiredVersion: "v2",
config: config, config: config,
cache: cache, cache: cache,
workerPool: make(chan bool, config.GetNumOfWorkers()), workerPool: make(chan bool, config.GetNumOfWorkers()),
@@ -67,12 +69,13 @@ func (t *TorrentManager) repairAll(wg *sync.WaitGroup) {
for _, torrent := range t.torrents { for _, torrent := range t.torrents {
if torrent.ForRepair { if torrent.ForRepair {
log.Println("Issues detected on", torrent.Name, "; fixing...") log.Println("Issues detected on", torrent.Name, "; fixing...")
t.repair(torrent.ID, torrent.SelectedFiles) t.repair(torrent.ID, torrent.SelectedFiles, true)
} }
if len(torrent.Links) == 0 { if len(torrent.Links) == 0 {
// If the torrent has no links // If the torrent has no links
// and already processing repair // and already processing repair
// delete it! // delete it!
log.Println("Deleting", torrent.Name, "as it has no links")
realdebrid.DeleteTorrent(t.config.GetToken(), torrent.ID) realdebrid.DeleteTorrent(t.config.GetToken(), torrent.ID)
} }
} }
@@ -91,13 +94,11 @@ func (t *TorrentManager) GetByDirectory(directory string) []Torrent {
return torrents return torrents
} }
// MarkFileAsDeleted marks a file as deleted // HideTheFile marks a file as deleted
func (t *TorrentManager) MarkFileAsDeleted(torrent *Torrent, file *File) { func (t *TorrentManager) HideTheFile(torrent *Torrent, file *File) {
log.Println("Marking file as deleted", file.Path)
file.Link = "" file.Link = ""
t.writeToFile(torrent) t.writeToFile(torrent)
log.Println("Healing a single file in the torrent", torrent.Name) t.repair(torrent.ID, []File{*file}, false)
t.repair(torrent.ID, []File{*file})
} }
// FindAllTorrentsWithName finds all torrents in a given directory with a given name // FindAllTorrentsWithName finds all torrents in a given directory with a given name
@@ -252,11 +253,16 @@ func (t *TorrentManager) addMoreInfo(torrent *Torrent) {
// file cache // file cache
torrentFromFile := t.readFromFile(torrent.ID) torrentFromFile := t.readFromFile(torrent.ID)
if torrentFromFile != nil { if torrentFromFile != nil {
fmt.Println("from file!")
// see if api data and file data still match // see if api data and file data still match
// then it means data is still usable // then it means data is still usable
if len(torrentFromFile.Links) == len(torrent.Links) { if len(torrentFromFile.Links) == len(torrent.Links) {
torrent.ForRepair = torrentFromFile.ForRepair torrent.ForRepair = torrentFromFile.ForRepair
torrent.SelectedFiles = torrentFromFile.SelectedFiles torrent.SelectedFiles = torrentFromFile.SelectedFiles[:]
if torrentFromFile.ID == "ABUNEKZP3UPMU" || torrentFromFile.ID == "TAA5WUJ6BVEAE" {
fmt.Println(">>>>>>>>>>>>>>>>", torrentFromFile.ID, len(torrentFromFile.Links), len(torrent.Links), len(torrent.SelectedFiles[0].Link))
torrent.Version = "v2"
}
return return
} }
} }
@@ -306,11 +312,13 @@ func (t *TorrentManager) addMoreInfo(torrent *Torrent) {
selectedFiles[i].Link = link selectedFiles[i].Link = link
} }
} }
// update file cache
if len(selectedFiles) > 0 {
// update the torrent with more data! // update the torrent with more data!
torrent.SelectedFiles = selectedFiles torrent.SelectedFiles = selectedFiles
torrent.ForRepair = forRepair torrent.ForRepair = forRepair
// update file cache
t.writeToFile(torrent) t.writeToFile(torrent)
}
} }
// getByID returns a torrent by its ID // getByID returns a torrent by its ID
@@ -333,6 +341,7 @@ func (t *TorrentManager) writeToFile(torrent *Torrent) {
} }
defer file.Close() defer file.Close()
torrent.Version = t.requiredVersion
dataEncoder := gob.NewEncoder(file) dataEncoder := gob.NewEncoder(file)
dataEncoder.Encode(torrent) dataEncoder.Encode(torrent)
} }
@@ -362,6 +371,9 @@ func (t *TorrentManager) readFromFile(torrentID string) *Torrent {
log.Fatalf("Failed decoding file: %s", err) log.Fatalf("Failed decoding file: %s", err)
return nil return nil
} }
if torrent.Version != t.requiredVersion {
return nil
}
return &torrent return &torrent
} }
@@ -489,7 +501,7 @@ func (t *TorrentManager) organizeChaos(info *realdebrid.Torrent, selectedFiles [
return selectedFiles, isChaotic return selectedFiles, isChaotic
} }
func (t *TorrentManager) repair(torrentID string, selectedFiles []File) { func (t *TorrentManager) repair(torrentID string, selectedFiles []File, tryReinsertionFirst bool) {
torrent := t.getByID(torrentID) torrent := t.getByID(torrentID)
if torrent == nil { if torrent == nil {
return return
@@ -540,7 +552,10 @@ func (t *TorrentManager) repair(torrentID string, selectedFiles []File) {
} }
// first solution: add the same selection, maybe it can be fixed by reinsertion? // first solution: add the same selection, maybe it can be fixed by reinsertion?
success := t.reinsertTorrent(torrent, "", true) success := false
if tryReinsertionFirst {
success = t.reinsertTorrent(torrent, "", true)
}
if !success { if !success {
// if not, last resort: add only the missing files and do it in 2 batches // if not, last resort: add only the missing files and do it in 2 batches
half := len(missingFiles) / 2 half := len(missingFiles) / 2

View File

@@ -3,6 +3,7 @@ package torrent
import "github.com/debridmediamanager.com/zurg/pkg/realdebrid" import "github.com/debridmediamanager.com/zurg/pkg/realdebrid"
type Torrent struct { type Torrent struct {
Version string
realdebrid.Torrent realdebrid.Torrent
Directories []string Directories []string
SelectedFiles []File SelectedFiles []File

View File

@@ -21,10 +21,13 @@ func RetryUntilOk[T any](fn func() (T, error)) T {
} }
func canFetchFirstByte(url string) bool { func canFetchFirstByte(url string) bool {
const maxAttempts = 3
for i := 0; i < maxAttempts; i++ {
// Create a new HTTP request // Create a new HTTP request
req, err := http.NewRequest("GET", url, nil) req, err := http.NewRequest("GET", url, nil)
if err != nil { if err != nil {
return false continue
} }
// Set the Range header to request only the first byte // Set the Range header to request only the first byte
@@ -33,7 +36,8 @@ func canFetchFirstByte(url string) bool {
// Execute the request // Execute the request
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
return false time.Sleep(1 * time.Second) // Add a delay before the next retry
continue
} }
defer resp.Body.Close() defer resp.Body.Close()
@@ -41,14 +45,18 @@ func canFetchFirstByte(url string) bool {
if resp.StatusCode == http.StatusPartialContent { if resp.StatusCode == http.StatusPartialContent {
buffer := make([]byte, 1) buffer := make([]byte, 1)
_, err := resp.Body.Read(buffer) _, err := resp.Body.Read(buffer)
return err == nil if err == nil {
} return true
if resp.StatusCode != http.StatusOK {
return false
} }
} else if resp.StatusCode == http.StatusOK {
// If server doesn't support partial content, try reading the first byte and immediately close // If server doesn't support partial content, try reading the first byte and immediately close
buffer := make([]byte, 1) buffer := make([]byte, 1)
_, err = resp.Body.Read(buffer) _, err = resp.Body.Read(buffer)
resp.Body.Close() // Close immediately after reading if err == nil {
return err == nil return true
}
}
time.Sleep(1 * time.Second) // Add a delay before the next retry
}
return false
} }

View File

@@ -2,4 +2,4 @@
type = http type = http
url = http://zurg:9999/http url = http://zurg:9999/http
no_head = false no_head = false
no_slash = true no_slash = false