Added a new widget for tvheadend

This commit is contained in:
Adam Price 2024-12-02 07:38:47 +00:00 committed by Adam Shephard-Price
parent cb3248117f
commit 24cd955fa6
7 changed files with 146 additions and 0 deletions

View File

@ -0,0 +1,21 @@
---
title: Tvheadend
description: Tvheadend Widget Configuration
---
Shows the status of the DVR feature (upcoming, finished, failed recordings) as well as a slist of active subscriptions.
Learn more about [Tvheadend](https://tvheadend.org/).
Allowed fields: `["upcoming", "finished", "failed"]`.
Uses basic auth to connect to tvheadend API
```yaml
widget:
type: tvheadend
url: http://tvheadend.host.or.ip
username: user
password: pass
fields: ["upcoming", "finished", "failed"]
```

View File

@ -1007,5 +1007,10 @@
"issues": "Issues",
"merges": "Merge Requests",
"projects": "Projects"
},
"tvheadend": {
"upcoming": "Upcoming",
"finished": "Finished",
"failed": "Failed"
}
}

View File

@ -127,6 +127,7 @@ const components = {
transmission: dynamic(() => import("./transmission/component")),
tubearchivist: dynamic(() => import("./tubearchivist/component")),
truenas: dynamic(() => import("./truenas/component")),
tvheadend: dynamic(() => import("./tvheadend/component")),
unifi: dynamic(() => import("./unifi/component")),
unmanic: dynamic(() => import("./unmanic/component")),
uptimekuma: dynamic(() => import("./uptimekuma/component")),

View File

@ -0,0 +1,87 @@
import { useTranslation } from "next-i18next";
import Container from "components/services/widget/container";
import Block from "components/services/widget/block";
import useWidgetAPI from "utils/proxy/use-widget-api";
import Subscription from "widgets/tvheadend/subscription";
function timeAgo(ticks) {
const now = Date.now(); // Current time in milliseconds
const inputTime = ticks * 1000; // Convert the input ticks from seconds to milliseconds
const difference = now - inputTime; // Difference in milliseconds
const seconds = Math.floor((difference / 1000) % 60);
const minutes = Math.floor((difference / (1000 * 60)) % 60);
const hours = Math.floor((difference / (1000 * 60 * 60)) % 24);
return { hours, minutes, seconds };
}
function timeAgoToString(ticks) {
const { hours, minutes, seconds } = timeAgo(ticks);
const parts = [];
if (hours > 0) {
parts.push(hours);
}
parts.push(minutes);
parts.push(seconds);
return parts.map((part) => part.toString().padStart(2, "0")).join(":");
}
export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service;
const { data: dvrData, error: dvrError } = useWidgetAPI(widget, "dvr", {
refreshInterval: 60000,
});
const { data: subscriptionsData, error: subscriptionsError } = useWidgetAPI(widget, "subscriptions", {
refreshInterval: 5000,
});
if (dvrError || subscriptionsError) {
const finalError = dvrError ?? subscriptionsError;
return <Container service={service} error={finalError} />;
}
if (!dvrData || !subscriptionsData) {
return (
<Container service={service}>
<Block label="tvheadend.upcoming" />
<Block label="tvheadend.finished" />
<Block label="tvheadend.failed" />
</Container>
);
}
const upcomingCount = dvrData.entries.filter((entry) => entry.sched_status === "scheduled").length;
const finishedCount = dvrData.entries.filter((entry) => entry.sched_status === "completed").length;
const failedCount = dvrData.entries.filter((entry) => entry.sched_status === "failed").length;
const hasSubscriptions = Array.isArray(subscriptionsData.entries) && subscriptionsData.entries.length > 0;
return (
<>
<Container service={service}>
<Block label="tvheadend.upcoming" value={t("common.number", { value: upcomingCount })} />
<Block label="tvheadend.finished" value={t("common.number", { value: finishedCount })} />
<Block label="tvheadend.failed" value={t("common.number", { value: failedCount })} />
</Container>
{hasSubscriptions &&
subscriptionsData.entries
.filter(
(entry) =>
entry.channel && // Only include entries with a valid channel
entry.state === "Running",
) // and being watched (Idle=downloading)
.sort((a, b) => a.channel.localeCompare(b.channel))
.map((subscription) => (
<Subscription
key={subscription.id}
channel={subscription.channel}
sinceStart={timeAgoToString(subscription.start)}
/>
))}
</>
);
}

View File

@ -0,0 +1,11 @@
export default function Subscription({ channel, sinceStart }) {
return (
<div className="flex flex-col pb-1 mx-1">
<div className="text-theme-700 dark:text-theme-200 text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1">
<div className="absolute left-2 text-xs mt-[2px]">{channel}</div>
<div className="grow " />
<div className="self-center text-xs flex justify-end mr-2 mt-[2px]">{sinceStart}</div>
</div>
</div>
);
}

View File

@ -0,0 +1,19 @@
import genericProxyHandler from "utils/proxy/handlers/generic";
const widget = {
api: "{url}/api/{endpoint}",
proxyHandler: genericProxyHandler,
mappings: {
dvr: {
endpoint: "dvr/entry/grid",
validate: ["entries"],
},
subscriptions: {
endpoint: "status/subscriptions",
validate: ["entries"],
},
},
};
export default widget;

View File

@ -118,6 +118,7 @@ import traefik from "./traefik/widget";
import transmission from "./transmission/widget";
import tubearchivist from "./tubearchivist/widget";
import truenas from "./truenas/widget";
import tvheadend from "./tvheadend/widget";
import unifi from "./unifi/widget";
import unmanic from "./unmanic/widget";
import uptimekuma from "./uptimekuma/widget";
@ -255,6 +256,7 @@ const widgets = {
transmission,
tubearchivist,
truenas,
tvheadend,
unifi,
unifi_console: unifi,
unmanic,