gfsd
This commit is contained in:
+267
-26
@@ -394,13 +394,20 @@ const buildFeedFilters = async ({
|
||||
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 (
|
||||
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}
|
||||
)`;
|
||||
) or items.id in (
|
||||
select ai.item_id from album_items ai
|
||||
join album_items_tags_assign aita on aita.album_item_id = ai.id
|
||||
join tags t on t.id = aita.tag_id
|
||||
join "user" u on u.id = aita.user_id
|
||||
where t.normalized like '%' || slugify(${term}) || '%'
|
||||
and u.user ilike ${tagger}
|
||||
))`;
|
||||
});
|
||||
tagFilter = db`${conditions}`;
|
||||
}
|
||||
@@ -408,17 +415,29 @@ const buildFeedFilters = async ({
|
||||
const terms = tag.split(',').map(t => t.trim()).filter(Boolean);
|
||||
if (terms.length > 0) {
|
||||
if (isStrict) {
|
||||
tagFilter = db`and items.id in (
|
||||
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}
|
||||
)`;
|
||||
) or items.id in (
|
||||
select ai.item_id
|
||||
from album_items ai
|
||||
join album_items_tags_assign aita on aita.album_item_id = ai.id
|
||||
join tags t on t.id = aita.tag_id
|
||||
where t.normalized = ANY(ARRAY(SELECT slugify(x) FROM unnest(${terms}::text[]) AS x))
|
||||
group by ai.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}) || '%')`;
|
||||
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}) || '%'
|
||||
) or items.id in (
|
||||
select ai.item_id from album_items ai join album_items_tags_assign aita on aita.album_item_id = ai.id join tags t on t.id = aita.tag_id where t.normalized like '%' || slugify(${term}) || '%'
|
||||
))`;
|
||||
});
|
||||
tagFilter = db`${conditions}`;
|
||||
}
|
||||
@@ -802,6 +821,73 @@ const f0cklib = {
|
||||
row.is_audio = !!(row.mime && row.mime.startsWith('audio/'));
|
||||
}
|
||||
|
||||
if (tag && !isTitleSearch && rows.some(r => r.is_album)) {
|
||||
const albumIds = rows.filter(r => r.is_album).map(r => r.id);
|
||||
const terms = tag.split(',').map(t => t.trim()).filter(Boolean);
|
||||
if (terms.length > 0 && albumIds.length > 0) {
|
||||
try {
|
||||
const matchingSubs = await db`
|
||||
SELECT
|
||||
ai.item_id,
|
||||
ai.id as subf0ck_id,
|
||||
ai.slug as subf0ck_slug,
|
||||
ai.dest as subf0ck_dest,
|
||||
ai.mime as subf0ck_mime,
|
||||
ai.order_index
|
||||
FROM album_items ai
|
||||
JOIN album_items_tags_assign aita ON aita.album_item_id = ai.id
|
||||
JOIN tags t ON t.id = aita.tag_id
|
||||
${tagger ? db`JOIN "user" u ON u.id = aita.user_id` : db``}
|
||||
WHERE ai.item_id = ANY(${albumIds}::int[])
|
||||
${tagger ? db`AND u.user ILIKE ${tagger}` : db``}
|
||||
AND (${isStrict
|
||||
? db`t.normalized = ANY(ARRAY(SELECT slugify(x) FROM unnest(${terms}::text[]) AS x))`
|
||||
: db`${terms.map(term => db`t.normalized LIKE '%' || slugify(${term}) || '%'`).reduce((a, b) => db`${a} OR ${b}`)}`
|
||||
})
|
||||
ORDER BY ai.item_id, ai.order_index ASC
|
||||
`;
|
||||
|
||||
const matchMap = new Map();
|
||||
for (const ms of matchingSubs) {
|
||||
if (!matchMap.has(ms.item_id)) {
|
||||
matchMap.set(ms.item_id, {
|
||||
first: ms,
|
||||
matchingIds: new Set([ms.subf0ck_id])
|
||||
});
|
||||
} else {
|
||||
matchMap.get(ms.item_id).matchingIds.add(ms.subf0ck_id);
|
||||
}
|
||||
}
|
||||
|
||||
for (const row of rows) {
|
||||
if (matchMap.has(row.id)) {
|
||||
const info = matchMap.get(row.id);
|
||||
const ms = info.first;
|
||||
row.target_subf0ck_slug = ms.subf0ck_slug || ms.subf0ck_id;
|
||||
if (ms.subf0ck_dest) {
|
||||
const subBase = ms.subf0ck_dest.replace(/\.[^.]+$/, '');
|
||||
row.thumb = `/t/${subBase}.webp`;
|
||||
row.matching_sub_thumb = `/t/${subBase}.webp`;
|
||||
row.matching_sub_dest = ms.subf0ck_dest;
|
||||
row.dest = ms.subf0ck_dest;
|
||||
}
|
||||
if (ms.subf0ck_mime) {
|
||||
row.matching_sub_mime = ms.subf0ck_mime;
|
||||
row.mime = ms.subf0ck_mime;
|
||||
}
|
||||
const mCount = info.matchingIds.size;
|
||||
row.album_count = mCount;
|
||||
if (mCount <= 1) {
|
||||
row.is_album = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[FEED] Error resolving matching subf0cks for tag search:', err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dynamic thumb sizing: applies to the main feed including mime/rating filters.
|
||||
// Only per-user profiles, tag searches, halls, and favorites disable it.
|
||||
const isMainFeed = cfg.websrv.enable_dynamic_thumbs
|
||||
@@ -864,7 +950,7 @@ const f0cklib = {
|
||||
view_mode: fav ? 'favs' : 'uploads'
|
||||
};
|
||||
},
|
||||
getf0ck: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, itemid: rawItemid, mode, ratings, session, strict, exclude, user_id, is_admin, fav, random, userHall: rawUserHall, userHallOwner: rawUserHallOwner, lang, ids } = {}) => {
|
||||
getf0ck: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, itemid: rawItemid, mode, ratings, session, strict, exclude, user_id, is_admin, fav, random, userHall: rawUserHall, userHallOwner: rawUserHallOwner, lang, ids, subf0ck } = {}) => {
|
||||
if (fav && rawUser) {
|
||||
const { isPrivate, isAllowed } = await checkFavoritesAccess(rawUser, { session, user_id, is_admin });
|
||||
if (isPrivate && !isAllowed) {
|
||||
@@ -920,7 +1006,7 @@ const f0cklib = {
|
||||
|
||||
const isNumeric = /^\d+$/.test(String(rawIdOrSlug));
|
||||
let itemLookup = isNumeric ? db`items.id = ${+rawIdOrSlug}` : db`items.slug = ${String(rawIdOrSlug)}`;
|
||||
let requestedSubf0ckSlug = null;
|
||||
let requestedSubf0ckSlug = subf0ck || null;
|
||||
|
||||
if (!isNumeric) {
|
||||
const itemRow = await db`SELECT id FROM items WHERE slug = ${String(rawIdOrSlug)} LIMIT 1`;
|
||||
@@ -1299,20 +1385,58 @@ const f0cklib = {
|
||||
}
|
||||
const coverartUrl = hasCoverart
|
||||
? `${cfg.websrv.paths.coverarts}/${actitem.id}.webp`
|
||||
: `/s/img/music.webp`;
|
||||
: null;
|
||||
|
||||
let album = [];
|
||||
let effectiveItemTags = tags;
|
||||
if (actitem.is_album) {
|
||||
try {
|
||||
const albumRows = await db`
|
||||
let albumRows = await db`
|
||||
SELECT id, dest, mime, size, width, height, order_index, slug, checksum
|
||||
FROM album_items
|
||||
WHERE item_id = ${itemid}
|
||||
ORDER BY order_index ASC
|
||||
`;
|
||||
if (albumRows.length > 0) {
|
||||
const itemIds = albumRows.map(r => r.id);
|
||||
const subTagsRows = await db`
|
||||
SELECT aita.album_item_id, t.id, t.tag, t.normalized
|
||||
FROM album_items_tags_assign aita
|
||||
JOIN tags t ON t.id = aita.tag_id
|
||||
WHERE aita.album_item_id IN ${db(itemIds)}
|
||||
ORDER BY (case when t.id = 1 then 0 when t.id = 2 then 1 when t.normalized = 'nsfl' then 2 else 3 end) asc, t.id asc
|
||||
`.catch(() => []);
|
||||
const tagsBySubId = new Map();
|
||||
for (const st of subTagsRows) {
|
||||
if (!tagsBySubId.has(st.album_item_id)) tagsBySubId.set(st.album_item_id, []);
|
||||
st.badge = lib.getBadge(st);
|
||||
tagsBySubId.get(st.album_item_id).push(st);
|
||||
}
|
||||
|
||||
if (tag && !isTitleSearch) {
|
||||
const terms = tag.split(',').map(t => t.trim()).filter(Boolean);
|
||||
if (terms.length > 0 && tagsBySubId.size > 0) {
|
||||
const matching = albumRows.filter((r, idx) => {
|
||||
let st = tagsBySubId.get(r.id) || [];
|
||||
if (st.length === 0 && (r.order_index === 0 || idx === 0)) {
|
||||
st = tags;
|
||||
}
|
||||
if (isStrict) {
|
||||
return strictParams.every(sp => st.some(t => t.normalized === sp));
|
||||
}
|
||||
return terms.some(term => {
|
||||
const slugTerm = lib.slugify(term);
|
||||
return st.some(t => t.normalized && t.normalized.includes(slugTerm));
|
||||
});
|
||||
});
|
||||
if (matching.length > 0) {
|
||||
albumRows = matching;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
album = await Promise.all(albumRows.map(async (r, idx) => {
|
||||
const order = r.order_index !== undefined && r.order_index !== null ? r.order_index : idx;
|
||||
const order = idx;
|
||||
const subSlug = r.slug || r.id;
|
||||
const subBase = r.dest.replace(/\.[^.]+$/, '');
|
||||
const isAudio = (r.mime || '').startsWith('audio/');
|
||||
@@ -1363,11 +1487,16 @@ const f0cklib = {
|
||||
subThumb = subCover;
|
||||
}
|
||||
} else {
|
||||
subCover = '/s/img/audio.webp';
|
||||
subThumb = '/s/img/audio.webp';
|
||||
subCover = null;
|
||||
subThumb = null;
|
||||
}
|
||||
}
|
||||
|
||||
let subTags = tagsBySubId.get(r.id) || [];
|
||||
if (subTags.length === 0 && (r.order_index === 0 || idx === 0)) {
|
||||
subTags = tags;
|
||||
}
|
||||
|
||||
return {
|
||||
id: r.id,
|
||||
slug: r.slug,
|
||||
@@ -1386,11 +1515,49 @@ const f0cklib = {
|
||||
is_video: (r.mime || '').startsWith('video/'),
|
||||
is_audio: isAudio,
|
||||
is_image: (r.mime || '').startsWith('image/'),
|
||||
has_coverart: isAudio ? (subCover && subCover !== '/s/img/audio.webp') : false,
|
||||
has_coverart: isAudio ? !!subCover : false,
|
||||
coverart: isAudio ? subCover : null,
|
||||
thumb: subThumb
|
||||
thumb: subThumb,
|
||||
tags: subTags
|
||||
};
|
||||
}));
|
||||
|
||||
let reqIdx = 0;
|
||||
if (requestedSubf0ckSlug && album.length > 0) {
|
||||
const found = album.findIndex(s => String(s.slug || '') === String(requestedSubf0ckSlug) || String(s.subf0ck_id || '') === String(requestedSubf0ckSlug) || String(s.id) === String(requestedSubf0ckSlug));
|
||||
if (found !== -1) {
|
||||
reqIdx = found;
|
||||
}
|
||||
}
|
||||
|
||||
if (album.length > 0 && album[reqIdx]) {
|
||||
album.forEach((s, i) => { s.is_first = (i === reqIdx); });
|
||||
const targetSub = album[reqIdx];
|
||||
if (requestedSubf0ckSlug || tag) {
|
||||
requestedSubf0ckSlug = targetSub.slug || targetSub.id;
|
||||
} else {
|
||||
requestedSubf0ckSlug = null;
|
||||
}
|
||||
actitem.dest = targetSub.filename || targetSub.dest.replace(/^\/b\//, '');
|
||||
actitem.mime = targetSub.mime;
|
||||
actitem.width = targetSub.width;
|
||||
actitem.height = targetSub.height;
|
||||
if (targetSub.size) {
|
||||
actitem.size = targetSub.size;
|
||||
}
|
||||
if (targetSub.coverart) {
|
||||
actitem.coverart = targetSub.coverart;
|
||||
}
|
||||
if (targetSub.tags && targetSub.tags.length > 0) {
|
||||
effectiveItemTags = targetSub.tags;
|
||||
if (!effectiveItemTags.some(t => t.id === 1 || t.id === 2 || t.normalized === 'nsfl')) {
|
||||
const ratingTag = tags.find(t => t.id === 1 || t.id === 2 || t.normalized === 'nsfl');
|
||||
if (ratingTag) {
|
||||
effectiveItemTags = [ratingTag, ...effectiveItemTags];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[GETF0CK] Failed to fetch album items:', err.message);
|
||||
@@ -1400,10 +1567,10 @@ const f0cklib = {
|
||||
const duration = Date.now() - startTime;
|
||||
console.log(`[${new Date().toISOString()}] [GETF0CK_OPT] Fetch complete in ${duration}ms`);
|
||||
|
||||
const isNsfl = cfg.enable_nsfl && tags.some(t => t.id == nsfl_id);
|
||||
const isNsfw = tags.some(t => t.id == 2);
|
||||
const isSfw = tags.some(t => t.id == 1);
|
||||
const isTagged = tags.length > 0;
|
||||
const isNsfl = cfg.enable_nsfl && effectiveItemTags.some(t => t.id == nsfl_id);
|
||||
const isNsfw = effectiveItemTags.some(t => t.id == 2);
|
||||
const isSfw = effectiveItemTags.some(t => t.id == 1);
|
||||
const isTagged = effectiveItemTags.length > 0;
|
||||
const isUntagged = !isSfw && !isNsfw && !isNsfl;
|
||||
// Guest rating restriction check for public uploads (visibility === 0)
|
||||
if (!session && !isOwnerOrAdmin && (actitem.visibility || 0) === 0) {
|
||||
@@ -1506,6 +1673,8 @@ const f0cklib = {
|
||||
return `${ratingLabel}${titlePart} · uploaded by ${actitem.username}`;
|
||||
})(),
|
||||
coverart: coverartUrl,
|
||||
has_coverart: !!hasCoverart,
|
||||
is_audio: (actitem.mime || '').startsWith('audio/'),
|
||||
dest: (() => {
|
||||
if (actitem.mime !== 'video/youtube') return `${cfg.websrv.paths.images}/${actitem.dest}`;
|
||||
if (actitem.dest && actitem.dest.startsWith('yt:')) return actitem.dest;
|
||||
@@ -1522,7 +1691,7 @@ const f0cklib = {
|
||||
timefull: new Date(actitem.stamp * 1e3).toISOString()
|
||||
},
|
||||
favorites: favorites,
|
||||
tags: tags,
|
||||
tags: effectiveItemTags,
|
||||
halls: itemHalls,
|
||||
user_halls: userHallsForItem,
|
||||
is_nsfw: isNsfw,
|
||||
@@ -2898,22 +3067,36 @@ const f0cklib = {
|
||||
const isStrict = !!strict || (tag && tag.includes(','));
|
||||
let tagConditions;
|
||||
if (isStrict) {
|
||||
tagConditions = terms.length > 0 ? [db`and items.id in (
|
||||
tagConditions = terms.length > 0 ? [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}
|
||||
)`] : [];
|
||||
) or items.id in (
|
||||
select ai.item_id
|
||||
from album_items ai
|
||||
join album_items_tags_assign aita on aita.album_item_id = ai.id
|
||||
join tags t on t.id = aita.tag_id
|
||||
where t.normalized = ANY(ARRAY(SELECT slugify(x) FROM unnest(${terms}::text[]) AS x))
|
||||
group by ai.item_id
|
||||
having count(distinct t.normalized) = ${terms.length}
|
||||
))` ] : [];
|
||||
} else {
|
||||
tagConditions = terms.map(term => {
|
||||
return db`and items.id in (
|
||||
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}) || '%'
|
||||
)`;
|
||||
) or items.id in (
|
||||
select ai.item_id
|
||||
from album_items ai
|
||||
join album_items_tags_assign aita on aita.album_item_id = ai.id
|
||||
join tags t on t.id = aita.tag_id
|
||||
where t.normalized like '%' || slugify(${term}) || '%'
|
||||
))`;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2975,7 +3158,7 @@ const f0cklib = {
|
||||
|
||||
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
|
||||
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, items.is_album
|
||||
FROM items
|
||||
WHERE items.active = true
|
||||
AND (items.is_deleted IS NOT TRUE)
|
||||
@@ -3009,6 +3192,59 @@ const f0cklib = {
|
||||
ORDER BY ${sortOrderMi}
|
||||
`;
|
||||
|
||||
if (rows.some(r => r.is_album)) {
|
||||
const albumIds = rows.filter(r => r.is_album).map(r => r.id);
|
||||
if (terms.length > 0 && albumIds.length > 0) {
|
||||
try {
|
||||
const matchingSubs = await db`
|
||||
SELECT
|
||||
ai.item_id,
|
||||
ai.id as subf0ck_id,
|
||||
ai.slug as subf0ck_slug,
|
||||
ai.dest as subf0ck_dest,
|
||||
ai.mime as subf0ck_mime,
|
||||
ai.order_index
|
||||
FROM album_items ai
|
||||
JOIN album_items_tags_assign aita ON aita.album_item_id = ai.id
|
||||
JOIN tags t ON t.id = aita.tag_id
|
||||
WHERE ai.item_id = ANY(${albumIds}::int[])
|
||||
AND (${isStrict
|
||||
? db`t.normalized = ANY(ARRAY(SELECT slugify(x) FROM unnest(${terms}::text[]) AS x))`
|
||||
: db`${terms.map(term => db`t.normalized LIKE '%' || slugify(${term}) || '%'`).reduce((a, b) => db`${a} OR ${b}`)}`
|
||||
})
|
||||
ORDER BY ai.item_id, ai.order_index ASC
|
||||
`;
|
||||
|
||||
const matchMap = new Map();
|
||||
for (const ms of matchingSubs) {
|
||||
if (!matchMap.has(ms.item_id)) {
|
||||
matchMap.set(ms.item_id, ms);
|
||||
}
|
||||
}
|
||||
|
||||
for (const row of rows) {
|
||||
if (matchMap.has(row.id)) {
|
||||
const ms = matchMap.get(row.id);
|
||||
row.target_subf0ck_slug = ms.subf0ck_slug || ms.subf0ck_id;
|
||||
if (ms.subf0ck_dest) {
|
||||
const subBase = ms.subf0ck_dest.replace(/\.[^.]+$/, '');
|
||||
row.thumb = `/t/${subBase}.webp`;
|
||||
row.matching_sub_thumb = `/t/${subBase}.webp`;
|
||||
row.matching_sub_dest = ms.subf0ck_dest;
|
||||
row.dest = ms.subf0ck_dest;
|
||||
}
|
||||
if (ms.subf0ck_mime) {
|
||||
row.matching_sub_mime = ms.subf0ck_mime;
|
||||
row.mime = ms.subf0ck_mime;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[TAG-FEED] Error resolving matching subf0cks for tag feed:', err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const items = rows.map(r => {
|
||||
const meta = xdScoreMeta(r.xd_score);
|
||||
const tagId = r.rating_tag_id;
|
||||
@@ -3017,8 +3253,13 @@ const f0cklib = {
|
||||
id: r.id,
|
||||
title: r.title || null,
|
||||
slug: r.slug || null,
|
||||
mime: r.mime,
|
||||
dest: r.dest,
|
||||
mime: r.matching_sub_mime || r.mime,
|
||||
dest: r.matching_sub_dest || r.dest,
|
||||
thumb: r.thumb || `/t/${r.id}.webp`,
|
||||
matching_sub_dest: r.matching_sub_dest || null,
|
||||
matching_sub_mime: r.matching_sub_mime || null,
|
||||
matching_sub_thumb: r.matching_sub_thumb || null,
|
||||
target_subf0ck_slug: r.target_subf0ck_slug || null,
|
||||
username: r.username,
|
||||
display_name: r.display_name || r.username,
|
||||
username_color: r.username_color || null,
|
||||
|
||||
Reference in New Issue
Block a user