This commit is contained in:
2026-09-16 17:40:52 +02:00
parent 93914ad02a
commit c8cfe07c70
9 changed files with 1142 additions and 253 deletions
+228
View File
@@ -6,6 +6,234 @@ import { createI18n } from "../i18n.mjs";
import { isAnonymizeSession } from "../settings.mjs";
export default (router, tpl) => {
// ── Merged random + item load: single request instead of two ────────────
router.get(/^\/ajax\/item\/random/, async (req, res) => {
const tAjaxStart = Date.now();
let query = {};
if (typeof req.url === 'string') {
const parsedUrl = url.parse(req.url, true);
query = parsedUrl.query;
} else {
query = req.url.qs || {};
}
const isGuest = !req.session || !req.session.user;
const reqMode = isGuest ? 0 : (query.mode !== undefined ? +query.mode : req.mode);
const ratingsRaw = req.cookies.ratings;
const ratingsArr = isGuest ? ['sfw'] : ((reqMode === 2 || reqMode === 3) ? null : (ratingsRaw ? decodeURIComponent(ratingsRaw).split(/[|,]/).filter(r => ['sfw','nsfw','nsfl','untagged'].includes(r)) : null));
const tag = query.tag || null;
const hall = query.hall || null;
const user = query.user || null;
const userHall = query.userHall || null;
const userHallOwner = query.userHallOwner || null;
const isFav = query.fav === 'true';
const isStrict = query.strict === '1';
const cookieMime = req.cookies?.mime !== undefined ? (decodeURIComponent(req.cookies.mime).trim() || null) : null;
const mime = (typeof query.mime !== 'undefined') ? (query.mime || null) : (cookieMime || null);
// Resolve random item ID
const randomData = await f0cklib.getRandom({
user, tag, hall, userHall, userHallOwner, mime,
fav: isFav,
mode: reqMode,
ratings: ratingsArr && ratingsArr.length > 0 ? ratingsArr : null,
strict: isStrict,
session: req.session,
exclude: req.session?.excluded_tags || [],
user_id: req.session?.id,
is_admin: req.session?.admin
});
const tRandom = Date.now();
if (!randomData || !randomData.itemid) {
return res.reply({
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ html: '', error: true, success: false, message: 'No items found' })
});
}
const itemid = String(randomData.itemid);
// Build context URL for the resolved item
let contextUrl = `/${itemid}`;
if (tag) contextUrl = `/tag/${encodeURIComponent(tag)}/${itemid}`;
if (hall) contextUrl = `/h/${encodeURIComponent(hall)}/${itemid}`;
if (userHall && userHallOwner) {
contextUrl = `/user/${encodeURIComponent(userHallOwner)}/hall/${encodeURIComponent(userHall)}/${itemid}`;
} else if (user) {
contextUrl = isFav
? `/user/${encodeURIComponent(user)}/favs/${itemid}`
: `/user/${encodeURIComponent(user)}/${itemid}`;
} else if (isFav) {
contextUrl = `/favs/${itemid}`;
}
if (mime) {
contextUrl = contextUrl.replace(new RegExp(`/${itemid}$`), `/${mime}/${itemid}`);
}
if (cfg.main.development) console.log(`[${new Date().toISOString()}] [AJAX-RANDOM] Resolved random item ${itemid} in ${Date.now() - tAjaxStart}ms`);
// Now run the full item load pipeline (same as /ajax/item/:id)
const bypassFilter = !!(query.bypass === '1');
const data = await f0cklib.getf0ck({
itemid: itemid,
mode: reqMode,
ratings: ratingsArr,
bypass_filter: bypassFilter,
session: req.session,
url: contextUrl,
user: user,
tag: tag,
hall: hall,
userHall: userHall,
userHallOwner: userHallOwner,
mime: mime,
fav: isFav,
ids: null,
random: true,
strict: isStrict || req.session?.strict_mode,
explicitStrict: isStrict,
exclude: req.session ? (req.session.excluded_tags || []) : [],
user_id: req.session?.id,
subf0ck: query.subf0ck || null
});
const tAjaxFetch = Date.now();
if (!data.success) {
const { t: tErr } = createI18n(req.session?.language || req.lang || 'en');
const modeLabels = { 0: 'SFW', 1: 'NSFW', 2: 'Untagged', 4: 'NSFL' };
const errorModeLabel = reqMode !== 3 ? (modeLabels[reqMode] || 'SFW') : null;
const errorHtml = tpl.render('error-partial', {
message: tErr('error.post_not_visible'),
tmp: null,
session: req.session ? { ...req.session } : false,
item_id: data.item?.id || itemid,
item_slug: data.item?.slug || null,
error_mode_label: errorModeLabel,
error_filter_hint: errorModeLabel ? tErr('error.filter_hint', { mode: `<strong>${errorModeLabel}</strong>` }) : null,
error_filter_hint_link: tErr('error.filter_hint_link'),
error_see_anyways: tErr('error.see_anyways')
}, req);
return res.reply({
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ html: errorHtml, pagination: '', error: true })
});
}
// xD Score + comments — parallelize subscription + comments fetch
if (req.session || !cfg.main.hide_comments_from_public) {
if (req.session?.id) {
f0cklib.markNotificationsRead(req.session.id, itemid).catch(() => {});
}
const [sub, commentsForScore] = await Promise.all([
req.session ? f0cklib.getSubscriptionStatus(req.session.id, itemid) : false,
f0cklib.getComments(itemid, 'old', false)
]);
data.isSubscribed = sub;
const xdScore = f0cklib.computeXdScore(commentsForScore);
const xdMeta = f0cklib.xdScoreMeta(xdScore);
data.item.xd_score = xdScore;
data.item.xd_tier = xdMeta.tier;
data.item.xd_label = xdMeta.label;
data.commentsJSON = null;
data.comments = [];
} else {
data.isSubscribed = false;
data.commentsJSON = null;
data.comments = [];
data.item.xd_score = 0;
data.item.xd_tier = 0;
data.item.xd_label = '';
}
const tAjaxAux = Date.now();
// Session + template vars
data.session = req.session ? { ...req.session } : false;
data.url = { pathname: contextUrl };
data.fullscreen = req.cookies.fullscreen || 0;
data.hidePagination = true;
// Precompute hall display data
if (data.item && data.item.halls && data.item.halls.length) {
const currentHallSlug = data.tmp && data.tmp.hall
? (typeof data.tmp.hall === 'object' ? data.tmp.hall.slug : data.tmp.hall)
: null;
data.item.primaryHall = data.item.halls.find(h => h.slug === currentHallSlug) || data.item.halls[0];
data.item.otherHalls = data.item.halls.filter(h => h.slug !== data.item.primaryHall.slug);
} else if (data.item) {
data.item.primaryHall = null;
data.item.otherHalls = [];
}
// Precomputed template booleans
if (data.item) {
const session = data.session;
const item = data.item;
if (isAnonymizeSession(req.session)) {
if (item.src) item.src = null;
item.username = 'anonymous';
item.author_banner_file = null;
item.author_banner_position = null;
item.author_banner_size = null;
item.author_avatar = null;
item.author_avatar_file = null;
item.author_color = null;
item.author_description = null;
item.author_display_name = null;
item.author_id = null;
if (data.uploader) {
data.uploader.name = 'anonymous';
data.uploader.id = null;
data.uploader.color = null;
}
if (Array.isArray(item.favorites)) {
item.favorites = item.favorites.map(f => {
const isSelf = session && session.id && f.user_id && Number(f.user_id) === Number(session.id);
if (isSelf) return f;
return { user_id: null, user: 'anonymous', login: 'anonymous', display_name: 'Anonymous', avatar: null, avatar_file: null, username_color: null, hide_fav_badge: f.hide_fav_badge, is_anon: true };
});
}
}
const isAnon = !!(session && (session.is_anon || (session.user && (session.user === 'anonymous' || session.user.startsWith('anon_')))));
data.is_mod_or_admin = !!(session && (session.admin || session.is_moderator));
data.can_manage_item = !isAnon && !!(session && (session.admin || session.is_moderator || (session.user && item.username && session.user.toLowerCase() === item.username.toLowerCase())));
data.can_extract_meta = !!(item.mime && item.mime.indexOf('flash') === -1 && !(item.mime.startsWith('application/') && cfg.mimes[item.mime] && !['swf', 'pdf'].includes(cfg.mimes[item.mime])));
data.user_has_favorited = lib.userHasFavorited(session, item.favorites);
data.halls_slugs = Array.isArray(item.halls) ? item.halls.map(h => h.slug).join(',') : '';
data.user_halls_slugs = Array.isArray(item.user_halls) ? item.user_halls.map(h => h.slug).join(',') : '';
data.item_rating_class = item.is_nsfl ? 'is-nsfl' : (item.is_nsfw ? 'is-nsfw' : (item.is_sfw ? 'is-sfw' : 'is-untagged'));
data.item_rating_label = item.is_nsfl ? 'NSFL' : (item.is_nsfw ? 'NSFW' : (item.is_sfw ? 'SFW' : '?'));
data.item_username_lower = (item.username || '').toLowerCase();
data.is_flash_item = !!(item.mime && (item.mime.indexOf('flash') !== -1 || item.mime.indexOf('shockwave') !== -1));
data.is_archive_item = !!(item.mime && item.mime.startsWith('application/') && cfg.mimes[item.mime] && !['swf', 'pdf'].includes(cfg.mimes[item.mime]));
data.current_hall_slug = (data.tmp && data.tmp.hall && typeof data.tmp.hall === 'object') ? data.tmp.hall.slug : (data.tmp && data.tmp.hall ? data.tmp.hall : '');
data.current_user_hall_slug = (data.tmp && data.tmp.userHall && typeof data.tmp.userHall === 'object') ? data.tmp.userHall.slug : (data.tmp && data.tmp.userHall ? data.tmp.userHall : '');
data.current_user_hall_owner = (data.tmp && data.tmp.userHallOwner) ? data.tmp.userHallOwner : '';
data.item_has_dimensions = !!(item.width && item.height);
}
// Render
const itemHtml = tpl.render('ajax-item', data, req);
const paginationHtml = tpl.render('snippets/pagination', data, req);
const tAjaxRender = Date.now();
// Detailed timing breakdown
console.log(`[AJAX-RANDOM] ${itemid} total=${tAjaxRender - tAjaxStart}ms | getRandom=${tRandom - tAjaxStart}ms | getf0ck=${tAjaxFetch - tRandom}ms | aux=${tAjaxAux - tAjaxFetch}ms | render=${tAjaxRender - tAjaxAux}ms`);
res.reply({
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
html: itemHtml,
pagination: paginationHtml,
title: data.title,
id: itemid,
slug: data.item?.slug || null,
page: null,
is_random: true
})
});
});
router.get(/^\/ajax\/item\/(?<itemid>[a-zA-Z0-9_-]{11}|\d+)/, async (req, res) => {
const tAjaxStart = Date.now();
let query = {};