This commit is contained in:
2026-09-10 00:07:22 +02:00
parent 43ff87ed62
commit 73ffe02bbb
12 changed files with 1327 additions and 275 deletions
+33 -7
View File
@@ -39,20 +39,42 @@ const storage = process.env.STORAGE_DIR;
const resolvePath = (defaultRel) => {
const local = path.resolve(path.join(base, defaultRel));
let target = local;
if (storage) {
const absStorage = path.resolve(storage);
if (defaultRel.startsWith('public/')) {
const sub = defaultRel.replace('public/', '');
if (sub === 's/emojis' || sub === 's/koepfe' || sub === 's/fonts') {
const storagePath = path.join(absStorage, sub.split('/').pop());
if (fs.existsSync(storagePath)) return path.resolve(storagePath);
return local;
if (fs.existsSync(storagePath)) target = path.resolve(storagePath);
else target = local;
} else {
target = path.resolve(path.join(absStorage, sub));
}
return path.resolve(path.join(absStorage, sub));
} else {
target = path.resolve(path.join(absStorage, defaultRel));
}
return path.resolve(path.join(absStorage, defaultRel));
}
return local;
try {
if (fs.existsSync(target)) {
return fs.realpathSync(target);
}
const parent = path.dirname(target);
if (fs.existsSync(parent)) {
return path.join(fs.realpathSync(parent), path.basename(target));
}
} catch (_) {}
return target;
};
const resolveImportPath = () => {
const p = storage ? path.resolve(path.join(path.resolve(storage), 'import')) : path.resolve(path.join(base, 'f0ckm-data/import'));
try {
if (fs.existsSync(p)) return fs.realpathSync(p);
const parent = path.dirname(p);
if (fs.existsSync(parent)) return path.join(fs.realpathSync(parent), path.basename(p));
} catch (_) {}
return p;
};
config.paths = {
@@ -61,7 +83,11 @@ config.paths = {
c: resolvePath('public/c'),
t: resolvePath('public/t'),
ca: resolvePath('public/ca'),
s: path.join(base, 'public/s'),
s: (() => {
const sPath = path.join(base, 'public/s');
try { if (fs.existsSync(sPath)) return fs.realpathSync(sPath); } catch (_) {}
return sPath;
})(),
emojis: resolvePath('public/s/emojis'),
koepfe: resolvePath('public/s/koepfe'),
fonts: resolvePath('public/s/fonts'),
@@ -71,7 +97,7 @@ config.paths = {
deleted: resolvePath('deleted'),
logs: resolvePath('logs'),
tmp: resolvePath('tmp'),
import: storage ? path.resolve(path.join(path.resolve(storage), 'import')) : path.resolve(path.join(base, 'f0ckm-data/import'))
import: resolveImportPath()
};
export default config;
+35 -8
View File
@@ -354,9 +354,12 @@ export default new class queue {
}
async genThumbnail(filename, mime, itemid, link, pending = false, size = 512) {
const bDir = pending ? path.join(cfg.paths.pending, 'b') : cfg.paths.b;
const tDir = pending ? path.join(cfg.paths.pending, 't') : cfg.paths.t;
const cDir = pending ? path.join(cfg.paths.pending, 'ca') : cfg.paths.ca;
let bDir = pending ? path.join(cfg.paths.pending, 'b') : cfg.paths.b;
let tDir = pending ? path.join(cfg.paths.pending, 't') : cfg.paths.t;
let cDir = pending ? path.join(cfg.paths.pending, 'ca') : cfg.paths.ca;
try { bDir = await fs.promises.realpath(bDir); } catch (_) {}
try { tDir = await fs.promises.realpath(tDir); } catch (_) {}
try { cDir = await fs.promises.realpath(cDir); } catch (_) {}
const tmpFile = path.join(os.tmpdir(), itemid + '.png');
const tmpJpg = path.join(os.tmpdir(), itemid + '.jpg');
const thumbSize = (size && size > 128) ? size : 128;
@@ -372,7 +375,13 @@ export default new class queue {
}
} catch (e) {}
const outPath = path.join(tDir, itemid + '.webp');
let outPath = path.join(tDir, itemid + '.webp');
try {
const outStat = await fs.promises.lstat(outPath);
if (outStat.isSymbolicLink()) {
await fs.promises.unlink(outPath).catch(() => {});
}
} catch (_) {}
try {
@@ -457,17 +466,17 @@ export default new class queue {
else if (mime.startsWith('audio/')) {
let coverExtracted = false;
this._lastCoverExtracted = false; // Reset state for this call
if (link.match(/soundcloud/)) {
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();
if (!cover.match(/default_avatar/)) {
if (cover && !cover.match(/default_avatar/)) {
cover = cover.replace(/-(large|original)\./, '-t500x500.');
try {
const curlArgs = ['-s', '-L', cover, '-o', tmpJpg];
if (proxyArgs.length > 0) curlArgs.push(...proxyArgs);
await this.spawn('curl', curlArgs);
const size = (await fs.promises.stat(tmpJpg)).size;
if (size >= 0) {
if (size > 0) {
await this.spawn('magick', [tmpJpg, tmpFile]);
await this.spawn('magick', [tmpJpg, path.join(cDir, itemid + '.webp')]);
coverExtracted = true;
@@ -497,6 +506,17 @@ export default new class queue {
}
} catch (err) { }
}
// If no new cover art extracted, check if cover art was already saved previously in cDir
if (!coverExtracted) {
const existingCover = path.join(cDir, itemid + '.webp');
try {
const stat = await fs.promises.stat(existingCover);
if (stat.size > 0) {
await this.spawn('magick', [existingCover, tmpFile]);
coverExtracted = true;
}
} catch (_) { }
}
// If no cover art found, use audio.webp as the thumbnail
if (!coverExtracted) {
const audioFallback = path.join(cfg.paths.s, 'img', 'audio.webp');
@@ -641,9 +661,16 @@ export default new class queue {
async genBlurredThumbnail(itemid, pending = false) {
const tDir = pending ? path.join(cfg.paths.pending, 't') : cfg.paths.t;
let tDir = pending ? path.join(cfg.paths.pending, 't') : cfg.paths.t;
try { tDir = await fs.promises.realpath(tDir); } catch (_) {}
const src = path.join(tDir, `${itemid}.webp`);
const dst = path.join(tDir, `${itemid}_blur.webp`);
try {
const dstStat = await fs.promises.lstat(dst);
if (dstStat.isSymbolicLink()) {
await fs.promises.unlink(dst).catch(() => {});
}
} catch (_) {}
try {
await this.spawn('magick', [src, '-blur', '0x48', dst]);
return true;
+331 -111
View File
@@ -5,6 +5,7 @@ import { getEnableItemSlugs } from "../settings.mjs";
import { updateHallsCache } from "../halls_cache.mjs";
import queue from "../queue.mjs";
import fs from "fs";
import path from "path";
import url from "url";
const getGlobalfilter = () => {
@@ -234,7 +235,283 @@ async function checkFavoritesAccess(rawUser, { session, user_id, is_admin } = {}
return { isPrivate: true, isAllowed };
}
const buildFeedFilters = async ({
rawUser,
rawTag,
rawHall,
rawMime,
mode,
ratings,
session,
strict,
exclude,
newer,
minXdScore,
rawUserHall,
rawUserHallOwner,
rawTagger
}) => {
const user = rawUser ? lib.escapeLike(decodeURI(rawUser)) : null;
// --- title: prefix — search items.title instead of the tags table ---
const _decodedTag = rawTag ? decodeURIComponent(rawTag) : '';
const isTitleSearch = _decodedTag.startsWith('title:');
const titleQuery = isTitleSearch ? _decodedTag.substring(6).trim() : null;
const tag = isTitleSearch ? null : lib.parseTag(rawTag ?? null);
let hall = rawHall ?? null;
let hallObj = null;
if (hall) {
const hallData = await db`SELECT name, slug, description FROM halls WHERE slug = ${hall} LIMIT 1`;
if (hallData.length) {
hallObj = { name: hallData[0].name, slug: hallData[0].slug, description: hallData[0].description };
}
}
// User hall context
const userHallSlug = rawUserHall ?? null;
const userHallOwner = rawUserHallOwner ?? null;
let userHallObj = null;
if (userHallSlug && userHallOwner) {
const uhData = await db`
SELECT uh.id, uh.name, uh.slug, uh.description, uh.is_private, u."user" as owner_name
FROM user_halls uh
JOIN "user" u ON u.id = uh.user_id
WHERE u."user" ILIKE ${userHallOwner} AND uh.slug = ${userHallSlug}
LIMIT 1
`;
if (uhData.length) userHallObj = uhData[0];
}
const mime = rawMime ?? null;
// Support multiple MIME types (comma separated)
const mimeParts = (mime || "").split(',').filter(m => ['video', 'audio', 'image', 'flash', 'pdf'].includes(m));
const mimeSQL = mimeParts.length > 0
? db`and (${mimeParts.map(m => m === 'flash'
? (flashMimes.length > 0
? flashMimes.map(fm => db`items.mime = ${fm}`).reduce((a, b) => db`${a} or ${b}`)
: db`false`)
: (m === 'pdf' ? db`items.mime = 'application/pdf'` : db`items.mime ilike ${m + '/%'}`)).reduce((a, b) => db`${a} or ${b}`)})`
: db``;
const excludedTags = session && exclude ? (exclude || []) : [];
const newerThan = newer ? parseInt(newer) : null;
const minXd = (minXdScore && +minXdScore > 0) ? +minXdScore : 0;
const xdFilter = minXd > 0 ? db`and items.xd_score >= ${minXd}` : db``;
const strictParams = ((strict || (tag && tag.includes(','))) && tag) ? tag.split(',').map(t => lib.slugify(t)).filter(t => t) : [];
const isStrict = strictParams.length > 0;
const tagger = rawTagger ? lib.escapeLike(rawTagger) : null;
const modequery = computeBaseMode(mode, ratings, session);
let tagFilter = db``;
let titleFilter = db``;
if (isTitleSearch && titleQuery) {
titleFilter = db`and items.title ILIKE ${'%' + titleQuery + '%'} and items.title IS NOT NULL`;
} else if (tagger && tag) {
const terms = tag.split(',').map(t => t.trim()).filter(Boolean);
if (terms.length > 0) {
const conditions = terms.map(term => {
return db`and items.id in (
select ta.item_id from tags_assign ta
join tags t on t.id = ta.tag_id
join "user" u on u.id = ta.user_id
where t.normalized like '%' || slugify(${term}) || '%'
and u.user ilike ${tagger}
)`;
});
tagFilter = db`${conditions}`;
}
} else if (tag) {
const terms = tag.split(',').map(t => t.trim()).filter(Boolean);
if (terms.length > 0) {
if (isStrict) {
tagFilter = db`and items.id in (
select ta.item_id
from tags_assign ta
join tags t on t.id = ta.tag_id
where t.normalized = ANY(ARRAY(SELECT slugify(x) FROM unnest(${terms}::text[]) AS x))
group by ta.item_id
having count(distinct t.normalized) = ${terms.length}
)`;
} else {
const conditions = terms.map(term => {
return db`and items.id in (select ta.item_id from tags_assign ta join tags t on t.id = ta.tag_id where t.normalized like '%' || slugify(${term}) || '%')`;
});
tagFilter = db`${conditions}`;
}
}
}
let hallFilter = db``;
if (hall) {
hallFilter = db`and items.id in (select item_id from halls_assign join halls on halls.id = halls_assign.hall_id where halls.slug = ${(hall && typeof hall === 'object') ? hall.slug : hall})`;
}
let userHallFilter = db``;
if (userHallObj) {
userHallFilter = db`and items.id in (select uha.item_id from user_halls_assign uha where uha.hall_id = ${userHallObj.id})`;
}
const isOwnerOrAdmin = (session && user && typeof user === 'string' && session.user && session.user.toLowerCase() === user.toLowerCase()) || (session && (session.admin || session.is_moderator));
const visibilityFilter = isOwnerOrAdmin
? db``
: (session && session.user
? db`and (coalesce(items.visibility, 0) = 0 or lower(items.username) = ${session.user.toLowerCase()})`
: db`and coalesce(items.visibility, 0) = 0`);
return {
user,
_decodedTag,
isTitleSearch,
titleQuery,
tag,
hall,
hallObj,
userHallSlug,
userHallOwner,
userHallObj,
mime,
mimeSQL,
excludedTags,
newerThan,
minXd,
xdFilter,
strictParams,
isStrict,
tagger,
modequery,
tagFilter,
titleFilter,
hallFilter,
userHallFilter,
visibilityFilter
};
};
export default {
getItemPage: async ({
targetItemId,
targetItemPinned,
user: rawUser,
tag: rawTag,
hall: rawHall,
mime: rawMime,
mode,
ratings,
fav,
session,
limit,
strict,
newer,
exclude,
user_id,
is_admin,
userHall: rawUserHall,
userHallOwner: rawUserHallOwner,
minXdScore,
tagger: rawTagger
} = {}) => {
const numId = await resolveNumericItemId(targetItemId);
if (!numId) return 1;
let isPinned = targetItemPinned;
if (isPinned === undefined) {
const rows = await db`SELECT is_pinned FROM items WHERE id = ${numId} LIMIT 1`;
if (!rows.length) return 1;
isPinned = Boolean(rows[0].is_pinned);
} else {
isPinned = Boolean(isPinned);
}
const eps = limit ?? cfg.websrv.eps;
const filters = await buildFeedFilters({
rawUser,
rawTag,
rawHall,
rawMime,
mode,
ratings,
session,
strict,
exclude,
newer,
minXdScore,
rawUserHall,
rawUserHallOwner,
rawTagger
});
const {
user,
mimeSQL,
excludedTags,
newerThan,
xdFilter,
modequery,
tagFilter,
titleFilter,
hallFilter,
userHallFilter,
visibilityFilter
} = filters;
// Check if the target item actually matches this feed's criteria
const itemMatch = await db`
select 1 from items
${fav ? db`inner join favorites on favorites.item_id = items.id inner join "user" fav_u on fav_u.id = favorites.user_id` : db``}
where
items.id = ${numId}
and ${db.unsafe(modequery)}
and items.active = true
${visibilityFilter}
${tagFilter}
${titleFilter}
${fav ? db`and fav_u.user ilike ${user}` : db``}
${!fav && user ? db`and items.username ilike ${user}` : db``}
${mimeSQL}
${hallFilter}
${userHallFilter}
${!session && getGlobalfilter() ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(getGlobalfilter())}))` : db``}
${excludedTags.length > 0 ? db`and not exists (select 1 from tags_assign where item_id = items.id and tag_id = any(${excludedTags}::int[]))` : db``}
${newerThan ? db`and items.id > ${newerThan}` : db``}
${xdFilter}
limit 1
`;
if (!itemMatch.length) {
return 1;
}
const countRows = await db`
select count(distinct items.id) as total_before
from items
${fav ? db`inner join favorites on favorites.item_id = items.id inner join "user" fav_u on fav_u.id = favorites.user_id` : db``}
where
${db.unsafe(modequery)}
and items.active = true
${visibilityFilter}
${tagFilter}
${titleFilter}
${fav ? db`and fav_u.user ilike ${user}` : db``}
${!fav && user ? db`and items.username ilike ${user}` : db``}
${mimeSQL}
${hallFilter}
${userHallFilter}
${!session && getGlobalfilter() ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(getGlobalfilter())}))` : db``}
${excludedTags.length > 0 ? db`and not exists (select 1 from tags_assign where item_id = items.id and tag_id = any(${excludedTags}::int[]))` : db``}
${newerThan ? db`and items.id > ${newerThan}` : db``}
${xdFilter}
and (
(items.is_pinned is true and ${isPinned} is false)
or (items.is_pinned = ${isPinned} and items.id > ${numId})
)
`;
const totalBefore = Number(countRows[0]?.total_before || 0);
return Math.floor(totalBefore / eps) + 1;
},
getf0cks: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, page, mode, ratings, fav, session, limit, strict, newer, exclude, user_id, is_admin, random, userHall: rawUserHall, userHallOwner: rawUserHallOwner, minXdScore, tagger: rawTagger } = {}) => {
if (fav && rawUser) {
const { isPrivate, isAllowed } = await checkFavoritesAccess(rawUser, { session, user_id, is_admin });
@@ -247,121 +524,54 @@ export default {
}
}
const user = rawUser ? lib.escapeLike(decodeURI(rawUser)) : null;
const filters = await buildFeedFilters({
rawUser,
rawTag,
rawHall,
rawMime,
mode,
ratings,
session,
strict,
exclude,
newer,
minXdScore,
rawUserHall,
rawUserHallOwner,
rawTagger
});
// --- title: prefix — search items.title instead of the tags table ---
const _decodedTag = rawTag ? decodeURIComponent(rawTag) : '';
const isTitleSearch = _decodedTag.startsWith('title:');
const titleQuery = isTitleSearch ? _decodedTag.substring(6).trim() : null;
const {
user,
_decodedTag,
isTitleSearch,
titleQuery,
tag,
hall,
hallObj,
userHallSlug,
userHallOwner,
userHallObj,
mime,
mimeSQL,
excludedTags,
newerThan,
minXd,
xdFilter,
strictParams,
isStrict,
tagger,
modequery,
tagFilter,
titleFilter,
hallFilter,
userHallFilter,
visibilityFilter
} = filters;
const tag = isTitleSearch ? null : lib.parseTag(rawTag ?? null);
let hall = rawHall ?? null;
let hallObj = null;
if (hall) {
const hallData = await db`SELECT name, slug, description FROM halls WHERE slug = ${hall} LIMIT 1`;
if (hallData.length) {
hallObj = { name: hallData[0].name, slug: hallData[0].slug, description: hallData[0].description };
}
}
// User hall context
const userHallSlug = rawUserHall ?? null;
const userHallOwner = rawUserHallOwner ?? null;
let userHallObj = null;
if (userHallSlug && userHallOwner) {
const uhData = await db`
SELECT uh.id, uh.name, uh.slug, uh.description, uh.is_private, u."user" as owner_name
FROM user_halls uh
JOIN "user" u ON u.id = uh.user_id
WHERE u."user" ILIKE ${userHallOwner} AND uh.slug = ${userHallSlug}
LIMIT 1
`;
if (uhData.length) userHallObj = uhData[0];
}
const mime = rawMime ?? null;
const actPage = +(page ?? 1);
// Support multiple MIME types (comma separated)
const mimeParts = (mime || "").split(',').filter(m => ['video', 'audio', 'image', 'flash', 'pdf'].includes(m));
const mimeSQL = mimeParts.length > 0
? db`and (${mimeParts.map(m => m === 'flash'
? (flashMimes.length > 0
? flashMimes.map(fm => db`items.mime = ${fm}`).reduce((a, b) => db`${a} or ${b}`)
: db`false`)
: (m === 'pdf' ? db`items.mime = 'application/pdf'` : db`items.mime ilike ${m + '/%'}`)).reduce((a, b) => db`${a} or ${b}`)})`
: db``;
const eps = limit ?? cfg.websrv.eps;
const excludedTags = session && exclude ? (exclude || []) : [];
const newerThan = newer ? parseInt(newer) : null;
const minXd = (minXdScore && +minXdScore > 0) ? +minXdScore : 0;
// xD filter: use materialized items.xd_score column (kept live by trigger) for fast indexed lookup
const xdFilter = minXd > 0 ? db`and items.xd_score >= ${minXd}` : db``;
const strictParams = ((strict || (tag && tag.includes(','))) && tag) ? tag.split(',').map(t => lib.slugify(t)).filter(t => t) : [];
const isStrict = strictParams.length > 0;
const tagger = rawTagger ? lib.escapeLike(rawTagger) : null;
const tmp = { user, tag: isTitleSearch ? _decodedTag : tag, hall: hallObj || hall, mime, page: actPage, mode: mode, view_mode: fav ? 'favs' : 'uploads', strict: strict, userHall: userHallObj || userHallSlug, userHallOwner, tagger };
const modequery = computeBaseMode(mode, ratings, session);
let tagFilter = db``;
let titleFilter = db``;
if (isTitleSearch && titleQuery) {
// Title search: match items.title ILIKE '%query%'
titleFilter = db`and items.title ILIKE ${'%' + titleQuery + '%'} and items.title IS NOT NULL`;
} else if (tagger && tag) {
// Tagger+tag filter: items where the specific user applied this specific tag
const terms = tag.split(',').map(t => t.trim()).filter(Boolean);
if (terms.length > 0) {
const conditions = terms.map(term => {
return db`and items.id in (
select ta.item_id from tags_assign ta
join tags t on t.id = ta.tag_id
join "user" u on u.id = ta.user_id
where t.normalized like '%' || slugify(${term}) || '%'
and u.user ilike ${tagger}
)`;
});
tagFilter = db`${conditions}`;
}
} else if (tag) {
const terms = tag.split(',').map(t => t.trim()).filter(Boolean);
if (terms.length > 0) {
if (isStrict) {
tagFilter = db`and items.id in (
select ta.item_id
from tags_assign ta
join tags t on t.id = ta.tag_id
where t.normalized = ANY(ARRAY(SELECT slugify(x) FROM unnest(${terms}::text[]) AS x))
group by ta.item_id
having count(distinct t.normalized) = ${terms.length}
)`;
} else {
// Non-strict intersection Logic (AND for partials)
// For each term, ensure there is AT LEAST one matching tag assigned to the item
const conditions = terms.map(term => {
return db`and items.id in (select ta.item_id from tags_assign ta join tags t on t.id = ta.tag_id where t.normalized like '%' || slugify(${term}) || '%')`;
});
tagFilter = db`${conditions}`;
}
}
}
let hallFilter = db``;
if (hall) {
hallFilter = db`and items.id in (select item_id from halls_assign join halls on halls.id = halls_assign.hall_id where halls.slug = ${(hall && typeof hall === 'object') ? hall.slug : hall})`;
}
let userHallFilter = db``;
if (userHallObj) {
userHallFilter = db`and items.id in (select uha.item_id from user_halls_assign uha where uha.hall_id = ${userHallObj.id})`;
}
const isOwnerOrAdmin = (session && user && typeof user === 'string' && session.user && session.user.toLowerCase() === user.toLowerCase()) || (session && (session.admin || session.is_moderator));
const visibilityFilter = isOwnerOrAdmin
? db``
: (session && session.user
? db`and (coalesce(items.visibility, 0) = 0 or lower(items.username) = ${session.user.toLowerCase()})`
: db`and coalesce(items.visibility, 0) = 0`);
const cacheKey = buildCountCacheKey({ modequery, tag, user, hall, mime, fav, session, excludedTags, newerThan, minXd, userHallObj, tagger });
let total = getCachedCount(cacheKey);
@@ -926,7 +1136,17 @@ export default {
// Efficient coverart fallback
const coverartUrl = actitem.has_coverart
let hasCoverart = actitem.has_coverart;
if (!hasCoverart && actitem.mime?.startsWith('audio/')) {
const caPath = path.join(cfg.paths.ca, `${actitem.id}.webp`);
try {
if (fs.existsSync(caPath)) {
hasCoverart = true;
db`UPDATE items SET has_coverart = TRUE WHERE id = ${actitem.id}`.catch(() => {});
}
} catch (_) {}
}
const coverartUrl = hasCoverart
? `${cfg.websrv.paths.coverarts}/${actitem.id}.webp`
: `/s/img/music.webp`;
+17 -4
View File
@@ -1302,7 +1302,7 @@ export default router => {
if (!itemid) return res.json({ success: false, msg: 'No itemid provided' }, 400);
const rows = await db`
SELECT id, dest, mime, username
SELECT id, dest, mime, username, src
FROM items
WHERE id = ${itemid} AND active = true AND is_deleted = false
LIMIT 1
@@ -1314,13 +1314,26 @@ export default router => {
const isAdmin = !!(req.session.admin || req.session.is_moderator);
if (!isOwner && !isAdmin) return res.json({ success: false, msg: 'Unauthorized' }, 403);
const ok = await queue.genThumbnail(item.dest, item.mime, item.id, '', false);
const ok = await queue.genThumbnail(item.dest, item.mime, item.id, item.src || '', false);
await queue.genBlurredThumbnail(item.id, false);
let hasCoverart = false;
if (item.mime && item.mime.startsWith('audio/')) {
const caFile = path.join(cfg.paths.ca, `${item.id}.webp`);
hasCoverart = !!queue._lastCoverExtracted;
if (!hasCoverart) {
try {
const stat = await fs.stat(caFile);
hasCoverart = stat.size > 0;
} catch (_) {}
}
await db`UPDATE items SET has_coverart = ${hasCoverart} WHERE id = ${item.id}`;
}
if (ok) {
db.notify('rethumb', JSON.stringify({ item_id: item.id })).catch(() => {});
db.notify('rethumb', JSON.stringify({ item_id: item.id, has_coverart: hasCoverart })).catch(() => {});
audit.log(req.session.id, 'rethumb_item', 'item', item.id, {}).catch(() => {});
return res.json({ success: true, itemid: item.id });
return res.json({ success: true, itemid: item.id, has_coverart: hasCoverart });
} else {
return res.json({ success: false, msg: 'Thumbnail regeneration failed' }, 500);
}
+99
View File
@@ -408,6 +408,105 @@ export default (router, tpl) => {
return res.reply({ body });
}
const isOnara = !!(cfg.onara !== undefined ? cfg.onara : cfg.websrv?.onara);
if (mode === 'item' && isOnara) {
const itemHtml = tpl.render('ajax-item', data, req);
const isRandom = req.cookies.random_mode === '1';
let itemPage = 1;
if (req.params.page) {
itemPage = parseInt(req.params.page, 10) || 1;
} else if (!isRandom && data.item) {
try {
itemPage = await f0cklib.getItemPage({
targetItemId: data.item.id,
targetItemPinned: data.item.is_pinned,
user: req.params.user,
tag: reqTag,
mime: req.cookies.mime !== undefined ? req.cookies.mime : (req.query?.mime || req.url.qs?.mime || req.params.mime || null),
hall: req.params.hall,
fav: req.params.mode == 'favs',
mode: req.mode,
ratings: (() => { if (req.mode === 2 || req.mode === 3) return null; const r = req.cookies.ratings; return r ? decodeURIComponent(r).split(/[|,]/).filter(x => ['sfw','nsfw','nsfl','untagged'].includes(x)) : null; })(),
session: req.session,
user_id: req.session?.id,
is_admin: req.session?.admin,
exclude: req.session ? (req.session.excluded_tags || []) : [],
strict: !!(req.query?.strict || req.url.qs?.strict || req.session?.strict_mode),
minXdScore: req.url.qs?.min_xd !== undefined ? +req.url.qs.min_xd : (req.session?.min_xd_score || 0),
tagger: req.url.qs?.tagger || null
});
} catch (err) {
console.error('[ONARA] getItemPage error:', err);
itemPage = 1;
}
}
let gridData = await f0cklib.getf0cks({
user: req.params.user,
tag: reqTag,
mime: req.cookies.mime !== undefined ? req.cookies.mime : (req.query?.mime || req.url.qs?.mime || req.params.mime || null),
page: itemPage,
hall: req.params.hall,
fav: req.params.mode == 'favs',
mode: req.mode,
ratings: (() => { if (req.mode === 2 || req.mode === 3) return null; const r = req.cookies.ratings; return r ? decodeURIComponent(r).split(/[|,]/).filter(x => ['sfw','nsfw','nsfl','untagged'].includes(x)) : null; })(),
session: req.session,
user_id: req.session?.id,
is_admin: req.session?.admin,
exclude: req.session ? (req.session.excluded_tags || []) : [],
url: decodeURIComponent(req.url.pathname || req.url),
strict: !!(req.query?.strict || req.url.qs?.strict || req.session?.strict_mode),
explicitStrict: !!(req.query?.strict || req.url.qs?.strict),
random: isRandom,
minXdScore: req.url.qs?.min_xd !== undefined ? +req.url.qs.min_xd : (req.session?.min_xd_score || 0),
lang: req.lang,
tagger: req.url.qs?.tagger || null
});
if (!gridData || !gridData.success) {
gridData = {
items: [],
pagination: { start: 1, end: 1, current: 1, page: 1, cheat: [1], prev: null, next: null },
total: 0,
success: true,
link: { main: '/', path: 'p/', suffix: '' },
tmp: {}
};
}
const activeId = data.item ? data.item.id : req.params.itemid;
const activeSlug = data.item ? data.item.slug : null;
// Mark the active item in the grid for instant highlight
let foundInGrid = false;
if (Array.isArray(gridData.items)) {
for (const it of gridData.items) {
if ((activeSlug && it.slug === activeSlug) || (it.id === activeId)) {
it.is_onara_active = true;
foundInGrid = true;
} else {
it.is_onara_active = false;
}
}
// If not in first page of feed, unshift it so thumbnail is present in background
if (!foundInGrid && data.item) {
gridData.items.unshift({ ...data.item, is_onara_active: true });
}
}
gridData.session = data.session;
gridData.is_onara_item = true;
gridData.onara_item_html = itemHtml;
gridData.item = data.item;
gridData.page_meta = data.page_meta;
gridData.hidePagination = false;
const body = tpl.render('index', gridData, req);
console.log(`[${new Date().toISOString()}] [ROUTE] Render complete (onara direct item) in ${Date.now() - tRenderStart}ms. Total route time: ${Date.now() - tRouteStart}ms`);
return res.reply({ body });
}
const body = tpl.render(mode, data, req);
console.log(`[${new Date().toISOString()}] [ROUTE] Render complete in ${Date.now() - tRenderStart}ms. Total route time: ${Date.now() - tRouteStart}ms`);