onara
This commit is contained in:
+331
-111
@@ -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`;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user