generate blur thumbnail for all items!

This commit is contained in:
2026-05-23 21:05:40 +02:00
parent 8c9e89c771
commit 2f044a8d02
11 changed files with 79 additions and 92 deletions

View File

@@ -14734,9 +14734,3 @@ body.scroller-active #gchat-reopen-bubble {
font-size: 11px !important; font-size: 11px !important;
line-height: 1 !important; line-height: 1 !important;
} }
/* Sidebar activity thumbnails blur support (SFW/Untagged only; NSFW/NSFL use pre-blurred static images directly) */
.blur-sfw-active .sidebar-thumb-link[data-mode="sfw"] img,
.blur-untagged-active .sidebar-thumb-link[data-mode="untagged"] img {
filter: blur(8px) contrast(0.85) brightness(0.85);
}

View File

@@ -145,17 +145,24 @@ window.cancelAnimFrame = (function () {
document.querySelectorAll('.lazy-thumb').forEach(thumb => { document.querySelectorAll('.lazy-thumb').forEach(thumb => {
let bg = thumb.dataset.bg; let bg = thumb.dataset.bg;
if (bg) { if (bg) {
const mode = thumb.getAttribute('data-mode'); const mode = thumb.getAttribute('data-mode');
const blurNsfw = localStorage.getItem('blurNsfw') === 'true'; const blurNsfw = localStorage.getItem('blurNsfw') === 'true';
const blurNsfl = localStorage.getItem('blurNsfl') === 'true'; const blurNsfl = localStorage.getItem('blurNsfl') === 'true';
if (mode === 'nsfw' && blurNsfw && !thumb.classList.contains('revealed')) { const blurSfw = localStorage.getItem('blurSfw') === 'true';
bg = bg.replace('.webp', '_blur.webp'); const blurUntagged = localStorage.getItem('blurUntagged') === 'true';
} else if (mode === 'nsfl' && blurNsfl && !thumb.classList.contains('revealed')) {
bg = bg.replace('.webp', '_blur.webp'); let shouldBlurThis = false;
} if (mode === 'nsfw') shouldBlurThis = blurNsfw;
const finalBg = window.applyThumbCacheBust(bg); else if (mode === 'nsfl') shouldBlurThis = blurNsfl;
thumb.style.backgroundImage = `url('${finalBg}')`; else if (mode === 'sfw') shouldBlurThis = blurSfw;
thumb.classList.remove('lazy-thumb'); else if (mode === 'null' || !mode) shouldBlurThis = blurUntagged;
if (shouldBlurThis && !thumb.classList.contains('revealed')) {
bg = bg.replace('.webp', '_blur.webp');
}
const finalBg = window.applyThumbCacheBust(bg);
thumb.style.backgroundImage = `url('${finalBg}')`;
thumb.classList.remove('lazy-thumb');
} }
}); });
return; return;
@@ -171,9 +178,16 @@ window.cancelAnimFrame = (function () {
const mode = thumb.getAttribute('data-mode'); const mode = thumb.getAttribute('data-mode');
const blurNsfw = localStorage.getItem('blurNsfw') === 'true'; const blurNsfw = localStorage.getItem('blurNsfw') === 'true';
const blurNsfl = localStorage.getItem('blurNsfl') === 'true'; const blurNsfl = localStorage.getItem('blurNsfl') === 'true';
if (mode === 'nsfw' && blurNsfw && !thumb.classList.contains('revealed')) { const blurSfw = localStorage.getItem('blurSfw') === 'true';
bg = bg.replace('.webp', '_blur.webp'); const blurUntagged = localStorage.getItem('blurUntagged') === 'true';
} else if (mode === 'nsfl' && blurNsfl && !thumb.classList.contains('revealed')) {
let shouldBlurThis = false;
if (mode === 'nsfw') shouldBlurThis = blurNsfw;
else if (mode === 'nsfl') shouldBlurThis = blurNsfl;
else if (mode === 'sfw') shouldBlurThis = blurSfw;
else if (mode === 'null' || !mode) shouldBlurThis = blurUntagged;
if (shouldBlurThis && !thumb.classList.contains('revealed')) {
bg = bg.replace('.webp', '_blur.webp'); bg = bg.replace('.webp', '_blur.webp');
} }
@@ -8831,7 +8845,13 @@ if (navigator.vibrate) {
// Put the pre-blurred background image back if applicable // Put the pre-blurred background image back if applicable
const baseBg = thumb.dataset.bg; const baseBg = thumb.dataset.bg;
const mode = thumb.getAttribute('data-mode'); const mode = thumb.getAttribute('data-mode');
if (baseBg && (mode === 'nsfw' || mode === 'nsfl')) { let shouldBlurThis = false;
if (mode === 'nsfw') shouldBlurThis = blurNsfw;
else if (mode === 'nsfl') shouldBlurThis = blurNsfl;
else if (mode === 'sfw') shouldBlurThis = blurSfw;
else if (mode === 'null' || !mode) shouldBlurThis = blurUntagged;
if (baseBg && shouldBlurThis) {
const finalBg = window.applyThumbCacheBust(baseBg.replace('.webp', '_blur.webp')); const finalBg = window.applyThumbCacheBust(baseBg.replace('.webp', '_blur.webp'));
thumb.style.backgroundImage = `url('${finalBg}')`; thumb.style.backgroundImage = `url('${finalBg}')`;
} }
@@ -8858,7 +8878,7 @@ if (navigator.vibrate) {
// Dynamically load the standard unblurred background image // Dynamically load the standard unblurred background image
const baseBg = thumb.dataset.bg; const baseBg = thumb.dataset.bg;
if (baseBg && (mode === 'nsfw' || mode === 'nsfl')) { if (baseBg && shouldBlurThis) {
const finalBg = window.applyThumbCacheBust(baseBg); const finalBg = window.applyThumbCacheBust(baseBg);
thumb.style.backgroundImage = `url('${finalBg}')`; thumb.style.backgroundImage = `url('${finalBg}')`;
} }

View File

@@ -410,7 +410,7 @@
else if (rClass === 'untagged' && blurUntagged) isBlurred = true; else if (rClass === 'untagged' && blurUntagged) isBlurred = true;
let thumbUrl = `/t/${c.item_id}.webp`; let thumbUrl = `/t/${c.item_id}.webp`;
if (isBlurred && (rClass === 'nsfw' || rClass === 'nsfl')) { if (isBlurred) {
thumbUrl = `/t/${c.item_id}_blur.webp`; thumbUrl = `/t/${c.item_id}_blur.webp`;
} }

View File

@@ -8,7 +8,7 @@
* node regen.mjs --all - Regenerate ALL items * node regen.mjs --all - Regenerate ALL items
* node regen.mjs --audio - Regenerate all audio items * node regen.mjs --audio - Regenerate all audio items
* node regen.mjs --pdf - Regenerate all PDF items * node regen.mjs --pdf - Regenerate all PDF items
* node regen.mjs --blur - Regenerate ONLY the blurred thumbnails for NSFW/NSFL items * node regen.mjs --blur - Regenerate ONLY the blurred thumbnails for all items
*/ */
import db from "../src/inc/sql.mjs"; import db from "../src/inc/sql.mjs";
@@ -27,7 +27,7 @@ if (args.length === 0) {
console.log(' node regen.mjs --audio - Regenerate all audio items'); console.log(' node regen.mjs --audio - Regenerate all audio items');
console.log(' node regen.mjs --pdf - Regenerate all PDF items'); console.log(' node regen.mjs --pdf - Regenerate all PDF items');
console.log(' node regen.mjs --youtube - Regenerate all YouTube thumbnails'); console.log(' node regen.mjs --youtube - Regenerate all YouTube thumbnails');
console.log(' node regen.mjs --blur - Regenerate ONLY the blurred thumbnails for NSFW/NSFL items'); console.log(' node regen.mjs --blur - Regenerate ONLY the blurred thumbnails for all items');
process.exit(0); process.exit(0);
} }
@@ -64,12 +64,9 @@ const regen = async (item) => {
console.log(`[${id}] ✓ Thumbnail regenerated`); console.log(`[${id}] ✓ Thumbnail regenerated`);
} }
// Regenerate blurred thumbnail if item has NSFW tag // Regenerate blurred thumbnail unconditionally
const nsfw = await db`SELECT 1 FROM tags_assign WHERE item_id = ${id} AND tag_id = 2 LIMIT 1`; await queue.genBlurredThumbnail(id, false);
if (nsfw.length > 0) { console.log(`[${id}] ✓ Blurred thumbnail regenerated`);
await queue.genBlurredThumbnail(id, false);
console.log(`[${id}] ✓ Blurred thumbnail regenerated`);
}
} catch (err) { } catch (err) {
console.error(`[${id}] ✗ FAILED:`, err.message || err); console.error(`[${id}] ✗ FAILED:`, err.message || err);
} }
@@ -91,16 +88,13 @@ try {
items = await db`SELECT id, dest, mime, src FROM items WHERE active = true AND is_deleted = false AND mime = 'video/youtube' ORDER BY id`; items = await db`SELECT id, dest, mime, src FROM items WHERE active = true AND is_deleted = false AND mime = 'video/youtube' ORDER BY id`;
console.log(`Regenerating ${items.length} YouTube items...\n`); console.log(`Regenerating ${items.length} YouTube items...\n`);
} else if (blurOnly) { } else if (blurOnly) {
const nsflTagId = cfg.nsfl_tag_id || 3;
items = await db` items = await db`
SELECT DISTINCT i.id, i.dest, i.mime, i.src SELECT id, dest, mime, src
FROM items i FROM items
JOIN tags_assign ta ON ta.item_id = i.id WHERE active = true AND is_deleted = false
WHERE i.active = true AND i.is_deleted = false ORDER BY id
AND ta.tag_id IN (2, ${nsflTagId})
ORDER BY i.id
`; `;
console.log(`Regenerating ONLY blurred thumbnails for ${items.length} NSFW/NSFL items...\n`); console.log(`Regenerating ONLY blurred thumbnails for all ${items.length} items...\n`);
} else { } else {
const ids = args.map(Number).filter(n => !isNaN(n) && n > 0); const ids = args.map(Number).filter(n => !isNaN(n) && n > 0);
if (ids.length === 0) { if (ids.length === 0) {

View File

@@ -1082,12 +1082,10 @@ export default router => {
VALUES (${itemid}, ${newRatingId}, ${req.session.id}) VALUES (${itemid}, ${newRatingId}, ${req.session.id})
`; `;
// If switching to NSFW/NSFL, ensure blurred thumbnail exists // Ensure blurred thumbnail exists
if (newRatingId === 2 || newRatingId === nsfl_id) { await queue.genBlurredThumbnail(itemid).catch(err => {
await queue.genBlurredThumbnail(itemid).catch(err => { console.error(`[RATING_TOGGLE] Blurred thumbnail generation failed for ${itemid}:`, err);
console.error(`[RATING_TOGGLE] Blurred thumbnail generation failed for ${itemid}:`, err); });
});
}
}); });
const newRating = newRatingId === 1 ? 'sfw' : (newRatingId === 2 ? 'nsfw' : 'nsfl'); const newRating = newRatingId === 1 ? 'sfw' : (newRatingId === 2 ? 'nsfw' : 'nsfl');

View File

@@ -115,14 +115,12 @@ export default router => {
await db`INSERT INTO tags_assign ${db({ tag_id: nextTagId, item_id: postid, user_id: +req.session.id })}`; await db`INSERT INTO tags_assign ${db({ tag_id: nextTagId, item_id: postid, user_id: +req.session.id })}`;
} }
// Automatically generate blurred thumbnail if cycling TO NSFW or NSFL // Automatically generate/verify blurred thumbnail on cycle
if (nextTagId === 2 || nextTagId === nsflId) { const blurPath = path.join(cfg.paths.t, `${postid}_blur.webp`);
const blurPath = path.join(cfg.paths.t, `${postid}_blur.webp`); try {
try { await fs.promises.access(blurPath);
await fs.promises.access(blurPath); } catch {
} catch { await queue.genBlurredThumbnail(postid, false);
await queue.genBlurredThumbnail(postid, false);
}
} }
const labels = { 1: { label: 'SFW', cls: 'sfw' }, 2: { label: 'NSFW', cls: 'nsfw' }, [nsflId]: { label: 'NSFL', cls: 'nsfl' } }; const labels = { 1: { label: 'SFW', cls: 'sfw' }, 2: { label: 'NSFW', cls: 'nsfw' }, [nsflId]: { label: 'NSFL', cls: 'nsfl' } };
@@ -180,16 +178,13 @@ export default router => {
await audit.log(req.session.id, 'toggle_tag', 'item', postid, auditDetails); await audit.log(req.session.id, 'toggle_tag', 'item', postid, auditDetails);
// Generate blurred thumbnail if toggling TO NSFW // Ensure blurred thumbnail exists on toggle
if (hasSFW && !hasNSFW) { const blurPath = path.join(cfg.paths.t, `${postid}_blur.webp`);
// Was SFW, now NSFW - check if blur exists and generate if not try {
const blurPath = path.join(cfg.paths.t, `${postid}_blur.webp`); await fs.promises.access(blurPath);
try { } catch {
await fs.promises.access(blurPath); // Doesn't exist - generate it
} catch { await queue.genBlurredThumbnail(postid, false);
// Doesn't exist - generate it
await queue.genBlurredThumbnail(postid, false);
}
} }
const freshTags = await lib.getTags(postid); const freshTags = await lib.getTags(postid);

View File

@@ -301,9 +301,7 @@ export default router => {
if (effectiveRating) { if (effectiveRating) {
const ratingTagId = effectiveRating === 'sfw' ? 1 : (effectiveRating === 'nsfw' ? 2 : (cfg.nsfl_tag_id || 3)); const ratingTagId = effectiveRating === 'sfw' ? 1 : (effectiveRating === 'nsfw' ? 2 : (cfg.nsfl_tag_id || 3));
await db`insert into tags_assign ${db({ item_id: itemid, tag_id: ratingTagId, user_id: req.session.id })} on conflict do nothing`; await db`insert into tags_assign ${db({ item_id: itemid, tag_id: ratingTagId, user_id: req.session.id })} on conflict do nothing`;
if (effectiveRating === 'nsfw' || effectiveRating === 'nsfl') { await queue.genBlurredThumbnail(itemid, isApprovalRequired).catch(() => {});
await queue.genBlurredThumbnail(itemid, isApprovalRequired).catch(() => {});
}
} }
// Assign user tags + auto-tags // Assign user tags + auto-tags
@@ -575,7 +573,7 @@ export default router => {
try { try {
await queue.genThumbnail(filename, mime, itemid, url, isApprovalRequired); await queue.genThumbnail(filename, mime, itemid, url, isApprovalRequired);
if (effectiveRating === 'nsfw' || effectiveRating === 'nsfl') await queue.genBlurredThumbnail(itemid, isApprovalRequired); await queue.genBlurredThumbnail(itemid, isApprovalRequired);
} catch (err) { } catch (err) {
const tDir = isApprovalRequired ? path.join(cfg.paths.pending, 't') : cfg.paths.t; 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(() => {}); await queue.spawn('magick', ['-size', '128x128', 'xc:#1a1a1a', path.join(tDir, `${itemid}.webp`)]).catch(() => {});

View File

@@ -449,7 +449,7 @@ export default (router) => {
// Process thumbnail // Process thumbnail
try { try {
await queue.genThumbnail(filename, mime, itemid, url, isApprovalRequired); await queue.genThumbnail(filename, mime, itemid, url, isApprovalRequired);
if (rating === 'nsfw' || rating === 'nsfl') await queue.genBlurredThumbnail(itemid, isApprovalRequired); await queue.genBlurredThumbnail(itemid, isApprovalRequired);
} catch (err) { } catch (err) {
console.error('[REHOST] Thumbnail error:', err); console.error('[REHOST] Thumbnail error:', err);
} }

View File

@@ -705,7 +705,7 @@ export default async bot => {
// Generate Thumbnail // Generate Thumbnail
try { try {
await queue.genThumbnail(filename, mime, itemid, link, manualApproval); await queue.genThumbnail(filename, mime, itemid, link, manualApproval);
if (isNSFW) await queue.genBlurredThumbnail(itemid, manualApproval); await queue.genBlurredThumbnail(itemid, manualApproval);
} catch (err) { } catch (err) {
const tDir = manualApproval ? path.join(cfg.paths.pending, 't') : cfg.paths.t; const tDir = manualApproval ? path.join(cfg.paths.pending, 't') : cfg.paths.t;
await queue.spawn('magick', ['./mugge.png', path.join(tDir, `${itemid}.webp`)]); await queue.spawn('magick', ['./mugge.png', path.join(tDir, `${itemid}.webp`)]);
@@ -815,7 +815,7 @@ export default async bot => {
// Generate Thumbnail // Generate Thumbnail
try { try {
await queue.genThumbnail(filename, mime, itemid, link, manualApproval); await queue.genThumbnail(filename, mime, itemid, link, manualApproval);
if (isNSFW) await queue.genBlurredThumbnail(itemid, manualApproval); await queue.genBlurredThumbnail(itemid, manualApproval);
} catch (err) { } catch (err) {
const tDir = manualApproval ? path.join(cfg.paths.pending, 't') : cfg.paths.t; const tDir = manualApproval ? path.join(cfg.paths.pending, 't') : cfg.paths.t;
await queue.spawn('magick', ['./mugge.png', path.join(tDir, `${itemid}.webp`)]); await queue.spawn('magick', ['./mugge.png', path.join(tDir, `${itemid}.webp`)]);

View File

@@ -130,16 +130,8 @@ export const handleRethumbUpload = async (req, res, itemId) => {
try { try {
await execFile('magick', [tmpPath, '-coalesce', '-resize', `${thumbSpec}^`, '-gravity', 'center', '-crop', `${thumbSpec}+0+0`, '+repage', finalPath]); await execFile('magick', [tmpPath, '-coalesce', '-resize', `${thumbSpec}^`, '-gravity', 'center', '-crop', `${thumbSpec}+0+0`, '+repage', finalPath]);
// Check if item contains NSFW or NSFL tag // Generate blurred thumbnail
const tags = await db` await queue.genBlurredThumbnail(item.id, !item.active);
select tag_id from tags_assign
where item_id = ${+item.id}
and tag_id in (2, ${cfg.nsfl_tag_id || 3})
`;
if (tags.length > 0) {
// Generate blurred thumbnail
await queue.genBlurredThumbnail(item.id, !item.active);
}
} catch (err) { } catch (err) {
console.error('[RETHUMB HANDLER] Magick error:', err); console.error('[RETHUMB HANDLER] Magick error:', err);

View File

@@ -406,10 +406,8 @@ export const handleUpload = async (req, res, self) => {
} }
} }
// Generate blurred thumbnail for NSFW/NSFL // Generate blurred thumbnail for all posts (SFW, NSFW, NSFL, Untagged)
if (effectiveRating === 'nsfw' || effectiveRating === 'nsfl') { await queue.genBlurredThumbnail(itemid, isPending);
await queue.genBlurredThumbnail(itemid, isPending);
}
// Insert optional first comment // Insert optional first comment
if (comment && comment.length > 0) { if (comment && comment.length > 0) {
@@ -553,14 +551,12 @@ export const handleUpload = async (req, res, self) => {
console.error(`[BACKGROUND ERROR] genThumbnail failed for item ${itemid}:`, err); console.error(`[BACKGROUND ERROR] genThumbnail failed for item ${itemid}:`, err);
} }
// Ensure blurred thumbnail exists if needed // Ensure blurred thumbnail exists
if (effectiveRating === 'nsfw' || effectiveRating === 'nsfl') { const tDir = isPending ? path.join(cfg.paths.pending, 't') : cfg.paths.t;
const tDir = isPending ? path.join(cfg.paths.pending, 't') : cfg.paths.t; const blurPath = path.join(tDir, `${itemid}_blur.webp`);
const blurPath = path.join(tDir, `${itemid}_blur.webp`); const blurExists = await fs.access(blurPath).then(() => true).catch(() => false);
const blurExists = await fs.access(blurPath).then(() => true).catch(() => false); if (!blurExists) {
if (!blurExists) { await queue.genBlurredThumbnail(itemid, isPending).catch(err => console.error(`[BACKGROUND ERROR] genBlurredThumbnail failed:`, err));
await queue.genBlurredThumbnail(itemid, isPending).catch(err => console.error(`[BACKGROUND ERROR] genBlurredThumbnail failed:`, err));
}
} }
// Note: video title metadata is surfaced to the user as a suggestion in the upload form. // Note: video title metadata is surfaced to the user as a suggestion in the upload form.