diff --git a/docs/widgets/services/overseerr.md b/docs/widgets/services/overseerr.md index 4d3d6bb1d..587c49b33 100644 --- a/docs/widgets/services/overseerr.md +++ b/docs/widgets/services/overseerr.md @@ -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. diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js index 67502a7a7..921a2d06e 100644 --- a/src/utils/config/service-helpers.js +++ b/src/utils/config/service-helpers.js @@ -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); diff --git a/src/widgets/overseerr/component.jsx b/src/widgets/overseerr/component.jsx index 336bc6fce..3622431a6 100644 --- a/src/widgets/overseerr/component.jsx +++ b/src/widgets/overseerr/component.jsx @@ -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 || pendingRequestsError || settingsError) { + const finalError = statsError ?? pendingRequestsError ?? settingsError; + return ; + } - if (statsError) { - return ; + 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 ( @@ -26,11 +49,25 @@ export default function Component({ service }) { } return ( - - - - - - + <> + + + + + + + + {pendingRequests.map((request) => ( + handleUpdateRequestStatus(request.id, "approve")} + onDecline={() => handleUpdateRequestStatus(request.id, "decline")} + /> + ))} + + ); } diff --git a/src/widgets/overseerr/pendingRequest.jsx b/src/widgets/overseerr/pendingRequest.jsx new file mode 100644 index 000000000..ea70e1a9b --- /dev/null +++ b/src/widgets/overseerr/pendingRequest.jsx @@ -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 ( +
+
+ + Your image + +
+
+ ); +} + +function ReleaseYear({ date }) { + const year = (date || "").split("-")[0]; + + if (!year) return null; + + return ({year}); +} + +export function RequestContainer({ children }) { + return
{children}
; +} + +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 ( +
+
+ {showImage && } + + + {manageRequests && ( +
+ onApprove(request.id)} + /> + onDecline(request.id)} + /> +
+ )} +
+
+ ); +} diff --git a/src/widgets/overseerr/widget.js b/src/widgets/overseerr/widget.js index 2faabdbc4..8b8d78cf6 100644 --- a/src/widgets/overseerr/widget.js +++ b/src/widgets/overseerr/widget.js @@ -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", + }, }, };