diff --git a/internal/app.go b/internal/app.go index b720cde..50e28a5 100644 --- a/internal/app.go +++ b/internal/app.go @@ -44,7 +44,7 @@ func MainApp(configPath string) { os.Exit(1) } - apiClient := http.NewHTTPClient(config.GetToken(), config.GetRetriesUntilFailed(), config.GetRealDebridTimeout(), false, config, log.Named("httpclient")) + apiClient := http.NewHTTPClient(config.GetToken(), config.GetRetriesUntilFailed(), config.GetApiTimeoutSecs(), false, config, log.Named("httpclient")) rd := realdebrid.NewRealDebrid(apiClient, log.Named("realdebrid")) @@ -71,8 +71,8 @@ func MainApp(configPath string) { utils.EnsureDirExists("data") // Ensure the data directory exists torrentMgr := torrent.NewTorrentManager(config, rd, workerPool, repairPool, log.Named("manager")) - downloadClient := http.NewHTTPClient(config.GetToken(), config.GetRetriesUntilFailed(), 0, true, config, log.Named("dlclient")) - downloader := universal.NewDownloader(downloadClient, config.GetRealDebridTimeout()) + downloadClient := http.NewHTTPClient(config.GetToken(), config.GetRetriesUntilFailed(), config.GetDownloadTimeoutSecs(), true, config, log.Named("dlclient")) + downloader := universal.NewDownloader(downloadClient) router := chi.NewRouter() handlers.AttachHandlers(router, downloader, torrentMgr, config, rd, log.Named("router")) diff --git a/internal/config/types.go b/internal/config/types.go index ef0ccd8..b58121c 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -5,8 +5,8 @@ type ConfigInterface interface { GetVersion() string GetToken() string GetNumOfWorkers() int - GetRefreshEverySeconds() int - GetRepairEveryMinutes() int + GetRefreshEverySecs() int + GetRepairEveryMins() int EnableRepair() bool GetHost() string GetPort() string @@ -23,10 +23,11 @@ type ConfigInterface interface { ShouldServeFromRclone() bool ShouldVerifyDownloadLink() bool ShouldForceIPv6() bool - GetRealDebridTimeout() int + GetApiTimeoutSecs() int + GetDownloadTimeoutSecs() int GetRetriesUntilFailed() int EnableDownloadMount() bool - GetRateLimitSleepSeconds() int + GetRateLimitSleepSecs() int ShouldDeleteRarFiles() bool } @@ -34,14 +35,14 @@ type ZurgConfig struct { Version string `yaml:"zurg" json:"-"` Token string `yaml:"token" json:"-"` - Host string `yaml:"host" json:"host"` - Port string `yaml:"port" json:"port"` - Username string `yaml:"username" json:"username"` - Password string `yaml:"password" json:"password"` - Proxy string `yaml:"proxy" json:"proxy"` - NumOfWorkers int `yaml:"concurrent_workers" json:"concurrent_workers"` - RefreshEverySeconds int `yaml:"check_for_changes_every_secs" json:"check_for_changes_every_secs"` - RepairEveryMins int `yaml:"repair_every_mins" json:"repair_every_mins"` + Host string `yaml:"host" json:"host"` + Port string `yaml:"port" json:"port"` + Username string `yaml:"username" json:"username"` + Password string `yaml:"password" json:"password"` + Proxy string `yaml:"proxy" json:"proxy"` + NumOfWorkers int `yaml:"concurrent_workers" json:"concurrent_workers"` + RefreshEverySecs int `yaml:"check_for_changes_every_secs" json:"check_for_changes_every_secs"` + RepairEveryMins int `yaml:"repair_every_mins" json:"repair_every_mins"` IgnoreRenames bool `yaml:"ignore_renames" json:"ignore_renames"` RetainRDTorrentName bool `yaml:"retain_rd_torrent_name" json:"retain_rd_torrent_name"` @@ -50,14 +51,15 @@ type ZurgConfig struct { CanRepair bool `yaml:"enable_repair" json:"enable_repair"` DeleteRarFiles bool `yaml:"auto_delete_rar_torrents" json:"auto_delete_rar_torrents"` - RealDebridTimeout int `yaml:"realdebrid_timeout_secs" json:"realdebrid_timeout_secs"` - DownloadMount bool `yaml:"enable_download_mount" json:"enable_download_mount"` - RateLimitSleepSeconds int `yaml:"rate_limit_sleep_secs" json:"rate_limit_sleep_secs"` - RetriesUntilFailed int `yaml:"retries_until_failed" json:"retries_until_failed"` - NetworkBufferSize int `yaml:"network_buffer_size" json:"network_buffer_size"` - ServeFromRclone bool `yaml:"serve_from_rclone" json:"serve_from_rclone"` - VerifyDownloadLink bool `yaml:"verify_download_link" json:"verify_download_link"` - ForceIPv6 bool `yaml:"force_ipv6" json:"force_ipv6"` + ApiTimeoutSecs int `yaml:"api_timeout_secs" json:"api_timeout_secs"` + DownloadTimeoutSecs int `yaml:"download_timeout_secs" json:"download_timeout_secs"` + DownloadMount bool `yaml:"enable_download_mount" json:"enable_download_mount"` + RateLimitSleepSecs int `yaml:"rate_limit_sleep_secs" json:"rate_limit_sleep_secs"` + RetriesUntilFailed int `yaml:"retries_until_failed" json:"retries_until_failed"` + NetworkBufferSize int `yaml:"network_buffer_size" json:"network_buffer_size"` + ServeFromRclone bool `yaml:"serve_from_rclone" json:"serve_from_rclone"` + VerifyDownloadLink bool `yaml:"verify_download_link" json:"verify_download_link"` + ForceIPv6 bool `yaml:"force_ipv6" json:"force_ipv6"` OnLibraryUpdate string `yaml:"on_library_update" json:"on_library_update"` } @@ -103,14 +105,14 @@ func (z *ZurgConfig) GetNumOfWorkers() int { return z.NumOfWorkers } -func (z *ZurgConfig) GetRefreshEverySeconds() int { - if z.RefreshEverySeconds == 0 { +func (z *ZurgConfig) GetRefreshEverySecs() int { + if z.RefreshEverySecs == 0 { return 60 } - return z.RefreshEverySeconds + return z.RefreshEverySecs } -func (z *ZurgConfig) GetRepairEveryMinutes() int { +func (z *ZurgConfig) GetRepairEveryMins() int { if z.RepairEveryMins == 0 { return 60 } @@ -167,18 +169,25 @@ func (z *ZurgConfig) EnableDownloadMount() bool { return z.DownloadMount } -func (z *ZurgConfig) GetRealDebridTimeout() int { - if z.RealDebridTimeout == 0 { +func (z *ZurgConfig) GetApiTimeoutSecs() int { + if z.ApiTimeoutSecs == 0 { return 4 } - return z.RealDebridTimeout + return z.ApiTimeoutSecs } -func (z *ZurgConfig) GetRateLimitSleepSeconds() int { - if z.RateLimitSleepSeconds == 0 { +func (z *ZurgConfig) GetDownloadTimeoutSecs() int { + if z.DownloadTimeoutSecs == 0 { + return 2 + } + return z.DownloadTimeoutSecs +} + +func (z *ZurgConfig) GetRateLimitSleepSecs() int { + if z.RateLimitSleepSecs == 0 { return 4 } - return z.RateLimitSleepSeconds + return z.RateLimitSleepSecs } func (z *ZurgConfig) ShouldDeleteRarFiles() bool { diff --git a/internal/handlers/home.go b/internal/handlers/home.go index 124c87c..fbe04c2 100644 --- a/internal/handlers/home.go +++ b/internal/handlers/home.go @@ -179,8 +179,8 @@ func (zr *Handlers) handleHome(resp http.ResponseWriter, req *http.Request) { %d - Refresh Every Seconds - %d + Refresh Every... + %d secs Retain RD Torrent Name @@ -199,16 +199,20 @@ func (zr *Handlers) handleHome(resp http.ResponseWriter, req *http.Request) { %t - RealDebrid Timeout - %d + API Timeout + %d secs + + + Download Timeout + %d secs Use Download Mount %t - Rate Limit Sleep Seconds - %d + Rate Limit Sleep for... + %d secs Retries Until Failed @@ -270,14 +274,15 @@ func (zr *Handlers) handleHome(resp http.ResponseWriter, req *http.Request) { response.Config.GetHost(), response.Config.GetPort(), response.Config.GetNumOfWorkers(), - response.Config.GetRefreshEverySeconds(), + response.Config.GetRefreshEverySecs(), response.Config.EnableRetainRDTorrentName(), response.Config.EnableRetainFolderNameExtension(), response.Config.EnableRepair(), response.Config.ShouldDeleteRarFiles(), - response.Config.GetRealDebridTimeout(), + response.Config.GetApiTimeoutSecs(), + response.Config.GetDownloadTimeoutSecs(), response.Config.EnableDownloadMount(), - response.Config.GetRateLimitSleepSeconds(), + response.Config.GetRateLimitSleepSecs(), response.Config.GetRetriesUntilFailed(), response.Config.GetNetworkBufferSize(), response.Config.ShouldServeFromRclone(), diff --git a/internal/torrent/refresh.go b/internal/torrent/refresh.go index b838a57..2af0a73 100644 --- a/internal/torrent/refresh.go +++ b/internal/torrent/refresh.go @@ -124,7 +124,7 @@ func (t *TorrentManager) startRefreshJob() { _ = t.workerPool.Submit(func() { t.log.Info("Starting periodic refresh job") for { - <-time.After(time.Duration(t.Config.GetRefreshEverySeconds()) * time.Second) + <-time.After(time.Duration(t.Config.GetRefreshEverySecs()) * time.Second) checksum := t.getCurrentState() if t.latestState.equal(checksum) { diff --git a/internal/torrent/repair.go b/internal/torrent/repair.go index 6f3795f..87cd2be 100644 --- a/internal/torrent/repair.go +++ b/internal/torrent/repair.go @@ -26,7 +26,7 @@ func (t *TorrentManager) startRepairJob() { // there is 1 repair worker, with max 1 blocking task _ = t.repairPool.Submit(func() { t.log.Info("Starting periodic repair job") - repairTicker := time.NewTicker(time.Duration(t.Config.GetRepairEveryMinutes()) * time.Minute) + repairTicker := time.NewTicker(time.Duration(t.Config.GetRepairEveryMins()) * time.Minute) defer repairTicker.Stop() for { diff --git a/internal/universal/downloader.go b/internal/universal/downloader.go index c186334..e6f915f 100644 --- a/internal/universal/downloader.go +++ b/internal/universal/downloader.go @@ -14,14 +14,12 @@ import ( ) type Downloader struct { - client *zurghttp.HTTPClient - timeoutSecs int + client *zurghttp.HTTPClient } -func NewDownloader(client *zurghttp.HTTPClient, timeoutSecs int) *Downloader { +func NewDownloader(client *zurghttp.HTTPClient) *Downloader { return &Downloader{ - client: client, - timeoutSecs: timeoutSecs, + client: client, } } @@ -157,7 +155,6 @@ func (dl *Downloader) streamFileToResponse(torrent *intTor.Torrent, file *intTor if req.Header.Get("Range") != "" { dlReq.Header.Add("Range", req.Header.Get("Range")) rangeLog = " (range: " + req.Header.Get("Range") + ")" - } if torrent != nil { @@ -166,12 +163,7 @@ func (dl *Downloader) streamFileToResponse(torrent *intTor.Torrent, file *intTor log.Debugf("Downloading unrestricted link %s (%s)%s", unrestrict.Download, unrestrict.Link, rangeLog) } - // timeout := time.Duration(dl.timeoutSecs) * time.Second - // ctx, cancel := context.WithTimeout(context.TODO(), timeout) - // dlReq = dlReq.WithContext(ctx) - download, err := dl.client.Do(dlReq) - if err != nil { log.Warnf("Cannot download file %s: %v", unrestrict.Download, err) if file != nil && unrestrict.Streamable == 1 { @@ -185,7 +177,6 @@ func (dl *Downloader) streamFileToResponse(torrent *intTor.Torrent, file *intTor http.Error(resp, "File is not available", http.StatusNotFound) return } - defer download.Body.Close() if download.StatusCode/100 != 2 { diff --git a/pkg/http/client.go b/pkg/http/client.go index be2fd65..dd68ebc 100644 --- a/pkg/http/client.go +++ b/pkg/http/client.go @@ -144,20 +144,16 @@ func (r *HTTPClient) Do(req *http.Request) (*http.Response, error) { var resp *http.Response var err error attempt := 0 + for { + if resp != nil && resp.Body != nil { + resp.Body.Close() + } r.replaceHostIfNeeded(req) // needed for ipv6 if !strings.Contains(req.URL.Host, "api.real-debrid.com") { r.log.Debugf("downloading %s", req.URL) } resp, err = r.client.Do(req) - // check if error is context deadline exceeded - if r.ensureIPv6Host && r.cfg.ShouldForceIPv6() && err != nil && strings.Contains(err.Error(), "context deadline exceeded") { - attempt += 1 - if attempt > r.maxRetries { - break - } - continue - } if resp != nil && resp.StatusCode/100 >= 4 { body, _ := io.ReadAll(resp.Body) if body != nil { @@ -169,7 +165,7 @@ func (r *HTTPClient) Do(req *http.Request) (*http.Response, error) { } } } - incr := r.shouldRetry(resp, reqHasRangeHeader, err, r.cfg.GetRateLimitSleepSeconds()) + incr := r.shouldRetry(resp, reqHasRangeHeader, err, r.cfg.GetRateLimitSleepSecs()) if incr > 0 { attempt += incr if attempt > r.maxRetries { @@ -255,6 +251,9 @@ func (r *HTTPClient) shouldRetry(resp *http.Response, reqHasRangeHeader bool, er } } } + if err != nil && strings.Contains(err.Error(), "timeout awaiting response headers") { + return 1 + } if resp != nil { if resp.StatusCode == 429 { time.Sleep(time.Duration(rateLimitSleep) * time.Second) @@ -279,7 +278,7 @@ func backoffFunc(attempt int) time.Duration { } func (r *HTTPClient) CanFetchFirstByte(url string) bool { - timeout := time.Duration(r.cfg.GetRealDebridTimeout()) * time.Second + timeout := time.Duration(r.timeoutSecs) * time.Second ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() req, err := http.NewRequest("GET", url, nil)