This commit is contained in:
2026-09-12 06:01:50 +02:00
parent 2913b03d50
commit 4f6223640f
15 changed files with 991 additions and 159 deletions
+172
View File
@@ -2517,6 +2517,178 @@ const f0cklib = {
return combined.slice(0, maxLimit);
},
getTagFeedItems: async ({
tag,
order = 'desc', // 'desc' = newest first (default), 'asc' = oldest first (chronological)
offset = 0,
limit = 20,
focus_id = null,
mode,
ratings,
session,
exclude,
user_id,
is_admin,
mime
} = {}) => {
if (!tag || !tag.trim()) return { items: [], total: 0 };
const ratingsArr = (Array.isArray(ratings) && ratings.length > 0) ? ratings : null;
const modequery = computeBaseMode(mode, ratingsArr, session);
const globalfilter = !session ? getGlobalfilter() : null;
const excludedTags = session && exclude ? (exclude || []) : [];
const maxLimit = Math.min(Math.max(1, Number(limit) || 20), 50);
const numOffset = Math.max(0, Number(offset) || 0);
const isOwnerOrAdmin = session && is_admin;
const visibilityFilter = isOwnerOrAdmin
? db``
: (session && user_id
? db`AND (COALESCE(items.visibility, 0) = 0 OR items.username = (SELECT "user" FROM "user" WHERE id = ${user_id}))`
: db`AND COALESCE(items.visibility, 0) = 0`);
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 terms = tag.split(',').map(t => t.trim()).filter(Boolean);
const tagConditions = 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}) || '%'
)`;
});
const isDesc = String(order).toLowerCase() === 'desc';
const sortOrderItems = isDesc ? db`items.id desc` : db`items.id asc`;
const sortOrderMi = isDesc ? db`mi.id desc` : db`mi.id asc`;
let computedOffset = numOffset;
let computedLimit = maxLimit;
if (focus_id) {
const numFocusId = await resolveNumericItemId(focus_id);
if (numFocusId) {
try {
const rankRes = await db`
WITH ordered AS (
SELECT items.id, ROW_NUMBER() OVER (ORDER BY ${sortOrderItems}) - 1 as row_num
FROM items
WHERE items.active = true
AND (items.is_deleted IS NOT TRUE)
${visibilityFilter}
${mimeSQL}
AND ${db.unsafe(modequery)}
${globalfilter ? db`AND NOT EXISTS (SELECT 1 FROM tags_assign WHERE item_id = items.id AND (${db.unsafe(globalfilter)}))` : 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``}
${tagConditions}
)
SELECT row_num FROM ordered WHERE id = ${numFocusId} LIMIT 1
`;
if (rankRes.length > 0) {
const rowNum = parseInt(rankRes[0].row_num, 10);
if (rowNum < 30) {
computedOffset = 0;
computedLimit = Math.min(50, Math.max(20, rowNum + 10));
} else {
computedOffset = Math.max(0, rowNum - 6);
computedLimit = 25;
}
}
} catch (err) {
console.error('[TAG-FEED] Error computing focus_id offset:', err);
}
}
}
const countRes = await db`
SELECT count(distinct items.id) as total
FROM items
WHERE items.active = true
AND (items.is_deleted IS NOT TRUE)
${visibilityFilter}
${mimeSQL}
AND ${db.unsafe(modequery)}
${globalfilter ? db`AND NOT EXISTS (SELECT 1 FROM tags_assign WHERE item_id = items.id AND (${db.unsafe(globalfilter)}))` : 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``}
${tagConditions}
`;
const total = parseInt(countRes[0]?.total || 0, 10);
const rows = await db`
WITH matched_items AS (
SELECT items.id, items.title, items.slug, items.mime, items.dest, items.username, items.stamp, items.xd_score, items.width, items.height, items.has_coverart
FROM items
WHERE items.active = true
AND (items.is_deleted IS NOT TRUE)
${visibilityFilter}
${mimeSQL}
AND ${db.unsafe(modequery)}
${globalfilter ? db`AND NOT EXISTS (SELECT 1 FROM tags_assign WHERE item_id = items.id AND (${db.unsafe(globalfilter)}))` : 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``}
${tagConditions}
ORDER BY ${sortOrderItems}
OFFSET ${computedOffset}
LIMIT ${computedLimit}
)
SELECT
mi.*,
uo.display_name,
uo.username_color,
uo.avatar,
uo.avatar_file,
(SELECT ta.tag_id FROM tags_assign ta WHERE ta.item_id = mi.id AND ta.tag_id = ANY(${[1, 2, cfg.nsfl_tag_id || 3]}::int[]) LIMIT 1) as rating_tag_id,
ARRAY(
SELECT t.tag
FROM tags_assign ta
JOIN tags t ON t.id = ta.tag_id
WHERE ta.item_id = mi.id AND ta.tag_id NOT IN (1, 2, 3)
LIMIT 3
) as tags
FROM matched_items mi
LEFT JOIN "user" u ON LOWER(u."user") = LOWER(mi.username)
LEFT JOIN user_options uo ON uo.user_id = u.id
ORDER BY ${sortOrderMi}
`;
const items = rows.map(r => {
const meta = xdScoreMeta(r.xd_score);
const tagId = r.rating_tag_id;
const ratingClass = tagId === 1 ? 'sfw' : (tagId === 2 ? 'nsfw' : (tagId === 3 ? 'nsfl' : 'untagged'));
return {
id: r.id,
title: r.title || null,
slug: r.slug || null,
mime: r.mime,
dest: r.dest,
username: r.username,
display_name: r.display_name || r.username,
username_color: r.username_color || null,
avatar: r.avatar || null,
avatar_file: r.avatar_file || null,
stamp: r.stamp,
tags: r.tags || [],
rating_tag_id: tagId,
rating_class: ratingClass,
xd_score: r.xd_score,
xd_tier: meta.tier,
xd_label: meta.label,
width: r.width,
height: r.height,
has_coverart: !!r.has_coverart
};
});
return { items, total, offset: computedOffset, limit: computedLimit };
},
computeBaseMode,
getGlobalfilter,