diff --git a/internal/dav/response.go b/internal/dav/response.go index c28ec5e..d8c397b 100644 --- a/internal/dav/response.go +++ b/internal/dav/response.go @@ -5,6 +5,7 @@ import ( "path/filepath" "github.com/debridmediamanager.com/zurg/pkg/dav" + "github.com/debridmediamanager.com/zurg/pkg/davextra" "github.com/debridmediamanager.com/zurg/pkg/realdebrid" "github.com/debridmediamanager.com/zurg/pkg/repo" ) @@ -45,21 +46,23 @@ func createSingleTorrentResponse(torrent realdebrid.Torrent, db *repo.Database) // Create a map for O(1) lookups of the cached links cachedLinksMap := make(map[string]*repo.DavFile) - for _, u := range davFiles.Files { - cachedLinksMap[u.Link] = u + for _, cached := range davFiles.Files { + cachedLinksMap[cached.Link] = cached } for _, link := range torrent.Links { - if u, exists := cachedLinksMap[link]; exists { - if u.Filesize == 0 { + if unrestrict, exists := cachedLinksMap[link]; exists { + if unrestrict.Filesize == 0 { // This link is cached but the filesize is 0 // This means that the link is dead continue } - path := filepath.Join(currentPath, u.Filename) + filenameV2 := davextra.InsertLinkFragment(unrestrict.Filename, davextra.GetLinkFragment(unrestrict.Link)) + path := filepath.Join(currentPath, filenameV2) response := dav.File( path, - int(u.Filesize), + int(unrestrict.Filesize), torrent.Added, // Assuming you want to use the torrent added time here + link, ) responses = append(responses, response) } else { @@ -67,8 +70,8 @@ func createSingleTorrentResponse(torrent realdebrid.Torrent, db *repo.Database) unrestrictFn := func() (realdebrid.UnrestrictResponse, error) { return realdebrid.UnrestrictCheck(os.Getenv("RD_TOKEN"), link) } - unrestrictResponse := realdebrid.RetryUntilOk(unrestrictFn) - if unrestrictResponse == nil { + unrestrict := realdebrid.RetryUntilOk(unrestrictFn) + if unrestrict == nil { db.Insert(torrent.Hash, torrent.Filename, realdebrid.UnrestrictResponse{ Filename: "", Filesize: 0, @@ -77,21 +80,20 @@ func createSingleTorrentResponse(torrent realdebrid.Torrent, db *repo.Database) }) continue } else { - db.Insert(torrent.Hash, torrent.Filename, *unrestrictResponse) + db.Insert(torrent.Hash, torrent.Filename, *unrestrict) } - - path := filepath.Join(currentPath, unrestrictResponse.Filename) + filenameV2 := davextra.InsertLinkFragment(unrestrict.Filename, davextra.GetLinkFragment(unrestrict.Link)) + path := filepath.Join(currentPath, filenameV2) response := dav.File( path, - int(unrestrictResponse.Filesize), + int(unrestrict.Filesize), torrent.Added, + link, ) responses = append(responses, response) } } - // TODO: dedupe the links in the response - return &dav.MultiStatus{ XMLNS: "DAV:", Response: responses, diff --git a/internal/dav/router.go b/internal/dav/router.go index 674ce2b..01f2122 100644 --- a/internal/dav/router.go +++ b/internal/dav/router.go @@ -14,7 +14,7 @@ import ( "github.com/debridmediamanager.com/zurg/pkg/repo" ) -func findTorrentByFilename(torrents []realdebrid.Torrent, filename string) *realdebrid.Torrent { +func findTorrentByName(torrents []realdebrid.Torrent, filename string) *realdebrid.Torrent { for _, torrent := range torrents { if torrent.Filename == filename { return &torrent @@ -41,9 +41,10 @@ func Router(mux *http.ServeMux, db *repo.Database) { mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { requestPath := path.Clean(r.URL.Path) + log.Println(r.Method, requestPath) + switch r.Method { case "PROPFIND": - log.Println("PROPFIND", requestPath) var output []byte var err error @@ -59,8 +60,8 @@ func Router(mux *http.ServeMux, db *repo.Database) { } output, err = xml.MarshalIndent(allTorrentsResponse, "", " ") } else { - lastSegment := path.Base(requestPath) - torrent := findTorrentByFilename(torrents, lastSegment) + torrentName := path.Base(requestPath) + torrent := findTorrentByName(torrents, torrentName) if torrent == nil { log.Println("Cannot find directory") http.Error(w, "Cannot find directory", http.StatusNotFound) @@ -88,33 +89,41 @@ func Router(mux *http.ServeMux, db *repo.Database) { fmt.Fprintf(w, "\n%s\n", output) case http.MethodOptions: - log.Println("OPTIONS", requestPath) w.WriteHeader(http.StatusOK) case http.MethodGet: - log.Println("GET", requestPath) segments := strings.Split(requestPath, "/") - - // If there are less than 2 segments, return an error or adjust as needed - if len(segments) < 2 { + // If there are less than 3 segments, return an error or adjust as needed + if len(segments) < 3 { log.Println("Cannot find file") http.Error(w, "Cannot find file", http.StatusNotFound) } // Get the last two segments - secondLast := segments[len(segments)-2] - last := segments[len(segments)-1] - unrestrict, dbErr := db.Get(secondLast, last) + torrentName := segments[len(segments)-2] + torrent := findTorrentByName(torrents, torrentName) + if torrent == nil { + log.Println("Cannot find directory") + http.Error(w, "Cannot find directory", http.StatusNotFound) + return + } + + filename := segments[len(segments)-1] + unrestrict, dbErr := db.Get(torrent.Hash, filename) if dbErr != nil { log.Printf("Cannot find file in db: %v", dbErr.Error()) http.Error(w, fmt.Sprintf("Cannot find file in db: %v", dbErr.Error()), http.StatusInternalServerError) return } - resp, err := realdebrid.UnrestrictLink(os.Getenv("RD_TOKEN"), unrestrict.Link) - if err != nil { + unrestrictFn := func() (realdebrid.UnrestrictResponse, error) { + return realdebrid.UnrestrictLink(os.Getenv("RD_TOKEN"), unrestrict.Link) + } + resp := realdebrid.RetryUntilOk(unrestrictFn) + if resp == nil { + // TODO: Delete the link from the database log.Printf("Cannot unrestrict link: %v", err.Error()) - http.Error(w, fmt.Sprintf("Cannot unrestrict link: %v", err.Error()), http.StatusInternalServerError) + http.Error(w, fmt.Sprintf("Cannot unrestrict link: %v", err.Error()), http.StatusNotFound) return } http.Redirect(w, r, resp.Download, http.StatusFound) diff --git a/pkg/dav/response.go b/pkg/dav/response.go index ac53ec7..ebb6933 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) Response { +func File(path string, fileSize int, added string, link string) Response { return Response{ Href: customPathEscape(path), Propstat: PropStat{ @@ -21,6 +21,7 @@ func File(path string, fileSize int, added string) Response { IsHidden: 0, CreationDate: added, LastModified: added, + Link: link, }, Status: "HTTP/1.1 200 OK", }, diff --git a/pkg/dav/types.go b/pkg/dav/types.go index 1458281..418af77 100644 --- a/pkg/dav/types.go +++ b/pkg/dav/types.go @@ -24,6 +24,8 @@ type Prop struct { CreationDate string `xml:"d:creationdate"` LastModified string `xml:"d:getlastmodified"` IsHidden int `xml:"d:ishidden"` + Filename string `xml:"-"` + Link string `xml:"-"` } type ResourceType struct { diff --git a/pkg/davextra/util.go b/pkg/davextra/util.go new file mode 100644 index 0000000..5cc8d60 --- /dev/null +++ b/pkg/davextra/util.go @@ -0,0 +1,26 @@ +package davextra + +import ( + "fmt" + "path/filepath" + "regexp" + "strings" +) + +func GetLinkFragment(link string) string { + re := regexp.MustCompile(`\w+`) + matches := re.FindAllString(link, -1) // Returns all matches + var longestMatch string + for _, match := range matches { + if len(match) > len(longestMatch) { + longestMatch = match + } + } + return longestMatch[:4] +} + +func InsertLinkFragment(filename, dupeID string) string { + ext := filepath.Ext(filename) + name := strings.TrimSuffix(filename, ext) + return fmt.Sprintf("%s DMM%s%s", name, dupeID, ext) +} diff --git a/pkg/realdebrid/api.go b/pkg/realdebrid/api.go index d8e5d46..dea48c9 100644 --- a/pkg/realdebrid/api.go +++ b/pkg/realdebrid/api.go @@ -88,7 +88,7 @@ func GetTorrents(accessToken string) ([]Torrent, error) { baseURL := "https://api.real-debrid.com/rest/1.0/torrents" var allTorrents []Torrent page := 1 - limit := 10 + limit := 2500 for { params := url.Values{} @@ -124,7 +124,7 @@ func GetTorrents(accessToken string) ([]Torrent, error) { allTorrents = append(allTorrents, torrents...) - totalCountHeader := "10" // resp.Header.Get("x-total-count") + totalCountHeader := resp.Header.Get("x-total-count") totalCount, err := strconv.Atoi(totalCountHeader) if err != nil { break @@ -152,9 +152,9 @@ func deduplicateTorrents(torrents []Torrent) []Torrent { } else { // If hash is different, delete old entry and create two new entries delete(mappedTorrents, t.Filename) - newKey1 := t.Filename + " - " + t.Hash[:4] - newKey2 := existing.Filename + " - " + existing.Hash[:4] + newKey1 := fmt.Sprintf("%s - %s", t.Filename, t.Hash[:4]) mappedTorrents[newKey1] = t + newKey2 := fmt.Sprintf("%s - %s", existing.Filename, existing.Hash[:4]) mappedTorrents[newKey2] = existing } } else { diff --git a/pkg/realdebrid/types.go b/pkg/realdebrid/types.go index 2cf602b..373ec11 100644 --- a/pkg/realdebrid/types.go +++ b/pkg/realdebrid/types.go @@ -6,12 +6,11 @@ type FileJSON struct { } type UnrestrictResponse struct { - Filename string `json:"filename"` - Filesize int64 `json:"filesize"` - Link string `json:"link"` - Host string `json:"host"` - Download string `json:"download,omitempty"` - Streamable int `json:"streamable,omitempty"` + Filename string `json:"filename"` + Filesize int64 `json:"filesize"` + Link string `json:"link"` + Host string `json:"host"` + Download string `json:"download,omitempty"` } type Torrent struct { diff --git a/pkg/repo/mysql.go b/pkg/repo/mysql.go index c7b36b7..4f3c030 100644 --- a/pkg/repo/mysql.go +++ b/pkg/repo/mysql.go @@ -6,8 +6,13 @@ import ( "encoding/gob" "fmt" "log" + "net/url" "path" + "path/filepath" + "regexp" + "strings" + "github.com/debridmediamanager.com/zurg/pkg/davextra" "github.com/debridmediamanager.com/zurg/pkg/realdebrid" _ "github.com/go-sql-driver/mysql" "github.com/qianbin/directcache" @@ -19,8 +24,8 @@ type Database struct { Cache *directcache.Cache } -func GenerateID(directory, filename string) string { - fullPath := path.Join(directory, filename) +func GenerateID(segment1, segment2, segment3 string) string { + fullPath := path.Join(segment1, segment2, segment3) hash := xxh3.HashString(fullPath) return fmt.Sprintf("%016x", hash) } @@ -36,13 +41,14 @@ func NewDatabase(dsn string) (*Database, error) { return &Database{Connection: db, Cache: cache}, nil } -func (db *Database) Insert(parentHash, directory string, resp realdebrid.UnrestrictResponse) { +func (db *Database) Insert(parentHash, torrentName string, resp realdebrid.UnrestrictResponse) { // Generate the ID for the link var id string if resp.Filename == "" { - id = GenerateID(directory, resp.Link) + // alternative ID for 404 links + id = GenerateID(parentHash, resp.Link, "") } else { - id = GenerateID(directory, resp.Filename) + id = GenerateID(parentHash, resp.Filename, davextra.GetLinkFragment(resp.Link)) } // Check if the link already exists in the database var exists int @@ -58,7 +64,7 @@ func (db *Database) Insert(parentHash, directory string, resp realdebrid.Unrestr VALUES (?, ?, ?, ?, ?, ?, ?)`, id, parentHash, - directory, + torrentName, resp.Filename, resp.Filesize, resp.Link, @@ -73,11 +79,12 @@ func (db *Database) Insert(parentHash, directory string, resp realdebrid.Unrestr } } -func (db *Database) Get(directory, filename string) (*DavFile, error) { - id := GenerateID(directory, filename) +func (db *Database) Get(parentHash, filename string) (*DavFile, error) { + filenameV2, linkFragment := extractIDFromFilename(filename) + id := GenerateID(parentHash, filenameV2, linkFragment) data, ok := db.Cache.Get([]byte(id)) if !ok { - resp, err := fetchFromDatabaseByID(db.Connection, id) + resp, err := fetchFromDatabaseByID(db.Connection, id, linkFragment) if err != nil { return nil, err } @@ -101,6 +108,26 @@ func (db *Database) Get(directory, filename string) (*DavFile, error) { return &resp, nil } +func extractIDFromFilename(filename string) (string, string) { + filenameV2, err := url.PathUnescape(filename) + if err != nil { + filenameV2 = filename + } + ext := filepath.Ext(filenameV2) + name := strings.TrimSuffix(filenameV2, ext) + + r := regexp.MustCompile(`\sDMM(\w+)`) + matches := r.FindStringSubmatch(name) + if len(matches) < 2 { + // No ID found + return filenameV2, "" + } + + // Remove ID from filename + originalName := strings.Replace(name, matches[0], "", 1) + return originalName + ext, matches[1] +} + func (db *Database) GetMultiple(parentHash string) (*DavFiles, error) { key := []byte(parentHash) data, ok := db.Cache.Get(key) @@ -130,24 +157,22 @@ func (db *Database) GetMultiple(parentHash string) (*DavFiles, error) { return &resps, nil } -func fetchFromDatabaseByID(conn *sql.DB, id string) (*DavFile, error) { +func fetchFromDatabaseByID(conn *sql.DB, id, linkFragment string) (*DavFile, error) { log.Printf("fetching from database: %s", id) var resp DavFile - err := conn.QueryRow(` - SELECT Filename, Filesize, Link - FROM Links WHERE ID = ?`, - id, - ).Scan( - &resp.Filename, - &resp.Filesize, - &resp.Link, - ) + query := ` + SELECT Filename, Filesize, Link + FROM Links WHERE ID = ? AND Link LIKE ?` + row := conn.QueryRow(query, id, "https://real-debrid.com/d/"+linkFragment+"%") + + err := row.Scan(&resp.Filename, &resp.Filesize, &resp.Link) if err != nil { if err == sql.ErrNoRows { - return &resp, nil + return nil, nil } log.Printf("failed to fetch record: %v", err) + return nil, err } return &resp, nil