Files
f0ckm/src/inc/routes/halls.mjs

113 lines
4.0 KiB
JavaScript

import db from "../sql.mjs";
import lib from "../lib.mjs";
import cfg from "../config.mjs";
import f0cklib from "../routeinc/f0cklib.mjs";
import url from "url";
import path from "path";
import fs from "fs/promises";
export default (router, tpl) => {
// Main Halls Overview
router.get(/^\/halls$/, async (req, res) => {
if (cfg.websrv.halls_enabled === false) return res.reply({ code: 404, body: tpl.render('error', { message: 'Not found', tmp: null }, req) });
const mode = req.mode ?? 0;
const excludedTags = req.session ? (req.session.excluded_tags || []) : [];
const hallsList = await f0cklib.getHallsOverview(mode, excludedTags);
const data = {
hallsList: hallsList,
tmp: null,
hidePagination: true,
session: (req.session && req.session.user) ? { ...req.session } : false,
page_meta: {
title: 'Halls',
description: `Browse curated item collections`,
url: `https://${cfg.main.url.domain}/halls`
}
};
if (req.headers['x-requested-with'] === 'XMLHttpRequest') {
return res.reply({
body: tpl.render('halls-partial', data, req)
});
}
res.reply({
body: tpl.render('halls', data, req)
});
});
// Hall Thumbnail Route
router.get(/^\/hall_image\/(?<hallSlug>.+)$/, async (req, res) => {
const hallSlug = path.basename(decodeURIComponent(req.params.hallSlug));
const mode = +(req.url.qs?.m ?? 0);
const CACHE_DIR = path.join(cfg.paths.s, '../hall_cache');
try {
const CUSTOM_DIR = path.join(cfg.paths.s, '../hall_custom');
const customPath = path.join(CUSTOM_DIR, `${hallSlug}.webp`);
// Serve custom image if it exists (skip mosaic entirely)
try {
const stat = await fs.stat(customPath);
const etag = '"' + stat.mtimeMs.toString(16) + '-' + stat.size.toString(16) + '"';
if (req.headers['if-none-match'] === etag) {
res.writeHead(304);
return res.end();
}
res.writeHead(200, { 'Content-Type': 'image/webp', 'Cache-Control': 'no-cache', 'ETag': etag });
return res.end(await fs.readFile(customPath));
} catch (e) { /* no custom image, fall through to mosaic */ }
const hash = (await import('crypto')).createHash('md5').update(`${hallSlug}_${mode}`).digest('hex');
const cachePath = path.join(CACHE_DIR, `${hash}.webp`);
// Try cache first
try {
await fs.access(cachePath);
res.writeHead(200, { 'Content-Type': 'image/webp', 'Cache-Control': 'public, max-age=3600' });
return res.end(await fs.readFile(cachePath));
} catch (e) {}
// Generate Mosaic
let modeFilter = db``;
if (mode === 0) modeFilter = db`JOIN tags_assign ta_sfw ON ta_sfw.item_id = i.id AND ta_sfw.tag_id = 1`;
else if (mode === 1) modeFilter = db`JOIN tags_assign ta_nsfw ON ta_nsfw.item_id = i.id AND ta_nsfw.tag_id = 2`;
const items = await db`
SELECT i.id
FROM items i
JOIN halls_assign ha ON ha.item_id = i.id
JOIN halls h ON h.id = ha.hall_id
${modeFilter}
WHERE h.slug = ${hallSlug} AND i.active = true
ORDER BY RANDOM()
LIMIT 3
`;
if (items.length > 0) {
const inputs = items.map(item => path.join(cfg.paths.t, `${item.id}.webp`));
await fs.mkdir(CACHE_DIR, { recursive: true });
const { execFile } = await import('child_process');
const util = await import('util');
const execFilePromise = util.promisify(execFile);
await execFilePromise('magick', [...inputs, '+append', '-background', 'none', '-resize', '600x300^', '-gravity', 'center', '-extent', '600x300', cachePath]);
res.writeHead(200, { 'Content-Type': 'image/webp', 'Cache-Control': 'public, max-age=3600' });
return res.end(await fs.readFile(cachePath));
}
} catch (e) {
console.error('[HALL_IMAGE] Error:', e);
}
// Default placeholder
res.writeHead(302, { 'Location': '/s/img/favicon.gif' });
res.end();
});
return router;
};