From 064d1842def4f28d83c48f9d1051b7cdfcd52499 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Wed, 15 Jul 2026 16:51:40 +0200 Subject: [PATCH] feat(qbittorrent): add configurable server-side sort order as widget option --- packages/api/src/router/widgets/downloads.ts | 14 ++++++-- .../qbittorrent/qbittorrent-integration.ts | 12 +++++-- .../downloads/download-client-integration.ts | 6 +++- packages/request-handler/src/downloads.ts | 2 +- packages/translation/src/lang/en.json | 24 ++++++++++++-- packages/widgets/src/downloads/component.tsx | 2 ++ packages/widgets/src/downloads/index.ts | 32 +++++++++++++++++++ 7 files changed, 84 insertions(+), 8 deletions(-) diff --git a/packages/api/src/router/widgets/downloads.ts b/packages/api/src/router/widgets/downloads.ts index bdce2473f..516d155f7 100644 --- a/packages/api/src/router/widgets/downloads.ts +++ b/packages/api/src/router/widgets/downloads.ts @@ -22,10 +22,20 @@ export const downloadsRouter = createTRPCRouter({ }, }) .concat(createDownloadClientIntegrationMiddleware("query")) - .input(z.object({ limitPerIntegration: z.number().default(50) })) + .input( + z.object({ + limitPerIntegration: z.number().default(50), + qbittorrentSort: z.string().optional(), + qbittorrentSortReverse: z.boolean().optional(), + }), + ) .query(async ({ ctx, input }) => { return await settleIntegrationQueries(ctx.integrations, async (integration) => { - const innerHandler = downloadClientRequestHandler.handler(integration, { limit: input.limitPerIntegration }); + const innerHandler = downloadClientRequestHandler.handler(integration, { + limit: input.limitPerIntegration, + qbittorrentSort: input.qbittorrentSort, + qbittorrentSortReverse: input.qbittorrentSortReverse, + }); const { data, timestamp } = await innerHandler.getDataAsync(); return { integration: { id: integration.id, name: integration.name, kind: integration.kind, updatedAt: timestamp }, diff --git a/packages/integrations/src/download-client/qbittorrent/qbittorrent-integration.ts b/packages/integrations/src/download-client/qbittorrent/qbittorrent-integration.ts index c08dd5848..c3afe7d90 100644 --- a/packages/integrations/src/download-client/qbittorrent/qbittorrent-integration.ts +++ b/packages/integrations/src/download-client/qbittorrent/qbittorrent-integration.ts @@ -27,10 +27,18 @@ export class QBitTorrentIntegration extends Integration implements IDownloadClie }; } - public async getClientJobsAndStatusAsync(input: { limit: number }): Promise { + public async getClientJobsAndStatusAsync(input: { + limit: number; + qbittorrentSort?: string; + qbittorrentSortReverse?: boolean; + }): Promise { const type = "torrent"; const client = await this.getClientAsync(); - const torrents = await client.listTorrents({ limit: input.limit }); + const torrents = await client.listTorrents({ + limit: input.limit, + sort: input.qbittorrentSort, + reverse: input.qbittorrentSortReverse, + }); const rates = torrents.reduce( ({ down, up }, { dlspeed, upspeed }) => ({ down: down + dlspeed, up: up + upspeed }), { down: 0, up: 0 }, diff --git a/packages/integrations/src/interfaces/downloads/download-client-integration.ts b/packages/integrations/src/interfaces/downloads/download-client-integration.ts index bc43bee27..eaf3a8278 100644 --- a/packages/integrations/src/interfaces/downloads/download-client-integration.ts +++ b/packages/integrations/src/interfaces/downloads/download-client-integration.ts @@ -3,7 +3,11 @@ import type { DownloadClientItem } from "./download-client-items"; export interface IDownloadClientIntegration { /** Get download client's status and list of all of it's items */ - getClientJobsAndStatusAsync(input: { limit: number }): Promise; + getClientJobsAndStatusAsync(input: { + limit: number; + qbittorrentSort?: string; + qbittorrentSortReverse?: boolean; + }): Promise; /** Pauses the client or all of it's items */ pauseQueueAsync(): Promise; /** Pause a single item using it's ID */ diff --git a/packages/request-handler/src/downloads.ts b/packages/request-handler/src/downloads.ts index 12106b07a..f9355165e 100644 --- a/packages/request-handler/src/downloads.ts +++ b/packages/request-handler/src/downloads.ts @@ -7,7 +7,7 @@ import { createIntegrationRequestHandler } from "./lib/integration-request-handl export const downloadClientRequestHandler = createIntegrationRequestHandler< DownloadClientJobsAndStatus, IntegrationKindByCategory<"downloadClient">, - { limit: number } + { limit: number; qbittorrentSort?: string; qbittorrentSortReverse?: boolean } >({ async requestAsync(integration, input) { const integrationInstance = await createIntegrationAsync(integration); diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index 594ab29a8..520790f57 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -3064,8 +3064,28 @@ "limitPerIntegration": { "label": "Limit items per integration", "description": "This will limit the number of items shown per integration, not globally" - } - }, + }, + "qbittorrentSort": { + "label": "qBittorrent sort order", + "option": { + "added_on": "Date Added", + "completion_on": "Completion Date", + "dlspeed": "Download Speed", + "eta": "ETA", + "name": "Name", + "priority": "Priority", + "progress": "Progress", + "ratio": "Ratio", + "size": "Size", + "state": "State", + "time_active": "Time Active", + "upspeed": "Upload Speed", + "uploaded": "Uploaded" + } + }, + "qbittorrentSortReverse": { + "label": "Reverse qBittorrent sort order" + }, "errors": { "noColumns": "Select Columns in Items", "noCommunications": "Can't load data from integration" diff --git a/packages/widgets/src/downloads/component.tsx b/packages/widgets/src/downloads/component.tsx index ff9b71664..a028bf3e9 100644 --- a/packages/widgets/src/downloads/component.tsx +++ b/packages/widgets/src/downloads/component.tsx @@ -91,6 +91,8 @@ export default function DownloadClientsWidget({ const { data: currentItems = [] } = clientApi.widget.downloads.getJobsAndStatuses.useQuery({ integrationIds, limitPerIntegration: options.limitPerIntegration, + qbittorrentSort: options.qbittorrentSort, + qbittorrentSortReverse: options.qbittorrentSortReverse, }); const t = useScopedI18n("widget.downloads"); diff --git a/packages/widgets/src/downloads/index.ts b/packages/widgets/src/downloads/index.ts index 0d9c66ddd..c6e367b80 100644 --- a/packages/widgets/src/downloads/index.ts +++ b/packages/widgets/src/downloads/index.ts @@ -88,6 +88,30 @@ export const { definition, componentLoader } = createWidgetDefinition("downloads validate: z.number().min(1), withDescription: true, }), + qbittorrentSort: factory.select({ + defaultValue: "added_on", + options: [ + "added_on", + "completion_on", + "dlspeed", + "eta", + "name", + "priority", + "progress", + "ratio", + "size", + "state", + "time_active", + "upspeed", + "uploaded", + ].map((value) => ({ + value, + label: (t) => t(`widget.downloads.option.qbittorrentSort.option.${value}`), + })), + }), + qbittorrentSortReverse: factory.switch({ + defaultValue: false, + }), }), { defaultSort: { @@ -116,6 +140,14 @@ export const { definition, componentLoader } = createWidgetDefinition("downloads shouldHide: (_, integrationKinds) => !getIntegrationKindsByCategory("torrent").some((kinds) => integrationKinds.includes(kinds)), }, + qbittorrentSort: { + shouldHide: (_, integrationKinds) => + !getIntegrationKindsByCategory("torrent").some((kinds) => integrationKinds.includes(kinds)), + }, + qbittorrentSortReverse: { + shouldHide: (_, integrationKinds) => + !getIntegrationKindsByCategory("torrent").some((kinds) => integrationKinds.includes(kinds)), + }, }, ); },