This commit is contained in:
2026-09-12 06:57:10 +02:00
parent acf29df204
commit 03abac6bef
11 changed files with 178 additions and 30 deletions
+30 -4
View File
@@ -3196,13 +3196,11 @@ body.sidebar-right-hidden #sidebar-drag-zone {
.sidebar-video-meta { .sidebar-video-meta {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 5px; flex-wrap: wrap;
gap: 3px 5px;
font-size: 0.7em; font-size: 0.7em;
color: #777; color: #777;
margin-top: 2px; margin-top: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
.sidebar-video-time { .sidebar-video-time {
@@ -3232,6 +3230,34 @@ body.sidebar-right-hidden #sidebar-drag-zone {
font-size: 0.9em; font-size: 0.9em;
} }
.sidebar-dev-score {
font-size: 0.85em;
font-weight: 700;
padding: 1px 5px;
border-radius: 3px;
display: inline-flex;
align-items: center;
gap: 3px;
background: rgba(255, 170, 0, 0.16);
color: #ffaa00;
border: 1px solid rgba(255, 170, 0, 0.4);
letter-spacing: 0.02em;
font-family: monospace, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas;
flex-shrink: 0;
}
.sidebar-dev-score i {
font-size: 0.85em;
opacity: 0.85;
}
html[theme='light'] .sidebar-dev-score,
html[theme='paper'] .sidebar-dev-score {
background: rgba(217, 119, 6, 0.14);
color: #b45309;
border-color: rgba(217, 119, 6, 0.35);
}
/* Light / Paper themes */ /* Light / Paper themes */
html[theme='light'] .sidebar-tabs, html[theme='light'] .sidebar-tabs,
html[theme='paper'] .sidebar-tabs { html[theme='paper'] .sidebar-tabs {
+26 -6
View File
@@ -6668,14 +6668,23 @@ window.cancelAnimFrame = (function () {
if (!tagname) return; if (!tagname) return;
suggestions.style.display = 'none'; suggestions.style.display = 'none';
try { try {
const csrf = window.f0ckSession?.csrf_token || document.querySelector('input[name="csrf_token"]')?.value || '';
const res = await fetch('/api/v2/settings/excluded_tags', { const res = await fetch('/api/v2/settings/excluded_tags', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: {
body: JSON.stringify({ tagname }) 'Content-Type': 'application/json',
'X-CSRF-Token': csrf
},
body: JSON.stringify({ tagname, csrf_token: csrf })
}); });
const data = await res.json(); const data = await res.json();
if (data.success) { renderTags(); input.value = ''; } if (data.success) {
else window.flashMessage(data.msg || 'Error adding tag', 3000, 'error'); renderTags();
input.value = '';
if (typeof gridCacheMap !== 'undefined') gridCacheMap.clear();
} else {
window.flashMessage(data.msg || 'Error adding tag', 3000, 'error');
}
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} finally { } finally {
@@ -6699,9 +6708,20 @@ window.cancelAnimFrame = (function () {
e.preventDefault(); e.preventDefault();
const tag = e.target.getAttribute('data-tag'); const tag = e.target.getAttribute('data-tag');
try { try {
const res = await fetch(`/api/v2/settings/excluded_tags/${encodeURIComponent(tag)}`, { method: 'DELETE' }); const csrf = window.f0ckSession?.csrf_token || document.querySelector('input[name="csrf_token"]')?.value || '';
const res = await fetch(`/api/v2/settings/excluded_tags/${encodeURIComponent(tag)}`, {
method: 'DELETE',
headers: {
'X-CSRF-Token': csrf
}
});
const data = await res.json(); const data = await res.json();
if (data.success) renderTags(); if (data.success) {
renderTags();
if (typeof gridCacheMap !== 'undefined') gridCacheMap.clear();
} else {
window.flashMessage(data.msg || 'Error removing tag', 3000, 'error');
}
} catch (e) { console.error(e); } } catch (e) { console.error(e); }
} }
}); });
+16 -1
View File
@@ -1199,6 +1199,19 @@
? `<span class="sidebar-personalized-pill" title="Personalized recommendation based on your interests"><i class="fa-solid fa-wand-magic-sparkles"></i> For You</span>` ? `<span class="sidebar-personalized-pill" title="Personalized recommendation based on your interests"><i class="fa-solid fa-wand-magic-sparkles"></i> For You</span>`
: ''; : '';
const isDev = !!(window.f0ckSession && window.f0ckSession.development);
const rawScore = (typeof video.score === 'number')
? video.score
: (typeof video.rank_score === 'number' ? video.rank_score : (video.xd_score || 0));
const formattedScore = (typeof rawScore === 'number')
? (Number.isInteger(rawScore) ? rawScore : +(rawScore.toFixed(1)))
: rawScore;
const devScoreAttr = isDev ? ` data-score="${escapeHtml(String(formattedScore))}"` : '';
const devScoreBadge = isDev
? `<span class="sidebar-video-score sidebar-dev-score" title="Development Score: ${formattedScore} (Algo: ${video.score ?? video.rank_score ?? 0}, xD: ${video.xd_score ?? 0})"><i class="fa-solid fa-code"></i> score: ${formattedScore}</span>`
: '';
const isTagFeed = options.isTagFeed || false; const isTagFeed = options.isTagFeed || false;
const tagContext = options.tag || null; const tagContext = options.tag || null;
const targetHref = (isTagFeed && tagContext) const targetHref = (isTagFeed && tagContext)
@@ -1207,7 +1220,7 @@
const activeClass = options.isActive ? ' active-tag-item' : ''; const activeClass = options.isActive ? ' active-tag-item' : '';
return ` return `
<div class="sidebar-video-card${activeClass}" data-id="${video.id}" data-slug="${escapeHtml(video.slug || '')}" data-file="${escapeHtml(video.dest || '')}" data-mime="${escapeHtml(video.mime || '')}" data-ext="${escapeHtml(ext ? ext.toLowerCase() : '')}" data-mode="${rClass}" data-personalized="${video.personalized ? 'true' : 'false'}"> <div class="sidebar-video-card${activeClass}" data-id="${video.id}" data-slug="${escapeHtml(video.slug || '')}" data-file="${escapeHtml(video.dest || '')}" data-mime="${escapeHtml(video.mime || '')}" data-ext="${escapeHtml(ext ? ext.toLowerCase() : '')}" data-mode="${rClass}" data-personalized="${video.personalized ? 'true' : 'false'}"${devScoreAttr}>
<a href="${targetHref}" class="sidebar-video-link" data-mode="${rClass}" data-inherit-context="false"> <a href="${targetHref}" class="sidebar-video-link" data-mode="${rClass}" data-inherit-context="false">
<div class="sidebar-video-thumb-wrap" data-file="${escapeHtml(video.dest || '')}" data-mime="${escapeHtml(video.mime || '')}" data-ext="${escapeHtml(ext ? ext.toLowerCase() : '')}" data-mode="${rClass}"> <div class="sidebar-video-thumb-wrap" data-file="${escapeHtml(video.dest || '')}" data-mime="${escapeHtml(video.mime || '')}" data-ext="${escapeHtml(ext ? ext.toLowerCase() : '')}" data-mode="${rClass}">
${thumbContentHtml} ${thumbContentHtml}
@@ -1226,6 +1239,8 @@
${xdBadge} ${xdBadge}
${((timeStr || xdBadge) && forYouBadge) ? `&bull;` : ''} ${((timeStr || xdBadge) && forYouBadge) ? `&bull;` : ''}
${forYouBadge} ${forYouBadge}
${((timeStr || xdBadge || forYouBadge) && devScoreBadge) ? `&bull;` : ''}
${devScoreBadge}
</div> </div>
</div> </div>
</div> </div>
+71 -7
View File
@@ -19,6 +19,10 @@ import queue from "../src/inc/queue.mjs";
import cfg from "../src/inc/config.mjs"; import cfg from "../src/inc/config.mjs";
import fs from "fs/promises"; import fs from "fs/promises";
import path from "path"; import path from "path";
import { fileURLToPath } from "url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const STATE_FILE = path.join(__dirname, '.regen_state.json');
const args = process.argv.slice(2); const args = process.argv.slice(2);
@@ -31,6 +35,8 @@ if (args.length === 0) {
console.log(' node regen.mjs --pdf - Regenerate all PDF items'); console.log(' node regen.mjs --pdf - Regenerate all PDF items');
console.log(' node regen.mjs --youtube - Regenerate all YouTube thumbnails'); console.log(' node regen.mjs --youtube - Regenerate all YouTube thumbnails');
console.log(' node regen.mjs --blur - Regenerate ONLY the blurred thumbnails for all items'); console.log(' node regen.mjs --blur - Regenerate ONLY the blurred thumbnails for all items');
console.log(' --from <id> - Resume/start from a specific item ID (inclusive)');
console.log(' --resume - Resume from last checkpoint saved in .regen_state.json');
process.exit(0); process.exit(0);
} }
@@ -45,6 +51,33 @@ const THUMB_SIZE = 512;
const blurOnly = args.includes('--blur'); const blurOnly = args.includes('--blur');
console.log(`[regen] Thumb size: ${THUMB_SIZE}px\n`); console.log(`[regen] Thumb size: ${THUMB_SIZE}px\n`);
let fromId = null;
const fromIdx = args.indexOf('--from');
if (fromIdx !== -1 && args[fromIdx + 1]) {
fromId = parseInt(args[fromIdx + 1], 10);
if (isNaN(fromId)) {
console.error('Invalid ID provided for --from');
process.exit(1);
}
}
if (!fromId && args.includes('--resume')) {
try {
const raw = await fs.readFile(STATE_FILE, 'utf8');
const state = JSON.parse(raw);
if (state.lastId) {
fromId = state.lastId;
console.log(`[regen] Resuming from checkpoint at item ID: ${fromId}\n`);
}
} catch (e) {
console.warn(`[regen] No previous state checkpoint found to resume from.\n`);
}
}
if (fromId) {
console.log(`[regen] Starting from item ID >= ${fromId}\n`);
}
const regen = async (item) => { const regen = async (item) => {
const { id, dest, mime, src } = item; const { id, dest, mime, src } = item;
@@ -89,46 +122,77 @@ const regen = async (item) => {
// Shared NOT IN clause for Flash exclusion // Shared NOT IN clause for Flash exclusion
const flashExclude = db`mime NOT IN ${db(FLASH_MIMES)}`; const flashExclude = db`mime NOT IN ${db(FLASH_MIMES)}`;
const fromClause = fromId ? db`AND id >= ${fromId}` : db``;
const saveState = async (id) => {
try {
await fs.writeFile(STATE_FILE, JSON.stringify({ lastId: id, timestamp: new Date().toISOString() }, null, 2));
} catch (_) {}
};
let currentItemId = null;
process.on('SIGINT', async () => {
if (currentItemId) {
await saveState(currentItemId);
console.log(`\n[regen] Interrupted! Saved state at item ID ${currentItemId}.`);
console.log(`[regen] Resume anytime with: node scripts/regen.mjs --resume (or --from ${currentItemId})\n`);
}
process.exit(130);
});
try { try {
let items; let items;
if (args.includes('--all')) { if (args.includes('--all')) {
items = await db`SELECT id, dest, mime, src FROM items WHERE active = true AND is_deleted = false AND ${flashExclude} ORDER BY id`; items = await db`SELECT id, dest, mime, src FROM items WHERE active = true AND is_deleted = false AND ${flashExclude} ${fromClause} ORDER BY id`;
console.log(`Regenerating ALL ${items.length} non-Flash items...\n`); console.log(`Regenerating ALL ${items.length} non-Flash items...\n`);
} else if (args.includes('--audio')) { } else if (args.includes('--audio')) {
items = await db`SELECT id, dest, mime, src FROM items WHERE active = true AND is_deleted = false AND mime ILIKE 'audio/%' ORDER BY id`; items = await db`SELECT id, dest, mime, src FROM items WHERE active = true AND is_deleted = false AND mime ILIKE 'audio/%' ${fromClause} ORDER BY id`;
console.log(`Regenerating ${items.length} audio items...\n`); console.log(`Regenerating ${items.length} audio items...\n`);
} else if (args.includes('--pdf')) { } else if (args.includes('--pdf')) {
items = await db`SELECT id, dest, mime, src FROM items WHERE active = true AND is_deleted = false AND mime = 'application/pdf' ORDER BY id`; items = await db`SELECT id, dest, mime, src FROM items WHERE active = true AND is_deleted = false AND mime = 'application/pdf' ${fromClause} ORDER BY id`;
console.log(`Regenerating ${items.length} PDF items...\n`); console.log(`Regenerating ${items.length} PDF items...\n`);
} else if (args.includes('--youtube')) { } else if (args.includes('--youtube')) {
items = await db`SELECT id, dest, mime, src FROM items WHERE active = true AND is_deleted = false AND mime = 'video/youtube' ORDER BY id`; items = await db`SELECT id, dest, mime, src FROM items WHERE active = true AND is_deleted = false AND mime = 'video/youtube' ${fromClause} ORDER BY id`;
console.log(`Regenerating ${items.length} YouTube items...\n`); console.log(`Regenerating ${items.length} YouTube items...\n`);
} else if (blurOnly) { } else if (blurOnly) {
items = await db` items = await db`
SELECT id, dest, mime, src SELECT id, dest, mime, src
FROM items FROM items
WHERE active = true AND is_deleted = false AND ${flashExclude} WHERE active = true AND is_deleted = false AND ${flashExclude} ${fromClause}
ORDER BY id ORDER BY id
`; `;
console.log(`Regenerating ONLY blurred thumbnails for all ${items.length} non-Flash items...\n`); console.log(`Regenerating ONLY blurred thumbnails for all ${items.length} non-Flash items...\n`);
} else { } else {
const ids = args.map(Number).filter(n => !isNaN(n) && n > 0); const positionalArgs = [];
for (let i = 0; i < args.length; i++) {
if (args[i] === '--from') {
i++;
continue;
}
if (args[i].startsWith('--')) continue;
positionalArgs.push(args[i]);
}
const ids = positionalArgs.map(Number).filter(n => !isNaN(n) && n > 0);
if (ids.length === 0) { if (ids.length === 0) {
console.error('No valid item IDs provided.'); console.error('No valid item IDs provided.');
process.exit(1); process.exit(1);
} }
items = await db`SELECT id, dest, mime, src FROM items WHERE id IN ${db(ids)} ORDER BY id`; items = await db`SELECT id, dest, mime, src FROM items WHERE id IN ${db(ids)} ${fromClause} ORDER BY id`;
const found = items.map(i => i.id); const found = items.map(i => i.id);
const missing = ids.filter(id => !found.includes(id)); const missing = ids.filter(id => !found.includes(id));
if (missing.length) console.warn(`Items not found: ${missing.join(', ')}\n`); if (missing.length) console.warn(`Items not found: ${missing.join(', ')}\n`);
} }
for (const item of items) { for (const item of items) {
currentItemId = item.id;
await regen(item); await regen(item);
await saveState(item.id);
} }
// Clean up state file on normal completion
await fs.unlink(STATE_FILE).catch(() => {});
console.log(`\nDone. ${items.length} items processed.`); console.log(`\nDone. ${items.length} items processed.`);
process.exit(0); process.exit(0);
} catch (err) { } catch (err) {
+2 -1
View File
@@ -277,7 +277,8 @@
"video": "Video", "video": "Video",
"audio": "Audio", "audio": "Audio",
"image": "Bild", "image": "Bild",
"flash": "Flash" "flash": "Flash",
"grid_mode": "Thumbnailgröße"
}, },
"shortcuts": { "shortcuts": {
"title": "Tastaturkürzel", "title": "Tastaturkürzel",
+2 -1
View File
@@ -277,7 +277,8 @@
"video": "Video", "video": "Video",
"audio": "Audio", "audio": "Audio",
"image": "Image", "image": "Image",
"flash": "Flash" "flash": "Flash",
"grid_mode": "Thumbnail size"
}, },
"shortcuts": { "shortcuts": {
"title": "Keyboard Shortcuts", "title": "Keyboard Shortcuts",
+2 -1
View File
@@ -275,7 +275,8 @@
"video": "Video", "video": "Video",
"audio": "Audio", "audio": "Audio",
"image": "Afbeelding", "image": "Afbeelding",
"flash": "Flash" "flash": "Flash",
"grid_mode": "Thumbnailgrootte"
}, },
"shortcuts": { "shortcuts": {
"title": "Sneltoetsen", "title": "Sneltoetsen",
+2 -1
View File
@@ -273,7 +273,8 @@
"video": "Video", "video": "Video",
"audio": "Tondatei", "audio": "Tondatei",
"image": "Bild", "image": "Bild",
"flash": "Blitz" "flash": "Blitz",
"grid_mode": "Vorschaubildgröße"
}, },
"shortcuts": { "shortcuts": {
"title": "Tastaturkürzel", "title": "Tastaturkürzel",
+9 -3
View File
@@ -2203,7 +2203,9 @@ const f0cklib = {
xd_label: meta.label, xd_label: meta.label,
width: r.width, width: r.width,
height: r.height, height: r.height,
has_coverart: !!r.has_coverart has_coverart: !!r.has_coverart,
score: 0,
rank_score: 0
}; };
}); });
}, },
@@ -2481,7 +2483,9 @@ const f0cklib = {
width: r.width, width: r.width,
height: r.height, height: r.height,
has_coverart: !!r.has_coverart, has_coverart: !!r.has_coverart,
personalized: true personalized: true,
score: r.rank_score != null ? Math.round(Number(r.rank_score) * 10) / 10 : 0,
rank_score: r.rank_score != null ? Math.round(Number(r.rank_score) * 10) / 10 : 0
}; };
}); });
} catch (err) { } catch (err) {
@@ -2682,7 +2686,9 @@ const f0cklib = {
xd_label: meta.label, xd_label: meta.label,
width: r.width, width: r.width,
height: r.height, height: r.height,
has_coverart: !!r.has_coverart has_coverart: !!r.has_coverart,
score: r.xd_score || 0,
rank_score: 0
}; };
}); });
+3 -1
View File
@@ -761,7 +761,9 @@ export default router => {
rating_class: item.rating_class, rating_class: item.rating_class,
xd_score: item.xd_score, xd_score: item.xd_score,
xd_tier: item.xd_tier, xd_tier: item.xd_tier,
personalized: !!item.personalized personalized: !!item.personalized,
score: typeof item.score === 'number' ? item.score : (typeof item.rank_score === 'number' ? item.rank_score : (item.xd_score || 0)),
rank_score: item.rank_score ?? item.score ?? 0
})) }))
}); });
} catch (err) { } catch (err) {
+15 -4
View File
@@ -83,7 +83,7 @@ export default router => {
}); });
group.post(/\/excluded_tags/, lib.loggedin, async (req, res) => { group.post(/\/excluded_tags/, lib.loggedin, async (req, res) => {
const tagname = req.post.tagname; const tagname = req.post?.tagname || req.body?.tagname;
if (!tagname) return res.json({ success: false, msg: 'No tag provided' }, 400); if (!tagname) return res.json({ success: false, msg: 'No tag provided' }, 400);
const tag = (await db`select id, tag, normalized from tags where normalized = slugify(${tagname})`)[0]; const tag = (await db`select id, tag, normalized from tags where normalized = slugify(${tagname})`)[0];
@@ -92,10 +92,17 @@ export default router => {
await db` await db`
update user_options update user_options
set excluded_tags = array_append(excluded_tags, ${tag.id}) set excluded_tags = array_append(coalesce(excluded_tags, '{}'), ${tag.id})
where user_id = ${+req.session.id} and not (${tag.id} = any(excluded_tags)) where user_id = ${+req.session.id} and not (${tag.id} = any(coalesce(excluded_tags, '{}')))
`; `;
if (req.session) {
if (!req.session.excluded_tags) req.session.excluded_tags = [];
if (!req.session.excluded_tags.includes(tag.id)) {
req.session.excluded_tags.push(tag.id);
}
}
// Return updated list // Return updated list
const tags = await db` const tags = await db`
select t.id, t.tag, t.normalized select t.id, t.tag, t.normalized
@@ -114,10 +121,14 @@ export default router => {
await db` await db`
update user_options update user_options
set excluded_tags = array_remove(excluded_tags, ${tag.id}) set excluded_tags = array_remove(coalesce(excluded_tags, '{}'), ${tag.id})
where user_id = ${+req.session.id} where user_id = ${+req.session.id}
`; `;
if (req.session && req.session.excluded_tags) {
req.session.excluded_tags = req.session.excluded_tags.filter(id => id !== tag.id);
}
const tags = await db` const tags = await db`
select t.id, t.tag, t.normalized select t.id, t.tag, t.normalized
from unnest((select excluded_tags from user_options where user_id = ${+req.session.id})) as et(id) from unnest((select excluded_tags from user_options where user_id = ${+req.session.id})) as et(id)