mirror of
				https://github.com/karl0ss/homepage.git
				synced 2025-11-04 08:20:58 +00:00 
			
		
		
		
	Merge pull request #1560 from Schoggi0815/feature/sonarr-radarr-queue-list
Feature/sonarr radarr queue list
This commit is contained in:
		
						commit
						2c62f180a9
					
				@ -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",
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,9 @@ export default function Container({ error = false, children, service }) {
 | 
			
		||||
    return <Error service={service} error={error} />
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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}`;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										18
									
								
								src/components/widgets/queue/queueEntry.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/components/widgets/queue/queueEntry.jsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
			
		||||
export default function QueueEntry({ title, activity, timeLeft, progress}) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="text-theme-700 dark:text-theme-200 relative h-5 rounded-md bg-theme-200/50 dark:bg-theme-900/20 m-1 px-1 flex">
 | 
			
		||||
      <div
 | 
			
		||||
        className="absolute h-5 rounded-md bg-theme-200 dark:bg-theme-900/40 z-0 -ml-1"
 | 
			
		||||
        style={{
 | 
			
		||||
          width: `${progress}%`,
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
      <div className="text-xs z-10 self-center ml-2 relative h-4 grow mr-2">
 | 
			
		||||
        <div className="absolute w-full whitespace-nowrap text-ellipsis overflow-hidden text-left">{title}</div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="self-center text-xs flex justify-end mr-1.5 pl-1 z-10 text-ellipsis overflow-hidden whitespace-nowrap">
 | 
			
		||||
        {timeLeft ? `${activity} - ${timeLeft}` : activity}
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@ -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;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -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 <Container service={service} error={finalError} />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!moviesData || !queuedData) {
 | 
			
		||||
  if (!moviesData || !queuedData || !queueDetailsData) {
 | 
			
		||||
    return (
 | 
			
		||||
      <Container service={service}>
 | 
			
		||||
        <Block label="radarr.wanted" />
 | 
			
		||||
@ -27,12 +46,27 @@ export default function Component({ service }) {
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const enableQueue = widget?.enableQueue && Array.isArray(queueDetailsData) && queueDetailsData.length > 0;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Container service={service}>
 | 
			
		||||
      <Block label="radarr.wanted" value={t("common.number", { value: moviesData.wanted })} />
 | 
			
		||||
      <Block label="radarr.missing" value={t("common.number", { value: moviesData.missing })} />
 | 
			
		||||
      <Block label="radarr.queued" value={t("common.number", { value: queuedData.totalCount })} />
 | 
			
		||||
      <Block label="radarr.movies" value={t("common.number", { value: moviesData.have })} />
 | 
			
		||||
    </Container>
 | 
			
		||||
    <>
 | 
			
		||||
      <Container service={service}>
 | 
			
		||||
        <Block label="radarr.wanted" value={t("common.number", { value: moviesData.wanted })} />
 | 
			
		||||
        <Block label="radarr.missing" value={t("common.number", { value: moviesData.missing })} />
 | 
			
		||||
        <Block label="radarr.queued" value={t("common.number", { value: queuedData.totalCount })} />
 | 
			
		||||
        <Block label="radarr.movies" value={t("common.number", { value: moviesData.have })} />
 | 
			
		||||
      </Container>
 | 
			
		||||
      {enableQueue &&
 | 
			
		||||
        queueDetailsData.map((queueEntry) => (
 | 
			
		||||
          <QueueEntry
 | 
			
		||||
            progress={getProgress(queueEntry.sizeLeft, queueEntry.size)}
 | 
			
		||||
            timeLeft={queueEntry.timeLeft}
 | 
			
		||||
            title={moviesData.all.find((entry) => entry.id === queueEntry.movieId)?.title ?? t("radarr.unknown")}
 | 
			
		||||
            activity={formatDownloadState(queueEntry.trackedDownloadState)}
 | 
			
		||||
            key={`${queueEntry.movieId}-${queueEntry.sizeLeft}`}
 | 
			
		||||
          />
 | 
			
		||||
        ))
 | 
			
		||||
      }
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
      })
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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 <Container service={service} error={finalError} />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!wantedData || !queuedData || !seriesData) {
 | 
			
		||||
  if (!wantedData || !queuedData || !seriesData || !queueDetailsData) {
 | 
			
		||||
    return (
 | 
			
		||||
      <Container service={service}>
 | 
			
		||||
        <Block label="sonarr.wanted" />
 | 
			
		||||
@ -27,11 +56,26 @@ export default function Component({ service }) {
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const enableQueue = widget?.enableQueue && Array.isArray(queueDetailsData) && queueDetailsData.length > 0;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Container service={service}>
 | 
			
		||||
      <Block label="sonarr.wanted" value={t("common.number", { value: wantedData.totalRecords })} />
 | 
			
		||||
      <Block label="sonarr.queued" value={t("common.number", { value: queuedData.totalRecords })} />
 | 
			
		||||
      <Block label="sonarr.series" value={t("common.number", { value: seriesData.total })} />
 | 
			
		||||
    </Container>
 | 
			
		||||
    <>
 | 
			
		||||
      <Container service={service}>
 | 
			
		||||
        <Block label="sonarr.wanted" value={t("common.number", { value: wantedData.totalRecords })} />
 | 
			
		||||
        <Block label="sonarr.queued" value={t("common.number", { value: queuedData.totalRecords })} />
 | 
			
		||||
        <Block label="sonarr.series" value={t("common.number", { value: seriesData.length })} />
 | 
			
		||||
      </Container>
 | 
			
		||||
      {enableQueue && 
 | 
			
		||||
        queueDetailsData.map((queueEntry) => (
 | 
			
		||||
          <QueueEntry
 | 
			
		||||
            progress={getProgress(queueEntry.sizeLeft, queueEntry.size)}
 | 
			
		||||
            timeLeft={queueEntry.timeLeft}
 | 
			
		||||
            title={getTitle(queueEntry, seriesData) ?? t("sonarr.unknown")}
 | 
			
		||||
            activity={formatDownloadState(queueEntry.trackedDownloadState)}
 | 
			
		||||
            key={`${queueEntry.seriesId}-${queueEntry.sizeLeft}`}
 | 
			
		||||
          />
 | 
			
		||||
        ))
 | 
			
		||||
      }
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user