feat(web): sync translated manga galleries, clean titles, synopsis integration, original source links, anthology badges, and mobile-first reader UI
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
import { NextRequest } from "next/server"
|
||||
import crypto from "crypto"
|
||||
import { createPost, getPostByGid, updatePost } from "@/lib/db"
|
||||
import { cleanTitle, slugify } from "@/lib/slug"
|
||||
import { cacheCover } from "@/lib/cover-cache"
|
||||
|
||||
function verifySignature(bodyText: string, signature: string, secret: string): boolean {
|
||||
try {
|
||||
const hmac = crypto.createHmac("sha256", secret)
|
||||
hmac.update(bodyText)
|
||||
const expected = hmac.digest("hex")
|
||||
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const rawBody = await req.text()
|
||||
const secret = process.env.WEBHOOK_SECRET || process.env.FANSUB_WEBHOOK_SECRET || ""
|
||||
|
||||
if (secret) {
|
||||
const signature = req.headers.get("x-signature-256") || req.headers.get("X-Signature-256") || ""
|
||||
if (!signature || !verifySignature(rawBody, signature, secret)) {
|
||||
return Response.json({ error: "Unauthorized: invalid signature" }, { status: 401 })
|
||||
}
|
||||
}
|
||||
|
||||
const payload = JSON.parse(rawBody)
|
||||
|
||||
const gid = payload.gid
|
||||
const rawTitle = payload.title
|
||||
if (!gid || !rawTitle) {
|
||||
return Response.json({ error: "Invalid payload: gid and title required" }, { status: 400 })
|
||||
}
|
||||
|
||||
const title = cleanTitle(rawTitle)
|
||||
const tags = Array.isArray(payload.tags) ? payload.tags : []
|
||||
const numPages = payload.num_pages || payload.pages || 0
|
||||
const synopsis = payload.synopsis || ""
|
||||
const sourceUrl = payload.url || (payload.source === "nhentai" ? `https://nhentai.net/g/${gid}/` : (payload.source === "ehentai" ? `https://e-hentai.org/g/${gid}/` : undefined))
|
||||
|
||||
const slug = slugify(title, gid)
|
||||
|
||||
let post = getPostByGid(gid)
|
||||
if (!post) {
|
||||
post = createPost({
|
||||
gid,
|
||||
title,
|
||||
title_jpn: payload.title_jpn || undefined,
|
||||
artist: payload.artist || undefined,
|
||||
parody: payload.parody || undefined,
|
||||
tags,
|
||||
num_pages: numPages,
|
||||
source: payload.source || undefined,
|
||||
cover_url: payload.cover_url || undefined,
|
||||
url: sourceUrl,
|
||||
summary: synopsis || undefined,
|
||||
slug,
|
||||
published: 1,
|
||||
})
|
||||
} else {
|
||||
post = updatePost(post.id, {
|
||||
title,
|
||||
title_jpn: payload.title_jpn || post.title_jpn || undefined,
|
||||
artist: payload.artist || post.artist || undefined,
|
||||
parody: payload.parody || post.parody || undefined,
|
||||
tags: tags.length > 0 ? tags : post.tags,
|
||||
num_pages: numPages || post.num_pages,
|
||||
source: payload.source || post.source || undefined,
|
||||
cover_url: payload.cover_url || post.cover_url || undefined,
|
||||
url: sourceUrl || post.url || undefined,
|
||||
summary: synopsis || post.summary || undefined,
|
||||
})!
|
||||
}
|
||||
|
||||
const port = process.env.PORT || "3000"
|
||||
const proxyUrl = `http://127.0.0.1:${port}/api/proxy/galleries/${gid}/cover`
|
||||
await cacheCover(gid, proxyUrl)
|
||||
|
||||
return Response.json({
|
||||
success: true,
|
||||
gid,
|
||||
post,
|
||||
})
|
||||
} catch (err) {
|
||||
return Response.json({ error: String(err) }, { status: 500 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user