Add Overseerr pending request management option
This commit is contained in:
parent
c268739e1f
commit
d30322378b
@ -14,4 +14,18 @@ widget:
|
||||
type: overseerr
|
||||
url: http://overseerr.host.or.ip
|
||||
key: apikeyapikeyapikeyapikeyapikey
|
||||
pendingRequests: # optional, must be object, see below
|
||||
manageRequests: true # optional, defaults to false
|
||||
showImage: true # optional, defaults to false
|
||||
showReleaseYear: true # options, defaults to false
|
||||
```
|
||||
|
||||
## Pending Requests
|
||||
|
||||
You can enable the ability to see and manage your pending requests on Overseerr using `pendingRequests` with the options below.
|
||||
|
||||
`manageRequests`: When set to `true` it displays two buttons for each request to approve or deny the request.
|
||||
|
||||
`showImage`: When set to `true` it displays a small image of the show/movie poster and makes the request panel larger.
|
||||
|
||||
`showReleaseYear`: When set to `true` it shows the release year in parenthesis after the show/movie title.
|
||||
|
||||
@ -436,6 +436,9 @@ export function cleanServiceGroups(groups) {
|
||||
// opnsense, pfsense
|
||||
wan,
|
||||
|
||||
// overseerr
|
||||
pendingRequests,
|
||||
|
||||
// proxmox
|
||||
node,
|
||||
|
||||
@ -507,6 +510,9 @@ export function cleanServiceGroups(groups) {
|
||||
if (["opnsense", "pfsense"].includes(type)) {
|
||||
if (wan) cleanedService.widget.wan = wan;
|
||||
}
|
||||
if (type === "overseerr") {
|
||||
if (pendingRequests !== undefined) cleanedService.widget.pendingRequests = pendingRequests;
|
||||
}
|
||||
if (["emby", "jellyfin"].includes(type)) {
|
||||
if (enableBlocks !== undefined) cleanedService.widget.enableBlocks = JSON.parse(enableBlocks);
|
||||
if (enableNowPlaying !== undefined) cleanedService.widget.enableNowPlaying = JSON.parse(enableNowPlaying);
|
||||
|
||||
@ -1,19 +1,42 @@
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import { PendingRequest, RequestContainer } from "./pendingRequest";
|
||||
|
||||
import Container from "components/services/widget/container";
|
||||
import Block from "components/services/widget/block";
|
||||
import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||
import { formatProxyUrlWithSegments } from "utils/proxy/api-helpers";
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
const { widget } = service;
|
||||
|
||||
const { data: statsData, error: statsError } = useWidgetAPI(widget, "request/count");
|
||||
const { data: statsData, error: statsError, mutate: statsMutate } = useWidgetAPI(widget, "request/count");
|
||||
const { data: settingsData, error: settingsError } = useWidgetAPI(widget, "mainSettings");
|
||||
const {
|
||||
data: pendingRequestsData,
|
||||
error: pendingRequestsError,
|
||||
mutate: pendingRequestsMutate,
|
||||
} = useWidgetAPI(widget, "pendingRequests");
|
||||
|
||||
if (statsError) {
|
||||
return <Container service={service} error={statsError} />;
|
||||
if (statsError || pendingRequestsError || settingsError) {
|
||||
const finalError = statsError ?? pendingRequestsError ?? settingsError;
|
||||
return <Container service={service} error={finalError} />;
|
||||
}
|
||||
|
||||
async function handleUpdateRequestStatus(requestId, status) {
|
||||
const url = formatProxyUrlWithSegments(widget, "updateRequestStatus", {
|
||||
id: requestId,
|
||||
status,
|
||||
});
|
||||
await fetch(url).then(() => {
|
||||
statsMutate();
|
||||
pendingRequestsMutate();
|
||||
});
|
||||
}
|
||||
|
||||
const pendingRequests = widget.pendingRequests ? pendingRequestsData?.results ?? [] : [];
|
||||
|
||||
if (!statsData) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
@ -26,11 +49,25 @@ export default function Component({ service }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="overseerr.pending" value={t("common.number", { value: statsData.pending })} />
|
||||
<Block label="overseerr.processing" value={t("common.number", { value: statsData.processing })} />
|
||||
<Block label="overseerr.approved" value={t("common.number", { value: statsData.approved })} />
|
||||
<Block label="overseerr.available" value={t("common.number", { value: statsData.available })} />
|
||||
</Container>
|
||||
<>
|
||||
<Container service={service}>
|
||||
<Block label="overseerr.pending" value={t("common.number", { value: statsData.pending })} />
|
||||
<Block label="overseerr.processing" value={t("common.number", { value: statsData.processing })} />
|
||||
<Block label="overseerr.approved" value={t("common.number", { value: statsData.approved })} />
|
||||
<Block label="overseerr.available" value={t("common.number", { value: statsData.available })} />
|
||||
</Container>
|
||||
<RequestContainer>
|
||||
{pendingRequests.map((request) => (
|
||||
<PendingRequest
|
||||
key={request.id}
|
||||
request={request}
|
||||
widget={widget}
|
||||
applicationUrl={settingsData?.applicationUrl}
|
||||
onApprove={() => handleUpdateRequestStatus(request.id, "approve")}
|
||||
onDecline={() => handleUpdateRequestStatus(request.id, "decline")}
|
||||
/>
|
||||
))}
|
||||
</RequestContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
95
src/widgets/overseerr/pendingRequest.jsx
Normal file
95
src/widgets/overseerr/pendingRequest.jsx
Normal file
@ -0,0 +1,95 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import Image from "next/image";
|
||||
import { IoMdCheckmarkCircleOutline, IoMdCloseCircleOutline } from "react-icons/io";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { formatProxyUrlWithSegments } from "utils/proxy/api-helpers";
|
||||
|
||||
const tmdbImageBaseUrl = "https://media.themoviedb.org/t/p/w220_and_h330_face";
|
||||
|
||||
function ImageThumbnail({ posterPath, requestUrl }) {
|
||||
const imageUrl = `${tmdbImageBaseUrl}/${posterPath}`;
|
||||
|
||||
return (
|
||||
<div className="h-10 w-7 mr-2">
|
||||
<div className="relative h-full">
|
||||
<a href={requestUrl} target="_blank" rel="noreferrer">
|
||||
<Image
|
||||
src={imageUrl}
|
||||
alt="Your image"
|
||||
layout="fill"
|
||||
objectFit="contain"
|
||||
className="rounded-sm transition-transform duration-300 transform-gpu hover:scale-125"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ReleaseYear({ date }) {
|
||||
const year = (date || "").split("-")[0];
|
||||
|
||||
if (!year) return null;
|
||||
|
||||
return <span className="pl-2">({year})</span>;
|
||||
}
|
||||
|
||||
export function RequestContainer({ children }) {
|
||||
return <div className="overflow-auto max-h-48">{children}</div>;
|
||||
}
|
||||
|
||||
export function PendingRequest({ widget, applicationUrl, request, onApprove, onDecline }) {
|
||||
const [media, setMedia] = useState({});
|
||||
const { showImage, showReleaseYear, manageRequests } = widget?.pendingRequests ?? {};
|
||||
const mediaType = request?.media?.mediaType;
|
||||
const mediaId = request?.media?.tmdbId ?? request?.media?.tvdbId;
|
||||
const requestUrl = new URL(`${mediaType}/${mediaId}`, applicationUrl).toString();
|
||||
|
||||
// Request details do not include media information such as title or image path
|
||||
// Fetch media details separately
|
||||
async function getMediaDetails() {
|
||||
if (!mediaId) return {};
|
||||
const url = formatProxyUrlWithSegments(widget, mediaType === "movie" ? "movieDetails" : "tvDetails", {
|
||||
id: mediaId,
|
||||
});
|
||||
|
||||
return fetch(url).then((res) => res.json());
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getMediaDetails().then(setMedia);
|
||||
}, [request, widget]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"flex flex-row text-theme-700 dark:text-theme-200 items-center text-xs relative rounded-md bg-theme-200/50 dark:bg-theme-900/20 m-1 p-2",
|
||||
showImage ? "h-12" : "h-5",
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-row w-full items-center">
|
||||
{showImage && <ImageThumbnail posterPath={media.posterPath} requestUrl={requestUrl} />}
|
||||
|
||||
<div className="flex-grow text-left">
|
||||
<a href={requestUrl} target="_blank" rel="noreferrer">
|
||||
<span>{media.title ?? media.name}</span>
|
||||
{showReleaseYear && <ReleaseYear date={media.releaseDate} />}
|
||||
</a>
|
||||
</div>
|
||||
{manageRequests && (
|
||||
<div className="w-10 text-base flex flex-row justify-between">
|
||||
<IoMdCheckmarkCircleOutline
|
||||
className="hover:text-green-500 hover:scale-125"
|
||||
onClick={() => onApprove(request.id)}
|
||||
/>
|
||||
<IoMdCloseCircleOutline
|
||||
className="hover:text-red-500 hover:scale-125"
|
||||
onClick={() => onDecline(request.id)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -9,6 +9,25 @@ const widget = {
|
||||
endpoint: "request/count",
|
||||
validate: ["pending", "processing", "approved", "available"],
|
||||
},
|
||||
pendingRequests: {
|
||||
endpoint: "request?filter=pending",
|
||||
},
|
||||
tvDetails: {
|
||||
endpoint: "tv/{id}",
|
||||
segments: ["id"],
|
||||
},
|
||||
movieDetails: {
|
||||
endpoint: "movie/{id}",
|
||||
segments: ["id"],
|
||||
},
|
||||
updateRequestStatus: {
|
||||
method: "POST",
|
||||
endpoint: "request/{id}/{status}",
|
||||
segments: ["id", "status"],
|
||||
},
|
||||
mainSettings: {
|
||||
endpoint: "settings/main",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user