feat(downloads): add option to limit amount of items (#3205)
This commit is contained in:
@@ -19,10 +19,11 @@ const createDownloadClientIntegrationMiddleware = (action: IntegrationAction) =>
|
|||||||
export const downloadsRouter = createTRPCRouter({
|
export const downloadsRouter = createTRPCRouter({
|
||||||
getJobsAndStatuses: publicProcedure
|
getJobsAndStatuses: publicProcedure
|
||||||
.concat(createDownloadClientIntegrationMiddleware("query"))
|
.concat(createDownloadClientIntegrationMiddleware("query"))
|
||||||
.query(async ({ ctx }) => {
|
.input(z.object({ limitPerIntegration: z.number().default(50) }))
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
return await Promise.all(
|
return await Promise.all(
|
||||||
ctx.integrations.map(async (integration) => {
|
ctx.integrations.map(async (integration) => {
|
||||||
const innerHandler = downloadClientRequestHandler.handler(integration, {});
|
const innerHandler = downloadClientRequestHandler.handler(integration, { limit: input.limitPerIntegration });
|
||||||
|
|
||||||
const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
||||||
|
|
||||||
@@ -40,7 +41,8 @@ export const downloadsRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
subscribeToJobsAndStatuses: publicProcedure
|
subscribeToJobsAndStatuses: publicProcedure
|
||||||
.concat(createDownloadClientIntegrationMiddleware("query"))
|
.concat(createDownloadClientIntegrationMiddleware("query"))
|
||||||
.subscription(({ ctx }) => {
|
.input(z.object({ limitPerIntegration: z.number().default(50) }))
|
||||||
|
.subscription(({ ctx, input }) => {
|
||||||
return observable<{
|
return observable<{
|
||||||
integration: Modify<Integration, { kind: IntegrationKindByCategory<"downloadClient"> }>;
|
integration: Modify<Integration, { kind: IntegrationKindByCategory<"downloadClient"> }>;
|
||||||
data: DownloadClientJobsAndStatus;
|
data: DownloadClientJobsAndStatus;
|
||||||
@@ -48,7 +50,9 @@ export const downloadsRouter = createTRPCRouter({
|
|||||||
const unsubscribes: (() => void)[] = [];
|
const unsubscribes: (() => void)[] = [];
|
||||||
for (const integrationWithSecrets of ctx.integrations) {
|
for (const integrationWithSecrets of ctx.integrations) {
|
||||||
const { decryptedSecrets: _, ...integration } = integrationWithSecrets;
|
const { decryptedSecrets: _, ...integration } = integrationWithSecrets;
|
||||||
const innerHandler = downloadClientRequestHandler.handler(integrationWithSecrets, {});
|
const innerHandler = downloadClientRequestHandler.handler(integrationWithSecrets, {
|
||||||
|
limit: input.limitPerIntegration,
|
||||||
|
});
|
||||||
const unsubscribe = innerHandler.subscribe((data) => {
|
const unsubscribe = innerHandler.subscribe((data) => {
|
||||||
emit.next({
|
emit.next({
|
||||||
integration,
|
integration,
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ export const downloadsJob = createCronJob("downloads", EVERY_5_SECONDS).withCall
|
|||||||
createRequestIntegrationJobHandler(downloadClientRequestHandler.handler, {
|
createRequestIntegrationJobHandler(downloadClientRequestHandler.handler, {
|
||||||
widgetKinds: ["downloads"],
|
widgetKinds: ["downloads"],
|
||||||
getInput: {
|
getInput: {
|
||||||
downloads: () => ({}),
|
downloads: (options) => ({
|
||||||
|
limit: options.limitPerIntegration,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import type { DownloadClientItem } from "../../interfaces/downloads/download-cli
|
|||||||
import type { Aria2Download, Aria2GetClient } from "./aria2-types";
|
import type { Aria2Download, Aria2GetClient } from "./aria2-types";
|
||||||
|
|
||||||
export class Aria2Integration extends DownloadClientIntegration {
|
export class Aria2Integration extends DownloadClientIntegration {
|
||||||
public async getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus> {
|
public async getClientJobsAndStatusAsync(input: { limit: number }): Promise<DownloadClientJobsAndStatus> {
|
||||||
const client = this.getClient();
|
const client = this.getClient();
|
||||||
const keys: (keyof Aria2Download)[] = [
|
const keys: (keyof Aria2Download)[] = [
|
||||||
"bittorrent",
|
"bittorrent",
|
||||||
@@ -27,12 +27,12 @@ export class Aria2Integration extends DownloadClientIntegration {
|
|||||||
];
|
];
|
||||||
const [activeDownloads, waitingDownloads, stoppedDownloads, globalStats] = await Promise.all([
|
const [activeDownloads, waitingDownloads, stoppedDownloads, globalStats] = await Promise.all([
|
||||||
client.tellActive(),
|
client.tellActive(),
|
||||||
client.tellWaiting(0, 1000, keys),
|
client.tellWaiting(0, input.limit, keys),
|
||||||
client.tellStopped(0, 1000, keys),
|
client.tellStopped(0, input.limit, keys),
|
||||||
client.getGlobalStat(),
|
client.getGlobalStat(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const downloads = [...activeDownloads, ...waitingDownloads, ...stoppedDownloads];
|
const downloads = [...activeDownloads, ...waitingDownloads, ...stoppedDownloads].slice(0, input.limit);
|
||||||
const allPaused = downloads.every((download) => download.status === "paused");
|
const allPaused = downloads.every((download) => download.status === "paused");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -29,9 +29,10 @@ export class DelugeIntegration extends DownloadClientIntegration {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus> {
|
public async getClientJobsAndStatusAsync(input: { limit: number }): Promise<DownloadClientJobsAndStatus> {
|
||||||
const type = "torrent";
|
const type = "torrent";
|
||||||
const client = await this.getClientAsync();
|
const client = await this.getClientAsync();
|
||||||
|
// Currently there is no way to limit the number of returned torrents
|
||||||
const {
|
const {
|
||||||
stats: { download_rate, upload_rate },
|
stats: { download_rate, upload_rate },
|
||||||
torrents: rawTorrents,
|
torrents: rawTorrents,
|
||||||
@@ -49,27 +50,29 @@ export class DelugeIntegration extends DownloadClientIntegration {
|
|||||||
},
|
},
|
||||||
types: [type],
|
types: [type],
|
||||||
};
|
};
|
||||||
const items = torrents.map((torrent): DownloadClientItem => {
|
const items = torrents
|
||||||
const state = DelugeIntegration.getTorrentState(torrent.state);
|
.map((torrent): DownloadClientItem => {
|
||||||
return {
|
const state = DelugeIntegration.getTorrentState(torrent.state);
|
||||||
type,
|
return {
|
||||||
id: torrent.id,
|
type,
|
||||||
index: torrent.queue,
|
id: torrent.id,
|
||||||
name: torrent.name,
|
index: torrent.queue,
|
||||||
size: torrent.total_wanted,
|
name: torrent.name,
|
||||||
sent: torrent.total_uploaded,
|
size: torrent.total_wanted,
|
||||||
downSpeed: torrent.progress !== 100 ? torrent.download_payload_rate : undefined,
|
sent: torrent.total_uploaded,
|
||||||
upSpeed: torrent.upload_payload_rate,
|
downSpeed: torrent.progress !== 100 ? torrent.download_payload_rate : undefined,
|
||||||
time:
|
upSpeed: torrent.upload_payload_rate,
|
||||||
torrent.progress === 100
|
time:
|
||||||
? Math.min((torrent.completed_time - dayjs().unix()) * 1000, -1)
|
torrent.progress === 100
|
||||||
: Math.max(torrent.eta * 1000, 0),
|
? Math.min((torrent.completed_time - dayjs().unix()) * 1000, -1)
|
||||||
added: torrent.time_added * 1000,
|
: Math.max(torrent.eta * 1000, 0),
|
||||||
state,
|
added: torrent.time_added * 1000,
|
||||||
progress: torrent.progress / 100,
|
state,
|
||||||
category: torrent.label,
|
progress: torrent.progress / 100,
|
||||||
};
|
category: torrent.label,
|
||||||
});
|
};
|
||||||
|
})
|
||||||
|
.slice(0, input.limit);
|
||||||
return { status, items };
|
return { status, items };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export class NzbGetIntegration extends DownloadClientIntegration {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus> {
|
public async getClientJobsAndStatusAsync(input: { limit: number }): Promise<DownloadClientJobsAndStatus> {
|
||||||
const type = "usenet";
|
const type = "usenet";
|
||||||
const queue = await this.nzbGetApiCallAsync("listgroups");
|
const queue = await this.nzbGetApiCallAsync("listgroups");
|
||||||
const history = await this.nzbGetApiCallAsync("history");
|
const history = await this.nzbGetApiCallAsync("history");
|
||||||
@@ -65,7 +65,8 @@ export class NzbGetIntegration extends DownloadClientIntegration {
|
|||||||
category: file.Category,
|
category: file.Category,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
);
|
)
|
||||||
|
.slice(0, input.limit);
|
||||||
return { status, items };
|
return { status, items };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,10 +26,10 @@ export class QBitTorrentIntegration extends DownloadClientIntegration {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus> {
|
public async getClientJobsAndStatusAsync(input: { limit: number }): Promise<DownloadClientJobsAndStatus> {
|
||||||
const type = "torrent";
|
const type = "torrent";
|
||||||
const client = await this.getClientAsync();
|
const client = await this.getClientAsync();
|
||||||
const torrents = await client.listTorrents();
|
const torrents = await client.listTorrents({ limit: input.limit });
|
||||||
const rates = torrents.reduce(
|
const rates = torrents.reduce(
|
||||||
({ down, up }, { dlspeed, upspeed }) => ({ down: down + dlspeed, up: up + upspeed }),
|
({ down, up }, { dlspeed, upspeed }) => ({ down: down + dlspeed, up: up + upspeed }),
|
||||||
{ down: 0, up: 0 },
|
{ down: 0, up: 0 },
|
||||||
|
|||||||
@@ -22,10 +22,14 @@ export class SabnzbdIntegration extends DownloadClientIntegration {
|
|||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus> {
|
public async getClientJobsAndStatusAsync(input: { limit: number }): Promise<DownloadClientJobsAndStatus> {
|
||||||
const type = "usenet";
|
const type = "usenet";
|
||||||
const { queue } = await queueSchema.parseAsync(await this.sabNzbApiCallAsync("queue"));
|
const { queue } = await queueSchema.parseAsync(
|
||||||
const { history } = await historySchema.parseAsync(await this.sabNzbApiCallAsync("history"));
|
await this.sabNzbApiCallAsync("queue", { limit: input.limit.toString() }),
|
||||||
|
);
|
||||||
|
const { history } = await historySchema.parseAsync(
|
||||||
|
await this.sabNzbApiCallAsync("history", { limit: input.limit.toString() }),
|
||||||
|
);
|
||||||
const status: DownloadClientStatus = {
|
const status: DownloadClientStatus = {
|
||||||
paused: queue.paused,
|
paused: queue.paused,
|
||||||
rates: { down: Math.floor(Number(queue.kbpersec) * 1024) }, //Actually rounded kiBps ()
|
rates: { down: Math.floor(Number(queue.kbpersec) * 1024) }, //Actually rounded kiBps ()
|
||||||
@@ -73,7 +77,8 @@ export class SabnzbdIntegration extends DownloadClientIntegration {
|
|||||||
category: slot.category,
|
category: slot.category,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
);
|
)
|
||||||
|
.slice(0, input.limit);
|
||||||
return { status, items };
|
return { status, items };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,9 +23,10 @@ export class TransmissionIntegration extends DownloadClientIntegration {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus> {
|
public async getClientJobsAndStatusAsync(input: { limit: number }): Promise<DownloadClientJobsAndStatus> {
|
||||||
const type = "torrent";
|
const type = "torrent";
|
||||||
const client = await this.getClientAsync();
|
const client = await this.getClientAsync();
|
||||||
|
// Currently there is no way to limit the number of returned torrents
|
||||||
const { torrents } = (await client.listTorrents()).arguments;
|
const { torrents } = (await client.listTorrents()).arguments;
|
||||||
const rates = torrents.reduce(
|
const rates = torrents.reduce(
|
||||||
({ down, up }, { rateDownload, rateUpload }) => ({ down: down + rateDownload, up: up + rateUpload }),
|
({ down, up }, { rateDownload, rateUpload }) => ({ down: down + rateDownload, up: up + rateUpload }),
|
||||||
@@ -34,27 +35,29 @@ export class TransmissionIntegration extends DownloadClientIntegration {
|
|||||||
const paused =
|
const paused =
|
||||||
torrents.find(({ status }) => TransmissionIntegration.getTorrentState(status) !== "paused") === undefined;
|
torrents.find(({ status }) => TransmissionIntegration.getTorrentState(status) !== "paused") === undefined;
|
||||||
const status: DownloadClientStatus = { paused, rates, types: [type] };
|
const status: DownloadClientStatus = { paused, rates, types: [type] };
|
||||||
const items = torrents.map((torrent): DownloadClientItem => {
|
const items = torrents
|
||||||
const state = TransmissionIntegration.getTorrentState(torrent.status);
|
.map((torrent): DownloadClientItem => {
|
||||||
return {
|
const state = TransmissionIntegration.getTorrentState(torrent.status);
|
||||||
type,
|
return {
|
||||||
id: torrent.hashString,
|
type,
|
||||||
index: torrent.queuePosition,
|
id: torrent.hashString,
|
||||||
name: torrent.name,
|
index: torrent.queuePosition,
|
||||||
size: torrent.totalSize,
|
name: torrent.name,
|
||||||
sent: torrent.uploadedEver,
|
size: torrent.totalSize,
|
||||||
downSpeed: torrent.percentDone !== 1 ? torrent.rateDownload : undefined,
|
sent: torrent.uploadedEver,
|
||||||
upSpeed: torrent.rateUpload,
|
downSpeed: torrent.percentDone !== 1 ? torrent.rateDownload : undefined,
|
||||||
time:
|
upSpeed: torrent.rateUpload,
|
||||||
torrent.percentDone === 1
|
time:
|
||||||
? Math.min(torrent.doneDate * 1000 - dayjs().valueOf(), -1)
|
torrent.percentDone === 1
|
||||||
: Math.max(torrent.eta * 1000, 0),
|
? Math.min(torrent.doneDate * 1000 - dayjs().valueOf(), -1)
|
||||||
added: torrent.addedDate * 1000,
|
: Math.max(torrent.eta * 1000, 0),
|
||||||
state,
|
added: torrent.addedDate * 1000,
|
||||||
progress: torrent.percentDone,
|
state,
|
||||||
category: torrent.labels,
|
progress: torrent.percentDone,
|
||||||
};
|
category: torrent.labels,
|
||||||
});
|
};
|
||||||
|
})
|
||||||
|
.slice(0, input.limit);
|
||||||
return { status, items };
|
return { status, items };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { DownloadClientItem } from "./download-client-items";
|
|||||||
|
|
||||||
export abstract class DownloadClientIntegration extends Integration {
|
export abstract class DownloadClientIntegration extends Integration {
|
||||||
/** Get download client's status and list of all of it's items */
|
/** Get download client's status and list of all of it's items */
|
||||||
public abstract getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus>;
|
public abstract getClientJobsAndStatusAsync(input: { limit: number }): Promise<DownloadClientJobsAndStatus>;
|
||||||
/** Pauses the client or all of it's items */
|
/** Pauses the client or all of it's items */
|
||||||
public abstract pauseQueueAsync(): Promise<void>;
|
public abstract pauseQueueAsync(): Promise<void>;
|
||||||
/** Pause a single item using it's ID */
|
/** Pause a single item using it's ID */
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ describe("Aria2 integration", () => {
|
|||||||
|
|
||||||
// Acts
|
// Acts
|
||||||
const actAsync = async () => await aria2Integration.pauseQueueAsync();
|
const actAsync = async () => await aria2Integration.pauseQueueAsync();
|
||||||
const getAsync = async () => await aria2Integration.getClientJobsAndStatusAsync();
|
const getAsync = async () => await aria2Integration.getClientJobsAndStatusAsync({ limit: 99 });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await expect(actAsync()).resolves.not.toThrow();
|
await expect(actAsync()).resolves.not.toThrow();
|
||||||
@@ -62,7 +62,7 @@ describe("Aria2 integration", () => {
|
|||||||
const aria2Integration = createAria2Intergration(startedContainer, API_KEY);
|
const aria2Integration = createAria2Intergration(startedContainer, API_KEY);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const getAsync = async () => await aria2Integration.getClientJobsAndStatusAsync();
|
const getAsync = async () => await aria2Integration.getClientJobsAndStatusAsync({ limit: 99 });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await expect(getAsync()).resolves.not.toThrow();
|
await expect(getAsync()).resolves.not.toThrow();
|
||||||
@@ -81,7 +81,7 @@ describe("Aria2 integration", () => {
|
|||||||
await aria2AddItemAsync(startedContainer, API_KEY, aria2Integration);
|
await aria2AddItemAsync(startedContainer, API_KEY, aria2Integration);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const getAsync = async () => await aria2Integration.getClientJobsAndStatusAsync();
|
const getAsync = async () => await aria2Integration.getClientJobsAndStatusAsync({ limit: 99 });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await expect(getAsync()).resolves.not.toThrow();
|
await expect(getAsync()).resolves.not.toThrow();
|
||||||
@@ -104,7 +104,7 @@ describe("Aria2 integration", () => {
|
|||||||
await expect(actAsync()).resolves.not.toThrow();
|
await expect(actAsync()).resolves.not.toThrow();
|
||||||
// NzbGet is slow and we wait for a second before querying the items. Test was flaky without this.
|
// NzbGet is slow and we wait for a second before querying the items. Test was flaky without this.
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
const result = await aria2Integration.getClientJobsAndStatusAsync();
|
const result = await aria2Integration.getClientJobsAndStatusAsync({ limit: 99 });
|
||||||
expect(result.items).toHaveLength(0);
|
expect(result.items).toHaveLength(0);
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
@@ -153,7 +153,7 @@ const aria2AddItemAsync = async (container: StartedTestContainer, apiKey: string
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
items: [item],
|
items: [item],
|
||||||
} = await integration.getClientJobsAndStatusAsync();
|
} = await integration.getClientJobsAndStatusAsync({ limit: 99 });
|
||||||
|
|
||||||
if (!item) {
|
if (!item) {
|
||||||
throw new Error("No item found");
|
throw new Error("No item found");
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ describe("Nzbget integration", () => {
|
|||||||
|
|
||||||
// Acts
|
// Acts
|
||||||
const actAsync = async () => await nzbGetIntegration.pauseQueueAsync();
|
const actAsync = async () => await nzbGetIntegration.pauseQueueAsync();
|
||||||
const getAsync = async () => await nzbGetIntegration.getClientJobsAndStatusAsync();
|
const getAsync = async () => await nzbGetIntegration.getClientJobsAndStatusAsync({ limit: 99 });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await expect(actAsync()).resolves.not.toThrow();
|
await expect(actAsync()).resolves.not.toThrow();
|
||||||
@@ -87,7 +87,7 @@ describe("Nzbget integration", () => {
|
|||||||
|
|
||||||
// Acts
|
// Acts
|
||||||
const actAsync = async () => await nzbGetIntegration.resumeQueueAsync();
|
const actAsync = async () => await nzbGetIntegration.resumeQueueAsync();
|
||||||
const getAsync = async () => await nzbGetIntegration.getClientJobsAndStatusAsync();
|
const getAsync = async () => await nzbGetIntegration.getClientJobsAndStatusAsync({ limit: 99 });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await expect(actAsync()).resolves.not.toThrow();
|
await expect(actAsync()).resolves.not.toThrow();
|
||||||
@@ -105,7 +105,7 @@ describe("Nzbget integration", () => {
|
|||||||
const nzbGetIntegration = createNzbGetIntegration(startedContainer, username, password);
|
const nzbGetIntegration = createNzbGetIntegration(startedContainer, username, password);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const getAsync = async () => await nzbGetIntegration.getClientJobsAndStatusAsync();
|
const getAsync = async () => await nzbGetIntegration.getClientJobsAndStatusAsync({ limit: 99 });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await expect(getAsync()).resolves.not.toThrow();
|
await expect(getAsync()).resolves.not.toThrow();
|
||||||
@@ -124,7 +124,7 @@ describe("Nzbget integration", () => {
|
|||||||
await nzbGetAddItemAsync(startedContainer, username, password, nzbGetIntegration);
|
await nzbGetAddItemAsync(startedContainer, username, password, nzbGetIntegration);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const getAsync = async () => await nzbGetIntegration.getClientJobsAndStatusAsync();
|
const getAsync = async () => await nzbGetIntegration.getClientJobsAndStatusAsync({ limit: 99 });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await expect(getAsync()).resolves.not.toThrow();
|
await expect(getAsync()).resolves.not.toThrow();
|
||||||
@@ -147,7 +147,7 @@ describe("Nzbget integration", () => {
|
|||||||
await expect(actAsync()).resolves.not.toThrow();
|
await expect(actAsync()).resolves.not.toThrow();
|
||||||
// NzbGet is slow and we wait for a second before querying the items. Test was flaky without this.
|
// NzbGet is slow and we wait for a second before querying the items. Test was flaky without this.
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
const result = await nzbGetIntegration.getClientJobsAndStatusAsync();
|
const result = await nzbGetIntegration.getClientJobsAndStatusAsync({ limit: 99 });
|
||||||
expect(result.items).toHaveLength(0);
|
expect(result.items).toHaveLength(0);
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
@@ -209,7 +209,7 @@ const nzbGetAddItemAsync = async (
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
items: [item],
|
items: [item],
|
||||||
} = await integration.getClientJobsAndStatusAsync();
|
} = await integration.getClientJobsAndStatusAsync({ limit: 99 });
|
||||||
|
|
||||||
if (!item) {
|
if (!item) {
|
||||||
throw new Error("No item found");
|
throw new Error("No item found");
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ describe("Sabnzbd integration", () => {
|
|||||||
|
|
||||||
// Acts
|
// Acts
|
||||||
const actAsync = async () => await sabnzbdIntegration.pauseQueueAsync();
|
const actAsync = async () => await sabnzbdIntegration.pauseQueueAsync();
|
||||||
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync();
|
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync({ limit: 99 });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await expect(actAsync()).resolves.not.toThrow();
|
await expect(actAsync()).resolves.not.toThrow();
|
||||||
@@ -85,7 +85,7 @@ describe("Sabnzbd integration", () => {
|
|||||||
|
|
||||||
// Acts
|
// Acts
|
||||||
const actAsync = async () => await sabnzbdIntegration.resumeQueueAsync();
|
const actAsync = async () => await sabnzbdIntegration.resumeQueueAsync();
|
||||||
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync();
|
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync({ limit: 99 });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await expect(actAsync()).resolves.not.toThrow();
|
await expect(actAsync()).resolves.not.toThrow();
|
||||||
@@ -103,7 +103,7 @@ describe("Sabnzbd integration", () => {
|
|||||||
const sabnzbdIntegration = createSabnzbdIntegration(startedContainer, DEFAULT_API_KEY);
|
const sabnzbdIntegration = createSabnzbdIntegration(startedContainer, DEFAULT_API_KEY);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync();
|
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync({ limit: 99 });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await expect(getAsync()).resolves.not.toThrow();
|
await expect(getAsync()).resolves.not.toThrow();
|
||||||
@@ -122,7 +122,7 @@ describe("Sabnzbd integration", () => {
|
|||||||
await sabNzbdAddItemAsync(startedContainer, DEFAULT_API_KEY, sabnzbdIntegration);
|
await sabNzbdAddItemAsync(startedContainer, DEFAULT_API_KEY, sabnzbdIntegration);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync();
|
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync({ limit: 99 });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await expect(getAsync()).resolves.not.toThrow();
|
await expect(getAsync()).resolves.not.toThrow();
|
||||||
@@ -140,7 +140,7 @@ describe("Sabnzbd integration", () => {
|
|||||||
|
|
||||||
// Act
|
// Act
|
||||||
const actAsync = async () => await sabnzbdIntegration.pauseItemAsync(item);
|
const actAsync = async () => await sabnzbdIntegration.pauseItemAsync(item);
|
||||||
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync();
|
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync({ limit: 99 });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await expect(getAsync()).resolves.toMatchObject({ items: [{ ...item, state: "downloading" }] });
|
await expect(getAsync()).resolves.toMatchObject({ items: [{ ...item, state: "downloading" }] });
|
||||||
@@ -160,7 +160,7 @@ describe("Sabnzbd integration", () => {
|
|||||||
|
|
||||||
// Act
|
// Act
|
||||||
const actAsync = async () => await sabnzbdIntegration.resumeItemAsync(item);
|
const actAsync = async () => await sabnzbdIntegration.resumeItemAsync(item);
|
||||||
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync();
|
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync({ limit: 99 });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await expect(getAsync()).resolves.toMatchObject({ items: [{ ...item, state: "paused" }] });
|
await expect(getAsync()).resolves.toMatchObject({ items: [{ ...item, state: "paused" }] });
|
||||||
@@ -180,7 +180,7 @@ describe("Sabnzbd integration", () => {
|
|||||||
// Act - fromDisk already doesn't work for sabnzbd, so only test deletion itself.
|
// Act - fromDisk already doesn't work for sabnzbd, so only test deletion itself.
|
||||||
const actAsync = async () =>
|
const actAsync = async () =>
|
||||||
await sabnzbdIntegration.deleteItemAsync({ ...item, progress: 0 } as DownloadClientItem, false);
|
await sabnzbdIntegration.deleteItemAsync({ ...item, progress: 0 } as DownloadClientItem, false);
|
||||||
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync();
|
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync({ limit: 99 });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await expect(actAsync()).resolves.not.toThrow();
|
await expect(actAsync()).resolves.not.toThrow();
|
||||||
@@ -242,7 +242,7 @@ const sabNzbdAddItemAsync = async (
|
|||||||
for (let i = 0; i < 5; i++) {
|
for (let i = 0; i < 5; i++) {
|
||||||
const {
|
const {
|
||||||
items: [item],
|
items: [item],
|
||||||
} = await integration.getClientJobsAndStatusAsync();
|
} = await integration.getClientJobsAndStatusAsync({ limit: 99 });
|
||||||
if (item) return item;
|
if (item) return item;
|
||||||
}
|
}
|
||||||
// Throws if it can't find the item
|
// Throws if it can't find the item
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ const optionMapping: OptionMapping = {
|
|||||||
descendingDefaultSort: () => false,
|
descendingDefaultSort: () => false,
|
||||||
showCompletedUsenet: () => true,
|
showCompletedUsenet: () => true,
|
||||||
showCompletedHttp: () => true,
|
showCompletedHttp: () => true,
|
||||||
|
limitPerIntegration: () => undefined,
|
||||||
},
|
},
|
||||||
weather: {
|
weather: {
|
||||||
forecastDayCount: (oldOptions) => oldOptions.forecastDays,
|
forecastDayCount: (oldOptions) => oldOptions.forecastDays,
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-
|
|||||||
export const downloadClientRequestHandler = createCachedIntegrationRequestHandler<
|
export const downloadClientRequestHandler = createCachedIntegrationRequestHandler<
|
||||||
DownloadClientJobsAndStatus,
|
DownloadClientJobsAndStatus,
|
||||||
IntegrationKindByCategory<"downloadClient">,
|
IntegrationKindByCategory<"downloadClient">,
|
||||||
Record<string, never>
|
{ limit: number }
|
||||||
>({
|
>({
|
||||||
async requestAsync(integration, _input) {
|
async requestAsync(integration, input) {
|
||||||
const integrationInstance = await createIntegrationAsync(integration);
|
const integrationInstance = await createIntegrationAsync(integration);
|
||||||
return await integrationInstance.getClientJobsAndStatusAsync();
|
return await integrationInstance.getClientJobsAndStatusAsync(input);
|
||||||
},
|
},
|
||||||
cacheDuration: dayjs.duration(5, "seconds"),
|
cacheDuration: dayjs.duration(5, "seconds"),
|
||||||
queryKey: "downloadClientJobStatus",
|
queryKey: "downloadClientJobStatus",
|
||||||
|
|||||||
@@ -1953,6 +1953,10 @@
|
|||||||
},
|
},
|
||||||
"applyFilterToRatio": {
|
"applyFilterToRatio": {
|
||||||
"label": "Use filter to calculate Ratio"
|
"label": "Use filter to calculate Ratio"
|
||||||
|
},
|
||||||
|
"limitPerIntegration": {
|
||||||
|
"label": "Limit items per integration",
|
||||||
|
"description": "This will limit the number of items shown per integration, not globally"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ export default function DownloadClientsWidget({
|
|||||||
const [currentItems] = clientApi.widget.downloads.getJobsAndStatuses.useSuspenseQuery(
|
const [currentItems] = clientApi.widget.downloads.getJobsAndStatuses.useSuspenseQuery(
|
||||||
{
|
{
|
||||||
integrationIds,
|
integrationIds,
|
||||||
|
limitPerIntegration: options.limitPerIntegration,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
refetchOnMount: false,
|
refetchOnMount: false,
|
||||||
@@ -126,6 +127,7 @@ export default function DownloadClientsWidget({
|
|||||||
clientApi.widget.downloads.subscribeToJobsAndStatuses.useSubscription(
|
clientApi.widget.downloads.subscribeToJobsAndStatuses.useSubscription(
|
||||||
{
|
{
|
||||||
integrationIds,
|
integrationIds,
|
||||||
|
limitPerIntegration: options.limitPerIntegration,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onData: (data) => {
|
onData: (data) => {
|
||||||
|
|||||||
@@ -82,6 +82,11 @@ export const { definition, componentLoader } = createWidgetDefinition("downloads
|
|||||||
applyFilterToRatio: factory.switch({
|
applyFilterToRatio: factory.switch({
|
||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
}),
|
}),
|
||||||
|
limitPerIntegration: factory.number({
|
||||||
|
defaultValue: 50,
|
||||||
|
validate: z.number().min(1),
|
||||||
|
withDescription: true,
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
defaultSort: {
|
defaultSort: {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ interface SelectInput<TOptions extends readonly SelectOption[]>
|
|||||||
searchable?: boolean;
|
searchable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NumberInput extends CommonInput<number | ""> {
|
interface NumberInput extends CommonInput<number> {
|
||||||
validate: z.ZodNumber;
|
validate: z.ZodNumber;
|
||||||
step?: number;
|
step?: number;
|
||||||
}
|
}
|
||||||
@@ -87,7 +87,7 @@ const optionsFactory = {
|
|||||||
}),
|
}),
|
||||||
number: (input: NumberInput) => ({
|
number: (input: NumberInput) => ({
|
||||||
type: "number" as const,
|
type: "number" as const,
|
||||||
defaultValue: input.defaultValue ?? ("" as const),
|
defaultValue: input.defaultValue ?? 0,
|
||||||
step: input.step,
|
step: input.step,
|
||||||
withDescription: input.withDescription ?? false,
|
withDescription: input.withDescription ?? false,
|
||||||
validate: input.validate,
|
validate: input.validate,
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export default function RssFeed({ options }: WidgetComponentProps<"rssFeed">) {
|
|||||||
dir={languageDir}
|
dir={languageDir}
|
||||||
c="dimmed"
|
c="dimmed"
|
||||||
size="sm"
|
size="sm"
|
||||||
lineClamp={options.textLinesClamp as number}
|
lineClamp={options.textLinesClamp}
|
||||||
dangerouslySetInnerHTML={{ __html: feedEntry.description }}
|
dangerouslySetInnerHTML={{ __html: feedEntry.description }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user