gfsd
This commit is contained in:
+57
-9
@@ -284,17 +284,65 @@ export default new class {
|
||||
|
||||
return false;
|
||||
};
|
||||
async getTags(itemid, session = null) {
|
||||
async getTags(itemid, session = null, subf0ckId = null) {
|
||||
const isAnonymized = isAnonymizeSession(session);
|
||||
const hasSession = !isAnonymized && !!(session && !session.is_anon && (typeof session === 'object' ? (session.id || session.user) : session));
|
||||
const tags = await db`
|
||||
select "tags".id, "tags".tag, "tags".normalized${hasSession ? db`, "user".user, uo.display_name` : db``}
|
||||
from "tags_assign"
|
||||
left join "tags" on "tags".id = "tags_assign".tag_id
|
||||
${hasSession ? db`left join "user" on "user".id = "tags_assign".user_id left join user_options uo on uo.user_id = "user".id` : db``}
|
||||
where "tags_assign".item_id = ${+itemid}
|
||||
order by (case when "tags".id = 1 then 0 when "tags".id = 2 then 1 when "tags".normalized = 'nsfl' then 2 else 3 end) asc, "tags".id asc
|
||||
`;
|
||||
|
||||
let albumItemId = null;
|
||||
if (subf0ckId !== null && subf0ckId !== undefined && subf0ckId !== '') {
|
||||
if (Number.isInteger(+subf0ckId) && +subf0ckId > 0) {
|
||||
const row = await db`
|
||||
SELECT id FROM album_items
|
||||
WHERE (id = ${+subf0ckId} OR (item_id = ${+itemid} AND order_index = ${+subf0ckId}))
|
||||
AND item_id = ${+itemid}
|
||||
LIMIT 1
|
||||
`;
|
||||
albumItemId = row?.[0]?.id || null;
|
||||
}
|
||||
if (!albumItemId) {
|
||||
const row = await db`
|
||||
SELECT id FROM album_items
|
||||
WHERE slug = ${String(subf0ckId)}
|
||||
AND item_id = ${+itemid}
|
||||
LIMIT 1
|
||||
`;
|
||||
albumItemId = row?.[0]?.id || null;
|
||||
}
|
||||
}
|
||||
|
||||
let tags;
|
||||
if (albumItemId) {
|
||||
tags = await db`
|
||||
select "tags".id, "tags".tag, "tags".normalized${hasSession ? db`, "user".user, uo.display_name` : db``}
|
||||
from "album_items_tags_assign"
|
||||
left join "tags" on "tags".id = "album_items_tags_assign".tag_id
|
||||
${hasSession ? db`left join "user" on "user".id = "album_items_tags_assign".user_id left join user_options uo on uo.user_id = "user".id` : db``}
|
||||
where "album_items_tags_assign".album_item_id = ${+albumItemId}
|
||||
order by (case when "tags".id = 1 then 0 when "tags".id = 2 then 1 when "tags".normalized = 'nsfl' then 2 else 3 end) asc, "tags".id asc
|
||||
`;
|
||||
if (tags.length === 0) {
|
||||
const isOrderZero = await db`SELECT order_index FROM album_items WHERE id = ${albumItemId} LIMIT 1`;
|
||||
if (isOrderZero?.[0]?.order_index === 0) {
|
||||
tags = await db`
|
||||
select "tags".id, "tags".tag, "tags".normalized${hasSession ? db`, "user".user, uo.display_name` : db``}
|
||||
from "tags_assign"
|
||||
left join "tags" on "tags".id = "tags_assign".tag_id
|
||||
${hasSession ? db`left join "user" on "user".id = "tags_assign".user_id left join user_options uo on uo.user_id = "user".id` : db``}
|
||||
where "tags_assign".item_id = ${+itemid}
|
||||
order by (case when "tags".id = 1 then 0 when "tags".id = 2 then 1 when "tags".normalized = 'nsfl' then 2 else 3 end) asc, "tags".id asc
|
||||
`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tags = await db`
|
||||
select "tags".id, "tags".tag, "tags".normalized${hasSession ? db`, "user".user, uo.display_name` : db``}
|
||||
from "tags_assign"
|
||||
left join "tags" on "tags".id = "tags_assign".tag_id
|
||||
${hasSession ? db`left join "user" on "user".id = "tags_assign".user_id left join user_options uo on uo.user_id = "user".id` : db``}
|
||||
where "tags_assign".item_id = ${+itemid}
|
||||
order by (case when "tags".id = 1 then 0 when "tags".id = 2 then 1 when "tags".normalized = 'nsfl' then 2 else 3 end) asc, "tags".id asc
|
||||
`;
|
||||
}
|
||||
let hasRating = false;
|
||||
const cleanTags = [];
|
||||
const excludedTagIds = (session && Array.isArray(session.excluded_tags)) ? session.excluded_tags : [];
|
||||
|
||||
@@ -849,6 +849,8 @@
|
||||
"add_more": "Weitere Subf0cks hinzufügen",
|
||||
"drop_hint": "Wähle oder ziehe mehrere Dateien (Subf0cks) hierher, um ein Album zu erstellen",
|
||||
"select_pictures": "Dateien für Album auswählen",
|
||||
"min_pictures": "Mindestens 2 Subf0cks für ein Album erforderlich"
|
||||
"min_pictures": "Mindestens 2 Subf0cks für ein Album erforderlich",
|
||||
"subf0ck_tags": "Tags für diesen Subf0ck",
|
||||
"subf0ck_tags_placeholder": "Tags für diesen Subf0ck (optional)"
|
||||
}
|
||||
}
|
||||
@@ -849,6 +849,8 @@
|
||||
"add_more": "Add more subf0cks",
|
||||
"drop_hint": "Select or drop multiple files (subf0cks) to create an Album",
|
||||
"select_pictures": "Select files for album",
|
||||
"min_pictures": "Add at least 2 subf0cks for an album"
|
||||
"min_pictures": "Add at least 2 subf0cks for an album",
|
||||
"subf0ck_tags": "Tags for this subf0ck",
|
||||
"subf0ck_tags_placeholder": "Tags for this subf0ck (optional)"
|
||||
}
|
||||
}
|
||||
@@ -839,6 +839,8 @@
|
||||
"add_more": "Meer afbeeldingen toevoegen",
|
||||
"drop_hint": "Selecteer of sleep meerdere afbeeldingen om een album te maken",
|
||||
"select_pictures": "Selecteer afbeeldingen voor album",
|
||||
"min_pictures": "Voeg minimaal 2 afbeeldingen toe voor een album"
|
||||
"min_pictures": "Voeg minimaal 2 afbeeldingen toe voor een album",
|
||||
"subf0ck_tags": "Tags voor deze subf0ck",
|
||||
"subf0ck_tags_placeholder": "Tags voor deze subf0ck (optioneel)"
|
||||
}
|
||||
}
|
||||
@@ -839,6 +839,8 @@
|
||||
"add_more": "Weitere Bildnisse hinzufügen",
|
||||
"drop_hint": "Wähle oder droppe mehrere Bilder für 1 Album",
|
||||
"select_pictures": "Bildnisse fürs Album auswählen",
|
||||
"min_pictures": "Mindestens 2 Bildnisse fürs Album nötig"
|
||||
"min_pictures": "Mindestens 2 Bildnisse fürs Album nötig",
|
||||
"subf0ck_tags": "Tags für dies Unterf0ck",
|
||||
"subf0ck_tags_placeholder": "Tags für dies Unterf0ck (optional)"
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -684,10 +684,10 @@ export default new class queue {
|
||||
const ptSize = String(Math.round(thumbSize * 0.35));
|
||||
try {
|
||||
await this.spawn('magick', [
|
||||
'-size', thumbSpec, 'radial-gradient:#334a15-#1e1e1e',
|
||||
'(', '+clone', '-font', faFont, '-pointsize', ptSize, '-gravity', 'center', '-fill', 'rgba(153,255,0,0.5)', '-annotate', '0', '\uf001', '-blur', '0x12', ')',
|
||||
'-size', thumbSpec, 'radial-gradient:#2c2c2c-#181818',
|
||||
'(', '+clone', '-font', faFont, '-pointsize', ptSize, '-gravity', 'center', '-fill', 'rgba(255,255,255,0.3)', '-annotate', '0', '\uf001', '-blur', '0x12', ')',
|
||||
'-composite',
|
||||
'-font', faFont, '-pointsize', ptSize, '-gravity', 'center', '-fill', '#99ff00', '-annotate', '0', '\uf001',
|
||||
'-font', faFont, '-pointsize', ptSize, '-gravity', 'center', '-fill', 'rgba(240,240,240,0.88)', '-annotate', '0', '\uf001',
|
||||
targetFile
|
||||
]);
|
||||
return true;
|
||||
@@ -698,8 +698,8 @@ export default new class queue {
|
||||
return true;
|
||||
} catch (copyErr) {
|
||||
await this.spawn('magick', [
|
||||
'-size', thumbSpec, 'radial-gradient:#334a15-#1e1e1e',
|
||||
'-gravity', 'center', '-fill', '#99ff00', '-pointsize', '60', '-annotate', '0', '♪',
|
||||
'-size', thumbSpec, 'radial-gradient:#2c2c2c-#181818',
|
||||
'-gravity', 'center', '-fill', 'rgba(240,240,240,0.88)', '-pointsize', '60', '-annotate', '0', '♪',
|
||||
targetFile
|
||||
]).catch(() => {});
|
||||
return false;
|
||||
|
||||
+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,
|
||||
|
||||
@@ -59,7 +59,8 @@ export default (router, tpl) => {
|
||||
strict: query.strict === '1' || query.strict === 'true' || req.session?.strict_mode,
|
||||
explicitStrict: query.strict === '1' || query.strict === 'true',
|
||||
exclude: req.session ? (req.session.excluded_tags || []) : [],
|
||||
user_id: req.session?.id
|
||||
user_id: req.session?.id,
|
||||
subf0ck: query.subf0ck || null
|
||||
});
|
||||
const tAjaxFetch = Date.now();
|
||||
|
||||
|
||||
@@ -1820,19 +1820,42 @@ export default router => {
|
||||
let newRatingId;
|
||||
let currentRatingId = null;
|
||||
|
||||
const subf0ck = req.body?.subf0ck_id || req.post?.subf0ck_id || req.url?.qs?.subf0ck_id || null;
|
||||
let albumItemId = null;
|
||||
if (subf0ck) {
|
||||
if (Number.isInteger(+subf0ck) && +subf0ck > 0) {
|
||||
const row = await db`SELECT id FROM album_items WHERE (id = ${+subf0ck} OR (item_id = ${itemid} AND order_index = ${+subf0ck})) AND item_id = ${itemid} LIMIT 1`;
|
||||
albumItemId = row?.[0]?.id || null;
|
||||
}
|
||||
if (!albumItemId) {
|
||||
const row = await db`SELECT id FROM album_items WHERE slug = ${String(subf0ck)} AND item_id = ${itemid} LIMIT 1`;
|
||||
albumItemId = row?.[0]?.id || null;
|
||||
}
|
||||
}
|
||||
|
||||
await db.begin(async sql => {
|
||||
// Lock the item row exclusively so any concurrent rating update for this post MUST wait
|
||||
await sql`SELECT id FROM items WHERE id = ${itemid} FOR UPDATE`;
|
||||
|
||||
const existingRating = await sql`
|
||||
SELECT tag_id FROM tags_assign
|
||||
WHERE item_id = ${itemid}
|
||||
AND (tag_id IN (1, 2, ${nsfl_id}) OR tag_id IN (SELECT id FROM tags WHERE normalized IN ('sfw', 'nsfw', 'nsfl')))
|
||||
ORDER BY tag_id DESC
|
||||
LIMIT 1
|
||||
`;
|
||||
if (albumItemId) {
|
||||
const existingRating = await sql`
|
||||
SELECT tag_id FROM album_items_tags_assign
|
||||
WHERE album_item_id = ${albumItemId}
|
||||
AND tag_id IN (1, 2, ${nsfl_id})
|
||||
LIMIT 1
|
||||
`;
|
||||
currentRatingId = existingRating.length > 0 ? existingRating[0].tag_id : null;
|
||||
} else {
|
||||
const existingRating = await sql`
|
||||
SELECT tag_id FROM tags_assign
|
||||
WHERE item_id = ${itemid}
|
||||
AND (tag_id IN (1, 2, ${nsfl_id}) OR tag_id IN (SELECT id FROM tags WHERE normalized IN ('sfw', 'nsfw', 'nsfl')))
|
||||
ORDER BY tag_id DESC
|
||||
LIMIT 1
|
||||
`;
|
||||
currentRatingId = existingRating.length > 0 ? existingRating[0].tag_id : null;
|
||||
}
|
||||
|
||||
currentRatingId = existingRating.length > 0 ? existingRating[0].tag_id : null;
|
||||
const reqRating = req.body?.rating || req.post?.rating || req.url?.qs?.rating;
|
||||
if (reqRating === 'sfw') {
|
||||
newRatingId = 1;
|
||||
@@ -1851,19 +1874,32 @@ export default router => {
|
||||
}
|
||||
}
|
||||
|
||||
// Remove ALL existing rating tags for this item atomically
|
||||
await sql`
|
||||
DELETE FROM tags_assign
|
||||
WHERE item_id = ${itemid}
|
||||
AND (tag_id IN (1, 2, ${nsfl_id}) OR tag_id IN (SELECT id FROM tags WHERE normalized IN ('sfw', 'nsfw', 'nsfl')))
|
||||
`;
|
||||
|
||||
// Insert new rating tag
|
||||
if (newRatingId > 0) {
|
||||
if (albumItemId) {
|
||||
await sql`
|
||||
INSERT INTO tags_assign (item_id, tag_id, user_id)
|
||||
VALUES (${itemid}, ${newRatingId}, ${req.session.id})
|
||||
DELETE FROM album_items_tags_assign
|
||||
WHERE album_item_id = ${albumItemId} AND tag_id IN (1, 2, ${nsfl_id})
|
||||
`;
|
||||
if (newRatingId > 0) {
|
||||
await sql`
|
||||
INSERT INTO album_items_tags_assign (album_item_id, tag_id, user_id)
|
||||
VALUES (${albumItemId}, ${newRatingId}, ${req.session.id})
|
||||
`;
|
||||
}
|
||||
} else {
|
||||
// Remove ALL existing rating tags for this item atomically
|
||||
await sql`
|
||||
DELETE FROM tags_assign
|
||||
WHERE item_id = ${itemid}
|
||||
AND (tag_id IN (1, 2, ${nsfl_id}) OR tag_id IN (SELECT id FROM tags WHERE normalized IN ('sfw', 'nsfw', 'nsfl')))
|
||||
`;
|
||||
|
||||
// Insert new rating tag
|
||||
if (newRatingId > 0) {
|
||||
await sql`
|
||||
INSERT INTO tags_assign (item_id, tag_id, user_id)
|
||||
VALUES (${itemid}, ${newRatingId}, ${req.session.id})
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure blurred thumbnail exists
|
||||
@@ -1875,15 +1911,16 @@ export default router => {
|
||||
const newRating = newRatingId === 1 ? 'sfw' : (newRatingId === 2 ? 'nsfw' : 'nsfl');
|
||||
const ratingLabels = { 1: 'sfw', 2: 'nsfw', [nsfl_id]: 'nsfl' };
|
||||
const oldRating = currentRatingId ? ratingLabels[currentRatingId] : 'none';
|
||||
audit.log(req.session.id, 'update_rating', 'item', itemid, { old: oldRating, new: newRating }).catch(() => {});
|
||||
audit.log(req.session.id, 'update_rating', 'item', itemid, { old: oldRating, new: newRating, subf0ck_id: subf0ck }).catch(() => {});
|
||||
|
||||
// Notify that tags have changed
|
||||
const freshTags = await lib.getTags(itemid);
|
||||
db.notify('tags', JSON.stringify({ item_id: itemid, fresh: true, tags: freshTags })).catch(() => {});
|
||||
const freshTags = await lib.getTags(itemid, req.session, subf0ck);
|
||||
db.notify('tags', JSON.stringify({ item_id: itemid, subf0ck_id: subf0ck, fresh: true, tags: freshTags })).catch(() => {});
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
itemid: itemid,
|
||||
subf0ck_id: subf0ck,
|
||||
rating: newRating,
|
||||
tags: freshTags
|
||||
});
|
||||
|
||||
+173
-62
@@ -272,9 +272,10 @@ export default router => {
|
||||
});
|
||||
}
|
||||
|
||||
const subf0ck = req.url?.qs?.subf0ck_id || req.url?.qs?.subf0ck || null;
|
||||
return res.json({
|
||||
success: true,
|
||||
tags: await lib.getTags(+req.params.postid, req.session)
|
||||
tags: await lib.getTags(+req.params.postid, req.session, subf0ck)
|
||||
});
|
||||
});
|
||||
|
||||
@@ -309,6 +310,19 @@ export default router => {
|
||||
});
|
||||
}
|
||||
|
||||
const subf0ck = req.post?.subf0ck_id || req.body?.subf0ck_id || req.post?.subf0ck || req.body?.subf0ck || null;
|
||||
let albumItemId = null;
|
||||
if (subf0ck) {
|
||||
if (Number.isInteger(+subf0ck) && +subf0ck > 0) {
|
||||
const row = await db`SELECT id FROM album_items WHERE (id = ${+subf0ck} OR (item_id = ${postid} AND order_index = ${+subf0ck})) AND item_id = ${postid} LIMIT 1`;
|
||||
albumItemId = row?.[0]?.id || null;
|
||||
}
|
||||
if (!albumItemId) {
|
||||
const row = await db`SELECT id FROM album_items WHERE slug = ${String(subf0ck)} AND item_id = ${postid} LIMIT 1`;
|
||||
albumItemId = row?.[0]?.id || null;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
let tagid = (await db`
|
||||
select id
|
||||
@@ -326,37 +340,51 @@ export default router => {
|
||||
`)[0].id;
|
||||
}
|
||||
|
||||
await db`
|
||||
insert into "tags_assign" ${db({
|
||||
tag_id: +tagid,
|
||||
item_id: +postid,
|
||||
user_id: +req.session.id
|
||||
})
|
||||
}
|
||||
`;
|
||||
if (albumItemId) {
|
||||
await db`
|
||||
INSERT INTO "album_items_tags_assign" (album_item_id, tag_id, user_id)
|
||||
VALUES (${+albumItemId}, ${+tagid}, ${+req.session.id})
|
||||
ON CONFLICT DO NOTHING
|
||||
`;
|
||||
await db`
|
||||
INSERT INTO "tags_assign" (item_id, tag_id, user_id)
|
||||
VALUES (${+postid}, ${+tagid}, ${+req.session.id})
|
||||
ON CONFLICT DO NOTHING
|
||||
`;
|
||||
} else {
|
||||
await db`
|
||||
insert into "tags_assign" ${db({
|
||||
tag_id: +tagid,
|
||||
item_id: +postid,
|
||||
user_id: +req.session.id
|
||||
})
|
||||
}
|
||||
`;
|
||||
}
|
||||
} catch (err) {
|
||||
const isDuplicate = err.code === '23505' || err.constraint?.includes('tags_assign');
|
||||
return res.json({
|
||||
success: false,
|
||||
msg: isDuplicate ? 'Tag already exists' : 'Failed to add tag',
|
||||
tags: await lib.getTags(postid)
|
||||
tags: await lib.getTags(postid, req.session, subf0ck)
|
||||
});
|
||||
}
|
||||
|
||||
const freshTags = await lib.getTags(postid);
|
||||
const freshTags = await lib.getTags(postid, req.session, subf0ck);
|
||||
if (req.session?.is_anon) {
|
||||
await logAnonActivity(req, {
|
||||
action: 'tag',
|
||||
targetId: postid,
|
||||
details: { tag: tagname }
|
||||
details: { tag: tagname, subf0ck_id: subf0ck }
|
||||
});
|
||||
}
|
||||
console.log(`[API] Notifying 'tags' for item ${postid} with ${freshTags.length} tags`);
|
||||
await db.notify('tags', JSON.stringify({ item_id: postid, fresh: true, tags: freshTags }));
|
||||
console.log(`[API] Notifying 'tags' for item ${postid} (subf0ck: ${subf0ck || 'none'}) with ${freshTags.length} tags`);
|
||||
await db.notify('tags', JSON.stringify({ item_id: postid, subf0ck_id: subf0ck, fresh: true, tags: freshTags }));
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
postid: postid,
|
||||
subf0ck_id: subf0ck,
|
||||
tag: tagname,
|
||||
tags: freshTags
|
||||
});
|
||||
@@ -391,44 +419,90 @@ export default router => {
|
||||
let nextTagId;
|
||||
let ratingTagId = 0;
|
||||
|
||||
const subf0ck = req.body?.subf0ck_id || req.post?.subf0ck_id || req.url?.qs?.subf0ck_id || null;
|
||||
let albumItemId = null;
|
||||
if (subf0ck) {
|
||||
if (Number.isInteger(+subf0ck) && +subf0ck > 0) {
|
||||
const row = await db`SELECT id FROM album_items WHERE (id = ${+subf0ck} OR (item_id = ${postid} AND order_index = ${+subf0ck})) AND item_id = ${postid} LIMIT 1`;
|
||||
albumItemId = row?.[0]?.id || null;
|
||||
}
|
||||
if (!albumItemId) {
|
||||
const row = await db`SELECT id FROM album_items WHERE slug = ${String(subf0ck)} AND item_id = ${postid} LIMIT 1`;
|
||||
albumItemId = row?.[0]?.id || null;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await db.begin(async sql => {
|
||||
// Lock the item row exclusively
|
||||
await sql`SELECT id FROM items WHERE id = ${postid} FOR UPDATE`;
|
||||
|
||||
const existingRating = await sql`
|
||||
SELECT tag_id FROM tags_assign
|
||||
WHERE item_id = ${postid}
|
||||
AND (tag_id IN (1, 2, ${nsflId}) OR tag_id IN (SELECT id FROM tags WHERE normalized IN ('sfw', 'nsfw', 'nsfl')))
|
||||
ORDER BY tag_id DESC
|
||||
LIMIT 1
|
||||
`;
|
||||
|
||||
ratingTagId = existingRating.length > 0 ? existingRating[0].tag_id : 0;
|
||||
const reqRating = req.body?.rating || req.post?.rating || req.url?.qs?.rating;
|
||||
if (reqRating === 'sfw') {
|
||||
nextTagId = 1;
|
||||
} else if (reqRating === 'nsfw') {
|
||||
nextTagId = 2;
|
||||
} else if (reqRating === 'nsfl') {
|
||||
nextTagId = nsflId;
|
||||
} else {
|
||||
const cycleIdx = cycle.indexOf(ratingTagId); // -1 if untagged → (−1+1)%3 = 0 → SFW
|
||||
nextTagId = cycle[(cycleIdx + 1) % cycle.length];
|
||||
}
|
||||
|
||||
// Remove ALL existing rating tags for this item atomically
|
||||
await sql`
|
||||
DELETE FROM tags_assign
|
||||
WHERE item_id = ${postid}
|
||||
AND (tag_id IN (1, 2, ${nsflId}) OR tag_id IN (SELECT id FROM tags WHERE normalized IN ('sfw', 'nsfw', 'nsfl')))
|
||||
`;
|
||||
|
||||
if (nextTagId > 0) {
|
||||
await sql`
|
||||
INSERT INTO tags_assign (item_id, tag_id, user_id)
|
||||
VALUES (${postid}, ${nextTagId}, ${+req.session.id})
|
||||
if (albumItemId) {
|
||||
const existingRating = await sql`
|
||||
SELECT tag_id FROM album_items_tags_assign
|
||||
WHERE album_item_id = ${albumItemId}
|
||||
AND tag_id IN (1, 2, ${nsflId})
|
||||
LIMIT 1
|
||||
`;
|
||||
ratingTagId = existingRating.length > 0 ? existingRating[0].tag_id : 0;
|
||||
const reqRating = req.body?.rating || req.post?.rating || req.url?.qs?.rating;
|
||||
if (reqRating === 'sfw') {
|
||||
nextTagId = 1;
|
||||
} else if (reqRating === 'nsfw') {
|
||||
nextTagId = 2;
|
||||
} else if (reqRating === 'nsfl') {
|
||||
nextTagId = nsflId;
|
||||
} else {
|
||||
const cycleIdx = cycle.indexOf(ratingTagId);
|
||||
nextTagId = cycle[(cycleIdx + 1) % cycle.length];
|
||||
}
|
||||
|
||||
await sql`
|
||||
DELETE FROM album_items_tags_assign
|
||||
WHERE album_item_id = ${albumItemId}
|
||||
AND tag_id IN (1, 2, ${nsflId})
|
||||
`;
|
||||
if (nextTagId > 0) {
|
||||
await sql`
|
||||
INSERT INTO album_items_tags_assign (album_item_id, tag_id, user_id)
|
||||
VALUES (${albumItemId}, ${nextTagId}, ${+req.session.id})
|
||||
`;
|
||||
}
|
||||
} else {
|
||||
const existingRating = await sql`
|
||||
SELECT tag_id FROM tags_assign
|
||||
WHERE item_id = ${postid}
|
||||
AND (tag_id IN (1, 2, ${nsflId}) OR tag_id IN (SELECT id FROM tags WHERE normalized IN ('sfw', 'nsfw', 'nsfl')))
|
||||
ORDER BY tag_id DESC
|
||||
LIMIT 1
|
||||
`;
|
||||
|
||||
ratingTagId = existingRating.length > 0 ? existingRating[0].tag_id : 0;
|
||||
const reqRating = req.body?.rating || req.post?.rating || req.url?.qs?.rating;
|
||||
if (reqRating === 'sfw') {
|
||||
nextTagId = 1;
|
||||
} else if (reqRating === 'nsfw') {
|
||||
nextTagId = 2;
|
||||
} else if (reqRating === 'nsfl') {
|
||||
nextTagId = nsflId;
|
||||
} else {
|
||||
const cycleIdx = cycle.indexOf(ratingTagId); // -1 if untagged → (−1+1)%3 = 0 → SFW
|
||||
nextTagId = cycle[(cycleIdx + 1) % cycle.length];
|
||||
}
|
||||
|
||||
// Remove ALL existing rating tags for this item atomically
|
||||
await sql`
|
||||
DELETE FROM tags_assign
|
||||
WHERE item_id = ${postid}
|
||||
AND (tag_id IN (1, 2, ${nsflId}) OR tag_id IN (SELECT id FROM tags WHERE normalized IN ('sfw', 'nsfw', 'nsfl')))
|
||||
`;
|
||||
|
||||
if (nextTagId > 0) {
|
||||
await sql`
|
||||
INSERT INTO tags_assign (item_id, tag_id, user_id)
|
||||
VALUES (${postid}, ${nextTagId}, ${+req.session.id})
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Automatically generate/verify blurred thumbnail on cycle
|
||||
@@ -534,46 +608,83 @@ export default router => {
|
||||
const postid = +req.params.postid;
|
||||
const tagname = decodeURIComponent(req.params.tagname);
|
||||
|
||||
const tags = await lib.getTags(postid);
|
||||
const subf0ck = req.post?.subf0ck_id || req.body?.subf0ck_id || req.url?.qs?.subf0ck_id || req.url?.qs?.subf0ck || null;
|
||||
let albumItemId = null;
|
||||
if (subf0ck) {
|
||||
if (Number.isInteger(+subf0ck) && +subf0ck > 0) {
|
||||
const row = await db`SELECT id FROM album_items WHERE (id = ${+subf0ck} OR (item_id = ${postid} AND order_index = ${+subf0ck})) AND item_id = ${postid} LIMIT 1`;
|
||||
albumItemId = row?.[0]?.id || null;
|
||||
}
|
||||
if (!albumItemId) {
|
||||
const row = await db`SELECT id FROM album_items WHERE slug = ${String(subf0ck)} AND item_id = ${postid} LIMIT 1`;
|
||||
albumItemId = row?.[0]?.id || null;
|
||||
}
|
||||
}
|
||||
|
||||
const tagid = tags.filter(t => t.tag === tagname)[0]?.id ?? null;
|
||||
const tags = await lib.getTags(postid, req.session, subf0ck);
|
||||
|
||||
const tagid = tags.filter(t => t.tag === tagname || t.normalized === tagname.toLowerCase())[0]?.id ?? null;
|
||||
|
||||
if (!tagid) {
|
||||
return res.json({
|
||||
success: false,
|
||||
msg: 'tag is not assigned',
|
||||
tags: await lib.getTags(postid)
|
||||
tags: await lib.getTags(postid, req.session, subf0ck)
|
||||
});
|
||||
}
|
||||
|
||||
let q = await db`
|
||||
delete from "tags_assign"
|
||||
where tag_id = ${+tagid}
|
||||
and item_id = ${+postid}
|
||||
`;
|
||||
const reply = !!q;
|
||||
let reply = false;
|
||||
if (albumItemId) {
|
||||
const q = await db`
|
||||
DELETE FROM album_items_tags_assign
|
||||
WHERE album_item_id = ${+albumItemId}
|
||||
AND tag_id = ${+tagid}
|
||||
`;
|
||||
reply = !!q;
|
||||
|
||||
const otherUses = await db`
|
||||
SELECT 1 FROM album_items_tags_assign aita
|
||||
JOIN album_items ai ON ai.id = aita.album_item_id
|
||||
WHERE ai.item_id = ${postid} AND aita.tag_id = ${+tagid}
|
||||
LIMIT 1
|
||||
`;
|
||||
if (otherUses.length === 0) {
|
||||
await db`
|
||||
DELETE FROM tags_assign
|
||||
WHERE item_id = ${postid} AND tag_id = ${+tagid}
|
||||
`.catch(() => {});
|
||||
}
|
||||
} else {
|
||||
let q = await db`
|
||||
delete from "tags_assign"
|
||||
where tag_id = ${+tagid}
|
||||
and item_id = ${+postid}
|
||||
`;
|
||||
reply = !!q;
|
||||
}
|
||||
|
||||
if (reply) {
|
||||
const reason = req.post.reason || req.url.qs.reason || 'No reason provided';
|
||||
await audit.log(req.session.id, 'delete_tag', 'item', postid, { tag: tagname, reason });
|
||||
const reason = req.post?.reason || req.url?.qs?.reason || 'No reason provided';
|
||||
await audit.log(req.session.id, 'delete_tag', 'item', postid, { tag: tagname, subf0ck_id: subf0ck, reason });
|
||||
if (req.session?.is_anon) {
|
||||
await logAnonActivity(req, {
|
||||
action: 'delete_tag',
|
||||
targetId: postid,
|
||||
details: { tag: tagname, reason }
|
||||
details: { tag: tagname, subf0ck_id: subf0ck, reason }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const freshTags = await lib.getTags(postid);
|
||||
console.log(`[API] Notifying 'tags' (delete) for item ${postid}`);
|
||||
await db.notify('tags', JSON.stringify({ item_id: postid, fresh: true, tags: freshTags }));
|
||||
const freshTags = await lib.getTags(postid, req.session, subf0ck);
|
||||
console.log(`[API] Notifying 'tags' (delete) for item ${postid} (subf0ck: ${subf0ck || 'none'})`);
|
||||
await db.notify('tags', JSON.stringify({ item_id: postid, subf0ck_id: subf0ck, fresh: true, tags: freshTags }));
|
||||
|
||||
return res.json({
|
||||
success: reply,
|
||||
tagid,
|
||||
subf0ck_id: subf0ck,
|
||||
tags: freshTags
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -285,7 +285,8 @@ export default (router, tpl) => {
|
||||
random: req.cookies.random_mode === '1',
|
||||
minXdScore: req.params.itemid ? 0 : (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
|
||||
tagger: req.url.qs?.tagger || null,
|
||||
subf0ck: req.query?.subf0ck || req.url.qs?.subf0ck || null
|
||||
});
|
||||
console.log(`[DEBUG] Checking strict mode: query=${req.query?.strict}, session=${req.session?.strict_mode}, effective=${!!(req.query?.strict || req.url.qs?.strict || req.session?.strict_mode)}`);
|
||||
console.log(`[${new Date().toISOString()}] [ROUTE] Data fetch complete in ${Date.now() - tRouteStart}ms`);
|
||||
|
||||
@@ -274,6 +274,17 @@ export default (router, tpl) => {
|
||||
if (f0ck[0].mime.startsWith('audio')) {
|
||||
await moveSafe(p.ca, caDst);
|
||||
}
|
||||
|
||||
if (f0ck[0].is_album) {
|
||||
try {
|
||||
const subItems = await db`SELECT dest FROM album_items WHERE item_id = ${id}`;
|
||||
for (const sub of subItems) {
|
||||
const subBase = sub.dest.replace(/\.[^.]+$/, '');
|
||||
await moveSafe(path.join(cfg.paths.pending, 'b', sub.dest), path.join(cfg.paths.b, sub.dest));
|
||||
await moveSafe(path.join(cfg.paths.pending, 't', `${subBase}.webp`), path.join(cfg.paths.t, `${subBase}.webp`));
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
break;
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import cfg from "../config.mjs";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import db from "../sql.mjs";
|
||||
import queue from "../queue.mjs";
|
||||
|
||||
export default (router, tpl) => {
|
||||
router.static({
|
||||
@@ -61,6 +63,52 @@ export default (router, tpl) => {
|
||||
return res.end(content);
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// Dynamic generation or fallback for subf0cks / albums
|
||||
const base = file.replace(/\.[^.]+$/, '').replace(/_blur$/, '');
|
||||
try {
|
||||
const subRow = await db`SELECT id, item_id, dest, mime FROM album_items WHERE dest LIKE ${base + '.%'} LIMIT 1`;
|
||||
if (subRow.length > 0) {
|
||||
const sub = subRow[0];
|
||||
const srcFile = path.join(cfg.paths.b, sub.dest);
|
||||
const thumbDest = path.join(cfg.paths.t, `${base}.webp`);
|
||||
const srcStat = await fs.stat(srcFile).catch(() => null);
|
||||
if (srcStat && srcStat.size > 0) {
|
||||
if (sub.mime.startsWith('image/')) {
|
||||
await queue.spawn('magick', [srcFile + '[0]', '-resize', '256x256^', '-gravity', 'center', '-crop', '256x256+0+0', '+repage', thumbDest]);
|
||||
} else if (sub.mime.startsWith('video/')) {
|
||||
await queue.spawn('ffmpegthumbnailer', ['-i', srcFile, '-s', '256', '-o', thumbDest]).catch(async () => {
|
||||
await queue.spawn('ffmpeg', ['-y', '-ss', '00:00:01', '-i', srcFile, '-vframes', '1', '-vf', 'scale=256:256:force_original_aspect_ratio=increase,crop=256:256', thumbDest]);
|
||||
});
|
||||
}
|
||||
const genStat = await fs.stat(thumbDest).catch(() => null);
|
||||
if (genStat && genStat.size > 0) {
|
||||
const content = await fs.readFile(thumbDest);
|
||||
res.writeHead(200, {
|
||||
'Content-Length': genStat.size,
|
||||
'Content-Type': 'image/webp',
|
||||
'Cache-Control': 'public, max-age=3600'
|
||||
});
|
||||
return res.end(content);
|
||||
}
|
||||
}
|
||||
// Fallback to parent album thumbnail if subf0ck media file is missing
|
||||
const parentThumb = path.join(cfg.paths.t, `${sub.item_id}.webp`);
|
||||
const pStat = await fs.stat(parentThumb).catch(() => null);
|
||||
if (pStat && pStat.size > 0) {
|
||||
const content = await fs.readFile(parentThumb);
|
||||
res.writeHead(200, {
|
||||
'Content-Length': pStat.size,
|
||||
'Content-Type': 'image/webp',
|
||||
'Cache-Control': 'public, max-age=3600'
|
||||
});
|
||||
return res.end(content);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[STATIC /t/] Dynamic subf0ck thumbnail generation error:', e.message);
|
||||
}
|
||||
|
||||
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
||||
return res.end('404 - file not found.');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user