diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 7f1a86de..e20e1908 100755 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -194,13 +194,17 @@ "sonarr": { "wanted": "Wanted", "queued": "Queued", - "series": "Series" + "series": "Series", + "queue": "Queue", + "unknown": "Unknown" }, "radarr": { "wanted": "Wanted", "missing": "Missing", "queued": "Queued", - "movies": "Movies" + "movies": "Movies", + "queue": "Queue", + "unknown": "Unknown" }, "lidarr": { "wanted": "Wanted", diff --git a/src/components/services/widget/container.jsx b/src/components/services/widget/container.jsx index f4d8c13e..4b8a06ca 100644 --- a/src/components/services/widget/container.jsx +++ b/src/components/services/widget/container.jsx @@ -15,7 +15,9 @@ export default function Container({ error = false, children, service }) { return } - let visibleChildren = children; + const childrenArray = Array.isArray(children) ? children : [children]; + + let visibleChildren = childrenArray; const fields = service?.widget?.fields; const type = service?.widget?.type; if (fields && type) { @@ -24,7 +26,7 @@ export default function Container({ error = false, children, service }) { // fields: [ "resources.cpu", "resources.mem", "field"] // or even // fields: [ "resources.cpu", "widget_type.field" ] - visibleChildren = children?.filter(child => fields.some(field => { + visibleChildren = childrenArray?.filter(child => fields.some(field => { let fullField = field; if (!field.includes(".")) { fullField = `${type}.${field}`; diff --git a/src/components/widgets/queue/queueEntry.jsx b/src/components/widgets/queue/queueEntry.jsx new file mode 100644 index 00000000..adea45ad --- /dev/null +++ b/src/components/widgets/queue/queueEntry.jsx @@ -0,0 +1,18 @@ +export default function QueueEntry({ title, activity, timeLeft, progress}) { + return ( +
+
+
+
{title}
+
+
+ {timeLeft ? `${activity} - ${timeLeft}` : activity} +
+
+ ); +} diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js index 7f9d45e4..41fe263a 100644 --- a/src/utils/config/service-helpers.js +++ b/src/utils/config/service-helpers.js @@ -168,7 +168,7 @@ export async function servicesFromKubernetes() { .filter((ingress) => ingress.metadata.annotations && ingress.metadata.annotations[`${ANNOTATION_BASE}/href`]) ingressList.items.push(...traefikServices); } - + if (!ingressList) { return []; } @@ -276,7 +276,8 @@ export function cleanServiceGroups(groups) { wan, // opnsense widget, pfsense widget enableBlocks, // emby/jellyfin enableNowPlaying, - volume, // diskstation widget + volume, // diskstation widget, + enableQueue, // sonarr/radarr } = cleanedService.widget; const fieldsList = typeof fields === 'string' ? JSON.parse(fields) : fields; @@ -312,6 +313,9 @@ export function cleanServiceGroups(groups) { if (enableBlocks !== undefined) cleanedService.widget.enableBlocks = JSON.parse(enableBlocks); if (enableNowPlaying !== undefined) cleanedService.widget.enableNowPlaying = JSON.parse(enableNowPlaying); } + if (["sonarr", "radarr"].includes(type)) { + if (enableQueue !== undefined) cleanedService.widget.enableQueue = JSON.parse(enableQueue); + } if (["diskstation", "qnap"].includes(type)) { if (volume) cleanedService.widget.volume = volume; } diff --git a/src/widgets/radarr/component.jsx b/src/widgets/radarr/component.jsx index f8a932ea..6ce2f599 100644 --- a/src/widgets/radarr/component.jsx +++ b/src/widgets/radarr/component.jsx @@ -1,22 +1,41 @@ import { useTranslation } from "next-i18next"; +import { useCallback } from 'react'; + +import QueueEntry from "../../components/widgets/queue/queueEntry"; import Container from "components/services/widget/container"; import Block from "components/services/widget/block"; import useWidgetAPI from "utils/proxy/use-widget-api"; +function getProgress(sizeLeft, size) { + return sizeLeft === 0 ? 100 : (1 - sizeLeft / size) * 100 +} + export default function Component({ service }) { const { t } = useTranslation(); const { widget } = service; const { data: moviesData, error: moviesError } = useWidgetAPI(widget, "movie"); const { data: queuedData, error: queuedError } = useWidgetAPI(widget, "queue/status"); + const { data: queueDetailsData, error: queueDetailsError } = useWidgetAPI(widget, "queue/details"); - if (moviesError || queuedError) { - const finalError = moviesError ?? queuedError; + const formatDownloadState = useCallback((downloadState) => { + switch (downloadState) { + case "importPending": + return "import pending"; + case "failedPending": + return "failed pending"; + default: + return downloadState; + } + }, []); + + if (moviesError || queuedError || queueDetailsError) { + const finalError = moviesError ?? queuedError ?? queueDetailsError; return ; } - if (!moviesData || !queuedData) { + if (!moviesData || !queuedData || !queueDetailsData) { return ( @@ -27,12 +46,27 @@ export default function Component({ service }) { ); } + const enableQueue = widget?.enableQueue && Array.isArray(queueDetailsData) && queueDetailsData.length > 0; + return ( - - - - - - + <> + + + + + + + {enableQueue && + queueDetailsData.map((queueEntry) => ( + entry.id === queueEntry.movieId)?.title ?? t("radarr.unknown")} + activity={formatDownloadState(queueEntry.trackedDownloadState)} + key={`${queueEntry.movieId}-${queueEntry.sizeLeft}`} + /> + )) + } + ); } diff --git a/src/widgets/radarr/widget.js b/src/widgets/radarr/widget.js index 78054219..3373975e 100644 --- a/src/widgets/radarr/widget.js +++ b/src/widgets/radarr/widget.js @@ -1,5 +1,5 @@ import genericProxyHandler from "utils/proxy/handlers/generic"; -import { jsonArrayFilter } from "utils/proxy/api-helpers"; +import { asJson, jsonArrayFilter } from "utils/proxy/api-helpers"; const widget = { api: "{url}/api/v3/{endpoint}?apikey={key}", @@ -12,6 +12,7 @@ const widget = { wanted: jsonArrayFilter(data, (item) => item.monitored && !item.hasFile && item.isAvailable).length, have: jsonArrayFilter(data, (item) => item.hasFile).length, missing: jsonArrayFilter(data, (item) => item.monitored && !item.hasFile).length, + all: asJson(data), }), }, "queue/status": { @@ -20,6 +21,37 @@ const widget = { "totalCount" ] }, + "queue/details": { + endpoint: "queue/details", + map: (data) => asJson(data).map((entry) => ({ + trackedDownloadState: entry.trackedDownloadState, + trackedDownloadStatus: entry.trackedDownloadStatus, + timeLeft: entry.timeleft, + size: entry.size, + sizeLeft: entry.sizeleft, + movieId: entry.movieId ?? entry.id, + status: entry.status + })).sort((a, b) => { + const downloadingA = a.trackedDownloadState === "downloading" + const downloadingB = b.trackedDownloadState === "downloading" + if (downloadingA && !downloadingB) { + return -1; + } + if (downloadingB && !downloadingA) { + return 1; + } + + const percentA = a.sizeLeft / a.size; + const percentB = b.sizeLeft / b.size; + if (percentA < percentB) { + return -1; + } + if (percentA > percentB) { + return 1; + } + return 0; + }) + }, }, }; diff --git a/src/widgets/sonarr/component.jsx b/src/widgets/sonarr/component.jsx index adbb8c30..27b1ab03 100644 --- a/src/widgets/sonarr/component.jsx +++ b/src/widgets/sonarr/component.jsx @@ -1,9 +1,26 @@ import { useTranslation } from "next-i18next"; +import { useCallback } from 'react'; + +import QueueEntry from "../../components/widgets/queue/queueEntry"; import Container from "components/services/widget/container"; import Block from "components/services/widget/block"; import useWidgetAPI from "utils/proxy/use-widget-api"; +function getProgress(sizeLeft, size) { + return sizeLeft === 0 ? 100 : (1 - sizeLeft / size) * 100 +} + +function getTitle(queueEntry, seriesData) { + let title = '' + const seriesTitle = seriesData.find((entry) => entry.id === queueEntry.seriesId)?.title; + if (seriesTitle) title += `${seriesTitle}: `; + const { episodeTitle } = queueEntry; + if (episodeTitle) title += episodeTitle; + if (title === '') return null; + return title; +} + export default function Component({ service }) { const { t } = useTranslation(); const { widget } = service; @@ -11,13 +28,25 @@ export default function Component({ service }) { const { data: wantedData, error: wantedError } = useWidgetAPI(widget, "wanted/missing"); const { data: queuedData, error: queuedError } = useWidgetAPI(widget, "queue"); const { data: seriesData, error: seriesError } = useWidgetAPI(widget, "series"); + const { data: queueDetailsData, error: queueDetailsError } = useWidgetAPI(widget, "queue/details"); - if (wantedError || queuedError || seriesError) { - const finalError = wantedError ?? queuedError ?? seriesError; + const formatDownloadState = useCallback((downloadState) => { + switch (downloadState) { + case "importPending": + return "import pending"; + case "failedPending": + return "failed pending"; + default: + return downloadState; + } + }, []); + + if (wantedError || queuedError || seriesError || queueDetailsError) { + const finalError = wantedError ?? queuedError ?? seriesError ?? queueDetailsError; return ; } - if (!wantedData || !queuedData || !seriesData) { + if (!wantedData || !queuedData || !seriesData || !queueDetailsData) { return ( @@ -27,11 +56,26 @@ export default function Component({ service }) { ); } + const enableQueue = widget?.enableQueue && Array.isArray(queueDetailsData) && queueDetailsData.length > 0; + return ( - - - - - + <> + + + + + + {enableQueue && + queueDetailsData.map((queueEntry) => ( + + )) + } + ); } diff --git a/src/widgets/sonarr/widget.js b/src/widgets/sonarr/widget.js index c1413975..7f658eb1 100644 --- a/src/widgets/sonarr/widget.js +++ b/src/widgets/sonarr/widget.js @@ -8,9 +8,10 @@ const widget = { mappings: { series: { endpoint: "series", - map: (data) => ({ - total: asJson(data).length, - }) + map: (data) => asJson(data).map((entry) => ({ + title: entry.title, + id: entry.id + })) }, queue: { endpoint: "queue", @@ -24,6 +25,39 @@ const widget = { "totalRecords" ] }, + "queue/details": { + endpoint: "queue/details", + map: (data) => asJson(data).map((entry) => ({ + trackedDownloadState: entry.trackedDownloadState, + trackedDownloadStatus: entry.trackedDownloadStatus, + timeLeft: entry.timeleft, + size: entry.size, + sizeLeft: entry.sizeleft, + seriesId: entry.seriesId, + episodeTitle: entry.episode?.title ?? entry.title, + episodeId: entry.episodeId ?? entry.id, + status: entry.status, + })).sort((a, b) => { + const downloadingA = a.trackedDownloadState === "downloading" + const downloadingB = b.trackedDownloadState === "downloading" + if (downloadingA && !downloadingB) { + return -1; + } + if (downloadingB && !downloadingA) { + return 1; + } + + const percentA = a.sizeLeft / a.size; + const percentB = b.sizeLeft / b.size; + if (percentA < percentB) { + return -1; + } + if (percentA > percentB) { + return 1; + } + return 0; + }) + } }, };