feat: worst-scan fansub web — initial release

Posts engine (SQLite + auto-publisher via poller), public feed with
clickable tags, reader, admin panel, submit/search/queue tools.
BFF proxy to pipeline API. Clean dark design. Docker-ready.
This commit is contained in:
renato97
2026-07-23 15:58:28 -03:00
commit 25449aa5df
71 changed files with 11296 additions and 0 deletions
+64
View File
@@ -0,0 +1,64 @@
import fs from "fs"
import path from "path"
const COVERS_DIR = process.env.COVERS_DIR || path.join(process.cwd(), "data", "covers")
function ensureDir() {
fs.mkdirSync(COVERS_DIR, { recursive: true })
}
function extFromContentType(ct: string | null): string {
if (!ct) return ".jpg"
const m = ct.match(/image\/(\w+)/)
if (!m) return ".jpg"
const exts: Record<string, string> = {
jpeg: ".jpg",
png: ".png",
webp: ".webp",
gif: ".gif",
}
return exts[m[1]] || ".jpg"
}
export async function cacheCover(gid: string, proxyUrl: string): Promise<string | null> {
ensureDir()
try {
const res = await fetch(proxyUrl, { signal: AbortSignal.timeout(15000) })
if (!res.ok) return null
const buffer = Buffer.from(await res.arrayBuffer())
const ext = extFromContentType(res.headers.get("content-type"))
const filePath = path.join(COVERS_DIR, `${gid}${ext}`)
fs.writeFileSync(filePath, buffer)
return filePath
} catch {
return null
}
}
export function getCoverPath(gid: string): string | null {
ensureDir()
const files = fs.readdirSync(COVERS_DIR).filter((f) => f.startsWith(gid))
if (files.length === 0) return null
return path.join(COVERS_DIR, files[0])
}
export function getCoverContentType(gid: string): string {
const fp = getCoverPath(gid)
if (!fp) return "image/jpeg"
const ext = path.extname(fp).toLowerCase()
const m: Record<string, string> = {
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".webp": "image/webp",
".gif": "image/gif",
}
return m[ext] || "image/jpeg"
}
export function deleteCover(gid: string): void {
const fp = getCoverPath(gid)
if (fp) fs.unlinkSync(fp)
}