Centralize download client timeout constant

Add DOWNLOAD_CLIENT_TIMEOUT (60000ms) in src/lib/constants/download-timeouts.ts and replace hardcoded 60000ms timeouts across Deluge, Prowlarr, qBittorrent and Transmission integrations. This centralizes the download/API timeout (gives headroom for indexers that enforce ~30s waits) and makes future adjustments easier without changing behavior.
This commit is contained in:
kikootwo
2026-02-24 01:09:58 -05:00
parent 3c680f2f38
commit b15a472bab
5 changed files with 25 additions and 11 deletions
+10
View File
@@ -0,0 +1,10 @@
/**
* Component: Download Client Timeout Constants
* Documentation: documentation/phase3/download-clients.md
*
* Some indexers (e.g. YGGtorrent) enforce a ~30s wait before allowing
* .torrent file downloads. 60s gives sufficient headroom.
*/
/** Timeout for download client API calls and .torrent file fetches (ms) */
export const DOWNLOAD_CLIENT_TIMEOUT = 60000;
+4 -3
View File
@@ -6,6 +6,7 @@
import axios, { AxiosInstance } from 'axios';
import https from 'https';
import path from 'path';
import { DOWNLOAD_CLIENT_TIMEOUT } from '../constants/download-timeouts';
import * as parseTorrentModule from 'parse-torrent';
import { RMABLogger } from '../utils/logger';
import { PathMapper, PathMappingConfig } from '../utils/path-mapper';
@@ -49,7 +50,7 @@ export class DelugeService implements IDownloadClient {
? new https.Agent({ rejectUnauthorized: false }) : undefined;
if (httpsAgent) logger.info('[Deluge] SSL certificate verification disabled');
this.client = axios.create({ baseURL: this.baseUrl, timeout: 60000, httpsAgent }); // 60 seconds - some indexers (e.g. yggtorrent) enforce a 30s wait before download
this.client = axios.create({ baseURL: this.baseUrl, timeout: DOWNLOAD_CLIENT_TIMEOUT, httpsAgent });
}
/** JSON-RPC call with automatic re-authentication on auth failure */
@@ -190,7 +191,7 @@ export class DelugeService implements IDownloadClient {
try {
torrentResponse = await axios.get(torrentUrl, {
responseType: 'arraybuffer', maxRedirects: 0,
validateStatus: (s) => s >= 200 && s < 300, timeout: 60000, // 60 seconds - some indexers (e.g. yggtorrent) enforce a 30s wait before download
validateStatus: (s) => s >= 200 && s < 300, timeout: DOWNLOAD_CLIENT_TIMEOUT,
});
if (torrentResponse.data.length > 0) {
const magnetMatch = torrentResponse.data.toString().match(/^magnet:\?[^\s]+$/);
@@ -203,7 +204,7 @@ export class DelugeService implements IDownloadClient {
const loc = error.response.headers['location'];
if (loc?.startsWith('magnet:')) return this.addMagnetLink(loc, category, options);
if (loc?.startsWith('http://') || loc?.startsWith('https://')) {
try { torrentResponse = await axios.get(loc, { responseType: 'arraybuffer', timeout: 60000, maxRedirects: 5 }); } // 60 seconds - some indexers (e.g. yggtorrent) enforce a 30s wait before download
try { torrentResponse = await axios.get(loc, { responseType: 'arraybuffer', timeout: DOWNLOAD_CLIENT_TIMEOUT, maxRedirects: 5 }); }
catch { throw new Error('Failed to download torrent file after redirect'); }
} else { throw new Error(`Invalid redirect location: ${loc}`); }
} else { throw new Error(`Failed to download torrent: HTTP ${status}`); }
+3 -2
View File
@@ -5,6 +5,7 @@
import axios, { AxiosInstance } from 'axios';
import { XMLParser } from 'fast-xml-parser';
import { DOWNLOAD_CLIENT_TIMEOUT } from '../constants/download-timeouts';
import { TorrentResult } from '../utils/ranking-algorithm';
import { RMABLogger } from '../utils/logger';
@@ -87,7 +88,7 @@ export class ProwlarrService {
headers: {
'X-Api-Key': this.apiKey,
},
timeout: 60000, // 60 seconds - some indexers (e.g. yggtorrent) enforce a 30s wait before download
timeout: DOWNLOAD_CLIENT_TIMEOUT,
paramsSerializer: {
serialize: (params) => {
// Custom serializer to handle arrays correctly for Prowlarr API
@@ -314,7 +315,7 @@ export class ProwlarrService {
limit: 100,
extended: 1,
},
timeout: 60000,
timeout: DOWNLOAD_CLIENT_TIMEOUT,
responseType: 'text', // Get XML as text
});
+4 -3
View File
@@ -6,6 +6,7 @@
import axios, { AxiosInstance } from 'axios';
import https from 'https';
import path from 'path';
import { DOWNLOAD_CLIENT_TIMEOUT } from '../constants/download-timeouts';
import * as parseTorrentModule from 'parse-torrent';
import FormData from 'form-data';
import { RMABLogger } from '../utils/logger';
@@ -140,7 +141,7 @@ export class QBittorrentService implements IDownloadClient {
this.client = axios.create({
baseURL: `${this.baseUrl}/api/v2`,
timeout: 60000, // 60 seconds - some indexers (e.g. yggtorrent) enforce a 30s wait before download
timeout: DOWNLOAD_CLIENT_TIMEOUT,
httpsAgent: this.httpsAgent,
// Support nginx/Apache reverse proxy with HTTP Basic Auth
auth: {
@@ -352,7 +353,7 @@ export class QBittorrentService implements IDownloadClient {
responseType: 'arraybuffer',
maxRedirects: 0,
validateStatus: (status) => status >= 200 && status < 300, // Only 2xx is success
timeout: 60000, // 60 seconds - some indexers (e.g. yggtorrent) enforce a 30s wait before download
timeout: DOWNLOAD_CLIENT_TIMEOUT,
});
logger.info(` Got 2xx response, size=${torrentResponse.data.length} bytes`);
@@ -394,7 +395,7 @@ export class QBittorrentService implements IDownloadClient {
try {
torrentResponse = await axios.get(location, {
responseType: 'arraybuffer',
timeout: 60000, // 60 seconds - some indexers (e.g. yggtorrent) enforce a 30s wait before download
timeout: DOWNLOAD_CLIENT_TIMEOUT,
maxRedirects: 5,
});
logger.info(` After following redirect: size=${torrentResponse.data.length} bytes`);
+4 -3
View File
@@ -6,6 +6,7 @@
import axios, { AxiosInstance } from 'axios';
import https from 'https';
import path from 'path';
import { DOWNLOAD_CLIENT_TIMEOUT } from '../constants/download-timeouts';
import * as parseTorrentModule from 'parse-torrent';
import { RMABLogger } from '../utils/logger';
import { PathMapper, PathMappingConfig } from '../utils/path-mapper';
@@ -106,7 +107,7 @@ export class TransmissionService implements IDownloadClient {
this.client = axios.create({
baseURL: this.baseUrl,
timeout: 60000, // 60 seconds - some indexers (e.g. yggtorrent) enforce a 30s wait before download
timeout: DOWNLOAD_CLIENT_TIMEOUT,
httpsAgent: this.httpsAgent,
});
}
@@ -274,7 +275,7 @@ export class TransmissionService implements IDownloadClient {
responseType: 'arraybuffer',
maxRedirects: 0,
validateStatus: (status) => status >= 200 && status < 300,
timeout: 60000, // 60 seconds - some indexers (e.g. yggtorrent) enforce a 30s wait before download
timeout: DOWNLOAD_CLIENT_TIMEOUT,
});
// Check if response body is a magnet link
@@ -302,7 +303,7 @@ export class TransmissionService implements IDownloadClient {
try {
torrentResponse = await axios.get(location, {
responseType: 'arraybuffer',
timeout: 60000, // 60 seconds - some indexers (e.g. yggtorrent) enforce a 30s wait before download
timeout: DOWNLOAD_CLIENT_TIMEOUT,
maxRedirects: 5,
});
} catch {