Testing: allow selecting multiple ratings for better filtering

This commit is contained in:
2026-05-26 19:10:24 +02:00
parent 0f7eced14d
commit 7c2619e492
10 changed files with 384 additions and 49 deletions

View File

@@ -81,6 +81,37 @@ export default new class {
}
return tmp;
};
/**
* Build a multi-rating SQL WHERE clause fragment from an array of rating strings.
* Supported values: 'sfw', 'nsfw', 'nsfl', 'untagged'
* Returns null if the ratings array is empty or contains all possible values (treat as ALL).
*/
getMultiRatingMode(ratings) {
if (!Array.isArray(ratings) || ratings.length === 0) return null;
const valid = ['sfw', 'nsfw', 'nsfl', 'untagged'];
const filtered = ratings.filter(r => valid.includes(r));
if (filtered.length === 0) return null;
// If all 4 are selected, treat as ALL
if (filtered.includes('sfw') && filtered.includes('nsfw') && filtered.includes('untagged') &&
(!cfg.enable_nsfl || filtered.includes('nsfl'))) return '1 = 1';
const parts = [];
if (filtered.includes('sfw')) {
parts.push('items.id in (select item_id from tags_assign where tag_id = 1)');
}
if (filtered.includes('nsfw')) {
parts.push('items.id in (select item_id from tags_assign where tag_id = 2)');
}
if (filtered.includes('nsfl') && cfg.enable_nsfl) {
parts.push(`items.id in (select item_id from tags_assign where tag_id = ${parseInt(cfg.nsfl_tag_id, 10) || 3})`);
}
if (filtered.includes('untagged')) {
parts.push('not exists (select 1 from tags_assign where item_id = items.id)');
}
if (parts.length === 0) return null;
return '(' + parts.join(' OR ') + ')';
};
createID() {
return crypto.randomBytes(16).toString("hex") + Date.now().toString(24);
};

View File

@@ -95,7 +95,7 @@ const xdScoreMeta = (score) => {
};
export default {
getf0cks: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, page, mode, fav, session, limit, strict, newer, exclude, user_id, random, userHall: rawUserHall, userHallOwner: rawUserHallOwner, minXdScore } = {}) => {
getf0cks: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, page, mode, ratings, fav, session, limit, strict, newer, exclude, user_id, random, userHall: rawUserHall, userHallOwner: rawUserHallOwner, minXdScore } = {}) => {
const user = rawUser ? lib.escapeLike(decodeURI(rawUser)) : null;
// --- title: prefix — search items.title instead of the tags table ---
@@ -149,7 +149,9 @@ export default {
const isStrict = strictParams.length > 0;
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 };
const baseMode = lib.getMode(mode ?? 0);
// Multi-rating support: if `ratings` array provided, build an OR-based SQL fragment
const multiRatingSQL = (Array.isArray(ratings) && ratings.length > 0) ? lib.getMultiRatingMode(ratings) : null;
const baseMode = multiRatingSQL ?? lib.getMode(mode ?? 0);
const modequery = baseMode;
let tagFilter = db``;
@@ -340,7 +342,7 @@ export default {
view_mode: fav ? 'favs' : 'uploads'
};
},
getf0ck: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, itemid: rawItemid, mode, session, strict, exclude, user_id, fav, random, userHall: rawUserHall, userHallOwner: rawUserHallOwner, lang } = {}) => {
getf0ck: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, itemid: rawItemid, mode, ratings, session, strict, exclude, user_id, fav, random, userHall: rawUserHall, userHallOwner: rawUserHallOwner, lang } = {}) => {
const user = rawUser ? lib.escapeLike(decodeURI(rawUser)) : null;
// --- title: prefix — search items.title instead of the tags table ---
@@ -387,7 +389,8 @@ export default {
const tmp = { user, tag: isTitleSearch ? _decodedTag : tag, hall, mime, itemid, strict: strict, userHall: userHallObj || userHallSlug, userHallOwner };
const effMode = Number(mode ?? 0);
const modequery = lib.getMode(effMode);
const multiRatingSQL = (Array.isArray(ratings) && ratings.length > 0) ? lib.getMultiRatingMode(ratings) : null;
const modequery = multiRatingSQL ?? lib.getMode(effMode);
if (itemid === null) {
return {
@@ -739,7 +742,7 @@ export default {
tmp
};
return data;
}, getRandom: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, mode, fav, session, strict, exclude, userHall: rawUserHall, userHallOwner: rawUserHallOwner } = {}) => {
}, getRandom: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, mode, ratings, fav, session, strict, exclude, userHall: rawUserHall, userHallOwner: rawUserHallOwner } = {}) => {
const user = rawUser ? lib.escapeLike(decodeURI(rawUser)) : null;
const hall = rawHall || null;
@@ -779,7 +782,8 @@ export default {
const strictParams = ((strict || (tag && tag.includes(','))) && tag) ? tag.split(',').map(t => lib.slugify(t)).filter(t => t) : [];
const isStrict = strictParams.length > 0;
const baseMode = lib.getMode(mode ?? 0);
const multiRatingSQL = (Array.isArray(ratings) && ratings.length > 0) ? lib.getMultiRatingMode(ratings) : null;
const baseMode = multiRatingSQL ?? lib.getMode(mode ?? 0);
const modequery = baseMode;
let item;
@@ -897,10 +901,13 @@ export default {
limit 1
`;
} else {
// Uniform random logic for global requests (no user/tag)
const baseMode = lib.getMode(mode ?? 0);
const modequery = baseMode;
const tagId = (mode === 0 || mode === 1 || mode === 4) ? (mode === 4 ? (cfg.nsfl_tag_id || 3) : (mode === 1 ? 2 : 1)) : null;
// Uniform random logic for global requests (no user/tag/hall)
// When multi-rating SQL is active, use it directly. Otherwise use the tag-join optimisation.
const globalModeQuery = multiRatingSQL ?? lib.getMode(mode ?? 0);
// tagId optimisation only applies for single native modes (not multi-rating)
const tagId = !multiRatingSQL && (mode === 0 || mode === 1 || mode === 4)
? (mode === 4 ? (cfg.nsfl_tag_id || 3) : (mode === 1 ? 2 : 1))
: null;
// If audio is included, we avoid the strict tagId optimization to ensure audio is visible
const useTagIdOpt = tagId && !mimeParts.includes('audio');
const nsfpIds = cfg.nsfp || [];
@@ -917,7 +924,7 @@ export default {
${mimeSQL}
${checkFilter ? db`AND filter_ta.tag_id IS NULL` : 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``}
${!useTagIdOpt ? db`AND ${db.unsafe(modequery)}` : db``}
${!useTagIdOpt ? db`AND ${db.unsafe(globalModeQuery)}` : db``}
ORDER BY random()
LIMIT 1
`;

View File

@@ -31,11 +31,14 @@ export default (router, tpl) => {
if (cfg.main.development) console.log(`[${new Date().toISOString()}] [AJAX] Starting item load for ${req.params.itemid}`);
const isRandom = query.random === '1' || req.cookies.random_mode === '1';
const ratingsRaw = req.cookies.ratings;
const ratingsArr = ratingsRaw ? decodeURIComponent(ratingsRaw).split(/[|,]/).filter(r => ['sfw','nsfw','nsfl','untagged'].includes(r)) : null;
const itemid = req.params.itemid || req.url.pathname.match(/\/ajax\/item\/(\d+)/)?.[1];
const data = await f0cklib.getf0ck({
itemid: itemid,
mode: query.mode !== undefined ? +query.mode : req.mode,
ratings: ratingsArr,
session: !!req.session,
url: contextUrl,
user: query.user,
@@ -191,6 +194,8 @@ export default (router, tpl) => {
const page = parseInt(query.page) || 1;
const isRandom = query.random === '1' || req.cookies.random_mode === '1';
const ratingsRaw = req.cookies.ratings;
const ratingsArr = ratingsRaw ? decodeURIComponent(ratingsRaw).split(/[|,]/).filter(r => ['sfw','nsfw','nsfl','untagged'].includes(r)) : null;
const data = await f0cklib.getf0cks({
page: page,
@@ -199,6 +204,7 @@ export default (router, tpl) => {
user: query.user || null,
mime: query.mime || (req.cookies.mime || null),
mode: query.mode !== undefined ? +query.mode : req.mode,
ratings: ratingsArr,
session: !!req.session,
exclude: req.session ? (req.session.excluded_tags || []) : [],
user_id: req.session?.id,

View File

@@ -497,6 +497,8 @@ export default router => {
const isFav = req.url.qs.fav === 'true';
const isStrict = req.url.qs.strict === '1';
const mode = req.session?.mode ?? 0;
const ratingsRaw = req.cookies.ratings;
const ratingsArr = ratingsRaw ? decodeURIComponent(ratingsRaw).split(/[|,]/).filter(r => ['sfw','nsfw','nsfl','untagged'].includes(r)) : null;
const data = await f0cklib.getRandom({
user,
@@ -507,6 +509,7 @@ export default router => {
mime,
fav: isFav,
mode,
ratings: ratingsArr && ratingsArr.length > 0 ? ratingsArr : null,
strict: isStrict,
session: !!req.session,
exclude: req.session?.excluded_tags || []

View File

@@ -146,7 +146,14 @@ export default (router, tpl) => {
mode = parseInt(req.url.qs.mode);
}
/* </mode-override> */
const modequery = lib.getMode(mode).replace(/items\.id/g, 'i.id');
// Multi-rating cookie support (same logic as other routes)
const ratingsRaw = req.cookies.ratings;
const ratingsArr = ratingsRaw ? decodeURIComponent(ratingsRaw).split(/[|,]/).filter(r => ['sfw','nsfw','nsfl','untagged'].includes(r)) : null;
const multiRatingSQL = (ratingsArr && ratingsArr.length > 0) ? lib.getMultiRatingMode(ratingsArr) : null;
// Build mode SQL — replace items.id alias with i.id used in the activity query
const modequery = (multiRatingSQL ?? lib.getMode(mode)).replace(/items\.id/g, 'i.id');
const comments = await db`
SELECT c.*, i.mime, i.id as item_id
@@ -843,7 +850,14 @@ export default (router, tpl) => {
mode = parseInt(req.url.qs.mode);
}
/* </mode-override> */
const modequery = lib.getMode(mode).replace(/items\.id/g, 'i.id');
// Multi-rating cookie support (same logic as other routes)
const ratingsRaw = req.cookies.ratings;
const ratingsArr = ratingsRaw ? decodeURIComponent(ratingsRaw).split(/[|,]/).filter(r => ['sfw','nsfw','nsfl','untagged'].includes(r)) : null;
const multiRatingSQL = (ratingsArr && ratingsArr.length > 0) ? lib.getMultiRatingMode(ratingsArr) : null;
// Build mode SQL — replace items.id alias with i.id used in the activity query
const modequery = (multiRatingSQL ?? lib.getMode(mode)).replace(/items\.id/g, 'i.id');
const globalfilter = cfg.nsfp.map(n => `tag_id = ${n}`).join(' or ');
const excludedTags = req.session ? (req.session.excluded_tags || []) : [];

View File

@@ -62,9 +62,12 @@ export default (router, tpl) => {
};
try {
const isRandom = req.cookies.random_mode === '1';
const ratingsRaw = req.cookies.ratings;
const ratingsArr = ratingsRaw ? decodeURIComponent(ratingsRaw).split(/[|,]/).filter(r => ['sfw','nsfw','nsfl','untagged'].includes(r)) : null;
f0cks = await f0cklib.getf0cks({
user: user,
mode: req.mode,
ratings: ratingsArr,
mime: mime,
fav: false,
session: !!req.session,
@@ -83,9 +86,12 @@ export default (router, tpl) => {
if (!userData.is_ghost) {
try {
const isRandom = req.cookies.random_mode === '1';
const ratingsRaw = req.cookies.ratings;
const ratingsArr = ratingsRaw ? decodeURIComponent(ratingsRaw).split(/[|,]/).filter(r => ['sfw','nsfw','nsfl','untagged'].includes(r)) : null;
favs = await f0cklib.getf0cks({
user: user,
mode: req.mode,
ratings: ratingsArr,
mime: mime,
fav: true,
session: !!req.session,
@@ -224,6 +230,7 @@ export default (router, tpl) => {
hall: req.params.hall,
fav: req.params.mode == 'favs',
mode: req.mode,
ratings: (() => { 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,
exclude: req.session ? (req.session.excluded_tags || []) : [],

View File

@@ -39,6 +39,10 @@ export default (router, tpl) => {
}
}
const ratingsRaw = req.cookies.ratings;
const ratingsArr = ratingsRaw ? decodeURIComponent(ratingsRaw).split(/[|,]/).filter(r => ['sfw','nsfw','nsfl','untagged'].includes(r)) : null;
console.log('[RANDOM] ratings cookie:', ratingsRaw, '→ parsed:', ratingsArr);
const data = await f0cklib.getRandom({
user: opts.user,
tag: opts.tag,
@@ -47,6 +51,7 @@ export default (router, tpl) => {
page: opts.page,
fav: opts.mode === 'favs',
mode: req.mode,
ratings: ratingsArr,
strict: opts.strict,
session: !!req.session
});