This commit is contained in:
2026-09-13 04:54:24 +02:00
parent 340c825019
commit 33c6d0e76d
15 changed files with 425 additions and 104 deletions
+12
View File
@@ -3164,6 +3164,18 @@ body.sidebar-right-hidden #sidebar-drag-zone {
display: none !important;
}
.thumb > .sidebar-media-placeholder {
z-index: 1;
font-size: 2.8em;
border-radius: inherit;
}
.thumb:has(.sidebar-media-placeholder),
.thumb.thumb-fallback:has(.sidebar-media-placeholder) {
opacity: 1 !important;
background-image: none !important;
}
.sidebar-video-details {
display: flex;
margin-top: 8px;
+193 -55
View File
@@ -297,7 +297,7 @@ window.cancelAnimFrame = (function () {
* Forcefully refreshes all thumbnail occurrences for a specific item in the DOM.
* Handles grid items (data-bg), images (src), and the background canvas.
*/
window.refreshItemThumbnails = (itemId, timestamp = Date.now()) => {
window.refreshItemThumbnails = (itemId, timestamp = Date.now(), hasCoverart = undefined) => {
if (!itemId) return;
const idStr = String(itemId);
@@ -340,15 +340,28 @@ window.cancelAnimFrame = (function () {
// Update audio cover in player if viewing this audio item
const audioCover = document.getElementById('f0ck-audio-cover');
if (audioCover && currentId === idStr) {
const coverUrl = `/ca/${idStr}.webp?t=${timestamp}`;
audioCover.src = coverUrl;
const parent = audioCover.parentElement;
if (parent) {
parent.style.background = `url('${coverUrl}') no-repeat center / contain black`;
}
const audioEl = document.querySelector('audio#my-video');
if (audioEl) {
audioEl.setAttribute('poster', coverUrl);
if (hasCoverart === true || (hasCoverart === undefined && audioCover.src && audioCover.src.includes('/ca/'))) {
const coverUrl = `/ca/${idStr}.webp?t=${timestamp}`;
audioCover.src = coverUrl;
const parent = audioCover.parentElement;
if (parent) {
parent.style.background = `url('${coverUrl}') no-repeat center / contain black`;
}
const audioEl = document.querySelector('audio#my-video');
if (audioEl) {
audioEl.setAttribute('poster', coverUrl);
}
} else if (hasCoverart === false) {
const fallbackUrl = '/s/img/audio.webp';
audioCover.src = fallbackUrl;
const parent = audioCover.parentElement;
if (parent) {
parent.style.background = `url('${fallbackUrl}') no-repeat center / contain black`;
}
const audioEl = document.querySelector('audio#my-video');
if (audioEl) {
audioEl.setAttribute('poster', fallbackUrl);
}
}
}
@@ -434,7 +447,19 @@ window.cancelAnimFrame = (function () {
applyThumb(thumb, finalBg);
} else {
img.onload = () => applyThumb(thumb, finalBg);
img.onerror = () => thumb.classList.remove('lazy-thumb');
img.onerror = () => {
const mime = thumb.dataset.mime || '';
if (mime.startsWith('audio/')) {
if (!thumb.querySelector('.sidebar-media-placeholder.audio')) {
const ph = document.createElement('div');
ph.className = 'sidebar-media-placeholder audio';
ph.innerHTML = '<i class="fa-solid fa-music"></i>';
thumb.prepend(ph);
}
thumb.classList.add('thumb-fallback');
}
thumb.classList.remove('lazy-thumb');
};
}
thumb.dataset.lazyObserved = 'true';
}
@@ -463,7 +488,12 @@ window.cancelAnimFrame = (function () {
} else {
const mime = thumb.dataset.mime || '';
if (mime.startsWith('audio/')) {
thumb.style.setProperty('--thumb-bg', `url('/s/img/audio.webp')`);
if (!thumb.querySelector('.sidebar-media-placeholder.audio')) {
const ph = document.createElement('div');
ph.className = 'sidebar-media-placeholder audio';
ph.innerHTML = '<i class="fa-solid fa-music"></i>';
thumb.prepend(ph);
}
thumb.classList.add('thumb-fallback');
}
thumb.classList.remove('lazy-thumb');
@@ -1346,8 +1376,8 @@ window.cancelAnimFrame = (function () {
};
window.openOnaraModal = openOnaraModal;
const updateOnaraActiveItem = (itemid, url) => {
if (!isOnaraActive()) return;
const updateOnaraActiveItem = (itemid, url, forceScroll = false, slug = null) => {
if (!isOnaraActive()) return null;
// Clear onara-active from any and all elements to guarantee only 1 item is selected
document.querySelectorAll('.onara-active').forEach(el => el.classList.remove('onara-active'));
@@ -1358,6 +1388,9 @@ window.cancelAnimFrame = (function () {
targetThumb = document.querySelector(`.posts > a.thumb[href="${parsedPath}"], .posts > a.thumb[href="${url}"]`);
} catch {}
}
if (!targetThumb && slug) {
targetThumb = document.querySelector(`.posts > a.thumb[href$="/${slug}"]`);
}
if (!targetThumb && itemid) {
targetThumb = document.querySelector(`.posts > a.thumb[href$="/${itemid}"], .posts > a.thumb[data-bg*="/${itemid}."]`);
}
@@ -1374,13 +1407,140 @@ window.cancelAnimFrame = (function () {
const vh = window.innerHeight || document.documentElement.clientHeight;
// If any part of the thumbnail is already visible in the viewport, do not scroll
const isVisible = rect.bottom > navbarH && rect.top < vh;
if (!isVisible) {
targetThumb.scrollIntoView({ block: 'nearest', inline: 'nearest', behavior: 'instant' });
if (!isVisible || forceScroll) {
targetThumb.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' });
}
}
return targetThumb;
};
window.updateOnaraActiveItem = updateOnaraActiveItem;
let _onaraSyncSeq = 0;
const syncOnaraBackgroundGrid = async (itemid, url, knownPage = null, slug = null) => {
if (!isOnaraActive()) return;
// Fast path: if target item is already in current background DOM, just highlight and ensure visibility
const existingThumb = updateOnaraActiveItem(itemid, url, false, slug);
if (existingThumb) {
return;
}
const currentSeq = ++_onaraSyncSeq;
const urlObj = new URL(url || window.location.href, window.location.origin);
const feedBasePath = urlObj.pathname.replace(/\/+$/, '').replace(/\/(?:\d+|[a-zA-Z0-9_-]{11})$/, '') || '/';
let targetPage = (typeof knownPage === 'number' && knownPage >= 1) ? knownPage : null;
if (!targetPage) {
try {
const pageParams = new URLSearchParams();
pageParams.set('id', itemid);
if (feedBasePath.startsWith('/tag/')) {
pageParams.set('tag', decodeURIComponent(feedBasePath.replace(/^\/tag\//, '')));
} else if (feedBasePath.startsWith('/h/')) {
pageParams.set('hall', decodeURIComponent(feedBasePath.replace(/^\/h\//, '')));
} else if (feedBasePath.match(/^\/user\/([^/]+)\/hall\/([^/]+)/)) {
const m = feedBasePath.match(/^\/user\/([^/]+)\/hall\/([^/]+)/);
pageParams.set('userHallOwner', decodeURIComponent(m[1]));
pageParams.set('userHall', decodeURIComponent(m[2]));
} else if (feedBasePath.match(/^\/user\/([^/]+)\/favs/)) {
const m = feedBasePath.match(/^\/user\/([^/]+)\/favs/);
pageParams.set('user', decodeURIComponent(m[1]));
pageParams.set('fav', 'true');
} else if (feedBasePath.match(/^\/user\/([^/]+)/)) {
const m = feedBasePath.match(/^\/user\/([^/]+)/);
pageParams.set('user', decodeURIComponent(m[1]));
} else if (feedBasePath === '/favs') {
pageParams.set('fav', 'true');
}
if (typeof window.activeMode !== 'undefined') {
pageParams.set('mode', window.activeMode);
}
const isStrict = window.f0ckSession?.strict_mode || (localStorage.getItem('search_strict') === 'true') || urlObj.searchParams.get('strict') === '1';
if (isStrict) {
pageParams.set('strict', '1');
}
const resp = await fetch(`/api/v2/item-page?${pageParams.toString()}`);
if (resp.ok) {
const pageData = await resp.json();
if (pageData && pageData.success && pageData.page) {
targetPage = pageData.page;
}
}
} catch (e) {
console.warn('[ONARA] Failed to fetch item-page:', e);
}
}
if (currentSeq !== _onaraSyncSeq) return;
if (!targetPage || targetPage < 1) targetPage = 1;
const pagePath = targetPage === 1 ? feedBasePath : (feedBasePath === '/' ? `/p/${targetPage}` : `${feedBasePath}/p/${targetPage}`);
const searchParams = new URLSearchParams(urlObj.search);
const searchStr = searchParams.toString() ? `?${searchParams.toString()}` : '';
const pageUrl = pagePath + searchStr;
try {
const pageRes = await fetch(pageUrl, {
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
if (currentSeq !== _onaraSyncSeq) return;
if (!pageRes.ok) return;
const html = await pageRes.text();
if (currentSeq !== _onaraSyncSeq) return;
const doc = new DOMParser().parseFromString(html, 'text/html');
const newPosts = doc.querySelector('.posts');
const oldPosts = document.querySelector('.posts');
if (newPosts && oldPosts) {
oldPosts.innerHTML = newPosts.innerHTML;
oldPosts.dataset.currentPage = targetPage;
if (newPosts.dataset.hasMore) {
oldPosts.dataset.hasMore = newPosts.dataset.hasMore;
}
if (typeof window.initLazyLoading === 'function') {
window.initLazyLoading();
}
}
const newPag = doc.querySelector('.pagination-wrapper');
const oldPag = document.querySelector('.pagination-wrapper');
if (newPag && oldPag) {
oldPag.innerHTML = newPag.innerHTML;
}
const newTitle = doc.querySelector('.page-title-wrap, .page-title, .user-profile-header, .hall-header');
const oldTitle = document.querySelector('.page-title-wrap, .page-title, .user-profile-header, .hall-header');
if (newTitle && oldTitle) {
oldTitle.replaceWith(newTitle);
}
window._onaraCurrentGridUrl = pageUrl;
window._onaraReturnUrl = pageUrl;
if (doc.title) {
window._onaraReturnTitle = doc.title;
}
const thumb = updateOnaraActiveItem(itemid, url, true, slug);
if (!thumb && oldPosts) {
const synthThumb = document.createElement('a');
synthThumb.href = url;
synthThumb.className = 'thumb lazy-thumb onara-active';
synthThumb.dataset.bg = `/t/${itemid}.webp`;
synthThumb.dataset.size = '1';
synthThumb.innerHTML = '<div class="thumb-indicators"></div><p></p>';
oldPosts.prepend(synthThumb);
if (typeof window.initLazyLoading === 'function') window.initLazyLoading();
synthThumb.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' });
}
} catch (err) {
console.error('[ONARA] Failed to sync background grid:', err);
}
};
window.syncOnaraBackgroundGrid = syncOnaraBackgroundGrid;
const isItemPath = (pathOrUrl) => {
try {
const p = new URL(pathOrUrl, window.location.origin).pathname;
@@ -4436,6 +4596,10 @@ window.cancelAnimFrame = (function () {
});
}
if (useOnara) {
params.append('onara', '1');
}
if (params.toString() !== '') {
ajaxUrl += (ajaxUrl.includes('?') ? '&' : '?') + params.toString();
}
@@ -4466,7 +4630,8 @@ window.cancelAnimFrame = (function () {
onaraMount.innerHTML = '';
}
_container = onaraMount;
updateOnaraActiveItem(itemid, url);
updateOnaraActiveItem(itemid, url, false, _cachedItem.slug);
syncOnaraBackgroundGrid(itemid, url, _cachedItem.page, _cachedItem.slug);
} else {
_container = document.querySelector('#main .container') || (document.getElementById('main')?.classList.contains('item-view') ? document.getElementById('main') : null);
const _isStructuralPage = !!document.querySelector('.pagewrapper');
@@ -4597,12 +4762,14 @@ window.cancelAnimFrame = (function () {
if (!freshText) return;
let freshHtml = freshText;
let freshSlug = null;
let freshPage = null;
try {
const d = JSON.parse(freshText);
if (d && typeof d.html === 'string') freshHtml = d.html;
if (d && (d.slug || d.item?.slug)) freshSlug = d.slug || d.item?.slug;
if (d && d.page) freshPage = d.page;
} catch(_) {}
itemCacheMap.set(_itemCacheKey, { html: freshHtml, slug: freshSlug, ts: Date.now() });
itemCacheMap.set(_itemCacheKey, { html: freshHtml, slug: freshSlug, page: freshPage, ts: Date.now() });
window.f0ckDebug('[itemCache] Background revalidation complete for', _itemCacheKey);
})
.catch(() => {});
@@ -4629,7 +4796,7 @@ window.cancelAnimFrame = (function () {
- Total Network: ${(tBody - tStart).toFixed(2)}ms
- Content Size: ${(rawText.length / 1024).toFixed(2)} KB`);
let html, paginationHtml, responseSlug = null;
let html, paginationHtml, responseSlug = null, responsePage = null;
try {
// Optimistically try to parse as JSON first
@@ -4643,6 +4810,7 @@ window.cancelAnimFrame = (function () {
html = data.html;
paginationHtml = data.pagination;
responseSlug = data.slug || data.item?.slug || null;
responsePage = data.page || null;
} else {
html = rawText;
}
@@ -4653,7 +4821,7 @@ window.cancelAnimFrame = (function () {
// ── Store in item cache (stale-while-revalidate) ───────────────────────
if (html && !options.noCacheStore) {
itemCacheMap.set(_itemCacheKey, { html, slug: responseSlug, ts: Date.now() });
itemCacheMap.set(_itemCacheKey, { html, slug: responseSlug, page: responsePage, ts: Date.now() });
if (itemCacheMap.size > ITEM_CACHE_MAX) {
// Evict oldest entry
itemCacheMap.delete(itemCacheMap.keys().next().value);
@@ -4673,7 +4841,8 @@ window.cancelAnimFrame = (function () {
onaraMount.innerHTML = '';
}
container = onaraMount;
updateOnaraActiveItem(itemid, url);
updateOnaraActiveItem(itemid, url, false, responseSlug);
syncOnaraBackgroundGrid(itemid, url, responsePage, responseSlug);
} else {
container = document.querySelector('#main .container') || (document.getElementById('main') && document.getElementById('main').classList.contains('item-view') ? document.getElementById('main') : null);
const isStructuralPage = !!document.querySelector('.pagewrapper');
@@ -5121,38 +5290,7 @@ window.cancelAnimFrame = (function () {
// Background grid sync — page number already in response, no extra fetch needed
if (isOnaraActive() && targetId) {
const postsEl = document.querySelector('.posts[data-current-page]');
const currentPage = postsEl ? parseInt(postsEl.dataset.currentPage, 10) : 1;
if (targetPage !== currentPage) {
// Build page URL from current grid base
let gridBase = window._onaraCurrentGridUrl
? window._onaraCurrentGridUrl.replace(/\/p\/\d+/, '').replace(/\/$/, '')
: window.location.pathname
.replace(/\/[a-zA-Z0-9_-]{11}$/, '')
.replace(/\/\d+$/, '')
.replace(/\/p\/\d+/, '')
.replace(/\/$/, '');
const pageUrl = targetPage === 1 ? (gridBase || '/') : `${gridBase}/p/${targetPage}`;
fetch(pageUrl)
.then(r => r.text())
.then(html => {
const doc = new DOMParser().parseFromString(html, 'text/html');
const newPosts = doc.querySelector('.posts');
const oldPosts = document.querySelector('.posts');
if (newPosts && oldPosts) {
oldPosts.innerHTML = newPosts.innerHTML;
oldPosts.dataset.currentPage = targetPage;
if (typeof window.initLazyLoading === 'function') window.initLazyLoading();
}
window._onaraCurrentGridUrl = pageUrl;
if (typeof updateOnaraActiveItem === 'function') {
updateOnaraActiveItem(targetId, `/${targetKey}`);
}
})
.catch(() => {});
}
syncOnaraBackgroundGrid(targetId, `/${targetKey}`, targetPage, targetKey);
}
} else if (params.has('tag') || params.has('hall') || params.has('user') || params.has('userHall')) {
// Context had no matching items with the active MIME filter — try global random with the same filter
@@ -9110,7 +9248,7 @@ class NotificationSystem {
} else if (data.type === 'rethumb') {
window.f0ckDebug(`[SSE] Rethumb update received for item ${data.data?.item_id}`);
if (data.data && data.data.item_id && window.refreshItemThumbnails) {
window.refreshItemThumbnails(data.data.item_id);
window.refreshItemThumbnails(data.data.item_id, Date.now(), data.data.has_coverart);
}
} else if (data.type === 'new_item') {
window.f0ckDebug(`[SSE] New item received:`, data.data);
@@ -13838,7 +13976,7 @@ document.addEventListener('click', (e) => {
// Cache-bust thumbnails and player media on page
const timestamp = Date.now();
if (typeof window.refreshItemThumbnails === 'function') {
window.refreshItemThumbnails(itemId, timestamp);
window.refreshItemThumbnails(itemId, timestamp, data.has_coverart);
} else {
document.querySelectorAll(`img[src*="/t/${itemId}.webp"]`).forEach(img => {
const url = new URL(img.src, window.location.origin);
+4
View File
@@ -0,0 +1,4 @@
{
"lastId": 48551,
"timestamp": "2026-09-13T02:54:17.890Z"
}
+44 -7
View File
@@ -468,7 +468,13 @@ export default new class queue {
this._lastCoverExtracted = false; // Reset state for this call
if (link && typeof link === 'string' && link.match(/soundcloud/)) {
const proxyArgs = (cfg.main.socks && cfg.main.socks !== 'undefined' && cfg.main.socks !== '') ? ['--proxy', cfg.main.socks.includes('://') ? cfg.main.socks : `socks5h://${cfg.main.socks}`] : [];
let cover = (await this.spawn('yt-dlp', [...proxyArgs, '-f', 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w', '--get-thumbnail', link])).stdout.trim().split('\n').map(l => l.trim()).filter(l => l.length > 0).pop();
let cover = null;
try {
const ytRes = await this.spawn('yt-dlp', [...proxyArgs, '--get-thumbnail', link], { quiet: true });
cover = ytRes.stdout.trim().split('\n').map(l => l.trim()).filter(l => l.length > 0).pop();
} catch (err) {
console.warn(`[QUEUE] yt-dlp thumbnail fetch failed for SoundCloud track (${link}):`, err.message || err);
}
if (cover && !cover.match(/default_avatar/)) {
cover = cover.replace(/-(large|original)\./, '-t500x500.');
try {
@@ -517,13 +523,9 @@ export default new class queue {
}
} catch (_) { }
}
// If no cover art found, use audio.webp as the thumbnail
// If no cover art found, generate audio placeholder matching .sidebar-media-placeholder.audio
if (!coverExtracted) {
const audioFallback = path.join(cfg.paths.s, 'img', 'audio.webp');
await fs.promises.copyFile(audioFallback, tmpFile).catch(async () => {
// If copy fails, fall back to generated placeholder
await this.spawn('magick', ['-size', thumbSpec, 'xc:#1a1a1a', '-gravity', 'center', '-fill', '#666', '-pointsize', '40', '-annotate', '0', '♪', tmpFile]).catch(() => {});
});
await this.genAudioPlaceholder(tmpFile, thumbSpec, thumbSize);
}
// Store extraction result for caller
this._lastCoverExtracted = coverExtracted;
@@ -647,6 +649,13 @@ export default new class queue {
// Cleanup temp files
await fs.promises.unlink(tmpFile).catch(() => {});
await fs.promises.unlink(tmpJpg).catch(() => {});
if (mime && mime.startsWith('audio/')) {
try {
await this.genAudioPlaceholder(outPath, thumbSpec, thumbSize);
console.warn(`[QUEUE] Used audio placeholder thumbnail for item ${itemid}`);
return false;
} catch (_) {}
}
// Fallback: copy 404.gif as the thumbnail
const fallback404 = path.join(cfg.paths.s, 'img', '404.gif');
try {
@@ -659,6 +668,34 @@ export default new class queue {
}
};
async genAudioPlaceholder(targetFile, thumbSpec = '512x512', thumbSize = 512) {
const faFont = path.join(cfg.paths.s, 'fa', 'webfonts', 'fa-solid-900.ttf');
const ptSize = String(Math.round(thumbSize * 0.35));
try {
await this.spawn('magick', [
'-size', thumbSpec, 'radial-gradient:#334a15-#1e1e1e',
'(', '+clone', '-font', faFont, '-pointsize', ptSize, '-gravity', 'center', '-fill', 'rgba(153,255,0,0.5)', '-annotate', '0', '\uf001', '-blur', '0x12', ')',
'-composite',
'-font', faFont, '-pointsize', ptSize, '-gravity', 'center', '-fill', '#99ff00', '-annotate', '0', '\uf001',
targetFile
]);
return true;
} catch (_) {
const audioFallback = path.join(cfg.paths.s, 'img', 'audio.webp');
try {
await fs.promises.copyFile(audioFallback, targetFile);
return true;
} catch (copyErr) {
await this.spawn('magick', [
'-size', thumbSpec, 'radial-gradient:#334a15-#1e1e1e',
'-gravity', 'center', '-fill', '#99ff00', '-pointsize', '60', '-annotate', '0', '♪',
targetFile
]).catch(() => {});
return false;
}
}
};
async genBlurredThumbnail(itemid, pending = false) {
let tDir = pending ? path.join(cfg.paths.pending, 't') : cfg.paths.t;
+2
View File
@@ -769,6 +769,7 @@ const f0cklib = {
items.is_pinned,
items.is_oc,
items.xd_score,
items.has_coverart,
${user_id ? db`max(coalesce(uvv.view_count, 0)) as my_views,` : db``}
${user_id ? db`EXISTS (SELECT 1 FROM notifications WHERE user_id = ${user_id} AND item_id = items.id AND is_read = false) as has_notification,` : db`false as has_notification,`}
(case when min(ta.tag_id) = 1 then 'SFW' when min(ta.tag_id) = 2 then 'NSFW' else 'NSFL' end) as tag,
@@ -794,6 +795,7 @@ const f0cklib = {
const meta = xdScoreMeta(row.xd_score);
row.xd_tier = meta.tier;
row.xd_label = meta.label;
row.is_audio = !!(row.mime && row.mime.startsWith('audio/'));
}
// Dynamic thumb sizing: applies to the main feed including mime/rating filters.
+28 -1
View File
@@ -189,6 +189,32 @@ export default (router, tpl) => {
- Comments/Sub: ${tAjaxAux - tAjaxFetch}ms
- Render: ${tAjaxRender - tAjaxAux}ms`);
let itemPage = null;
if (query.onara === '1' || req.cookies?.onara === '1' || cfg.onara || query.get_page === '1') {
try {
itemPage = await f0cklib.getItemPage({
targetItemId: data.item?.id || itemid,
targetItemPinned: data.item?.is_pinned,
user: query.user,
tag: query.tag,
hall: query.hall,
userHall: query.userHall,
userHallOwner: query.userHallOwner,
mime: query.mime || (req.cookies.mime || null),
fav: query.fav === 'true',
mode: reqMode,
ratings: ratingsArr,
strict: query.strict === '1' || query.strict === 'true' || req.session?.strict_mode,
session: req.session,
exclude: req.session ? (req.session.excluded_tags || []) : [],
user_id: req.session?.id,
is_admin: req.session?.admin,
minXdScore: req.session?.min_xd_score || 0,
tagger: query.tagger || null
});
} catch (_) {}
}
res.reply({
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -196,7 +222,8 @@ export default (router, tpl) => {
pagination: paginationHtml,
title: data.title,
id: itemid,
slug: data.item?.slug || null
slug: data.item?.slug || null,
page: itemPage
})
});
});
+15 -13
View File
@@ -575,25 +575,25 @@ export default router => {
// Lightweight page-lookup for Onara grid sync.
// Called async after random navigation — user never waits for this.
group.get(/\/item-page$/, async (req, res) => {
const id = parseInt(req.url.qs.id, 10) || 0;
if (!id) return res.json({ success: false, page: 1 });
const rawId = req.url.qs?.id || req.url.qs?.slug || req.url.qs?.item;
if (!rawId) return res.json({ success: false, page: 1 });
const tag = req.url.qs.tag || null;
const hall = req.url.qs.hall || null;
const userHall = req.url.qs.userHall || null;
const userHallOwner= req.url.qs.userHallOwner|| null;
const user = req.url.qs.user || null;
const cookieMime = req.cookies?.mime !== undefined ? (decodeURIComponent(req.cookies.mime).trim() || null) : null;
const mime = (typeof req.url.qs.mime !== 'undefined') ? (req.url.qs.mime || null) : (cookieMime || null);
const isFav = req.url.qs.fav === 'true';
const isStrict = req.url.qs.strict === '1';
const tag = req.url.qs?.tag || null;
const hall = req.url.qs?.hall || null;
const userHall = req.url.qs?.userHall || null;
const userHallOwner= req.url.qs?.userHallOwner|| null;
const user = req.url.qs?.user || null;
const cookieMime = req.cookies?.mime !== undefined ? (decodeURIComponent(req.cookies.mime).trim() || null) : null;
const mime = (typeof req.url.qs?.mime !== 'undefined') ? (req.url.qs.mime || null) : (cookieMime || null);
const isFav = req.url.qs?.fav === 'true';
const isStrict = req.url.qs?.strict === '1';
const mode = req.mode ?? 0;
const ratingsRaw = req.cookies.ratings;
const ratingsArr = ratingsRaw ? decodeURIComponent(ratingsRaw).split(/[|,]/).filter(r => ['sfw','nsfw','nsfl','untagged'].includes(r)) : null;
try {
const page = await f0cklib.getItemPage({
targetItemId: id,
targetItemId: rawId,
user, tag, hall, userHall, userHallOwner, mime,
fav: isFav,
mode,
@@ -602,7 +602,9 @@ export default router => {
session: req.session,
exclude: req.session?.excluded_tags || [],
user_id: req.session?.id,
is_admin: req.session?.admin
is_admin: req.session?.admin,
minXdScore: req.url.qs?.min_xd !== undefined ? +req.url.qs.min_xd : (req.session?.min_xd_score || 0),
tagger: req.url.qs?.tagger || null
});
return res.json({ success: true, page });
} catch (e) {
+9 -1
View File
@@ -824,10 +824,18 @@ export default router => {
try {
await queue.genThumbnail(filename, mime, itemid, url, isApprovalRequired);
if (mime.startsWith('audio/') && queue._lastCoverExtracted) {
await db`UPDATE items SET has_coverart = TRUE WHERE id = ${itemid}`;
}
await queue.genBlurredThumbnail(itemid, isApprovalRequired);
} catch (err) {
const tDir = isApprovalRequired ? path.join(cfg.paths.pending, 't') : cfg.paths.t;
await queue.spawn('magick', ['-size', '128x128', 'xc:#1a1a1a', path.join(tDir, `${itemid}.webp`)]).catch(() => {});
const outPath = path.join(tDir, `${itemid}.webp`);
if (mime.startsWith('audio/')) {
await queue.genAudioPlaceholder(outPath).catch(() => {});
} else {
await queue.spawn('magick', ['-size', '128x128', 'xc:#1a1a1a', outPath]).catch(() => {});
}
}
// Assign rating tag (only if a rating was selected)
+20 -8
View File
@@ -87,17 +87,29 @@ export default (router, tpl) => {
`;
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 inputs = [];
for (const item of items) {
const filePath = path.join(cfg.paths.t, `${item.id}.webp`);
try {
await fs.access(filePath);
inputs.push(`${filePath}[0]`);
} catch {
// Ignore missing thumbnail
}
}
const { execFile } = await import('child_process');
const util = await import('util');
const execFilePromise = util.promisify(execFile);
if (inputs.length > 0) {
await fs.mkdir(CACHE_DIR, { recursive: true });
await execFilePromise('magick', [...inputs, '+append', '-background', 'none', '-resize', '600x300^', '-gravity', 'center', '-extent', '600x300', cachePath]);
const { execFile } = await import('child_process');
const util = await import('util');
const execFilePromise = util.promisify(execFile);
res.writeHead(200, { 'Content-Type': 'image/webp', 'Cache-Control': 'public, max-age=3600' });
return res.end(await fs.readFile(cachePath));
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);
+42 -6
View File
@@ -38,14 +38,50 @@ export default (router, tpl) => {
route: /^\/s\//
});
router.static({
dir: cfg.paths.t,
route: /^\/t\//
const getImgMime = (filename) => {
if (filename.endsWith('.webp')) return 'image/webp';
if (filename.endsWith('.png')) return 'image/png';
if (filename.endsWith('.gif')) return 'image/gif';
if (filename.endsWith('.jpg') || filename.endsWith('.jpeg')) return 'image/jpeg';
return 'application/octet-stream';
};
router.get(/^\/t\/(?<file>.+)$/, async (req, res) => {
const file = req.params.file;
const filePath = path.join(cfg.paths.t, file);
try {
const stat = await fs.stat(filePath);
if (stat.isFile()) {
const content = await fs.readFile(filePath);
res.writeHead(200, {
'Content-Length': stat.size,
'Content-Type': getImgMime(file),
'Cache-Control': 'public, max-age=3600'
});
return res.end(content);
}
} catch (_) {}
res.writeHead(404, { 'Content-Type': 'text/plain' });
return res.end('404 - file not found.');
});
router.static({
dir: cfg.paths.ca,
route: /^\/ca\//
router.get(/^\/ca\/(?<file>.+)$/, async (req, res) => {
const file = req.params.file;
const filePath = path.join(cfg.paths.ca, file);
try {
const stat = await fs.stat(filePath);
if (stat.isFile()) {
const content = await fs.readFile(filePath);
res.writeHead(200, {
'Content-Length': stat.size,
'Content-Type': getImgMime(file),
'Cache-Control': 'public, max-age=3600'
});
return res.end(content);
}
} catch (_) {}
res.writeHead(404, { 'Content-Type': 'text/plain' });
return res.end('404 - file not found.');
});
router.static({
+15 -4
View File
@@ -40,11 +40,22 @@ export async function regenerateTagImage(tag, mode) {
`;
if (items.length > 0) {
const inputs = items.map(item => path.join(cfg.paths.t, `${item.id}.webp`));
const inputs = [];
for (const item of items) {
const filePath = path.join(cfg.paths.t, `${item.id}.webp`);
try {
await fs.access(filePath);
inputs.push(`${filePath}[0]`);
} catch {
// Ignore missing thumbnail
}
}
await fs.mkdir(path.dirname(cachePath), { recursive: true });
await execFilePromise('magick', [...inputs, '+append', '-background', 'none', '-resize', '600x300^', '-gravity', 'center', '-extent', '600x300', cachePath]);
return cachePath;
if (inputs.length > 0) {
await fs.mkdir(path.dirname(cachePath), { recursive: true });
await execFilePromise('magick', [...inputs, '+append', '-background', 'none', '-resize', '600x300^', '-gravity', 'center', '-extent', '600x300', cachePath]);
return cachePath;
}
}
} catch (err) {
console.error(`[TAG_IMAGE] Failed to generate image for tag "${tag}" (mode ${mode}):`, err);
+20 -8
View File
@@ -295,14 +295,26 @@ export default (router, tpl) => {
`;
if (items.length > 0) {
const inputs = items.map(item => path.join(cfg.paths.t, `${item.id}.webp`));
await fs.mkdir(CACHE_DIR, { recursive: true });
await execFile('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));
const inputs = [];
for (const item of items) {
const filePath = path.join(cfg.paths.t, `${item.id}.webp`);
try {
await fs.access(filePath);
inputs.push(`${filePath}[0]`);
} catch {
// Ignore missing thumbnail
}
}
if (inputs.length > 0) {
await fs.mkdir(CACHE_DIR, { recursive: true });
await execFile('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('[USER_HALL_IMAGE]', e);
+5
View File
@@ -4,6 +4,11 @@
<div class="posts" data-current-page="{{ pagination.current }}" data-has-more="{{ pagination.next ? 'true' : 'false' }}">
@each(items as item)
<a href="{{ link.main }}{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}" class="{{ item.is_pinned ? 'anim-boxshadow ' : '' }}thumb lazy-thumb {{ item.has_notification ? 'has-notif' : '' }} {{ item.is_pinned ? 'is-pinned' : '' }} {{ item.is_onara_active ? 'onara-active' : '' }}" data-file="{{ item.dest }}" data-mime="{{ item.mime }}" data-user="@if(!is_anonymized){!! item.display_name || item.username !!}@else anonymous@endif" data-ext="{{ item.mime.split('/')[1].replace('youtube', 'yt').replace('x-shockwave-flash', 'flash').replace('vnd.adobe.flash.movie', 'flash').toUpperCase() }}" data-mode="{{ item.tag_id == nsfl_tag_id ? 'nsfl' : (item.tag_id == 2 ? 'nsfw' : (item.tag_id == 1 ? 'sfw' : 'null')) }}" data-bg="/t/{{ item.id }}.webp" data-size="{{ enable_dynamic_thumbs ? (item.thumb_size || 1) : 1 }}">
@if(item.is_audio && !item.has_coverart)
<div class="sidebar-media-placeholder audio">
<i class="fa-solid fa-music"></i>
</div>
@endif
<div class="thumb-indicators">
@if(item.is_pinned)
<i class="fa-solid fa-thumbtack pin-indicator anim"></i>
+5
View File
@@ -1,5 +1,10 @@
@each(items as item)
<a href="{{ link.main }}{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}" class="{{ item.is_pinned ? 'anim-boxshadow ' : '' }}thumb lazy-thumb {{ item.has_notification ? 'has-notif' : '' }} {{ item.is_pinned ? 'is-pinned' : '' }}" data-file="{{ item.dest }}" data-mime="{{ item.mime }}" data-user="@if(!is_anonymized){!! item.display_name || item.username !!}@else anonymous@endif" data-ext="{{ item.mime.split('/')[1].replace('youtube', 'yt').replace('x-shockwave-flash', 'flash').replace('vnd.adobe.flash.movie', 'flash').replace('x-zip-compressed', 'zip').replace('x-rar-compressed', 'rar').replace('vnd.rar', 'rar').replace('x-7z-compressed', '7z').replace('x-tar', 'tar').replace('x-bzip2', 'bz2').replace('x-xz', 'xz').toUpperCase() }}" data-mode="{{ item.tag_id == nsfl_tag_id ? 'nsfl' : (item.tag_id == 2 ? 'nsfw' : (item.tag_id == 1 ? 'sfw' : 'null')) }}" data-bg="/t/{{ item.id }}.webp" data-size="{{ enable_dynamic_thumbs ? (item.thumb_size || 1) : 1 }}">
@if(item.is_audio && !item.has_coverart)
<div class="sidebar-media-placeholder audio">
<i class="fa-solid fa-music"></i>
</div>
@endif
<div class="thumb-indicators">
@if(item.is_pinned)
<i class="fa-solid fa-thumbtack pin-indicator anim"></i>
+10
View File
@@ -124,6 +124,11 @@
<div class="posts no-infinite-scroll">
@each(f0cks.items as item)
<a href="{{ f0cks.link.main }}{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}" class="{{ item.is_pinned ? 'anim-boxshadow ' : '' }}thumb lazy-thumb {{ item.is_pinned ? 'is-pinned' : '' }}" data-file="{{ item.dest }}" data-mime="{{ item.mime }}" data-user="{!! item.display_name || item.username !!}" data-ext="{{ item.mime.split('/')[1].replace('youtube', 'yt').replace('x-shockwave-flash', 'flash').replace('vnd.adobe.flash.movie', 'flash').toUpperCase() }}" data-mode="{{ item.tag_id == nsfl_tag_id ? 'nsfl' : (item.tag_id == 2 ? 'nsfw' : (item.tag_id == 1 ? 'sfw' : 'null')) }}" data-bg="/t/{{ item.id }}.webp">
@if(item.is_audio && !item.has_coverart)
<div class="sidebar-media-placeholder audio">
<i class="fa-solid fa-music"></i>
</div>
@endif
<div class="thumb-indicators">
@if(item.is_pinned)
<i class="fa-solid fa-thumbtack pin-indicator anim"></i>
@@ -157,6 +162,11 @@
<div class="posts no-infinite-scroll">
@each(favs.items as item)
<a href="{{ favs.link.main }}{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}" class="{{ item.is_pinned ? 'anim-boxshadow ' : '' }}thumb lazy-thumb {{ item.is_pinned ? 'is-pinned' : '' }}" data-file="{{ item.dest }}" data-mime="{{ item.mime }}" data-user="{!! item.display_name || item.username !!}" data-ext="{{ item.mime.split('/')[1].replace('youtube', 'yt').replace('x-shockwave-flash', 'flash').replace('vnd.adobe.flash.movie', 'flash').toUpperCase() }}" data-mode="{{ item.tag_id == nsfl_tag_id ? 'nsfl' : (item.tag_id == 2 ? 'nsfw' : (item.tag_id == 1 ? 'sfw' : 'null')) }}" data-bg="/t/{{ item.id }}.webp">
@if(item.is_audio && !item.has_coverart)
<div class="sidebar-media-placeholder audio">
<i class="fa-solid fa-music"></i>
</div>
@endif
<div class="thumb-indicators">
@if(item.is_pinned)
<i class="fa-solid fa-thumbtack pin-indicator anim"></i>