-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feature: suwayomi Service Widget (#4273)
Co-authored-by: shamoon <[email protected]>
- Loading branch information
Showing
7 changed files
with
256 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
--- | ||
title: Suwayomi | ||
description: Suwayomi Widget Configuration | ||
--- | ||
|
||
Learn more about [Suwayomi](https://github.com/Suwayomi/Suwayomi-Server). | ||
|
||
Allowed fields: ["download", "nondownload", "read", "unread", "downloadedread", "downloadedunread", "nondownloadedread", "nondownloadedunread"] | ||
|
||
The widget defaults to the first four above. If more than four fields are provided, only the first 4 are displayed. | ||
Category IDs can be obtained from the url when navigating to it, `?tab={categoryID}`. | ||
|
||
```yaml | ||
widget: | ||
type: suwayomi | ||
url: http://suwayomi.host.or.ip | ||
username: username #optional | ||
password: password #optional | ||
category: 0 #optional, defaults to all categories | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
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"; | ||
|
||
export default function Component({ service }) { | ||
const { t } = useTranslation(); | ||
|
||
const { widget } = service; | ||
|
||
const { data: suwayomiData, error: suwayomiError } = useWidgetAPI(widget); | ||
|
||
if (suwayomiError) { | ||
return <Container service={service} error={suwayomiError} />; | ||
} | ||
|
||
if (!suwayomiData) { | ||
if (!widget.fields || widget.fields.length === 0) { | ||
widget.fields = ["download", "nondownload", "read", "unread"]; | ||
} else if (widget.fields.length > 4) { | ||
widget.fields = widget.fields.slice(0, 4); | ||
} | ||
return ( | ||
<Container service={service}> | ||
{widget.fields.map((field) => ( | ||
<Block key={field} label={`suwayomi.${field}`} /> | ||
))} | ||
</Container> | ||
); | ||
} | ||
|
||
return ( | ||
<Container service={service}> | ||
{suwayomiData.map((data) => ( | ||
<Block key={data.label} label={data.label} value={t("common.number", { value: data.count })} /> | ||
))} | ||
</Container> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
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"; | ||
import widgets from "widgets/widgets"; | ||
|
||
const proxyName = "suwayomiProxyHandler"; | ||
const logger = createLogger(proxyName); | ||
|
||
const countsToExtract = { | ||
download: { | ||
condition: (c) => c.isDownloaded, | ||
gqlCondition: "isDownloaded: true", | ||
}, | ||
nondownload: { | ||
condition: (c) => !c.isDownloaded, | ||
gqlCondition: "isDownloaded: false", | ||
}, | ||
read: { | ||
condition: (c) => c.isRead, | ||
gqlCondition: "isRead: true", | ||
}, | ||
unread: { | ||
condition: (c) => !c.isRead, | ||
gqlCondition: "isRead: false", | ||
}, | ||
downloadedread: { | ||
condition: (c) => c.isDownloaded && c.isRead, | ||
gqlCondition: "isDownloaded: true, isRead: true", | ||
}, | ||
downloadedunread: { | ||
condition: (c) => c.isDownloaded && !c.isRead, | ||
gqlCondition: "isDownloaded: true, isRead: false", | ||
}, | ||
nondownloadedread: { | ||
condition: (c) => !c.isDownloaded && c.isRead, | ||
gqlCondition: "isDownloaded: false, isRead: true", | ||
}, | ||
nondownloadedunread: { | ||
condition: (c) => !c.isDownloaded && !c.isRead, | ||
gqlCondition: "isDownloaded: false, isRead: false", | ||
}, | ||
}; | ||
|
||
function makeBody(fields, category = "all") { | ||
if (Number.isNaN(Number(category))) { | ||
let query = ""; | ||
fields.forEach((field) => { | ||
query += ` | ||
${field}: chapters( | ||
condition: {${countsToExtract[field].gqlCondition}} | ||
filter: {inLibrary: {equalTo: true}} | ||
) { | ||
totalCount | ||
}`; | ||
}); | ||
return JSON.stringify({ | ||
operationName: "Counts", | ||
query: ` | ||
query Counts { | ||
${query} | ||
}`, | ||
}); | ||
} | ||
|
||
return JSON.stringify({ | ||
operationName: "category", | ||
query: ` | ||
query category($id: Int!) { | ||
category(id: $id) { | ||
# name | ||
mangas { | ||
nodes { | ||
chapters { | ||
nodes { | ||
isRead | ||
isDownloaded | ||
} | ||
} | ||
} | ||
} | ||
} | ||
}`, | ||
variables: { | ||
id: Number(category), | ||
}, | ||
}); | ||
} | ||
|
||
function extractCounts(responseJSON, fields) { | ||
if (!("category" in responseJSON.data)) { | ||
return fields.map((field) => ({ | ||
count: responseJSON.data[field].totalCount, | ||
label: `suwayomi.${field}`, | ||
})); | ||
} | ||
const tmp = responseJSON.data.category.mangas.nodes.reduce( | ||
(accumulator, manga) => { | ||
manga.chapters.nodes.forEach((chapter) => { | ||
fields.forEach((field, i) => { | ||
if (countsToExtract[field].condition(chapter)) { | ||
accumulator[i] += 1; | ||
} | ||
}); | ||
}); | ||
return accumulator; | ||
}, | ||
[0, 0, 0, 0], | ||
); | ||
return fields.map((field, i) => ({ | ||
count: tmp[i], | ||
label: `suwayomi.${field}`, | ||
})); | ||
} | ||
|
||
export default async function suwayomiProxyHandler(req, res) { | ||
const { group, service, endpoint } = req.query; | ||
|
||
if (!group || !service) { | ||
logger.debug("Invalid or missing service '%s' or group '%s'", service, group); | ||
return res.status(400).json({ error: "Invalid proxy service type" }); | ||
} | ||
|
||
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: "Invalid proxy service type" }); | ||
} | ||
|
||
if (!widget.fields || widget.fields.length === 0) { | ||
widget.fields = ["download", "nondownload", "read", "unread"]; | ||
} else if (widget.fields.length > 4) { | ||
widget.fields = widget.fields.slice(0, 4); | ||
} | ||
|
||
const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget })); | ||
|
||
const body = makeBody(widget.fields, widget.category); | ||
|
||
const headers = { | ||
"Content-Type": "application/json", | ||
}; | ||
|
||
if (widget.username && widget.password) { | ||
headers.Authorization = `Basic ${Buffer.from(`${widget.username}:${widget.password}`).toString("base64")}`; | ||
} | ||
|
||
const [status, contentType, data] = await httpProxy(url, { | ||
method: "POST", | ||
body, | ||
headers, | ||
}); | ||
|
||
if (status === 401) { | ||
logger.error("Invalid or missing username or password for service '%s' in group '%s'", service, group); | ||
return res.status(status).send({ error: { message: "401: unauthorized, username or password is incorrect." } }); | ||
} | ||
|
||
if (status !== 200) { | ||
logger.error( | ||
"Error getting data from Suwayomi for service '%s' in group '%s': %d. Data: %s", | ||
service, | ||
group, | ||
status, | ||
data, | ||
); | ||
return res.status(status).send({ error: { message: "Error getting data. body: %s, data: %s", body, data } }); | ||
} | ||
|
||
const returnData = extractCounts(JSON.parse(data), widget.fields); | ||
|
||
if (contentType) res.setHeader("Content-Type", contentType); | ||
return res.status(status).send(returnData); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import suwayomiProxyHandler from "./proxy"; | ||
|
||
const widget = { | ||
api: "{url}/api/graphql", | ||
proxyHandler: suwayomiProxyHandler, | ||
}; | ||
|
||
export default widget; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters