finalize experimental
This commit is contained in:
@@ -10,3 +10,4 @@ torrents.json
|
|||||||
**/dist/
|
**/dist/
|
||||||
*.json
|
*.json
|
||||||
.ven/**
|
.ven/**
|
||||||
|
docs/**
|
||||||
|
|||||||
@@ -61,6 +61,6 @@ EXPOSE 8181 8282
|
|||||||
VOLUME ["/app"]
|
VOLUME ["/app"]
|
||||||
USER nonroot:nonroot
|
USER nonroot:nonroot
|
||||||
|
|
||||||
HEALTHCHECK CMD ["/usr/bin/healthcheck"]
|
HEALTHCHECK --retries=3 CMD ["/usr/bin/healthcheck", "--config", "/app"]
|
||||||
|
|
||||||
CMD ["/usr/bin/decypharr", "--config", "/app"]
|
CMD ["/usr/bin/decypharr", "--config", "/app"]
|
||||||
@@ -1,22 +1,141 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"cmp"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"github.com/sirrobot01/decypharr/internal/config"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// HealthStatus represents the status of various components
|
||||||
|
type HealthStatus struct {
|
||||||
|
QbitAPI bool `json:"qbit_api"`
|
||||||
|
WebUI bool `json:"web_ui"`
|
||||||
|
WebDAVService bool `json:"webdav_service,omitempty"`
|
||||||
|
OverallStatus bool `json:"overall_status"`
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
port := cmp.Or(os.Getenv("QBIT_PORT"), "8282")
|
var configPath string
|
||||||
resp, err := http.Get("http://localhost:" + port + "/api/v2/app/version")
|
flag.StringVar(&configPath, "config", "/data", "path to the data folder")
|
||||||
if err != nil {
|
flag.Parse()
|
||||||
|
config.SetConfigPath(configPath)
|
||||||
|
cfg := config.Get()
|
||||||
|
// Get port from environment variable or use default
|
||||||
|
qbitPort := getEnvOrDefault("QBIT_PORT", cfg.QBitTorrent.Port)
|
||||||
|
webdavPath := ""
|
||||||
|
for _, debrid := range cfg.Debrids {
|
||||||
|
if debrid.UseWebDav {
|
||||||
|
webdavPath = debrid.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize status
|
||||||
|
status := HealthStatus{
|
||||||
|
QbitAPI: false,
|
||||||
|
WebUI: false,
|
||||||
|
WebDAVService: false,
|
||||||
|
OverallStatus: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a context with timeout for all HTTP requests
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Check qBittorrent API
|
||||||
|
if checkQbitAPI(ctx, qbitPort) {
|
||||||
|
status.QbitAPI = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Web UI
|
||||||
|
if checkWebUI(ctx, qbitPort) {
|
||||||
|
status.WebUI = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check WebDAV if enabled
|
||||||
|
if webdavPath != "" {
|
||||||
|
if checkWebDAV(ctx, qbitPort, webdavPath) {
|
||||||
|
status.WebDAVService = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If WebDAV is not enabled, consider it healthy
|
||||||
|
status.WebDAVService = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine overall status
|
||||||
|
// Consider the application healthy if core services are running
|
||||||
|
status.OverallStatus = status.QbitAPI && status.WebUI
|
||||||
|
if webdavPath != "" {
|
||||||
|
status.OverallStatus = status.OverallStatus && status.WebDAVService
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional: output health status as JSON for logging
|
||||||
|
if os.Getenv("DEBUG") == "true" {
|
||||||
|
statusJSON, _ := json.MarshalIndent(status, "", " ")
|
||||||
|
fmt.Println(string(statusJSON))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit with appropriate code
|
||||||
|
if status.OverallStatus {
|
||||||
|
os.Exit(0)
|
||||||
|
} else {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnvOrDefault(key, defaultValue string) string {
|
||||||
|
if value, exists := os.LookupEnv(key); exists {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkQbitAPI(ctx context.Context, port string) bool {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("http://localhost:%s/api/v2/app/version", port), nil)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
return resp.StatusCode == http.StatusOK
|
||||||
os.Exit(1)
|
}
|
||||||
|
|
||||||
|
func checkWebUI(ctx context.Context, port string) bool {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("http://localhost:%s/", port), nil)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
os.Exit(0)
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
return resp.StatusCode == http.StatusOK
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkWebDAV(ctx context.Context, port, path string) bool {
|
||||||
|
url := fmt.Sprintf("http://localhost:%s/webdav/%s", port, path)
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "PROPFIND", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
return resp.StatusCode == 207 || resp.StatusCode == http.StatusOK
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.6.0
|
||||||
|
|
||||||
|
- Add WebDAV support for all debrid providers
|
||||||
|
- Some refactors and code cleanup
|
||||||
|
- Fixes
|
||||||
|
- Fix Alldebrid not downloading torrents
|
||||||
|
- Fix Alldebrid not downloading uncached torrents
|
||||||
|
- Fix uncached torrents not being downloaded for RealDebrid
|
||||||
|
- Add support for multiple download API keys for debrid providers
|
||||||
|
|
||||||
|
|
||||||
## 0.5.0
|
## 0.5.0
|
||||||
|
|
||||||
- A more refined repair worker (with more control)
|
- A more refined repair worker (with more control)
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ Each Arr application supports the following options:
|
|||||||
#### Sonarr/Radarr/Lidarr
|
#### Sonarr/Radarr/Lidarr
|
||||||
|
|
||||||
1. Go to Sonarr > Settings > General
|
1. Go to Sonarr > Settings > General
|
||||||
2. Look for "API Key" in the "General" section
|
2. Look for "API Key" in the "Security" section
|
||||||
3. Copy the API key
|
3. Copy the API key
|
||||||
|
|
||||||
### Multiple Arr Applications
|
### Multiple Arr Applications
|
||||||
|
|||||||
@@ -43,7 +43,17 @@ Each Debrid provider accepts the following configuration options:
|
|||||||
- `download_uncached`: Whether to download uncached torrents (disabled by default)
|
- `download_uncached`: Whether to download uncached torrents (disabled by default)
|
||||||
- `check_cached`: Whether to check if torrents are cached (disabled by default)
|
- `check_cached`: Whether to check if torrents are cached (disabled by default)
|
||||||
- `use_webdav`: Whether to create a WebDAV server for this Debrid provider (disabled by default)
|
- `use_webdav`: Whether to create a WebDAV server for this Debrid provider (disabled by default)
|
||||||
|
- `torrents_refresh_interval`: Interval for refreshing torrent data (e.g., `15s`, `1m`, `1h`).
|
||||||
|
- `download_links_refresh_interval`: Interval for refreshing download links (e.g., `40m`, `1h`).
|
||||||
|
- `workers`: Number of concurrent workers for processing requests.
|
||||||
|
- folder_naming: Naming convention for folders:
|
||||||
|
- `original_no_ext`: Original file name without extension
|
||||||
|
- `original`: Original file name with extension
|
||||||
|
- `filename`: Torrent filename
|
||||||
|
- `filename_no_ext`: Torrent filename without extension
|
||||||
|
- `id`: Torrent ID
|
||||||
|
- `auto_expire_links_after`: Time after which download links will expire (e.g., `3d`, `1w`).
|
||||||
|
- `rc_url`, `rc_user`, `rc_pass`: Rclone RC configuration for VFS refreshes
|
||||||
|
|
||||||
### Using Multiple API Keys
|
### Using Multiple API Keys
|
||||||
For services that support it, you can provide multiple download API keys for better load balancing:
|
For services that support it, you can provide multiple download API keys for better load balancing:
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ Here's a minimal configuration to get started:
|
|||||||
"name": "realdebrid",
|
"name": "realdebrid",
|
||||||
"host": "https://api.real-debrid.com/rest/1.0",
|
"host": "https://api.real-debrid.com/rest/1.0",
|
||||||
"api_key": "realdebrid_key",
|
"api_key": "realdebrid_key",
|
||||||
"folder": "/mnt/remote/realdebrid/__all__/"
|
"folder": "/mnt/remote/realdebrid/__all__/",
|
||||||
|
"use_webdav": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"qbittorrent": {
|
"qbittorrent": {
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ You can mount the WebDAV server locally using Rclone. Example configuration:
|
|||||||
```conf
|
```conf
|
||||||
[decypharr]
|
[decypharr]
|
||||||
type = webdav
|
type = webdav
|
||||||
url = http://localhost:8080/webdav/realdebrid
|
url = http://localhost:8282/webdav/realdebrid
|
||||||
vendor = other
|
vendor = other
|
||||||
```
|
```
|
||||||
For a complete Rclone configuration example, see our [sample rclone.conf](../extras/rclone.conf).
|
For a complete Rclone configuration example, see our [sample rclone.conf](../extras/rclone.conf).
|
||||||
4
go.mod
4
go.mod
@@ -8,15 +8,12 @@ require (
|
|||||||
github.com/anacrolix/torrent v1.55.0
|
github.com/anacrolix/torrent v1.55.0
|
||||||
github.com/beevik/etree v1.5.0
|
github.com/beevik/etree v1.5.0
|
||||||
github.com/cavaliergopher/grab/v3 v3.0.1
|
github.com/cavaliergopher/grab/v3 v3.0.1
|
||||||
github.com/elazarl/goproxy v0.0.0-20240726154733-8b0c20506380
|
|
||||||
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2
|
|
||||||
github.com/go-chi/chi/v5 v5.1.0
|
github.com/go-chi/chi/v5 v5.1.0
|
||||||
github.com/goccy/go-json v0.10.5
|
github.com/goccy/go-json v0.10.5
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/gorilla/sessions v1.4.0
|
github.com/gorilla/sessions v1.4.0
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.5.1
|
github.com/puzpuzpuz/xsync/v3 v3.5.1
|
||||||
github.com/rs/zerolog v1.33.0
|
github.com/rs/zerolog v1.33.0
|
||||||
github.com/valyala/fastjson v1.6.4
|
|
||||||
golang.org/x/crypto v0.33.0
|
golang.org/x/crypto v0.33.0
|
||||||
golang.org/x/net v0.35.0
|
golang.org/x/net v0.35.0
|
||||||
golang.org/x/sync v0.12.0
|
golang.org/x/sync v0.12.0
|
||||||
@@ -38,5 +35,4 @@ require (
|
|||||||
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
||||||
github.com/stretchr/testify v1.10.0 // indirect
|
github.com/stretchr/testify v1.10.0 // indirect
|
||||||
golang.org/x/sys v0.30.0 // indirect
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
golang.org/x/text v0.22.0 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
9
go.sum
9
go.sum
@@ -61,10 +61,6 @@ github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25Kn
|
|||||||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||||
github.com/elazarl/goproxy v0.0.0-20240726154733-8b0c20506380 h1:1NyRx2f4W4WBRyg0Kys0ZbaNmDDzZ2R/C7DTi+bbsJ0=
|
|
||||||
github.com/elazarl/goproxy v0.0.0-20240726154733-8b0c20506380/go.mod h1:thX175TtLTzLj3p7N/Q9IiKZ7NF+p72cvL91emV0hzo=
|
|
||||||
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 h1:dWB6v3RcOy03t/bUadywsbyrQwCqZeNIEX6M1OtSZOM=
|
|
||||||
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8=
|
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
@@ -190,7 +186,6 @@ github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4
|
|||||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
|
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||||
github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc=
|
|
||||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
@@ -214,8 +209,6 @@ github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf
|
|||||||
github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||||
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||||
github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||||
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
|
|
||||||
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
|
|
||||||
github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
||||||
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
||||||
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||||
@@ -269,8 +262,6 @@ golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
|||||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
|
||||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
|
||||||
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
||||||
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
|||||||
@@ -202,9 +202,8 @@ func validateConfig(config *Config) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetConfigPath(path string) error {
|
func SetConfigPath(path string) {
|
||||||
configPath = path
|
configPath = path
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Get() *Config {
|
func Get() *Config {
|
||||||
|
|||||||
@@ -62,23 +62,6 @@ type Client struct {
|
|||||||
retryableStatus map[int]struct{}
|
retryableStatus map[int]struct{}
|
||||||
logger zerolog.Logger
|
logger zerolog.Logger
|
||||||
proxy string
|
proxy string
|
||||||
|
|
||||||
// cooldown
|
|
||||||
statusCooldowns map[int]time.Duration
|
|
||||||
statusCooldownsMu sync.RWMutex
|
|
||||||
lastStatusTime map[int]time.Time
|
|
||||||
lastStatusTimeMu sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithStatusCooldown(statusCode int, cooldown time.Duration) ClientOption {
|
|
||||||
return func(c *Client) {
|
|
||||||
c.statusCooldownsMu.Lock()
|
|
||||||
if c.statusCooldowns == nil {
|
|
||||||
c.statusCooldowns = make(map[int]time.Duration)
|
|
||||||
}
|
|
||||||
c.statusCooldowns[statusCode] = cooldown
|
|
||||||
c.statusCooldownsMu.Unlock()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithMaxRetries sets the maximum number of retry attempts
|
// WithMaxRetries sets the maximum number of retry attempts
|
||||||
@@ -194,40 +177,7 @@ func (c *Client) Do(req *http.Request) (*http.Response, error) {
|
|||||||
}
|
}
|
||||||
c.headersMu.RUnlock()
|
c.headersMu.RUnlock()
|
||||||
|
|
||||||
if attempt > 0 && resp != nil {
|
|
||||||
c.statusCooldownsMu.RLock()
|
|
||||||
cooldown, exists := c.statusCooldowns[resp.StatusCode]
|
|
||||||
c.statusCooldownsMu.RUnlock()
|
|
||||||
|
|
||||||
if exists {
|
|
||||||
c.lastStatusTimeMu.RLock()
|
|
||||||
lastTime, timeExists := c.lastStatusTime[resp.StatusCode]
|
|
||||||
c.lastStatusTimeMu.RUnlock()
|
|
||||||
|
|
||||||
if timeExists {
|
|
||||||
elapsed := time.Since(lastTime)
|
|
||||||
if elapsed < cooldown {
|
|
||||||
// We need to wait longer for this status code
|
|
||||||
waitTime := cooldown - elapsed
|
|
||||||
select {
|
|
||||||
case <-req.Context().Done():
|
|
||||||
return nil, req.Context().Err()
|
|
||||||
case <-time.After(waitTime):
|
|
||||||
// Continue after waiting
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err = c.doRequest(req)
|
resp, err = c.doRequest(req)
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
c.lastStatusTimeMu.Lock()
|
|
||||||
c.lastStatusTime[resp.StatusCode] = time.Now()
|
|
||||||
c.lastStatusTimeMu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Check if this is a network error that might be worth retrying
|
// Check if this is a network error that might be worth retrying
|
||||||
if attempt < c.maxRetries {
|
if attempt < c.maxRetries {
|
||||||
@@ -321,12 +271,10 @@ func New(options ...ClientOption) *Client {
|
|||||||
http.StatusServiceUnavailable: struct{}{},
|
http.StatusServiceUnavailable: struct{}{},
|
||||||
http.StatusGatewayTimeout: struct{}{},
|
http.StatusGatewayTimeout: struct{}{},
|
||||||
},
|
},
|
||||||
logger: logger.New("request"),
|
logger: logger.New("request"),
|
||||||
timeout: 60 * time.Second,
|
timeout: 60 * time.Second,
|
||||||
proxy: "",
|
proxy: "",
|
||||||
headers: make(map[string]string), // Initialize headers map
|
headers: make(map[string]string),
|
||||||
statusCooldowns: make(map[int]time.Duration),
|
|
||||||
lastStatusTime: make(map[int]time.Time),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// default http client
|
// default http client
|
||||||
@@ -345,28 +293,6 @@ func New(options ...ClientOption) *Client {
|
|||||||
TLSClientConfig: &tls.Config{
|
TLSClientConfig: &tls.Config{
|
||||||
InsecureSkipVerify: client.skipTLSVerify,
|
InsecureSkipVerify: client.skipTLSVerify,
|
||||||
},
|
},
|
||||||
// Connection pooling
|
|
||||||
MaxIdleConns: 100,
|
|
||||||
MaxIdleConnsPerHost: 50,
|
|
||||||
MaxConnsPerHost: 100,
|
|
||||||
|
|
||||||
// Timeouts
|
|
||||||
IdleConnTimeout: 90 * time.Second,
|
|
||||||
TLSHandshakeTimeout: 10 * time.Second,
|
|
||||||
ResponseHeaderTimeout: 10 * time.Second,
|
|
||||||
ExpectContinueTimeout: 1 * time.Second,
|
|
||||||
|
|
||||||
// TCP keep-alive
|
|
||||||
DialContext: (&net.Dialer{
|
|
||||||
Timeout: 30 * time.Second,
|
|
||||||
KeepAlive: 30 * time.Second,
|
|
||||||
}).DialContext,
|
|
||||||
|
|
||||||
// Enable HTTP/2
|
|
||||||
ForceAttemptHTTP2: true,
|
|
||||||
|
|
||||||
// Disable compression to save CPU
|
|
||||||
DisableCompression: false,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure proxy if needed
|
// Configure proxy if needed
|
||||||
|
|||||||
18
main.go
18
main.go
@@ -5,10 +5,7 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"github.com/sirrobot01/decypharr/cmd/decypharr"
|
"github.com/sirrobot01/decypharr/cmd/decypharr"
|
||||||
"github.com/sirrobot01/decypharr/internal/config"
|
"github.com/sirrobot01/decypharr/internal/config"
|
||||||
"github.com/sirrobot01/decypharr/pkg/version"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
|
||||||
_ "net/http/pprof" // registers pprof handlers
|
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
@@ -22,23 +19,10 @@ func main() {
|
|||||||
debug.PrintStack()
|
debug.PrintStack()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if version.GetInfo().Channel == "dev" {
|
|
||||||
log.Println("Running in dev mode")
|
|
||||||
go func() {
|
|
||||||
if err := http.ListenAndServe(":6060", nil); err != nil {
|
|
||||||
log.Fatalf("pprof server failed: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
var configPath string
|
var configPath string
|
||||||
flag.StringVar(&configPath, "config", "/data", "path to the data folder")
|
flag.StringVar(&configPath, "config", "/data", "path to the data folder")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
config.SetConfigPath(configPath)
|
||||||
if err := config.SetConfigPath(configPath); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
config.Get()
|
config.Get()
|
||||||
|
|
||||||
// Create a context that's cancelled on SIGINT/SIGTERM
|
// Create a context that's cancelled on SIGINT/SIGTERM
|
||||||
|
|||||||
@@ -262,15 +262,13 @@ func (ad *AllDebrid) GenerateDownloadLinks(t *types.Torrent) error {
|
|||||||
for _, file := range t.Files {
|
for _, file := range t.Files {
|
||||||
go func(file types.File) {
|
go func(file types.File) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
link, accountId, err := ad.GetDownloadLink(t, &file)
|
link, err := ad.GetDownloadLink(t, &file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errCh <- err
|
errCh <- err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
file.DownloadLink = link
|
file.DownloadLink = link
|
||||||
file.Generated = time.Now()
|
if link != nil {
|
||||||
file.AccountId = accountId
|
|
||||||
if link == "" {
|
|
||||||
errCh <- fmt.Errorf("error getting download links %w", err)
|
errCh <- fmt.Errorf("error getting download links %w", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -298,7 +296,7 @@ func (ad *AllDebrid) GenerateDownloadLinks(t *types.Torrent) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ad *AllDebrid) GetDownloadLink(t *types.Torrent, file *types.File) (string, string, error) {
|
func (ad *AllDebrid) GetDownloadLink(t *types.Torrent, file *types.File) (*types.DownloadLink, error) {
|
||||||
url := fmt.Sprintf("%s/link/unlock", ad.Host)
|
url := fmt.Sprintf("%s/link/unlock", ad.Host)
|
||||||
query := gourl.Values{}
|
query := gourl.Values{}
|
||||||
query.Add("link", file.Link)
|
query.Add("link", file.Link)
|
||||||
@@ -306,17 +304,25 @@ func (ad *AllDebrid) GetDownloadLink(t *types.Torrent, file *types.File) (string
|
|||||||
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
||||||
resp, err := ad.client.MakeRequest(req)
|
resp, err := ad.client.MakeRequest(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
var data DownloadLink
|
var data DownloadLink
|
||||||
if err = json.Unmarshal(resp, &data); err != nil {
|
if err = json.Unmarshal(resp, &data); err != nil {
|
||||||
return "", "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
link := data.Data.Link
|
link := data.Data.Link
|
||||||
if link == "" {
|
if link == "" {
|
||||||
return "", "", fmt.Errorf("error getting download links %s", data.Error.Message)
|
return nil, fmt.Errorf("error getting download links %s", data.Error.Message)
|
||||||
}
|
}
|
||||||
return link, "0", nil
|
return &types.DownloadLink{
|
||||||
|
Link: file.Link,
|
||||||
|
DownloadLink: link,
|
||||||
|
Id: data.Data.Id,
|
||||||
|
Size: file.Size,
|
||||||
|
Filename: file.Name,
|
||||||
|
Generated: time.Now(),
|
||||||
|
AccountId: "0",
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ad *AllDebrid) GetCheckCached() bool {
|
func (ad *AllDebrid) GetCheckCached() bool {
|
||||||
@@ -355,7 +361,7 @@ func (ad *AllDebrid) GetTorrents() ([]*types.Torrent, error) {
|
|||||||
return torrents, nil
|
return torrents, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ad *AllDebrid) GetDownloads() (map[string]types.DownloadLinks, error) {
|
func (ad *AllDebrid) GetDownloads() (map[string]types.DownloadLink, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,3 +387,6 @@ func (ad *AllDebrid) DisableAccount(accountId string) {
|
|||||||
func (ad *AllDebrid) ResetActiveDownloadKeys() {
|
func (ad *AllDebrid) ResetActiveDownloadKeys() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
func (ad *AllDebrid) DeleteDownloadLink(linkId string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ type CachedTorrent struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type downloadLinkCache struct {
|
type downloadLinkCache struct {
|
||||||
|
Id string
|
||||||
Link string
|
Link string
|
||||||
AccountId string
|
AccountId string
|
||||||
ExpiresAt time.Time
|
ExpiresAt time.Time
|
||||||
@@ -619,7 +620,7 @@ func (c *Cache) GetDownloadLink(torrentId, filename, fileLink string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.logger.Trace().Msgf("Getting download link for %s", filename)
|
c.logger.Trace().Msgf("Getting download link for %s", filename)
|
||||||
downloadLink, accountId, err := c.client.GetDownloadLink(ct.Torrent, &file)
|
downloadLink, err := c.client.GetDownloadLink(ct.Torrent, &file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, request.HosterUnavailableError) {
|
if errors.Is(err, request.HosterUnavailableError) {
|
||||||
c.logger.Debug().Err(err).Msgf("Hoster is unavailable. Triggering repair for %s", ct.Name)
|
c.logger.Debug().Err(err).Msgf("Hoster is unavailable. Triggering repair for %s", ct.Name)
|
||||||
@@ -631,41 +632,32 @@ func (c *Cache) GetDownloadLink(torrentId, filename, fileLink string) string {
|
|||||||
c.logger.Debug().Msgf("Reinserted torrent %s", ct.Name)
|
c.logger.Debug().Msgf("Reinserted torrent %s", ct.Name)
|
||||||
file = ct.Files[filename]
|
file = ct.Files[filename]
|
||||||
// Retry getting the download link
|
// Retry getting the download link
|
||||||
downloadLink, accountId, err = c.client.GetDownloadLink(ct.Torrent, &file)
|
downloadLink, err = c.client.GetDownloadLink(ct.Torrent, &file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Debug().Err(err).Msgf("Failed to get download link for %s", file.Link)
|
c.logger.Debug().Err(err).Msgf("Failed to get download link for %s", file.Link)
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
if downloadLink == "" {
|
if downloadLink == nil {
|
||||||
c.logger.Debug().Msgf("Download link is empty for %s", file.Link)
|
c.logger.Debug().Msgf("Download link is empty for %s", file.Link)
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
file.DownloadLink = downloadLink
|
c.updateDownloadLink(downloadLink)
|
||||||
file.Generated = time.Now()
|
return downloadLink.DownloadLink
|
||||||
file.AccountId = accountId
|
|
||||||
ct.Files[filename] = file
|
|
||||||
go func() {
|
|
||||||
c.updateDownloadLink(file.Link, downloadLink, accountId)
|
|
||||||
c.setTorrent(ct)
|
|
||||||
}()
|
|
||||||
return file.DownloadLink
|
|
||||||
} else if errors.Is(err, request.TrafficExceededError) {
|
} else if errors.Is(err, request.TrafficExceededError) {
|
||||||
// This is likely a fair usage limit error
|
// This is likely a fair usage limit error
|
||||||
|
c.logger.Debug().Err(err).Msgf("Traffic exceeded for %s", ct.Name)
|
||||||
} else {
|
} else {
|
||||||
c.logger.Debug().Err(err).Msgf("Failed to get download link for %s", file.Link)
|
c.logger.Debug().Err(err).Msgf("Failed to get download link for %s", file.Link)
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
file.DownloadLink = downloadLink
|
|
||||||
file.Generated = time.Now()
|
|
||||||
file.AccountId = accountId
|
|
||||||
ct.Files[filename] = file
|
|
||||||
|
|
||||||
go func() {
|
if downloadLink == nil {
|
||||||
c.updateDownloadLink(file.Link, downloadLink, file.AccountId)
|
c.logger.Debug().Msgf("Download link is empty for %s", file.Link)
|
||||||
c.setTorrent(ct)
|
return ""
|
||||||
}()
|
}
|
||||||
return file.DownloadLink
|
c.updateDownloadLink(downloadLink)
|
||||||
|
return downloadLink.DownloadLink
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cache) GenerateDownloadLinks(t *CachedTorrent) {
|
func (c *Cache) GenerateDownloadLinks(t *CachedTorrent) {
|
||||||
@@ -673,7 +665,10 @@ func (c *Cache) GenerateDownloadLinks(t *CachedTorrent) {
|
|||||||
c.logger.Error().Err(err).Msg("Failed to generate download links")
|
c.logger.Error().Err(err).Msg("Failed to generate download links")
|
||||||
}
|
}
|
||||||
for _, file := range t.Files {
|
for _, file := range t.Files {
|
||||||
c.updateDownloadLink(file.Link, file.DownloadLink, file.AccountId)
|
if file.DownloadLink != nil {
|
||||||
|
c.updateDownloadLink(file.DownloadLink)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.SaveTorrent(t)
|
c.SaveTorrent(t)
|
||||||
@@ -701,11 +696,12 @@ func (c *Cache) AddTorrent(t *types.Torrent) error {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cache) updateDownloadLink(link, downloadLink string, accountId string) {
|
func (c *Cache) updateDownloadLink(dl *types.DownloadLink) {
|
||||||
c.downloadLinks.Store(link, downloadLinkCache{
|
c.downloadLinks.Store(dl.Link, downloadLinkCache{
|
||||||
Link: downloadLink,
|
Id: dl.Id,
|
||||||
|
Link: dl.DownloadLink,
|
||||||
ExpiresAt: time.Now().Add(c.autoExpiresLinksAfter),
|
ExpiresAt: time.Now().Add(c.autoExpiresLinksAfter),
|
||||||
AccountId: accountId,
|
AccountId: dl.AccountId,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -728,7 +724,18 @@ func (c *Cache) MarkDownloadLinkAsInvalid(link, downloadLink, reason string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.downloadLinks.Delete(link) // Remove the download link from cache
|
c.removeDownloadLink(link)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) removeDownloadLink(link string) {
|
||||||
|
if dl, ok := c.downloadLinks.Load(link); ok {
|
||||||
|
// Delete dl from cache
|
||||||
|
c.downloadLinks.Delete(link)
|
||||||
|
// Delete dl from debrid
|
||||||
|
if dl.Id != "" {
|
||||||
|
_ = c.client.DeleteDownloadLink(dl.Id)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cache) IsDownloadLinkInvalid(downloadLink string) bool {
|
func (c *Cache) IsDownloadLinkInvalid(downloadLink string) bool {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -61,25 +60,17 @@ func (c *Cache) refreshListings() {
|
|||||||
// Atomic store of the complete ready-to-use slice
|
// Atomic store of the complete ready-to-use slice
|
||||||
c.listings.Store(files)
|
c.listings.Store(files)
|
||||||
_ = c.refreshXml()
|
_ = c.refreshXml()
|
||||||
if err := c.RefreshRclone(); err != nil {
|
|
||||||
c.logger.Trace().Err(err).Msg("Failed to refresh rclone") // silent error
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cache) refreshTorrents() {
|
func (c *Cache) refreshTorrents() {
|
||||||
|
// Use a mutex to prevent concurrent refreshes
|
||||||
if c.torrentsRefreshMu.TryLock() {
|
if c.torrentsRefreshMu.TryLock() {
|
||||||
defer c.torrentsRefreshMu.Unlock()
|
defer c.torrentsRefreshMu.Unlock()
|
||||||
} else {
|
} else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Create a copy of the current torrents to avoid concurrent issues
|
|
||||||
torrents := make(map[string]string, c.torrents.Size()) // a mpa of id and name
|
|
||||||
c.torrents.Range(func(key string, t *CachedTorrent) bool {
|
|
||||||
torrents[t.Id] = t.Name
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
// Get new torrents from the debrid service
|
// Get all torrents from the debrid service
|
||||||
debTorrents, err := c.client.GetTorrents()
|
debTorrents, err := c.client.GetTorrents()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Debug().Err(err).Msg("Failed to get torrents")
|
c.logger.Debug().Err(err).Msg("Failed to get torrents")
|
||||||
@@ -91,38 +82,26 @@ func (c *Cache) refreshTorrents() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the newly added torrents only
|
currentTorrentIds := make(map[string]struct{}, len(debTorrents))
|
||||||
_newTorrents := make([]*types.Torrent, 0)
|
|
||||||
idStore := make(map[string]struct{}, len(debTorrents))
|
|
||||||
for _, t := range debTorrents {
|
for _, t := range debTorrents {
|
||||||
idStore[t.Id] = struct{}{}
|
currentTorrentIds[t.Id] = struct{}{}
|
||||||
if _, ok := torrents[t.Id]; !ok {
|
|
||||||
_newTorrents = append(_newTorrents, t)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for deleted torrents
|
// Because of how fast AddTorrent is, a torrent might be added before we check
|
||||||
deletedTorrents := make([]string, 0)
|
// So let's disable the deletion of torrents for now
|
||||||
for id := range torrents {
|
// Deletion now moved to the cleanupWorker
|
||||||
if _, ok := idStore[id]; !ok {
|
|
||||||
deletedTorrents = append(deletedTorrents, id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
newTorrents := make([]*types.Torrent, 0)
|
newTorrents := make([]*types.Torrent, 0)
|
||||||
for _, t := range _newTorrents {
|
for _, t := range debTorrents {
|
||||||
if !slices.Contains(deletedTorrents, t.Id) {
|
if _, exists := c.torrents.Load(t.Id); !exists {
|
||||||
newTorrents = append(newTorrents, t)
|
newTorrents = append(newTorrents, t)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(deletedTorrents) > 0 {
|
|
||||||
c.DeleteTorrents(deletedTorrents)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(newTorrents) == 0 {
|
if len(newTorrents) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.logger.Info().Msgf("Found %d new torrents", len(newTorrents))
|
c.logger.Debug().Msgf("Found %d new torrents", len(newTorrents))
|
||||||
|
|
||||||
workChan := make(chan *types.Torrent, min(100, len(newTorrents)))
|
workChan := make(chan *types.Torrent, min(100, len(newTorrents)))
|
||||||
errChan := make(chan error, len(newTorrents))
|
errChan := make(chan error, len(newTorrents))
|
||||||
@@ -236,6 +215,8 @@ func (c *Cache) refreshDownloadLinks() {
|
|||||||
timeSince := time.Since(v.Generated)
|
timeSince := time.Since(v.Generated)
|
||||||
if timeSince < c.autoExpiresLinksAfter {
|
if timeSince < c.autoExpiresLinksAfter {
|
||||||
c.downloadLinks.Store(k, downloadLinkCache{
|
c.downloadLinks.Store(k, downloadLinkCache{
|
||||||
|
Id: v.Id,
|
||||||
|
AccountId: v.AccountId,
|
||||||
Link: v.DownloadLink,
|
Link: v.DownloadLink,
|
||||||
ExpiresAt: v.Generated.Add(c.autoExpiresLinksAfter - timeSince),
|
ExpiresAt: v.Generated.Add(c.autoExpiresLinksAfter - timeSince),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import "time"
|
|||||||
|
|
||||||
func (c *Cache) Refresh() error {
|
func (c *Cache) Refresh() error {
|
||||||
// For now, we just want to refresh the listing and download links
|
// For now, we just want to refresh the listing and download links
|
||||||
//go c.refreshDownloadLinksWorker()
|
go c.refreshDownloadLinksWorker()
|
||||||
go c.refreshTorrentsWorker()
|
go c.refreshTorrentsWorker()
|
||||||
go c.resetInvalidLinksWorker()
|
go c.resetInvalidLinksWorker()
|
||||||
|
go c.cleanupWorker()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,3 +74,36 @@ func (c *Cache) resetInvalidLinksWorker() {
|
|||||||
c.resetInvalidLinks()
|
c.resetInvalidLinks()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Cache) cleanupWorker() {
|
||||||
|
// Cleanup every hour
|
||||||
|
// Removes deleted torrents from the cache
|
||||||
|
|
||||||
|
ticker := time.NewTicker(1 * time.Hour)
|
||||||
|
|
||||||
|
for range ticker.C {
|
||||||
|
torrents, err := c.client.GetTorrents()
|
||||||
|
if err != nil {
|
||||||
|
c.logger.Error().Err(err).Msg("Failed to get torrents")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
idStore := make(map[string]struct{})
|
||||||
|
for _, t := range torrents {
|
||||||
|
idStore[t.Id] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
deletedTorrents := make([]string, 0)
|
||||||
|
c.torrents.Range(func(key string, _ *CachedTorrent) bool {
|
||||||
|
if _, exists := idStore[key]; !exists {
|
||||||
|
deletedTorrents = append(deletedTorrents, key)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(deletedTorrents) > 0 {
|
||||||
|
c.DeleteTorrents(deletedTorrents)
|
||||||
|
c.logger.Info().Msgf("Deleted %d torrents", len(deletedTorrents))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,36 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// resetPropfindResponse resets the propfind response cache for the specified parent directories.
|
||||||
|
func (c *Cache) resetPropfindResponse() error {
|
||||||
|
// Right now, parents are hardcoded
|
||||||
|
parents := []string{"__all__", "torrents"}
|
||||||
|
// Reset only the parent directories
|
||||||
|
// Convert the parents to a keys
|
||||||
|
// This is a bit hacky, but it works
|
||||||
|
// Instead of deleting all the keys, we only delete the parent keys, e.g __all__/ or torrents/
|
||||||
|
keys := make([]string, 0, len(parents))
|
||||||
|
for _, p := range parents {
|
||||||
|
// Construct the key
|
||||||
|
// construct url
|
||||||
|
url := path.Clean(path.Join("/webdav", c.client.GetName(), p))
|
||||||
|
key0 := fmt.Sprintf("propfind:%s:0", url)
|
||||||
|
key1 := fmt.Sprintf("propfind:%s:1", url)
|
||||||
|
keys = append(keys, key0, key1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the keys
|
||||||
|
for _, k := range keys {
|
||||||
|
c.PropfindResp.Delete(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.RefreshRclone(); err != nil {
|
||||||
|
c.logger.Trace().Err(err).Msg("Failed to refresh rclone") // silent error
|
||||||
|
}
|
||||||
|
c.logger.Trace().Msgf("Reset XML cache for %s", c.client.GetName())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Cache) refreshXml() error {
|
func (c *Cache) refreshXml() error {
|
||||||
parents := []string{"__all__", "torrents"}
|
parents := []string{"__all__", "torrents"}
|
||||||
torrents := c.GetListing()
|
torrents := c.GetListing()
|
||||||
@@ -18,6 +48,9 @@ func (c *Cache) refreshXml() error {
|
|||||||
return fmt.Errorf("failed to refresh XML for %s: %v", parent, err)
|
return fmt.Errorf("failed to refresh XML for %s: %v", parent, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err := c.RefreshRclone(); err != nil {
|
||||||
|
c.logger.Trace().Err(err).Msg("Failed to refresh rclone") // silent error
|
||||||
|
}
|
||||||
c.logger.Trace().Msgf("Refreshed XML cache for %s", c.client.GetName())
|
c.logger.Trace().Msgf("Refreshed XML cache for %s", c.client.GetName())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,12 +137,18 @@ func (dl *DebridLink) UpdateTorrent(t *types.Torrent) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
file := types.File{
|
file := types.File{
|
||||||
Id: f.ID,
|
Id: f.ID,
|
||||||
Name: f.Name,
|
Name: f.Name,
|
||||||
Size: f.Size,
|
Size: f.Size,
|
||||||
Path: f.Name,
|
Path: f.Name,
|
||||||
DownloadLink: f.DownloadURL,
|
DownloadLink: &types.DownloadLink{
|
||||||
Link: f.DownloadURL,
|
Filename: f.Name,
|
||||||
|
Link: f.DownloadURL,
|
||||||
|
DownloadLink: f.DownloadURL,
|
||||||
|
Generated: time.Now(),
|
||||||
|
AccountId: "0",
|
||||||
|
},
|
||||||
|
Link: f.DownloadURL,
|
||||||
}
|
}
|
||||||
t.Files[f.Name] = file
|
t.Files[f.Name] = file
|
||||||
}
|
}
|
||||||
@@ -183,13 +189,19 @@ func (dl *DebridLink) SubmitMagnet(t *types.Torrent) (*types.Torrent, error) {
|
|||||||
t.Debrid = dl.Name
|
t.Debrid = dl.Name
|
||||||
for _, f := range data.Files {
|
for _, f := range data.Files {
|
||||||
file := types.File{
|
file := types.File{
|
||||||
Id: f.ID,
|
Id: f.ID,
|
||||||
Name: f.Name,
|
Name: f.Name,
|
||||||
Size: f.Size,
|
Size: f.Size,
|
||||||
Path: f.Name,
|
Path: f.Name,
|
||||||
Link: f.DownloadURL,
|
Link: f.DownloadURL,
|
||||||
DownloadLink: f.DownloadURL,
|
DownloadLink: &types.DownloadLink{
|
||||||
Generated: time.Now(),
|
Filename: f.Name,
|
||||||
|
Link: f.DownloadURL,
|
||||||
|
DownloadLink: f.DownloadURL,
|
||||||
|
Generated: time.Now(),
|
||||||
|
AccountId: "0",
|
||||||
|
},
|
||||||
|
Generated: time.Now(),
|
||||||
}
|
}
|
||||||
t.Files[f.Name] = file
|
t.Files[f.Name] = file
|
||||||
}
|
}
|
||||||
@@ -241,12 +253,12 @@ func (dl *DebridLink) GenerateDownloadLinks(t *types.Torrent) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dl *DebridLink) GetDownloads() (map[string]types.DownloadLinks, error) {
|
func (dl *DebridLink) GetDownloads() (map[string]types.DownloadLink, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dl *DebridLink) GetDownloadLink(t *types.Torrent, file *types.File) (string, string, error) {
|
func (dl *DebridLink) GetDownloadLink(t *types.Torrent, file *types.File) (*types.DownloadLink, error) {
|
||||||
return file.DownloadLink, "0", nil
|
return file.DownloadLink, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dl *DebridLink) GetDownloadingStatus() []string {
|
func (dl *DebridLink) GetDownloadingStatus() []string {
|
||||||
@@ -358,12 +370,18 @@ func (dl *DebridLink) getTorrents(page, perPage int) ([]*types.Torrent, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
file := types.File{
|
file := types.File{
|
||||||
Id: f.ID,
|
Id: f.ID,
|
||||||
Name: f.Name,
|
Name: f.Name,
|
||||||
Size: f.Size,
|
Size: f.Size,
|
||||||
Path: f.Name,
|
Path: f.Name,
|
||||||
DownloadLink: f.DownloadURL,
|
DownloadLink: &types.DownloadLink{
|
||||||
Link: f.DownloadURL,
|
Filename: f.Name,
|
||||||
|
Link: f.DownloadURL,
|
||||||
|
DownloadLink: f.DownloadURL,
|
||||||
|
Generated: time.Now(),
|
||||||
|
AccountId: "0",
|
||||||
|
},
|
||||||
|
Link: f.DownloadURL,
|
||||||
}
|
}
|
||||||
torrent.Files[f.Name] = file
|
torrent.Files[f.Name] = file
|
||||||
}
|
}
|
||||||
@@ -385,3 +403,7 @@ func (dl *DebridLink) DisableAccount(accountId string) {
|
|||||||
|
|
||||||
func (dl *DebridLink) ResetActiveDownloadKeys() {
|
func (dl *DebridLink) ResetActiveDownloadKeys() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (dl *DebridLink) DeleteDownloadLink(linkId string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,8 +28,9 @@ type RealDebrid struct {
|
|||||||
Name string
|
Name string
|
||||||
Host string `json:"host"`
|
Host string `json:"host"`
|
||||||
|
|
||||||
APIKey string
|
APIKey string
|
||||||
DownloadKeys *xsync.MapOf[string, types.Account] // index | Account
|
currentDownloadKey string
|
||||||
|
DownloadKeys *xsync.MapOf[string, types.Account] // index | Account
|
||||||
|
|
||||||
DownloadUncached bool
|
DownloadUncached bool
|
||||||
client *request.Client
|
client *request.Client
|
||||||
@@ -49,7 +50,7 @@ func New(dc config.Debrid) *RealDebrid {
|
|||||||
_log := logger.New(dc.Name)
|
_log := logger.New(dc.Name)
|
||||||
|
|
||||||
accounts := xsync.NewMapOf[string, types.Account]()
|
accounts := xsync.NewMapOf[string, types.Account]()
|
||||||
firstDownloadKey := dc.DownloadAPIKeys[0]
|
currentDownloadKey := dc.DownloadAPIKeys[0]
|
||||||
for idx, key := range dc.DownloadAPIKeys {
|
for idx, key := range dc.DownloadAPIKeys {
|
||||||
id := strconv.Itoa(idx)
|
id := strconv.Itoa(idx)
|
||||||
accounts.Store(id, types.Account{
|
accounts.Store(id, types.Account{
|
||||||
@@ -60,7 +61,7 @@ func New(dc config.Debrid) *RealDebrid {
|
|||||||
}
|
}
|
||||||
|
|
||||||
downloadHeaders := map[string]string{
|
downloadHeaders := map[string]string{
|
||||||
"Authorization": fmt.Sprintf("Bearer %s", firstDownloadKey),
|
"Authorization": fmt.Sprintf("Bearer %s", currentDownloadKey),
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadClient := request.New(
|
downloadClient := request.New(
|
||||||
@@ -70,7 +71,6 @@ func New(dc config.Debrid) *RealDebrid {
|
|||||||
request.WithMaxRetries(5),
|
request.WithMaxRetries(5),
|
||||||
request.WithRetryableStatus(429, 447),
|
request.WithRetryableStatus(429, 447),
|
||||||
request.WithProxy(dc.Proxy),
|
request.WithProxy(dc.Proxy),
|
||||||
request.WithStatusCooldown(447, 10*time.Second), // 447 is a fair use error
|
|
||||||
)
|
)
|
||||||
|
|
||||||
client := request.New(
|
client := request.New(
|
||||||
@@ -83,16 +83,17 @@ func New(dc config.Debrid) *RealDebrid {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return &RealDebrid{
|
return &RealDebrid{
|
||||||
Name: "realdebrid",
|
Name: "realdebrid",
|
||||||
Host: dc.Host,
|
Host: dc.Host,
|
||||||
APIKey: dc.APIKey,
|
APIKey: dc.APIKey,
|
||||||
DownloadKeys: accounts,
|
DownloadKeys: accounts,
|
||||||
DownloadUncached: dc.DownloadUncached,
|
DownloadUncached: dc.DownloadUncached,
|
||||||
client: client,
|
client: client,
|
||||||
downloadClient: downloadClient,
|
downloadClient: downloadClient,
|
||||||
MountPath: dc.Folder,
|
currentDownloadKey: currentDownloadKey,
|
||||||
logger: logger.New(dc.Name),
|
MountPath: dc.Folder,
|
||||||
CheckCached: dc.CheckCached,
|
logger: logger.New(dc.Name),
|
||||||
|
CheckCached: dc.CheckCached,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,14 +377,13 @@ func (r *RealDebrid) GenerateDownloadLinks(t *types.Torrent) error {
|
|||||||
go func(file types.File) {
|
go func(file types.File) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
link, accountId, err := r.GetDownloadLink(t, &file)
|
link, err := r.GetDownloadLink(t, &file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errCh <- err
|
errCh <- err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
file.DownloadLink = link
|
file.DownloadLink = link
|
||||||
file.AccountId = accountId
|
|
||||||
filesCh <- file
|
filesCh <- file
|
||||||
}(f)
|
}(f)
|
||||||
}
|
}
|
||||||
@@ -427,95 +427,112 @@ func (r *RealDebrid) CheckLink(link string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RealDebrid) _getDownloadLink(file *types.File) (string, error) {
|
func (r *RealDebrid) _getDownloadLink(file *types.File) (*types.DownloadLink, error) {
|
||||||
url := fmt.Sprintf("%s/unrestrict/link/", r.Host)
|
url := fmt.Sprintf("%s/unrestrict/link/", r.Host)
|
||||||
payload := gourl.Values{
|
payload := gourl.Values{
|
||||||
"link": {file.Link},
|
"link": {file.Link},
|
||||||
}
|
}
|
||||||
req, _ := http.NewRequest(http.MethodPost, url, strings.NewReader(payload.Encode()))
|
req, _ := http.NewRequest(http.MethodPost, url, strings.NewReader(payload.Encode()))
|
||||||
resp, err := r.downloadClient.Do(req)
|
resp, err := r.downloadClient.Do(req)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
// Read the response body to get the error message
|
// Read the response body to get the error message
|
||||||
b, err := io.ReadAll(resp.Body)
|
b, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
var data ErrorResponse
|
var data ErrorResponse
|
||||||
if err = json.Unmarshal(b, &data); err != nil {
|
if err = json.Unmarshal(b, &data); err != nil {
|
||||||
return "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
switch data.ErrorCode {
|
switch data.ErrorCode {
|
||||||
case 23:
|
case 23:
|
||||||
return "", request.TrafficExceededError
|
return nil, request.TrafficExceededError
|
||||||
case 24:
|
case 24:
|
||||||
return "", request.HosterUnavailableError // Link has been nerfed
|
return nil, request.HosterUnavailableError // Link has been nerfed
|
||||||
case 19:
|
case 19:
|
||||||
return "", request.HosterUnavailableError // File has been removed
|
return nil, request.HosterUnavailableError // File has been removed
|
||||||
case 36:
|
case 36:
|
||||||
return "", request.TrafficExceededError // traffic exceeded
|
return nil, request.TrafficExceededError // traffic exceeded
|
||||||
case 34:
|
case 34:
|
||||||
return "", request.TrafficExceededError // traffic exceeded
|
return nil, request.TrafficExceededError // traffic exceeded
|
||||||
default:
|
default:
|
||||||
return "", fmt.Errorf("realdebrid API error: Status: %d || Code: %d", resp.StatusCode, data.ErrorCode)
|
return nil, fmt.Errorf("realdebrid API error: Status: %d || Code: %d", resp.StatusCode, data.ErrorCode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
b, err := io.ReadAll(resp.Body)
|
b, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
var data UnrestrictResponse
|
var data UnrestrictResponse
|
||||||
if err = json.Unmarshal(b, &data); err != nil {
|
if err = json.Unmarshal(b, &data); err != nil {
|
||||||
return "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
return data.Download, nil
|
if data.Download == "" {
|
||||||
|
return nil, fmt.Errorf("realdebrid API error: download link not found")
|
||||||
|
}
|
||||||
|
return &types.DownloadLink{
|
||||||
|
Filename: data.Filename,
|
||||||
|
Size: data.Filesize,
|
||||||
|
Link: data.Link,
|
||||||
|
DownloadLink: data.Download,
|
||||||
|
Generated: time.Now(),
|
||||||
|
}, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RealDebrid) GetDownloadLink(t *types.Torrent, file *types.File) (string, string, error) {
|
func (r *RealDebrid) GetDownloadLink(t *types.Torrent, file *types.File) (*types.DownloadLink, error) {
|
||||||
defer r.downloadClient.SetHeader("Authorization", fmt.Sprintf("Bearer %s", r.APIKey))
|
|
||||||
var (
|
|
||||||
downloadLink string
|
|
||||||
accountId string
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
accounts := r.getActiveAccounts()
|
|
||||||
if len(accounts) < 1 {
|
|
||||||
// No active download keys. It's likely that the key has reached bandwidth limit
|
|
||||||
return "", "", fmt.Errorf("no active download keys")
|
|
||||||
}
|
|
||||||
for _, account := range accounts {
|
|
||||||
r.downloadClient.SetHeader("Authorization", fmt.Sprintf("Bearer %s", account.Token))
|
|
||||||
downloadLink, err = r._getDownloadLink(file)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, request.TrafficExceededError) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// If the error is not traffic exceeded, skip generating the link with a new key
|
|
||||||
return "", "", err
|
|
||||||
} else {
|
|
||||||
// If we successfully generated a link, break the loop
|
|
||||||
accountId = account.ID
|
|
||||||
file.AccountId = accountId
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if r.currentDownloadKey == "" {
|
||||||
|
// If no download key is set, use the first one
|
||||||
|
r.DownloadKeys.Range(func(key string, value types.Account) bool {
|
||||||
|
if !value.Disabled {
|
||||||
|
r.currentDownloadKey = value.Token
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
if downloadLink != "" {
|
|
||||||
// If we successfully generated a link, return it
|
r.downloadClient.SetHeader("Authorization", fmt.Sprintf("Bearer %s", r.currentDownloadKey))
|
||||||
return downloadLink, accountId, nil
|
downloadLink, err := r._getDownloadLink(file)
|
||||||
}
|
|
||||||
// If we reach here, it means all keys are disabled or traffic exceeded
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, request.TrafficExceededError) {
|
accountsFunc := func() (*types.DownloadLink, error) {
|
||||||
return "", "", request.TrafficExceededError
|
accounts := r.getActiveAccounts()
|
||||||
|
var err error
|
||||||
|
if len(accounts) < 1 {
|
||||||
|
// No active download keys. It's likely that the key has reached bandwidth limit
|
||||||
|
return nil, fmt.Errorf("no active download keys")
|
||||||
|
}
|
||||||
|
for _, account := range accounts {
|
||||||
|
r.downloadClient.SetHeader("Authorization", fmt.Sprintf("Bearer %s", account.Token))
|
||||||
|
downloadLink, err := r._getDownloadLink(file)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, request.TrafficExceededError) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// If the error is not traffic exceeded, skip generating the link with a new key
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
// If we successfully generated a link, break the loop
|
||||||
|
downloadLink.AccountId = account.ID
|
||||||
|
return downloadLink, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// If we reach here, it means all keys have been exhausted
|
||||||
|
if errors.Is(err, request.TrafficExceededError) {
|
||||||
|
return nil, request.TrafficExceededError
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to generate download link: %w", err)
|
||||||
}
|
}
|
||||||
return "", "", fmt.Errorf("error generating download link: %v", err)
|
return accountsFunc()
|
||||||
}
|
}
|
||||||
return "", "", fmt.Errorf("error generating download link: %v", err)
|
return downloadLink, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RealDebrid) GetCheckCached() bool {
|
func (r *RealDebrid) GetCheckCached() bool {
|
||||||
@@ -552,7 +569,7 @@ func (r *RealDebrid) getTorrents(offset int, limit int) (int, []*types.Torrent,
|
|||||||
totalItems, _ := strconv.Atoi(resp.Header.Get("X-Total-Count"))
|
totalItems, _ := strconv.Atoi(resp.Header.Get("X-Total-Count"))
|
||||||
var data []TorrentsResponse
|
var data []TorrentsResponse
|
||||||
if err = json.Unmarshal(body, &data); err != nil {
|
if err = json.Unmarshal(body, &data); err != nil {
|
||||||
return 0, nil, err
|
return 0, torrents, err
|
||||||
}
|
}
|
||||||
filenames := map[string]struct{}{}
|
filenames := map[string]struct{}{}
|
||||||
for _, t := range data {
|
for _, t := range data {
|
||||||
@@ -621,8 +638,8 @@ func (r *RealDebrid) GetTorrents() ([]*types.Torrent, error) {
|
|||||||
return allTorrents, nil
|
return allTorrents, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RealDebrid) GetDownloads() (map[string]types.DownloadLinks, error) {
|
func (r *RealDebrid) GetDownloads() (map[string]types.DownloadLink, error) {
|
||||||
links := make(map[string]types.DownloadLinks)
|
links := make(map[string]types.DownloadLink)
|
||||||
offset := 0
|
offset := 0
|
||||||
limit := 1000
|
limit := 1000
|
||||||
|
|
||||||
@@ -655,7 +672,7 @@ func (r *RealDebrid) GetDownloads() (map[string]types.DownloadLinks, error) {
|
|||||||
return links, nil
|
return links, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RealDebrid) _getDownloads(offset int, limit int) ([]types.DownloadLinks, error) {
|
func (r *RealDebrid) _getDownloads(offset int, limit int) ([]types.DownloadLink, error) {
|
||||||
url := fmt.Sprintf("%s/downloads?limit=%d", r.Host, limit)
|
url := fmt.Sprintf("%s/downloads?limit=%d", r.Host, limit)
|
||||||
if offset > 0 {
|
if offset > 0 {
|
||||||
url = fmt.Sprintf("%s&offset=%d", url, offset)
|
url = fmt.Sprintf("%s&offset=%d", url, offset)
|
||||||
@@ -669,9 +686,9 @@ func (r *RealDebrid) _getDownloads(offset int, limit int) ([]types.DownloadLinks
|
|||||||
if err = json.Unmarshal(resp, &data); err != nil {
|
if err = json.Unmarshal(resp, &data); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
links := make([]types.DownloadLinks, 0)
|
links := make([]types.DownloadLink, 0)
|
||||||
for _, d := range data {
|
for _, d := range data {
|
||||||
links = append(links, types.DownloadLinks{
|
links = append(links, types.DownloadLink{
|
||||||
Filename: d.Filename,
|
Filename: d.Filename,
|
||||||
Size: d.Filesize,
|
Size: d.Filesize,
|
||||||
Link: d.Link,
|
Link: d.Link,
|
||||||
@@ -697,8 +714,15 @@ func (r *RealDebrid) GetMountPath() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *RealDebrid) DisableAccount(accountId string) {
|
func (r *RealDebrid) DisableAccount(accountId string) {
|
||||||
|
if r.DownloadKeys.Size() == 1 {
|
||||||
|
r.logger.Info().Msgf("Cannot disable last account: %s", accountId)
|
||||||
|
return
|
||||||
|
}
|
||||||
if value, ok := r.DownloadKeys.Load(accountId); ok {
|
if value, ok := r.DownloadKeys.Load(accountId); ok {
|
||||||
value.Disabled = true
|
value.Disabled = true
|
||||||
|
if value.Token == r.currentDownloadKey {
|
||||||
|
r.currentDownloadKey = ""
|
||||||
|
}
|
||||||
r.DownloadKeys.Store(accountId, value)
|
r.DownloadKeys.Store(accountId, value)
|
||||||
r.logger.Info().Msgf("Disabled account Index: %s", value.ID)
|
r.logger.Info().Msgf("Disabled account Index: %s", value.ID)
|
||||||
}
|
}
|
||||||
@@ -726,3 +750,12 @@ func (r *RealDebrid) getActiveAccounts() []types.Account {
|
|||||||
})
|
})
|
||||||
return accounts
|
return accounts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *RealDebrid) DeleteDownloadLink(linkId string) error {
|
||||||
|
url := fmt.Sprintf("%s/downloads/delete/%s", r.Host, linkId)
|
||||||
|
req, _ := http.NewRequest(http.MethodDelete, url, nil)
|
||||||
|
if _, err := r.downloadClient.MakeRequest(req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Torbox struct {
|
type Torbox struct {
|
||||||
@@ -291,13 +292,12 @@ func (tb *Torbox) GenerateDownloadLinks(t *types.Torrent) error {
|
|||||||
for _, file := range t.Files {
|
for _, file := range t.Files {
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
link, accountId, err := tb.GetDownloadLink(t, &file)
|
link, err := tb.GetDownloadLink(t, &file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errCh <- err
|
errCh <- err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
file.DownloadLink = link
|
file.DownloadLink = link
|
||||||
file.AccountId = accountId
|
|
||||||
filesCh <- file
|
filesCh <- file
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
@@ -324,7 +324,7 @@ func (tb *Torbox) GenerateDownloadLinks(t *types.Torrent) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tb *Torbox) GetDownloadLink(t *types.Torrent, file *types.File) (string, string, error) {
|
func (tb *Torbox) GetDownloadLink(t *types.Torrent, file *types.File) (*types.DownloadLink, error) {
|
||||||
url := fmt.Sprintf("%s/api/torrents/requestdl/", tb.Host)
|
url := fmt.Sprintf("%s/api/torrents/requestdl/", tb.Host)
|
||||||
query := gourl.Values{}
|
query := gourl.Values{}
|
||||||
query.Add("torrent_id", t.Id)
|
query.Add("torrent_id", t.Id)
|
||||||
@@ -334,17 +334,26 @@ func (tb *Torbox) GetDownloadLink(t *types.Torrent, file *types.File) (string, s
|
|||||||
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
||||||
resp, err := tb.client.MakeRequest(req)
|
resp, err := tb.client.MakeRequest(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
var data DownloadLinksResponse
|
var data DownloadLinksResponse
|
||||||
if err = json.Unmarshal(resp, &data); err != nil {
|
if err = json.Unmarshal(resp, &data); err != nil {
|
||||||
return "", "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
if data.Data == nil {
|
if data.Data == nil {
|
||||||
return "", "", fmt.Errorf("error getting download links")
|
return nil, fmt.Errorf("error getting download links")
|
||||||
}
|
}
|
||||||
link := *data.Data
|
link := *data.Data
|
||||||
return link, "0", nil
|
if link == "" {
|
||||||
|
return nil, fmt.Errorf("error getting download links")
|
||||||
|
}
|
||||||
|
return &types.DownloadLink{
|
||||||
|
Link: file.Link,
|
||||||
|
DownloadLink: link,
|
||||||
|
Id: file.Id,
|
||||||
|
AccountId: "0",
|
||||||
|
Generated: time.Now(),
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tb *Torbox) GetDownloadingStatus() []string {
|
func (tb *Torbox) GetDownloadingStatus() []string {
|
||||||
@@ -363,7 +372,7 @@ func (tb *Torbox) GetDownloadUncached() bool {
|
|||||||
return tb.DownloadUncached
|
return tb.DownloadUncached
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tb *Torbox) GetDownloads() (map[string]types.DownloadLinks, error) {
|
func (tb *Torbox) GetDownloads() (map[string]types.DownloadLink, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,3 +390,7 @@ func (tb *Torbox) DisableAccount(accountId string) {
|
|||||||
func (tb *Torbox) ResetActiveDownloadKeys() {
|
func (tb *Torbox) ResetActiveDownloadKeys() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (tb *Torbox) DeleteDownloadLink(linkId string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ type Client interface {
|
|||||||
SubmitMagnet(tr *Torrent) (*Torrent, error)
|
SubmitMagnet(tr *Torrent) (*Torrent, error)
|
||||||
CheckStatus(tr *Torrent, isSymlink bool) (*Torrent, error)
|
CheckStatus(tr *Torrent, isSymlink bool) (*Torrent, error)
|
||||||
GenerateDownloadLinks(tr *Torrent) error
|
GenerateDownloadLinks(tr *Torrent) error
|
||||||
GetDownloadLink(tr *Torrent, file *File) (string, string, error)
|
GetDownloadLink(tr *Torrent, file *File) (*DownloadLink, error)
|
||||||
DeleteTorrent(torrentId string) error
|
DeleteTorrent(torrentId string) error
|
||||||
IsAvailable(infohashes []string) map[string]bool
|
IsAvailable(infohashes []string) map[string]bool
|
||||||
GetCheckCached() bool
|
GetCheckCached() bool
|
||||||
@@ -18,9 +18,10 @@ type Client interface {
|
|||||||
GetName() string
|
GetName() string
|
||||||
GetLogger() zerolog.Logger
|
GetLogger() zerolog.Logger
|
||||||
GetDownloadingStatus() []string
|
GetDownloadingStatus() []string
|
||||||
GetDownloads() (map[string]DownloadLinks, error)
|
GetDownloads() (map[string]DownloadLink, error)
|
||||||
CheckLink(link string) error
|
CheckLink(link string) error
|
||||||
GetMountPath() string
|
GetMountPath() string
|
||||||
DisableAccount(string)
|
DisableAccount(string)
|
||||||
ResetActiveDownloadKeys()
|
ResetActiveDownloadKeys()
|
||||||
|
DeleteDownloadLink(linkId string) error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,13 +39,18 @@ type Torrent struct {
|
|||||||
DownloadUncached bool `json:"-"`
|
DownloadUncached bool `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DownloadLinks struct {
|
type DownloadLink struct {
|
||||||
Filename string `json:"filename"`
|
Filename string `json:"filename"`
|
||||||
Link string `json:"link"`
|
Link string `json:"link"`
|
||||||
DownloadLink string `json:"download_link"`
|
DownloadLink string `json:"download_link"`
|
||||||
Generated time.Time `json:"generated"`
|
Generated time.Time `json:"generated"`
|
||||||
Size int64 `json:"size"`
|
Size int64 `json:"size"`
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
|
AccountId string `json:"account_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DownloadLink) String() string {
|
||||||
|
return d.DownloadLink
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Torrent) GetSymlinkFolder(parent string) string {
|
func (t *Torrent) GetSymlinkFolder(parent string) string {
|
||||||
@@ -72,14 +77,14 @@ func (t *Torrent) GetMountFolder(rClonePath string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type File struct {
|
type File struct {
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Size int64 `json:"size"`
|
Size int64 `json:"size"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
Link string `json:"link"`
|
Link string `json:"link"`
|
||||||
DownloadLink string `json:"download_link"`
|
DownloadLink *DownloadLink `json:"-"`
|
||||||
AccountId string `json:"account_id"`
|
AccountId string `json:"account_id"`
|
||||||
Generated time.Time `json:"generated"`
|
Generated time.Time `json:"generated"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *File) IsValid() bool {
|
func (f *File) IsValid() bool {
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ func (q *QBit) downloadFiles(torrent *Torrent, parent string) {
|
|||||||
HTTPClient: request.New(request.WithTimeout(0)),
|
HTTPClient: request.New(request.WithTimeout(0)),
|
||||||
}
|
}
|
||||||
for _, file := range debridTorrent.Files {
|
for _, file := range debridTorrent.Files {
|
||||||
if file.DownloadLink == "" {
|
if file.DownloadLink == nil {
|
||||||
q.logger.Info().Msgf("No download link found for %s", file.Name)
|
q.logger.Info().Msgf("No download link found for %s", file.Name)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -109,7 +109,7 @@ func (q *QBit) downloadFiles(torrent *Torrent, parent string) {
|
|||||||
|
|
||||||
err := Download(
|
err := Download(
|
||||||
client,
|
client,
|
||||||
file.DownloadLink,
|
file.DownloadLink.DownloadLink,
|
||||||
filepath.Join(parent, filename),
|
filepath.Join(parent, filename),
|
||||||
progressCallback,
|
progressCallback,
|
||||||
)
|
)
|
||||||
@@ -186,6 +186,8 @@ func (q *QBit) createSymlinks(debridTorrent *debrid.Torrent, rclonePath, torrent
|
|||||||
|
|
||||||
if err := q.preCacheFile(debridTorrent.Name, filePaths); err != nil {
|
if err := q.preCacheFile(debridTorrent.Name, filePaths); err != nil {
|
||||||
q.logger.Error().Msgf("Failed to pre-cache file: %s", err)
|
q.logger.Error().Msgf("Failed to pre-cache file: %s", err)
|
||||||
|
} else {
|
||||||
|
q.logger.Debug().Msgf("Pre-cached %d files", len(filePaths))
|
||||||
}
|
}
|
||||||
}() // Pre-cache the files in the background
|
}() // Pre-cache the files in the background
|
||||||
// Pre-cache the first 256KB and 1MB of the file
|
// Pre-cache the first 256KB and 1MB of the file
|
||||||
@@ -222,21 +224,21 @@ func (q *QBit) preCacheFile(name string, filePaths []string) error {
|
|||||||
if len(filePaths) == 0 {
|
if len(filePaths) == 0 {
|
||||||
return fmt.Errorf("no file paths provided")
|
return fmt.Errorf("no file paths provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, filePath := range filePaths {
|
for _, filePath := range filePaths {
|
||||||
func() {
|
func(f string) {
|
||||||
file, err := os.Open(filePath)
|
|
||||||
defer func(file *os.File) {
|
file, err := os.Open(f)
|
||||||
_ = file.Close()
|
|
||||||
}(file)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
// Pre-cache the file header (first 256KB) using 16KB chunks.
|
// Pre-cache the file header (first 256KB) using 16KB chunks.
|
||||||
q.readSmallChunks(file, 0, 256*1024, 16*1024)
|
q.readSmallChunks(file, 0, 256*1024, 16*1024)
|
||||||
q.readSmallChunks(file, 1024*1024, 64*1024, 16*1024)
|
q.readSmallChunks(file, 1024*1024, 64*1024, 16*1024)
|
||||||
}()
|
}(filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -604,6 +604,7 @@ func (r *Repair) getWebdavBrokenFiles(media arr.Content) []arr.ContentFile {
|
|||||||
torrent := cache.GetTorrentByName(torrentName)
|
torrent := cache.GetTorrentByName(torrentName)
|
||||||
if torrent == nil {
|
if torrent == nil {
|
||||||
r.logger.Debug().Msgf("No torrent found for %s. Skipping", torrentName)
|
r.logger.Debug().Msgf("No torrent found for %s. Skipping", torrentName)
|
||||||
|
brokenFiles = append(brokenFiles, f...)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
files := make([]string, 0)
|
files := make([]string, 0)
|
||||||
|
|||||||
@@ -144,9 +144,7 @@ func (ui *Handler) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
"Page": "login",
|
"Page": "login",
|
||||||
"Title": "Login",
|
"Title": "Login",
|
||||||
}
|
}
|
||||||
if err := templates.ExecuteTemplate(w, "layout", data); err != nil {
|
_ = templates.ExecuteTemplate(w, "layout", data)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,9 +198,7 @@ func (ui *Handler) SetupHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
"Page": "setup",
|
"Page": "setup",
|
||||||
"Title": "Setup",
|
"Title": "Setup",
|
||||||
}
|
}
|
||||||
if err := templates.ExecuteTemplate(w, "layout", data); err != nil {
|
_ = templates.ExecuteTemplate(w, "layout", data)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,10 +245,7 @@ func (ui *Handler) IndexHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
"Page": "index",
|
"Page": "index",
|
||||||
"Title": "Torrents",
|
"Title": "Torrents",
|
||||||
}
|
}
|
||||||
if err := templates.ExecuteTemplate(w, "layout", data); err != nil {
|
_ = templates.ExecuteTemplate(w, "layout", data)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ui *Handler) DownloadHandler(w http.ResponseWriter, r *http.Request) {
|
func (ui *Handler) DownloadHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -260,10 +253,7 @@ func (ui *Handler) DownloadHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
"Page": "download",
|
"Page": "download",
|
||||||
"Title": "Download",
|
"Title": "Download",
|
||||||
}
|
}
|
||||||
if err := templates.ExecuteTemplate(w, "layout", data); err != nil {
|
_ = templates.ExecuteTemplate(w, "layout", data)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ui *Handler) RepairHandler(w http.ResponseWriter, r *http.Request) {
|
func (ui *Handler) RepairHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -271,10 +261,7 @@ func (ui *Handler) RepairHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
"Page": "repair",
|
"Page": "repair",
|
||||||
"Title": "Repair",
|
"Title": "Repair",
|
||||||
}
|
}
|
||||||
if err := templates.ExecuteTemplate(w, "layout", data); err != nil {
|
_ = templates.ExecuteTemplate(w, "layout", data)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ui *Handler) ConfigHandler(w http.ResponseWriter, r *http.Request) {
|
func (ui *Handler) ConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -282,10 +269,7 @@ func (ui *Handler) ConfigHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
"Page": "config",
|
"Page": "config",
|
||||||
"Title": "Config",
|
"Title": "Config",
|
||||||
}
|
}
|
||||||
if err := templates.ExecuteTemplate(w, "layout", data); err != nil {
|
_ = templates.ExecuteTemplate(w, "layout", data)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ui *Handler) handleGetArrs(w http.ResponseWriter, r *http.Request) {
|
func (ui *Handler) handleGetArrs(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var sharedClient = &http.Client{
|
var sharedClient = &http.Client{
|
||||||
Transport: &http.Transport{
|
Transport: &http.Transport{
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
Proxy: http.ProxyFromEnvironment,
|
|
||||||
MaxIdleConns: 100,
|
MaxIdleConns: 100,
|
||||||
MaxIdleConnsPerHost: 20,
|
MaxIdleConnsPerHost: 20,
|
||||||
MaxConnsPerHost: 50,
|
MaxConnsPerHost: 50,
|
||||||
@@ -47,8 +47,6 @@ type File struct {
|
|||||||
link string
|
link string
|
||||||
}
|
}
|
||||||
|
|
||||||
// You can not download this file because you have exceeded your traffic on this hoster
|
|
||||||
|
|
||||||
// File interface implementations for File
|
// File interface implementations for File
|
||||||
|
|
||||||
func (f *File) Close() error {
|
func (f *File) Close() error {
|
||||||
@@ -67,6 +65,7 @@ func (f *File) getDownloadLink() string {
|
|||||||
}
|
}
|
||||||
downloadLink := f.cache.GetDownloadLink(f.torrentId, f.name, f.link)
|
downloadLink := f.cache.GetDownloadLink(f.torrentId, f.name, f.link)
|
||||||
if downloadLink != "" && isValidURL(downloadLink) {
|
if downloadLink != "" && isValidURL(downloadLink) {
|
||||||
|
f.downloadLink = downloadLink
|
||||||
return downloadLink
|
return downloadLink
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
@@ -103,17 +102,27 @@ func (f *File) stream() (*http.Response, error) {
|
|||||||
|
|
||||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent {
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent {
|
||||||
|
|
||||||
|
f.downloadLink = ""
|
||||||
|
|
||||||
closeResp := func() {
|
closeResp := func() {
|
||||||
_, _ = io.Copy(io.Discard, resp.Body)
|
_, _ = io.Copy(io.Discard, resp.Body)
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode == http.StatusServiceUnavailable {
|
if resp.StatusCode == http.StatusServiceUnavailable {
|
||||||
closeResp()
|
b, _ := io.ReadAll(resp.Body)
|
||||||
// Read the body to consume the response
|
err := resp.Body.Close()
|
||||||
f.cache.MarkDownloadLinkAsInvalid(f.link, downloadLink, "bandwidth_exceeded")
|
if err != nil {
|
||||||
// Retry with a different API key if it's available
|
return nil, err
|
||||||
return f.stream()
|
}
|
||||||
|
if strings.Contains(string(b), "You can not download this file because you have exceeded your traffic on this hoster") {
|
||||||
|
_log.Error().Msgf("Failed to get download link for %s. Download link expired", f.name)
|
||||||
|
f.cache.MarkDownloadLinkAsInvalid(f.link, downloadLink, "bandwidth_exceeded")
|
||||||
|
// Retry with a different API key if it's available
|
||||||
|
return f.stream()
|
||||||
|
} else {
|
||||||
|
return resp, fmt.Errorf("link not found")
|
||||||
|
}
|
||||||
|
|
||||||
} else if resp.StatusCode == http.StatusNotFound {
|
} else if resp.StatusCode == http.StatusNotFound {
|
||||||
closeResp()
|
closeResp()
|
||||||
@@ -269,10 +278,6 @@ func (f *File) ReadAt(p []byte, off int64) (n int, err error) {
|
|||||||
|
|
||||||
// Read the data
|
// Read the data
|
||||||
n, err = f.Read(p)
|
n, err = f.Read(p)
|
||||||
|
|
||||||
// Don't restore position for Infuse compatibility
|
|
||||||
// Infuse expects sequential reads after the initial seek
|
|
||||||
|
|
||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -95,8 +95,7 @@ func (h *Handler) getParentFiles() []os.FileInfo {
|
|||||||
}
|
}
|
||||||
if item == "version.txt" {
|
if item == "version.txt" {
|
||||||
f.isDir = false
|
f.isDir = false
|
||||||
versionInfo := version.GetInfo().String()
|
f.size = int64(len(version.GetInfo().String()))
|
||||||
f.size = int64(len(versionInfo))
|
|
||||||
}
|
}
|
||||||
rootFiles = append(rootFiles, f)
|
rootFiles = append(rootFiles, f)
|
||||||
}
|
}
|
||||||
@@ -107,10 +106,7 @@ func (h *Handler) OpenFile(ctx context.Context, name string, flag int, perm os.F
|
|||||||
name = path.Clean("/" + name)
|
name = path.Clean("/" + name)
|
||||||
rootDir := h.getRootPath()
|
rootDir := h.getRootPath()
|
||||||
|
|
||||||
metadataOnly := false
|
metadataOnly := ctx.Value("metadataOnly") != nil
|
||||||
if ctx.Value("metadataOnly") != nil {
|
|
||||||
metadataOnly = true
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
@@ -122,7 +118,7 @@ func (h *Handler) OpenFile(ctx context.Context, name string, flag int, perm os.F
|
|||||||
isDir: true,
|
isDir: true,
|
||||||
children: h.getParentFiles(),
|
children: h.getParentFiles(),
|
||||||
name: "/",
|
name: "/",
|
||||||
metadataOnly: metadataOnly,
|
metadataOnly: true,
|
||||||
modTime: now,
|
modTime: now,
|
||||||
}, nil
|
}, nil
|
||||||
case path.Join(rootDir, "version.txt"):
|
case path.Join(rootDir, "version.txt"):
|
||||||
@@ -244,10 +240,16 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
ctx := context.WithValue(r.Context(), "metadataOnly", true)
|
ctx := context.WithValue(r.Context(), "metadataOnly", true)
|
||||||
r = r.WithContext(ctx)
|
r = r.WithContext(ctx)
|
||||||
cleanPath := path.Clean(r.URL.Path)
|
cleanPath := path.Clean(r.URL.Path)
|
||||||
depth := r.Header.Get("Depth")
|
r.Header.Set("Depth", "1")
|
||||||
if depth == "" {
|
if r.Header.Get("Depth") == "" {
|
||||||
depth = "1"
|
r.Header.Set("Depth", "1")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reject "infinity" depth
|
||||||
|
if r.Header.Get("Depth") == "infinity" {
|
||||||
|
r.Header.Set("Depth", "1")
|
||||||
|
}
|
||||||
|
depth := r.Header.Get("Depth")
|
||||||
// Use both path and Depth header to form the cache key.
|
// Use both path and Depth header to form the cache key.
|
||||||
cacheKey := fmt.Sprintf("propfind:%s:%s", cleanPath, depth)
|
cacheKey := fmt.Sprintf("propfind:%s:%s", cleanPath, depth)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user