From 2ada1aa249235cb80ce01427d7d79bbc462d491e Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 10 Jul 2026 16:55:20 +0200 Subject: [PATCH] fix(qbittorrent): handle v5 stopped torrent states for pause functionality (#6211) --- packages/api/src/router/widgets/downloads.ts | 5 ++++ .../qbittorrent/qbittorrent-integration.ts | 7 ++++- .../src/lib/integration-request-handler.ts | 28 +++++++++++++------ .../src/lib/request-handler.ts | 4 +++ 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/packages/api/src/router/widgets/downloads.ts b/packages/api/src/router/widgets/downloads.ts index 50aee350e..bdce2473f 100644 --- a/packages/api/src/router/widgets/downloads.ts +++ b/packages/api/src/router/widgets/downloads.ts @@ -49,6 +49,7 @@ export const downloadsRouter = createTRPCRouter({ await integrationInstance.pauseQueueAsync(); }), ); + downloadClientRequestHandler.invalidateCache(); }), pauseItem: protectedProcedure .concat(createDownloadClientIntegrationMiddleware("interact")) @@ -60,6 +61,7 @@ export const downloadsRouter = createTRPCRouter({ await integrationInstance.pauseItemAsync(input.item); }), ); + downloadClientRequestHandler.invalidateCache(); }), resume: protectedProcedure .meta({ @@ -77,6 +79,7 @@ export const downloadsRouter = createTRPCRouter({ await integrationInstance.resumeQueueAsync(); }), ); + downloadClientRequestHandler.invalidateCache(); }), resumeItem: protectedProcedure .concat(createDownloadClientIntegrationMiddleware("interact")) @@ -88,6 +91,7 @@ export const downloadsRouter = createTRPCRouter({ await integrationInstance.resumeItemAsync(input.item); }), ); + downloadClientRequestHandler.invalidateCache(); }), deleteItem: protectedProcedure .concat(createDownloadClientIntegrationMiddleware("interact")) @@ -99,5 +103,6 @@ export const downloadsRouter = createTRPCRouter({ await integrationInstance.deleteItemAsync(input.item, input.fromDisk); }), ); + downloadClientRequestHandler.invalidateCache(); }), }); diff --git a/packages/integrations/src/download-client/qbittorrent/qbittorrent-integration.ts b/packages/integrations/src/download-client/qbittorrent/qbittorrent-integration.ts index a142fb1a3..c08dd5848 100644 --- a/packages/integrations/src/download-client/qbittorrent/qbittorrent-integration.ts +++ b/packages/integrations/src/download-client/qbittorrent/qbittorrent-integration.ts @@ -99,11 +99,14 @@ export class QBitTorrentIntegration extends Integration implements IDownloadClie password: this.getSecretValue("password"), }; - return new QBittorrent({ + const client = new QBittorrent({ ...credentials, baseUrl: this.url("/").toString(), dispatcher: dispatcher ?? (await createCertificateAgentAsync()), }); + // Populate version state so the library uses correct v5 endpoints (stop/start vs pause/resume) + await client.getAppVersion(); + return client; } private static getTorrentState(state: string): DownloadClientItem["state"] { @@ -125,6 +128,8 @@ export class QBitTorrentIntegration extends Integration implements IDownloadClie return "seeding"; case "pausedDL": case "pausedUP": + case "stoppedDL": + case "stoppedUP": return "paused"; case "stalledDL": return "stalled"; diff --git a/packages/request-handler/src/lib/integration-request-handler.ts b/packages/request-handler/src/lib/integration-request-handler.ts index d2fa0f1a4..359d63dbc 100644 --- a/packages/request-handler/src/lib/integration-request-handler.ts +++ b/packages/request-handler/src/lib/integration-request-handler.ts @@ -20,11 +20,23 @@ export const createIntegrationRequestHandler = < TInput extends Record, >( options: Options, -) => ({ - handler: (integration: IntegrationOfKind, itemOptions: TInput) => { - const inner = createRequestHandler({ - requestAsync: async (input) => options.requestAsync(integration, input.options), - }); - return inner.handler({ integrationId: integration.id, options: itemOptions }); - }, -}); +) => { + const integrationMap = new Map>(); + const inner = createRequestHandler({ + requestAsync: async (input) => { + const integration = integrationMap.get(input.integrationId); + if (!integration) { + throw new Error(`Integration ${input.integrationId} not found in cache`); + } + return options.requestAsync(integration, input.options); + }, + }); + + return { + invalidateCache: inner.invalidateCache, + handler: (integration: IntegrationOfKind, itemOptions: TInput) => { + integrationMap.set(integration.id, integration); + return inner.handler({ integrationId: integration.id, options: itemOptions }); + }, + }; +}; diff --git a/packages/request-handler/src/lib/request-handler.ts b/packages/request-handler/src/lib/request-handler.ts index bafcc8c8a..f733078ee 100644 --- a/packages/request-handler/src/lib/request-handler.ts +++ b/packages/request-handler/src/lib/request-handler.ts @@ -22,6 +22,10 @@ export const createRequestHandler = >>(); return { + invalidateCache: () => { + cache.clear(); + inflight.clear(); + }, handler: (input: TInput) => ({ async getDataAsync(): Promise<{ data: TData; timestamp: Date }> { const ttl = options.cacheTtlMs ?? DEFAULT_TTL_MS;