From 44216343e2c2752346ee1adebb64b7ba2cee5bc2 Mon Sep 17 00:00:00 2001 From: Ben Sarmiento Date: Tue, 17 Oct 2023 12:36:33 +0200 Subject: [PATCH] Add more metadata --- internal/dav/response.go | 13 ++--- internal/dav/router.go | 2 +- internal/dav/util.go | 15 +++++ pkg/dav/response.go | 2 +- pkg/dav/types.go | 2 +- pkg/dav/util.go | 10 +++- pkg/realdebrid/api.go | 115 +++++++++++++++++++++++++++++++-------- pkg/realdebrid/types.go | 10 ++++ pkg/realdebrid/util.go | 9 +-- 9 files changed, 139 insertions(+), 39 deletions(-) create mode 100644 internal/dav/util.go diff --git a/internal/dav/response.go b/internal/dav/response.go index d8c397b..e35510e 100644 --- a/internal/dav/response.go +++ b/internal/dav/response.go @@ -60,14 +60,14 @@ func createSingleTorrentResponse(torrent realdebrid.Torrent, db *repo.Database) path := filepath.Join(currentPath, filenameV2) response := dav.File( path, - int(unrestrict.Filesize), - torrent.Added, // Assuming you want to use the torrent added time here + unrestrict.Filesize, + convertDate(torrent.Added), link, ) responses = append(responses, response) } else { // This link is not cached yet - unrestrictFn := func() (realdebrid.UnrestrictResponse, error) { + unrestrictFn := func() (*realdebrid.UnrestrictResponse, error) { return realdebrid.UnrestrictCheck(os.Getenv("RD_TOKEN"), link) } unrestrict := realdebrid.RetryUntilOk(unrestrictFn) @@ -79,15 +79,14 @@ func createSingleTorrentResponse(torrent realdebrid.Torrent, db *repo.Database) Host: "", }) continue - } else { - db.Insert(torrent.Hash, torrent.Filename, *unrestrict) } + db.Insert(torrent.Hash, torrent.Filename, *unrestrict) filenameV2 := davextra.InsertLinkFragment(unrestrict.Filename, davextra.GetLinkFragment(unrestrict.Link)) path := filepath.Join(currentPath, filenameV2) response := dav.File( path, - int(unrestrict.Filesize), - torrent.Added, + unrestrict.Filesize, + convertDate(torrent.Added), link, ) responses = append(responses, response) diff --git a/internal/dav/router.go b/internal/dav/router.go index 01f2122..a259e3e 100644 --- a/internal/dav/router.go +++ b/internal/dav/router.go @@ -116,7 +116,7 @@ func Router(mux *http.ServeMux, db *repo.Database) { return } - unrestrictFn := func() (realdebrid.UnrestrictResponse, error) { + unrestrictFn := func() (*realdebrid.UnrestrictResponse, error) { return realdebrid.UnrestrictLink(os.Getenv("RD_TOKEN"), unrestrict.Link) } resp := realdebrid.RetryUntilOk(unrestrictFn) diff --git a/internal/dav/util.go b/internal/dav/util.go new file mode 100644 index 0000000..ac35cee --- /dev/null +++ b/internal/dav/util.go @@ -0,0 +1,15 @@ +package dav + +import ( + "fmt" + "time" +) + +func convertDate(input string) string { + t, err := time.Parse(time.RFC3339, input) + if err != nil { + fmt.Println("Error:", err) + return "" + } + return t.Format("Mon, 02 Jan 2006 15:04:05 GMT") +} diff --git a/pkg/dav/response.go b/pkg/dav/response.go index ebb6933..60d65f9 100644 --- a/pkg/dav/response.go +++ b/pkg/dav/response.go @@ -12,7 +12,7 @@ func Directory(path string) Response { } } -func File(path string, fileSize int, added string, link string) Response { +func File(path string, fileSize int64, added string, link string) Response { return Response{ Href: customPathEscape(path), Propstat: PropStat{ diff --git a/pkg/dav/types.go b/pkg/dav/types.go index 418af77..453960d 100644 --- a/pkg/dav/types.go +++ b/pkg/dav/types.go @@ -20,7 +20,7 @@ type PropStat struct { type Prop struct { ResourceType ResourceType `xml:"d:resourcetype"` - ContentLength int `xml:"d:getcontentlength"` + ContentLength int64 `xml:"d:getcontentlength"` CreationDate string `xml:"d:creationdate"` LastModified string `xml:"d:getlastmodified"` IsHidden int `xml:"d:ishidden"` diff --git a/pkg/dav/util.go b/pkg/dav/util.go index 4d4cdb3..b83fa8d 100644 --- a/pkg/dav/util.go +++ b/pkg/dav/util.go @@ -6,9 +6,17 @@ import ( ) func customPathEscape(input string) string { + // First, URL-escape the path segments segments := strings.Split(input, "/") for i, segment := range segments { segments[i] = url.PathEscape(segment) } - return strings.Join(segments, "/") + escapedPath := strings.Join(segments, "/") + // Convert any XML-escaped sequences back to URL-escaped sequences + escapedPath = strings.Replace(escapedPath, "&", "%26", -1) // for & + escapedPath = strings.Replace(escapedPath, "<", "%3C", -1) // for < + escapedPath = strings.Replace(escapedPath, ">", "%3E", -1) // for > + escapedPath = strings.Replace(escapedPath, "\"", "%22", -1) // for " + escapedPath = strings.Replace(escapedPath, "'", "%27", -1) // for ' + return escapedPath } diff --git a/pkg/realdebrid/api.go b/pkg/realdebrid/api.go index dea48c9..736a746 100644 --- a/pkg/realdebrid/api.go +++ b/pkg/realdebrid/api.go @@ -8,15 +8,16 @@ import ( "net/http" "net/url" "strconv" + "time" ) -func UnrestrictCheck(accessToken, link string) (UnrestrictResponse, error) { +func UnrestrictCheck(accessToken, link string) (*UnrestrictResponse, error) { data := url.Values{} data.Set("link", link) req, err := http.NewRequest("POST", "https://api.real-debrid.com/rest/1.0/unrestrict/check", bytes.NewBufferString(data.Encode())) if err != nil { - return UnrestrictResponse{}, err + return nil, err } req.Header.Set("Authorization", "Bearer "+accessToken) @@ -25,35 +26,35 @@ func UnrestrictCheck(accessToken, link string) (UnrestrictResponse, error) { client := &http.Client{} resp, err := client.Do(req) if err != nil { - return UnrestrictResponse{}, err + return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { - return UnrestrictResponse{}, err + return nil, err } if resp.StatusCode != http.StatusOK { - return UnrestrictResponse{}, fmt.Errorf("HTTP error: %s", resp.Status) + return nil, fmt.Errorf("HTTP error: %s", resp.Status) } var response UnrestrictResponse err = json.Unmarshal(body, &response) if err != nil { - return UnrestrictResponse{}, err + return nil, err } - return response, nil + return &response, nil } -func UnrestrictLink(accessToken, link string) (UnrestrictResponse, error) { +func UnrestrictLink(accessToken, link string) (*UnrestrictResponse, error) { data := url.Values{} data.Set("link", link) req, err := http.NewRequest("POST", "https://api.real-debrid.com/rest/1.0/unrestrict/link", bytes.NewBufferString(data.Encode())) if err != nil { - return UnrestrictResponse{}, err + return nil, err } req.Header.Set("Authorization", "Bearer "+accessToken) @@ -62,33 +63,33 @@ func UnrestrictLink(accessToken, link string) (UnrestrictResponse, error) { client := &http.Client{} resp, err := client.Do(req) if err != nil { - return UnrestrictResponse{}, err + return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { - return UnrestrictResponse{}, err + return nil, err } if resp.StatusCode != http.StatusOK { - return UnrestrictResponse{}, fmt.Errorf("HTTP error: %s", resp.Status) + return nil, fmt.Errorf("HTTP error: %s", resp.Status) } var response UnrestrictResponse err = json.Unmarshal(body, &response) if err != nil { - return UnrestrictResponse{}, err + return nil, err } - return response, nil + return &response, nil } func GetTorrents(accessToken string) ([]Torrent, error) { baseURL := "https://api.real-debrid.com/rest/1.0/torrents" var allTorrents []Torrent page := 1 - limit := 2500 + limit := 100 for { params := url.Values{} @@ -124,7 +125,7 @@ func GetTorrents(accessToken string) ([]Torrent, error) { allTorrents = append(allTorrents, torrents...) - totalCountHeader := resp.Header.Get("x-total-count") + totalCountHeader := "100" // resp.Header.Get("x-total-count") totalCount, err := strconv.Atoi(totalCountHeader) if err != nil { break @@ -140,25 +141,67 @@ func GetTorrents(accessToken string) ([]Torrent, error) { return deduplicateTorrents(allTorrents), nil } +func GetTorrentInfo(accessToken, id string) (*Torrent, error) { + url := "https://api.real-debrid.com/rest/1.0/torrents/info/" + id + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + + req.Header.Set("Authorization", "Bearer "+accessToken) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("HTTP error: %s", resp.Status) + } + + var response Torrent + err = json.Unmarshal(body, &response) + if err != nil { + return nil, err + } + + return &response, nil +} + func deduplicateTorrents(torrents []Torrent) []Torrent { mappedTorrents := make(map[string]Torrent) for _, t := range torrents { - if existing, ok := mappedTorrents[t.Filename]; ok { + torrentName := t.Filename + if existing, ok := mappedTorrents[torrentName]; ok { if existing.Hash == t.Hash { // If hash is the same, combine the links - existing.Links = append(existing.Links, t.Links...) - mappedTorrents[t.Filename] = existing + existing.ID += "," + t.ID + // existing.Links = append(existing.Links, t.Links...) + for _, link := range t.Links { + existing.Links = appendIfNotExists(existing.Links, link) + } + existing.Bytes += t.Bytes + existing.Added = moreRecent(existing.Added, t.Added) + mappedTorrents[torrentName] = existing } else { // If hash is different, delete old entry and create two new entries - delete(mappedTorrents, t.Filename) - newKey1 := fmt.Sprintf("%s - %s", t.Filename, t.Hash[:4]) + delete(mappedTorrents, torrentName) + newKey1 := fmt.Sprintf("%s - %s", torrentName, t.Hash[:4]) mappedTorrents[newKey1] = t newKey2 := fmt.Sprintf("%s - %s", existing.Filename, existing.Hash[:4]) mappedTorrents[newKey2] = existing } } else { - mappedTorrents[t.Filename] = t + mappedTorrents[torrentName] = t } } @@ -170,3 +213,31 @@ func deduplicateTorrents(torrents []Torrent) []Torrent { return deduplicated } + +func contains(slice []string, str string) bool { + for _, v := range slice { + if v == str { + return true + } + } + return false +} + +func appendIfNotExists(slice []string, str string) []string { + if !contains(slice, str) { + slice = append(slice, str) + } + return slice +} + +func moreRecent(time1, time2 string) string { + tTime1, err1 := time.Parse(time.RFC3339, time1) + tTime2, err2 := time.Parse(time.RFC3339, time2) + if err1 != nil || err2 != nil { + return time1 + } + if tTime2.After(tTime1) { + time1 = time2 + } + return time1 +} diff --git a/pkg/realdebrid/types.go b/pkg/realdebrid/types.go index 373ec11..7f6c0fd 100644 --- a/pkg/realdebrid/types.go +++ b/pkg/realdebrid/types.go @@ -14,9 +14,19 @@ type UnrestrictResponse struct { } type Torrent struct { + ID string `json:"id"` Filename string `json:"filename"` Hash string `json:"hash"` Progress int `json:"progress"` Added string `json:"added"` + Bytes int64 `json:"bytes"` Links []string `json:"links"` + Files []File `json:"files,omitempty"` +} + +type File struct { + ID int `json:"id"` + Path string `json:"path"` + Bytes int `json:"bytes"` + Selected int `json:"selected"` } diff --git a/pkg/realdebrid/util.go b/pkg/realdebrid/util.go index 74ec045..1f8507e 100644 --- a/pkg/realdebrid/util.go +++ b/pkg/realdebrid/util.go @@ -6,16 +6,13 @@ import ( "time" ) -func RetryUntilOk[T any](fn func() (T, error)) *T { +func RetryUntilOk[T any](fn func() (T, error)) T { const initialDelay = 2 * time.Second const maxDelay = 128 * time.Second for i := 0; ; i++ { result, err := fn() - if err == nil { - return &result - } - if strings.Contains(err.Error(), "404") { - return nil + if err == nil || strings.Contains(err.Error(), "404") { + return result } delay := time.Duration(math.Min(float64(initialDelay*time.Duration(math.Pow(2, float64(i)))), float64(maxDelay))) time.Sleep(delay)