mirror of
				https://github.com/karl0ss/homepage.git
				synced 2025-11-04 08:20:58 +00:00 
			
		
		
		
	Add queue list
This commit is contained in:
		
							parent
							
								
									caa1b94fd6
								
							
						
					
					
						commit
						28e39e46ae
					
				
							
								
								
									
										31
									
								
								src/components/services/widget/block-list.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/components/services/widget/block-list.jsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,31 @@
 | 
			
		||||
import { useTranslation } from "next-i18next";
 | 
			
		||||
import { useCallback, useState } from 'react';
 | 
			
		||||
import classNames from "classnames";
 | 
			
		||||
 | 
			
		||||
import ResolvedIcon from '../../resolvedicon';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export default function BlockList({ label, children, childHeight }) {
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  const [isOpen, setOpen] = useState(false);
 | 
			
		||||
 | 
			
		||||
  const changeState = useCallback(() => setOpen(!isOpen), [isOpen, setOpen]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      className={classNames(
 | 
			
		||||
        "bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 w-full p-1",
 | 
			
		||||
        children === undefined ? "animate-pulse" : ""
 | 
			
		||||
      )}>
 | 
			
		||||
      <button type="button" onClick={changeState} className="w-full flex-1 flex flex-col items-center justify-center text-center">
 | 
			
		||||
        <div className="font-bold text-xs uppercase">{t(label)}</div>
 | 
			
		||||
        <ResolvedIcon icon={isOpen ? "mdi-chevron-down" : "mdi-chevron-up"} />
 | 
			
		||||
      </button>
 | 
			
		||||
      <div
 | 
			
		||||
        className="w-full flex-1 flex flex-col items-center justify-center text-center overflow-hidden transition-height duration-500"
 | 
			
		||||
        style={{height: isOpen ? childHeight * (children?.length ?? 1) : 0}}>
 | 
			
		||||
        {children}
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@ -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}`;
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,10 @@
 | 
			
		||||
import { useTranslation } from "next-i18next";
 | 
			
		||||
import { useCallback } from 'react';
 | 
			
		||||
import classNames from 'classnames';
 | 
			
		||||
 | 
			
		||||
import Container from "components/services/widget/container";
 | 
			
		||||
import Block from "components/services/widget/block";
 | 
			
		||||
import BlockList from "components/services/widget/block-list";
 | 
			
		||||
import useWidgetAPI from "utils/proxy/use-widget-api";
 | 
			
		||||
 | 
			
		||||
export default function Component({ service }) {
 | 
			
		||||
@ -10,29 +13,75 @@ export default function Component({ 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;
 | 
			
		||||
  // information taken from the Radarr docs: https://radarr.video/docs/api/
 | 
			
		||||
  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" />
 | 
			
		||||
        <Block label="radarr.missing" />
 | 
			
		||||
        <Block label="radarr.queued" />
 | 
			
		||||
        <Block label="radarr.movies" />
 | 
			
		||||
      </Container>
 | 
			
		||||
      <>
 | 
			
		||||
        <Container service={service}>
 | 
			
		||||
          <Block label="radarr.wanted" />
 | 
			
		||||
          <Block label="radarr.missing" />
 | 
			
		||||
          <Block label="radarr.queued" />
 | 
			
		||||
          <Block label="radarr.movies" />
 | 
			
		||||
        </Container>
 | 
			
		||||
        <Container service={service}>
 | 
			
		||||
          <BlockList label="radarr.queued" />
 | 
			
		||||
        </Container>
 | 
			
		||||
      </>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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>
 | 
			
		||||
      <Container service={service}>
 | 
			
		||||
        <BlockList label="radarr.queued" childHeight={52}>
 | 
			
		||||
          {Array.isArray(queueDetailsData) ? queueDetailsData.map((queueEntry) => (
 | 
			
		||||
            <div className="my-0.5 w-full flex flex-col justify-between items-center" key={queueEntry.movieId}>
 | 
			
		||||
              <div className="h-6 w-full flex flex-row justify-between items-center">
 | 
			
		||||
                <div className="overflow-ellipsis whitespace-nowrap overflow-hidden w-3/4 text-left">{moviesData.all.find((entry) => entry.id === queueEntry.movieId)?.title}</div>
 | 
			
		||||
                <div>{formatDownloadState(queueEntry.trackedDownloadState)}</div>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div className="h-6 w-full flex flex-row justify-between items-center">
 | 
			
		||||
                <div className="mr-5 w-full bg-theme-800/30 rounded-full h-full dark:bg-theme-200/20">
 | 
			
		||||
                  <div
 | 
			
		||||
                    className={classNames(
 | 
			
		||||
                      "h-full rounded-full transition-all duration-1000",
 | 
			
		||||
                      queueEntry.trackedDownloadStatus === "ok" ? "bg-blue-500/80" : "bg-orange-500/80"
 | 
			
		||||
                    )}
 | 
			
		||||
                    style={{
 | 
			
		||||
                      width: `${(1 - queueEntry.sizeLeft / queueEntry.size) * 100}%`,
 | 
			
		||||
                    }}
 | 
			
		||||
                  />
 | 
			
		||||
                </div>
 | 
			
		||||
                <div className="w-24 text-right">{queueEntry.timeLeft}</div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          )) : undefined}
 | 
			
		||||
        </BlockList>
 | 
			
		||||
      </Container>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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,36 @@ 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
 | 
			
		||||
      })).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,8 +1,11 @@
 | 
			
		||||
import { useTranslation } from "next-i18next";
 | 
			
		||||
import classNames from 'classnames';
 | 
			
		||||
import { useCallback } from 'react';
 | 
			
		||||
 | 
			
		||||
import Container from "components/services/widget/container";
 | 
			
		||||
import Block from "components/services/widget/block";
 | 
			
		||||
import useWidgetAPI from "utils/proxy/use-widget-api";
 | 
			
		||||
import BlockList from 'components/services/widget/block-list';
 | 
			
		||||
 | 
			
		||||
export default function Component({ service }) {
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
@ -11,27 +14,73 @@ 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;
 | 
			
		||||
  // information taken from the Sonarr docs: https://sonarr.tv/docs/api/
 | 
			
		||||
  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" />
 | 
			
		||||
        <Block label="sonarr.queued" />
 | 
			
		||||
        <Block label="sonarr.series" />
 | 
			
		||||
      </Container>
 | 
			
		||||
      <>
 | 
			
		||||
        <Container service={service}>
 | 
			
		||||
          <Block label="sonarr.wanted" />
 | 
			
		||||
          <Block label="sonarr.queued" />
 | 
			
		||||
          <Block label="sonarr.series" />
 | 
			
		||||
        </Container>
 | 
			
		||||
        <Container service={service}>
 | 
			
		||||
          <BlockList label="sonarr.queued" />
 | 
			
		||||
        </Container>
 | 
			
		||||
      </>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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>
 | 
			
		||||
      <Container service={service}>
 | 
			
		||||
        <BlockList label="sonarr.queued" childHeight={52}>
 | 
			
		||||
          {Array.isArray(queueDetailsData) ? queueDetailsData.map((queueEntry) => (
 | 
			
		||||
            <div className="my-0.5 w-full flex flex-col justify-between items-center" key={queueEntry.episodeId}>
 | 
			
		||||
              <div className="h-6 w-full flex flex-row justify-between items-center">
 | 
			
		||||
                <div className="overflow-ellipsis whitespace-nowrap overflow-hidden w-3/4 text-left">{seriesData.find((entry) => entry.id === queueEntry.seriesId).title} • {queueEntry.episodeTitle}</div>
 | 
			
		||||
                <div>{formatDownloadState(queueEntry.trackedDownloadState)}</div>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div className="h-6 w-full flex flex-row justify-between items-center">
 | 
			
		||||
                <div className="mr-5 w-full bg-theme-800/30 rounded-full h-full dark:bg-theme-200/20">
 | 
			
		||||
                  <div
 | 
			
		||||
                    className={classNames(
 | 
			
		||||
                      "h-full rounded-full transition-all duration-1000",
 | 
			
		||||
                      queueEntry.trackedDownloadStatus === "ok" ? "bg-blue-500/80" : "bg-orange-500/80"
 | 
			
		||||
                    )}
 | 
			
		||||
                    style={{
 | 
			
		||||
                      width: `${(1 - queueEntry.sizeLeft / queueEntry.size) * 100}%`,
 | 
			
		||||
                    }}
 | 
			
		||||
                  />
 | 
			
		||||
                </div>
 | 
			
		||||
                <div className="w-24 text-right">{queueEntry.timeLeft}</div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          )) : undefined}
 | 
			
		||||
        </BlockList>
 | 
			
		||||
      </Container>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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,38 @@ 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,
 | 
			
		||||
        episodeId: entry.episodeId
 | 
			
		||||
      })).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;
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -29,6 +29,9 @@ module.exports = {
 | 
			
		||||
        '3xl': '1800px',
 | 
			
		||||
        // => @media (min-width: 1800px) { ... }
 | 
			
		||||
      },
 | 
			
		||||
      transitionProperty: {
 | 
			
		||||
        'height': 'height'
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  plugins: [tailwindForms, tailwindScrollbars],
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user