Added support for Linkwarden
This commit is contained in:
parent
b9b7c482d4
commit
2bf62d1d78
@ -55,6 +55,7 @@ You can also find a list of all available service widgets in the sidebar navigat
|
|||||||
- [Komga](komga.md)
|
- [Komga](komga.md)
|
||||||
- [Kopia](kopia.md)
|
- [Kopia](kopia.md)
|
||||||
- [Lidarr](lidarr.md)
|
- [Lidarr](lidarr.md)
|
||||||
|
- [Linkwarden](linkwarden.md)
|
||||||
- [Mastodon](mastodon.md)
|
- [Mastodon](mastodon.md)
|
||||||
- [Mealie](mealie.md)
|
- [Mealie](mealie.md)
|
||||||
- [Medusa](medusa.md)
|
- [Medusa](medusa.md)
|
||||||
|
|||||||
62
docs/widgets/services/linkwarden.md
Normal file
62
docs/widgets/services/linkwarden.md
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
---
|
||||||
|
title: Linkwarden
|
||||||
|
description: Linkwarden Widget Configuration
|
||||||
|
---
|
||||||
|
|
||||||
|
Learn more about [Linkwarden](https://linkwarden.app/).
|
||||||
|
|
||||||
|
Allowed fields: `["links", "collections", "tags"]`.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
widget:
|
||||||
|
type: linkwarden
|
||||||
|
url: http://linkwarden.host.or.ip
|
||||||
|
key: myApiKeyHere # On your Linkwarden install, go to Settings > Access Tokens. Generate a token.
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `mode` to show the stats found in the fields and/or the recent bookmarks.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
`mode: ["stats", "recent"]` or `mode: ["stats"]` or `mode: ["recent"]`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
widget:
|
||||||
|
type: linkwarden
|
||||||
|
url: http://linkwarden.host.or.ip
|
||||||
|
key: myApiKeyHere
|
||||||
|
mode: ["stats", "recent"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `params` to set which collections and/or tags to display links from.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
params:
|
||||||
|
collectionIds: ["8", "13", "6"] # ID's of collections
|
||||||
|
```
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
params:
|
||||||
|
tagIds: ["84", "66", "88", "69"] # ID's of tags
|
||||||
|
```
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
params:
|
||||||
|
collectionIds: ["8", "13", "6"] # ID's of collections
|
||||||
|
tagIds: ["84", "66", "88", "69"] # ID's of tags
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
widget:
|
||||||
|
type: linkwarden
|
||||||
|
url: http://linkwarden.host.or.ip
|
||||||
|
key: myApiKeyHere
|
||||||
|
params:
|
||||||
|
collectionIds: ["8", "13", "6"] # ID's of collections
|
||||||
|
tagIds: ["84", "66", "88", "69"] # ID's of tags
|
||||||
|
```
|
||||||
@ -80,6 +80,7 @@ nav:
|
|||||||
- widgets/services/komga.md
|
- widgets/services/komga.md
|
||||||
- widgets/services/kopia.md
|
- widgets/services/kopia.md
|
||||||
- widgets/services/lidarr.md
|
- widgets/services/lidarr.md
|
||||||
|
- widgets/services/linkwarden.md
|
||||||
- widgets/services/mastodon.md
|
- widgets/services/mastodon.md
|
||||||
- widgets/services/mealie.md
|
- widgets/services/mealie.md
|
||||||
- widgets/services/medusa.md
|
- widgets/services/medusa.md
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -115,7 +115,10 @@ export async function servicesFromDocker() {
|
|||||||
return constructedService;
|
return constructedService;
|
||||||
});
|
});
|
||||||
|
|
||||||
return { server: serverName, services: discovered.filter((filteredService) => filteredService) };
|
return {
|
||||||
|
server: serverName,
|
||||||
|
services: discovered.filter((filteredService) => filteredService),
|
||||||
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error("Error getting services from Docker server '%s': %s", serverName, e);
|
logger.error("Error getting services from Docker server '%s': %s", serverName, e);
|
||||||
|
|
||||||
@ -438,6 +441,11 @@ export function cleanServiceGroups(groups) {
|
|||||||
namespace,
|
namespace,
|
||||||
podSelector,
|
podSelector,
|
||||||
|
|
||||||
|
// linkwarden
|
||||||
|
mode,
|
||||||
|
params,
|
||||||
|
url,
|
||||||
|
|
||||||
// mjpeg
|
// mjpeg
|
||||||
fit,
|
fit,
|
||||||
stream,
|
stream,
|
||||||
@ -575,6 +583,11 @@ export function cleanServiceGroups(groups) {
|
|||||||
if (pointsLimit) cleanedService.widget.pointsLimit = pointsLimit;
|
if (pointsLimit) cleanedService.widget.pointsLimit = pointsLimit;
|
||||||
if (diskUnits) cleanedService.widget.diskUnits = diskUnits;
|
if (diskUnits) cleanedService.widget.diskUnits = diskUnits;
|
||||||
}
|
}
|
||||||
|
if (type === "linkwarden") {
|
||||||
|
if (mode) cleanedService.widget.mode = mode;
|
||||||
|
if (params) cleanedService.widget.params = params;
|
||||||
|
if (url) cleanedService.widget.url = url;
|
||||||
|
}
|
||||||
if (type === "mjpeg") {
|
if (type === "mjpeg") {
|
||||||
if (stream) cleanedService.widget.stream = stream;
|
if (stream) cleanedService.widget.stream = stream;
|
||||||
if (fit) cleanedService.widget.fit = fit;
|
if (fit) cleanedService.widget.fit = fit;
|
||||||
|
|||||||
@ -54,6 +54,7 @@ const components = {
|
|||||||
komga: dynamic(() => import("./komga/component")),
|
komga: dynamic(() => import("./komga/component")),
|
||||||
kopia: dynamic(() => import("./kopia/component")),
|
kopia: dynamic(() => import("./kopia/component")),
|
||||||
lidarr: dynamic(() => import("./lidarr/component")),
|
lidarr: dynamic(() => import("./lidarr/component")),
|
||||||
|
linkwarden: dynamic(() => import("./linkwarden/component")),
|
||||||
mastodon: dynamic(() => import("./mastodon/component")),
|
mastodon: dynamic(() => import("./mastodon/component")),
|
||||||
mealie: dynamic(() => import("./mealie/component")),
|
mealie: dynamic(() => import("./mealie/component")),
|
||||||
medusa: dynamic(() => import("./medusa/component")),
|
medusa: dynamic(() => import("./medusa/component")),
|
||||||
|
|||||||
258
src/widgets/linkwarden/component.jsx
Normal file
258
src/widgets/linkwarden/component.jsx
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
import React, { useState, useEffect, useMemo, 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";
|
||||||
|
|
||||||
|
export default function Component({ service }) {
|
||||||
|
const { widget } = service;
|
||||||
|
|
||||||
|
// Assign icons. Assign recent/collections/tags to query by id(s)
|
||||||
|
const bookmarkTypes = useMemo(
|
||||||
|
() => ({
|
||||||
|
recent: { ids: widget.mode.includes("recent") ? ["0"] : [] }, // "0" Is a made-up number used to allow looping in processBookmarks()
|
||||||
|
collection: {
|
||||||
|
ids: widget.params?.collectionIds ? widget.params.collectionIds : [],
|
||||||
|
},
|
||||||
|
tag: { ids: widget.params?.tagIds ? widget.params.tagIds : [] },
|
||||||
|
}),
|
||||||
|
[widget],
|
||||||
|
);
|
||||||
|
|
||||||
|
// State to hold Stats
|
||||||
|
const [stats, setStats] = useState({
|
||||||
|
totalLinks: null,
|
||||||
|
collections: { list: null, total: null },
|
||||||
|
tags: { list: null, total: null },
|
||||||
|
});
|
||||||
|
|
||||||
|
// State to hold Recent/Collection/Tag Bookmarks
|
||||||
|
const [bookmarks, setBookmarks] = useState({
|
||||||
|
recent: { icon: "📚️", data: {} },
|
||||||
|
collection: { icon: "📁", data: {} },
|
||||||
|
tag: { icon: "🏷️", data: {} },
|
||||||
|
});
|
||||||
|
|
||||||
|
const [fetchingMore, setFetchingMore] = useState({
|
||||||
|
recent: {},
|
||||||
|
collection: {},
|
||||||
|
tag: {},
|
||||||
|
});
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
const { data: collectionsStatsData, error: collectionsStatsError } = useWidgetAPI(widget, "collections"); // Fetch Collection Stats
|
||||||
|
const { data: tagsStatsData, error: tagsStatsError } = useWidgetAPI(widget, "tags"); // Fetch Tag Stats
|
||||||
|
|
||||||
|
// Effect to update Stats when collectionsStatsData or tagsStatsData changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (collectionsStatsData?.response && tagsStatsData?.response) {
|
||||||
|
/* eslint-disable no-underscore-dangle */
|
||||||
|
setStats({
|
||||||
|
totalLinks: collectionsStatsData.response.reduce((sum, collection) => sum + (collection._count?.links || 0), 0),
|
||||||
|
collections: {
|
||||||
|
list: collectionsStatsData.response,
|
||||||
|
total: collectionsStatsData.response.length,
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
list: tagsStatsData.response,
|
||||||
|
total: tagsStatsData.response.length,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
/* eslint-enable no-underscore-dangle */
|
||||||
|
}
|
||||||
|
}, [collectionsStatsData, tagsStatsData]);
|
||||||
|
|
||||||
|
// Reusable function to fetch bookmarks based on ids.recent/ids.collection/ids.tag and type
|
||||||
|
const fetchBookmarks = useCallback(async (ids, type, cursor, currentWidget) => {
|
||||||
|
try {
|
||||||
|
const promises = ids.map(async (id) => {
|
||||||
|
const baseQuery = { sort: 0, cursor: cursor || "" };
|
||||||
|
const query = type === "recent" ? baseQuery : { ...baseQuery, [`${type}Id`]: id };
|
||||||
|
|
||||||
|
const queryParams = new URLSearchParams({
|
||||||
|
group: currentWidget.service_group,
|
||||||
|
service: currentWidget.service_name,
|
||||||
|
endpoint: "links",
|
||||||
|
query: JSON.stringify(query),
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetch(`/api/services/proxy?${queryParams}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 401) {
|
||||||
|
setError("Unauthorized access. Please log in again.");
|
||||||
|
} else {
|
||||||
|
setError(`HTTP error! Status: ${response.status}`);
|
||||||
|
}
|
||||||
|
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||||
|
}
|
||||||
|
return { id, bookmarks: await response.json() };
|
||||||
|
});
|
||||||
|
|
||||||
|
return await Promise.all(promises);
|
||||||
|
} catch (fetchError) {
|
||||||
|
setError("An error occurred while fetching bookmarks.");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const processBookmarks = useCallback(
|
||||||
|
async (ids, type, cursor, currentWidget, currentStats, updateBookmarks, append = false) => {
|
||||||
|
try {
|
||||||
|
const fetchedBookmarks = await fetchBookmarks(ids, type, cursor, currentWidget);
|
||||||
|
updateBookmarks((prev) => {
|
||||||
|
const newBookmarks = fetchedBookmarks.map((item) => ({
|
||||||
|
id: item.id,
|
||||||
|
title:
|
||||||
|
type === "recent"
|
||||||
|
? "Recent Bookmarks"
|
||||||
|
: currentStats[`${type}s`]?.list?.find((statItem) => statItem.id.toString() === item.id)?.name ||
|
||||||
|
item.id,
|
||||||
|
url: `${currentWidget.url}${type === "recent" ? "/links/" : `/${type}s/${item.id}/`}`,
|
||||||
|
/* eslint-disable no-underscore-dangle */
|
||||||
|
total:
|
||||||
|
type === "recent"
|
||||||
|
? currentStats.totalLinks
|
||||||
|
: currentStats[`${type}s`]?.list?.find((foundStatItem) => foundStatItem.id.toString() === item.id)
|
||||||
|
?._count?.links || 0,
|
||||||
|
/* eslint-enable no-underscore-dangle */
|
||||||
|
cursor: item.bookmarks.response.length
|
||||||
|
? item.bookmarks.response[item.bookmarks.response.length - 1]?.id
|
||||||
|
: "end",
|
||||||
|
bookmarks: append
|
||||||
|
? [...(prev[type].data[item.id]?.bookmarks || []), ...item.bookmarks.response]
|
||||||
|
: item.bookmarks.response,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
[type]: {
|
||||||
|
...prev[type],
|
||||||
|
data: {
|
||||||
|
...prev[type].data,
|
||||||
|
...Object.fromEntries(newBookmarks.map((bookmark) => [bookmark.id, bookmark])),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch (processError) {
|
||||||
|
const errorMessage = `Error setting ${type} bookmarks: ${processError.message}`;
|
||||||
|
setError(errorMessage);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[fetchBookmarks],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Effect to fetch and update Recent/Collection/Tag Bookmarks
|
||||||
|
useEffect(() => {
|
||||||
|
if (error) return; // Stop fetching if there's an error
|
||||||
|
|
||||||
|
const fetchAndProcessBookmarks = async () => {
|
||||||
|
try {
|
||||||
|
const bookmarkFetchPromises = Object.entries(bookmarkTypes)
|
||||||
|
.filter(([type, { ids }]) => ids.length > 0 && Object.keys(bookmarks[type].data).length === 0)
|
||||||
|
.map(async ([type, { ids }]) => {
|
||||||
|
// Process bookmarks for each type
|
||||||
|
await processBookmarks(ids, type, null, widget, stats, setBookmarks);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(bookmarkFetchPromises);
|
||||||
|
} catch (fetchError) {
|
||||||
|
setError(`Error processing bookmarks: ${fetchError.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchAndProcessBookmarks();
|
||||||
|
}, [bookmarkTypes, processBookmarks, widget, stats, bookmarks, error]);
|
||||||
|
|
||||||
|
const handleScroll = async (event, id, type, cursor) => {
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = event.target;
|
||||||
|
if (scrollHeight - scrollTop <= clientHeight + 1 && cursor !== "end") {
|
||||||
|
if (!fetchingMore[type][id]) {
|
||||||
|
setFetchingMore((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[type]: { ...prev[type], [id]: true },
|
||||||
|
}));
|
||||||
|
try {
|
||||||
|
await processBookmarks([id], type, cursor, widget, stats, setBookmarks, true);
|
||||||
|
} finally {
|
||||||
|
setFetchingMore((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[type]: { ...prev[type], [id]: false },
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle errors
|
||||||
|
if (error) {
|
||||||
|
return <Container service={service} error={error} />;
|
||||||
|
}
|
||||||
|
if (collectionsStatsError || tagsStatsError) {
|
||||||
|
return <Container service={service} error={collectionsStatsError || tagsStatsError} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render when data is available
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{widget.mode.includes("stats") && (
|
||||||
|
<Container service={service}>
|
||||||
|
<Block label="linkwarden.links" value={stats.totalLinks} />
|
||||||
|
<Block label="linkwarden.collections" value={stats.collections.total} />
|
||||||
|
<Block label="linkwarden.tags" value={stats.tags.total} />
|
||||||
|
</Container>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{Object.keys(bookmarks).map((type) => (
|
||||||
|
<div
|
||||||
|
key={type}
|
||||||
|
className="service-container grid gap-2 p-1"
|
||||||
|
style={{
|
||||||
|
gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Object.values(bookmarks[type].data).map((bookmarkList) => (
|
||||||
|
<div key={bookmarkList.id} className="relative w-full text-left">
|
||||||
|
<div className="flex text-sm mb-2">
|
||||||
|
<a href={bookmarkList.url} target="_blank" rel="noopener noreferrer" className="grow font-bold">
|
||||||
|
{`${bookmarks[type].icon} ${bookmarkList.title}`}
|
||||||
|
</a>
|
||||||
|
<span>{`(${bookmarkList.total})`}</span>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
className="max-h-[17em] overflow-scroll flex flex-col gap-2"
|
||||||
|
onScroll={(e) => handleScroll(e, bookmarkList.id, type, bookmarkList.cursor)}
|
||||||
|
>
|
||||||
|
{Object.values(bookmarkList.bookmarks).map(({ id, url, name, description }) => (
|
||||||
|
<li id={id} key={`${bookmarkList.title}-${type}-${id}`}>
|
||||||
|
<a
|
||||||
|
href={url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="bg-theme-200/50 dark:bg-theme-900/20 hover:bg-theme-200/75 hover:dark:bg-theme-900/50 flex-1 flex gap-2 rounded p-2 service-block"
|
||||||
|
>
|
||||||
|
<span className="w-8 min-w-8 flex items-center justify-center">🔗</span>
|
||||||
|
<div className="flex flex-col grow">
|
||||||
|
<div className="font-bold text-xs uppercase break-all overflow-hidden line-clamp-1 overflow-ellipsis">
|
||||||
|
{name || description}
|
||||||
|
</div>
|
||||||
|
<div className="font-thin text-xs break-all overflow-hidden line-clamp-1 overflow-ellipsis">
|
||||||
|
{url}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
{fetchingMore[type][bookmarkList.id] && (
|
||||||
|
<li className="text-center">
|
||||||
|
<span className="text-sm">Loading more...</span>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
81
src/widgets/linkwarden/proxy.js
Normal file
81
src/widgets/linkwarden/proxy.js
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { httpProxy } from "utils/proxy/http";
|
||||||
|
import { formatApiCall } from "utils/proxy/api-helpers";
|
||||||
|
import getServiceWidget from "utils/config/service-helpers";
|
||||||
|
import createLogger from "utils/logger";
|
||||||
|
|
||||||
|
const proxyName = "linkwardenProxyHandler";
|
||||||
|
const logger = createLogger(proxyName);
|
||||||
|
|
||||||
|
const CONTENT_TYPE_JSON = "application/json";
|
||||||
|
const AUTHORIZATION = "Authorization";
|
||||||
|
const BEARER = "Bearer";
|
||||||
|
const ERROR_INVALID_SERVICE = "Invalid proxy service type";
|
||||||
|
const ERROR_MISSING_TOKEN = "Missing widget token";
|
||||||
|
|
||||||
|
async function retrieveFromAPI(url, token) {
|
||||||
|
const headers = {
|
||||||
|
"Content-Type": CONTENT_TYPE_JSON,
|
||||||
|
[AUTHORIZATION]: `${BEARER} ${token}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const [status, , data] = await httpProxy(url, { headers });
|
||||||
|
|
||||||
|
if (status === 401) {
|
||||||
|
const errorResponse = JSON.parse(Buffer.from(data).toString());
|
||||||
|
throw new Error(`Unauthorized: ${errorResponse.response}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status !== 200) {
|
||||||
|
throw new Error(`Error getting data from Linkwarden: ${status}. Data: ${data.toString()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.parse(Buffer.from(data).toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function linkwardenProxyHandler(req, res) {
|
||||||
|
const { group, service, endpoint, query } = req.query;
|
||||||
|
|
||||||
|
if (!group || !service) {
|
||||||
|
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
|
||||||
|
return res.status(400).json({ error: ERROR_INVALID_SERVICE });
|
||||||
|
}
|
||||||
|
|
||||||
|
const widget = await getServiceWidget(group, service);
|
||||||
|
|
||||||
|
if (!widget) {
|
||||||
|
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
|
||||||
|
return res.status(400).json({ error: ERROR_INVALID_SERVICE });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!widget.token) {
|
||||||
|
logger.debug("Invalid or missing token for service '%s' in group '%s'", service, group);
|
||||||
|
return res.status(400).json({ error: ERROR_MISSING_TOKEN });
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiURL = "{url}/api/v1/{endpoint}";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = new URL(formatApiCall(apiURL, { endpoint, ...widget }));
|
||||||
|
|
||||||
|
// Parse the query JSON if it exists
|
||||||
|
if (query) {
|
||||||
|
try {
|
||||||
|
const parsedQuery = JSON.parse(query);
|
||||||
|
Object.entries(parsedQuery).forEach(([key, value]) => {
|
||||||
|
url.searchParams.append(key, value);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error parsing query JSON:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Constructed API URL: ${url.toString()}`);
|
||||||
|
|
||||||
|
const data = await retrieveFromAPI(url, widget.token);
|
||||||
|
|
||||||
|
return res.status(200).json(data);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(e.message);
|
||||||
|
return res.status(500).json({ error: { message: e.message } });
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/widgets/linkwarden/widget.js
Normal file
20
src/widgets/linkwarden/widget.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import linkwardenProxyHandler from "./proxy";
|
||||||
|
|
||||||
|
const widget = {
|
||||||
|
api: "{url}/api/v1/{endpoint}",
|
||||||
|
proxyHandler: linkwardenProxyHandler,
|
||||||
|
|
||||||
|
mappings: {
|
||||||
|
collections: {
|
||||||
|
endpoint: "collections",
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
endpoint: "tags",
|
||||||
|
},
|
||||||
|
links: {
|
||||||
|
endpoint: "links",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default widget;
|
||||||
@ -46,6 +46,7 @@ import kavita from "./kavita/widget";
|
|||||||
import komga from "./komga/widget";
|
import komga from "./komga/widget";
|
||||||
import kopia from "./kopia/widget";
|
import kopia from "./kopia/widget";
|
||||||
import lidarr from "./lidarr/widget";
|
import lidarr from "./lidarr/widget";
|
||||||
|
import linkwarden from "./linkwarden/widget";
|
||||||
import mastodon from "./mastodon/widget";
|
import mastodon from "./mastodon/widget";
|
||||||
import mealie from "./mealie/widget";
|
import mealie from "./mealie/widget";
|
||||||
import medusa from "./medusa/widget";
|
import medusa from "./medusa/widget";
|
||||||
@ -167,6 +168,7 @@ const widgets = {
|
|||||||
komga,
|
komga,
|
||||||
kopia,
|
kopia,
|
||||||
lidarr,
|
lidarr,
|
||||||
|
linkwarden,
|
||||||
mastodon,
|
mastodon,
|
||||||
mealie,
|
mealie,
|
||||||
medusa,
|
medusa,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user