fdsa
This commit is contained in:
+44
-7
@@ -468,7 +468,13 @@ export default new class queue {
|
||||
this._lastCoverExtracted = false; // Reset state for this call
|
||||
if (link && typeof link === 'string' && link.match(/soundcloud/)) {
|
||||
const proxyArgs = (cfg.main.socks && cfg.main.socks !== 'undefined' && cfg.main.socks !== '') ? ['--proxy', cfg.main.socks.includes('://') ? cfg.main.socks : `socks5h://${cfg.main.socks}`] : [];
|
||||
let cover = (await this.spawn('yt-dlp', [...proxyArgs, '-f', 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w', '--get-thumbnail', link])).stdout.trim().split('\n').map(l => l.trim()).filter(l => l.length > 0).pop();
|
||||
let cover = null;
|
||||
try {
|
||||
const ytRes = await this.spawn('yt-dlp', [...proxyArgs, '--get-thumbnail', link], { quiet: true });
|
||||
cover = ytRes.stdout.trim().split('\n').map(l => l.trim()).filter(l => l.length > 0).pop();
|
||||
} catch (err) {
|
||||
console.warn(`[QUEUE] yt-dlp thumbnail fetch failed for SoundCloud track (${link}):`, err.message || err);
|
||||
}
|
||||
if (cover && !cover.match(/default_avatar/)) {
|
||||
cover = cover.replace(/-(large|original)\./, '-t500x500.');
|
||||
try {
|
||||
@@ -517,13 +523,9 @@ export default new class queue {
|
||||
}
|
||||
} catch (_) { }
|
||||
}
|
||||
// If no cover art found, use audio.webp as the thumbnail
|
||||
// If no cover art found, generate audio placeholder matching .sidebar-media-placeholder.audio
|
||||
if (!coverExtracted) {
|
||||
const audioFallback = path.join(cfg.paths.s, 'img', 'audio.webp');
|
||||
await fs.promises.copyFile(audioFallback, tmpFile).catch(async () => {
|
||||
// If copy fails, fall back to generated placeholder
|
||||
await this.spawn('magick', ['-size', thumbSpec, 'xc:#1a1a1a', '-gravity', 'center', '-fill', '#666', '-pointsize', '40', '-annotate', '0', '♪', tmpFile]).catch(() => {});
|
||||
});
|
||||
await this.genAudioPlaceholder(tmpFile, thumbSpec, thumbSize);
|
||||
}
|
||||
// Store extraction result for caller
|
||||
this._lastCoverExtracted = coverExtracted;
|
||||
@@ -647,6 +649,13 @@ export default new class queue {
|
||||
// Cleanup temp files
|
||||
await fs.promises.unlink(tmpFile).catch(() => {});
|
||||
await fs.promises.unlink(tmpJpg).catch(() => {});
|
||||
if (mime && mime.startsWith('audio/')) {
|
||||
try {
|
||||
await this.genAudioPlaceholder(outPath, thumbSpec, thumbSize);
|
||||
console.warn(`[QUEUE] Used audio placeholder thumbnail for item ${itemid}`);
|
||||
return false;
|
||||
} catch (_) {}
|
||||
}
|
||||
// Fallback: copy 404.gif as the thumbnail
|
||||
const fallback404 = path.join(cfg.paths.s, 'img', '404.gif');
|
||||
try {
|
||||
@@ -659,6 +668,34 @@ export default new class queue {
|
||||
}
|
||||
};
|
||||
|
||||
async genAudioPlaceholder(targetFile, thumbSpec = '512x512', thumbSize = 512) {
|
||||
const faFont = path.join(cfg.paths.s, 'fa', 'webfonts', 'fa-solid-900.ttf');
|
||||
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', ')',
|
||||
'-composite',
|
||||
'-font', faFont, '-pointsize', ptSize, '-gravity', 'center', '-fill', '#99ff00', '-annotate', '0', '\uf001',
|
||||
targetFile
|
||||
]);
|
||||
return true;
|
||||
} catch (_) {
|
||||
const audioFallback = path.join(cfg.paths.s, 'img', 'audio.webp');
|
||||
try {
|
||||
await fs.promises.copyFile(audioFallback, targetFile);
|
||||
return true;
|
||||
} catch (copyErr) {
|
||||
await this.spawn('magick', [
|
||||
'-size', thumbSpec, 'radial-gradient:#334a15-#1e1e1e',
|
||||
'-gravity', 'center', '-fill', '#99ff00', '-pointsize', '60', '-annotate', '0', '♪',
|
||||
targetFile
|
||||
]).catch(() => {});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
async genBlurredThumbnail(itemid, pending = false) {
|
||||
let tDir = pending ? path.join(cfg.paths.pending, 't') : cfg.paths.t;
|
||||
|
||||
@@ -769,6 +769,7 @@ const f0cklib = {
|
||||
items.is_pinned,
|
||||
items.is_oc,
|
||||
items.xd_score,
|
||||
items.has_coverart,
|
||||
${user_id ? db`max(coalesce(uvv.view_count, 0)) as my_views,` : db``}
|
||||
${user_id ? db`EXISTS (SELECT 1 FROM notifications WHERE user_id = ${user_id} AND item_id = items.id AND is_read = false) as has_notification,` : db`false as has_notification,`}
|
||||
(case when min(ta.tag_id) = 1 then 'SFW' when min(ta.tag_id) = 2 then 'NSFW' else 'NSFL' end) as tag,
|
||||
@@ -794,6 +795,7 @@ const f0cklib = {
|
||||
const meta = xdScoreMeta(row.xd_score);
|
||||
row.xd_tier = meta.tier;
|
||||
row.xd_label = meta.label;
|
||||
row.is_audio = !!(row.mime && row.mime.startsWith('audio/'));
|
||||
}
|
||||
|
||||
// Dynamic thumb sizing: applies to the main feed including mime/rating filters.
|
||||
|
||||
+28
-1
@@ -189,6 +189,32 @@ export default (router, tpl) => {
|
||||
- Comments/Sub: ${tAjaxAux - tAjaxFetch}ms
|
||||
- Render: ${tAjaxRender - tAjaxAux}ms`);
|
||||
|
||||
let itemPage = null;
|
||||
if (query.onara === '1' || req.cookies?.onara === '1' || cfg.onara || query.get_page === '1') {
|
||||
try {
|
||||
itemPage = await f0cklib.getItemPage({
|
||||
targetItemId: data.item?.id || itemid,
|
||||
targetItemPinned: data.item?.is_pinned,
|
||||
user: query.user,
|
||||
tag: query.tag,
|
||||
hall: query.hall,
|
||||
userHall: query.userHall,
|
||||
userHallOwner: query.userHallOwner,
|
||||
mime: query.mime || (req.cookies.mime || null),
|
||||
fav: query.fav === 'true',
|
||||
mode: reqMode,
|
||||
ratings: ratingsArr,
|
||||
strict: query.strict === '1' || query.strict === 'true' || req.session?.strict_mode,
|
||||
session: req.session,
|
||||
exclude: req.session ? (req.session.excluded_tags || []) : [],
|
||||
user_id: req.session?.id,
|
||||
is_admin: req.session?.admin,
|
||||
minXdScore: req.session?.min_xd_score || 0,
|
||||
tagger: query.tagger || null
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
res.reply({
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -196,7 +222,8 @@ export default (router, tpl) => {
|
||||
pagination: paginationHtml,
|
||||
title: data.title,
|
||||
id: itemid,
|
||||
slug: data.item?.slug || null
|
||||
slug: data.item?.slug || null,
|
||||
page: itemPage
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
@@ -575,25 +575,25 @@ export default router => {
|
||||
// Lightweight page-lookup for Onara grid sync.
|
||||
// Called async after random navigation — user never waits for this.
|
||||
group.get(/\/item-page$/, async (req, res) => {
|
||||
const id = parseInt(req.url.qs.id, 10) || 0;
|
||||
if (!id) return res.json({ success: false, page: 1 });
|
||||
const rawId = req.url.qs?.id || req.url.qs?.slug || req.url.qs?.item;
|
||||
if (!rawId) return res.json({ success: false, page: 1 });
|
||||
|
||||
const tag = req.url.qs.tag || null;
|
||||
const hall = req.url.qs.hall || null;
|
||||
const userHall = req.url.qs.userHall || null;
|
||||
const userHallOwner= req.url.qs.userHallOwner|| null;
|
||||
const user = req.url.qs.user || null;
|
||||
const cookieMime = req.cookies?.mime !== undefined ? (decodeURIComponent(req.cookies.mime).trim() || null) : null;
|
||||
const mime = (typeof req.url.qs.mime !== 'undefined') ? (req.url.qs.mime || null) : (cookieMime || null);
|
||||
const isFav = req.url.qs.fav === 'true';
|
||||
const isStrict = req.url.qs.strict === '1';
|
||||
const tag = req.url.qs?.tag || null;
|
||||
const hall = req.url.qs?.hall || null;
|
||||
const userHall = req.url.qs?.userHall || null;
|
||||
const userHallOwner= req.url.qs?.userHallOwner|| null;
|
||||
const user = req.url.qs?.user || null;
|
||||
const cookieMime = req.cookies?.mime !== undefined ? (decodeURIComponent(req.cookies.mime).trim() || null) : null;
|
||||
const mime = (typeof req.url.qs?.mime !== 'undefined') ? (req.url.qs.mime || null) : (cookieMime || null);
|
||||
const isFav = req.url.qs?.fav === 'true';
|
||||
const isStrict = req.url.qs?.strict === '1';
|
||||
const mode = req.mode ?? 0;
|
||||
const ratingsRaw = req.cookies.ratings;
|
||||
const ratingsArr = ratingsRaw ? decodeURIComponent(ratingsRaw).split(/[|,]/).filter(r => ['sfw','nsfw','nsfl','untagged'].includes(r)) : null;
|
||||
|
||||
try {
|
||||
const page = await f0cklib.getItemPage({
|
||||
targetItemId: id,
|
||||
targetItemId: rawId,
|
||||
user, tag, hall, userHall, userHallOwner, mime,
|
||||
fav: isFav,
|
||||
mode,
|
||||
@@ -602,7 +602,9 @@ export default router => {
|
||||
session: req.session,
|
||||
exclude: req.session?.excluded_tags || [],
|
||||
user_id: req.session?.id,
|
||||
is_admin: req.session?.admin
|
||||
is_admin: req.session?.admin,
|
||||
minXdScore: req.url.qs?.min_xd !== undefined ? +req.url.qs.min_xd : (req.session?.min_xd_score || 0),
|
||||
tagger: req.url.qs?.tagger || null
|
||||
});
|
||||
return res.json({ success: true, page });
|
||||
} catch (e) {
|
||||
|
||||
@@ -824,10 +824,18 @@ export default router => {
|
||||
|
||||
try {
|
||||
await queue.genThumbnail(filename, mime, itemid, url, isApprovalRequired);
|
||||
if (mime.startsWith('audio/') && queue._lastCoverExtracted) {
|
||||
await db`UPDATE items SET has_coverart = TRUE WHERE id = ${itemid}`;
|
||||
}
|
||||
await queue.genBlurredThumbnail(itemid, isApprovalRequired);
|
||||
} catch (err) {
|
||||
const tDir = isApprovalRequired ? path.join(cfg.paths.pending, 't') : cfg.paths.t;
|
||||
await queue.spawn('magick', ['-size', '128x128', 'xc:#1a1a1a', path.join(tDir, `${itemid}.webp`)]).catch(() => {});
|
||||
const outPath = path.join(tDir, `${itemid}.webp`);
|
||||
if (mime.startsWith('audio/')) {
|
||||
await queue.genAudioPlaceholder(outPath).catch(() => {});
|
||||
} else {
|
||||
await queue.spawn('magick', ['-size', '128x128', 'xc:#1a1a1a', outPath]).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// Assign rating tag (only if a rating was selected)
|
||||
|
||||
@@ -87,17 +87,29 @@ export default (router, tpl) => {
|
||||
`;
|
||||
|
||||
if (items.length > 0) {
|
||||
const inputs = items.map(item => path.join(cfg.paths.t, `${item.id}.webp`));
|
||||
await fs.mkdir(CACHE_DIR, { recursive: true });
|
||||
|
||||
const { execFile } = await import('child_process');
|
||||
const util = await import('util');
|
||||
const execFilePromise = util.promisify(execFile);
|
||||
const inputs = [];
|
||||
for (const item of items) {
|
||||
const filePath = path.join(cfg.paths.t, `${item.id}.webp`);
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
inputs.push(`${filePath}[0]`);
|
||||
} catch {
|
||||
// Ignore missing thumbnail
|
||||
}
|
||||
}
|
||||
|
||||
await execFilePromise('magick', [...inputs, '+append', '-background', 'none', '-resize', '600x300^', '-gravity', 'center', '-extent', '600x300', cachePath]);
|
||||
if (inputs.length > 0) {
|
||||
await fs.mkdir(CACHE_DIR, { recursive: true });
|
||||
|
||||
const { execFile } = await import('child_process');
|
||||
const util = await import('util');
|
||||
const execFilePromise = util.promisify(execFile);
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'image/webp', 'Cache-Control': 'public, max-age=3600' });
|
||||
return res.end(await fs.readFile(cachePath));
|
||||
await execFilePromise('magick', [...inputs, '+append', '-background', 'none', '-resize', '600x300^', '-gravity', 'center', '-extent', '600x300', cachePath]);
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'image/webp', 'Cache-Control': 'public, max-age=3600' });
|
||||
return res.end(await fs.readFile(cachePath));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[HALL_IMAGE] Error:', e);
|
||||
|
||||
@@ -38,14 +38,50 @@ export default (router, tpl) => {
|
||||
route: /^\/s\//
|
||||
});
|
||||
|
||||
router.static({
|
||||
dir: cfg.paths.t,
|
||||
route: /^\/t\//
|
||||
const getImgMime = (filename) => {
|
||||
if (filename.endsWith('.webp')) return 'image/webp';
|
||||
if (filename.endsWith('.png')) return 'image/png';
|
||||
if (filename.endsWith('.gif')) return 'image/gif';
|
||||
if (filename.endsWith('.jpg') || filename.endsWith('.jpeg')) return 'image/jpeg';
|
||||
return 'application/octet-stream';
|
||||
};
|
||||
|
||||
router.get(/^\/t\/(?<file>.+)$/, async (req, res) => {
|
||||
const file = req.params.file;
|
||||
const filePath = path.join(cfg.paths.t, file);
|
||||
try {
|
||||
const stat = await fs.stat(filePath);
|
||||
if (stat.isFile()) {
|
||||
const content = await fs.readFile(filePath);
|
||||
res.writeHead(200, {
|
||||
'Content-Length': stat.size,
|
||||
'Content-Type': getImgMime(file),
|
||||
'Cache-Control': 'public, max-age=3600'
|
||||
});
|
||||
return res.end(content);
|
||||
}
|
||||
} catch (_) {}
|
||||
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
||||
return res.end('404 - file not found.');
|
||||
});
|
||||
|
||||
router.static({
|
||||
dir: cfg.paths.ca,
|
||||
route: /^\/ca\//
|
||||
router.get(/^\/ca\/(?<file>.+)$/, async (req, res) => {
|
||||
const file = req.params.file;
|
||||
const filePath = path.join(cfg.paths.ca, file);
|
||||
try {
|
||||
const stat = await fs.stat(filePath);
|
||||
if (stat.isFile()) {
|
||||
const content = await fs.readFile(filePath);
|
||||
res.writeHead(200, {
|
||||
'Content-Length': stat.size,
|
||||
'Content-Type': getImgMime(file),
|
||||
'Cache-Control': 'public, max-age=3600'
|
||||
});
|
||||
return res.end(content);
|
||||
}
|
||||
} catch (_) {}
|
||||
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
||||
return res.end('404 - file not found.');
|
||||
});
|
||||
|
||||
router.static({
|
||||
|
||||
@@ -40,11 +40,22 @@ export async function regenerateTagImage(tag, mode) {
|
||||
`;
|
||||
|
||||
if (items.length > 0) {
|
||||
const inputs = items.map(item => path.join(cfg.paths.t, `${item.id}.webp`));
|
||||
const inputs = [];
|
||||
for (const item of items) {
|
||||
const filePath = path.join(cfg.paths.t, `${item.id}.webp`);
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
inputs.push(`${filePath}[0]`);
|
||||
} catch {
|
||||
// Ignore missing thumbnail
|
||||
}
|
||||
}
|
||||
|
||||
await fs.mkdir(path.dirname(cachePath), { recursive: true });
|
||||
await execFilePromise('magick', [...inputs, '+append', '-background', 'none', '-resize', '600x300^', '-gravity', 'center', '-extent', '600x300', cachePath]);
|
||||
return cachePath;
|
||||
if (inputs.length > 0) {
|
||||
await fs.mkdir(path.dirname(cachePath), { recursive: true });
|
||||
await execFilePromise('magick', [...inputs, '+append', '-background', 'none', '-resize', '600x300^', '-gravity', 'center', '-extent', '600x300', cachePath]);
|
||||
return cachePath;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[TAG_IMAGE] Failed to generate image for tag "${tag}" (mode ${mode}):`, err);
|
||||
|
||||
@@ -295,14 +295,26 @@ export default (router, tpl) => {
|
||||
`;
|
||||
|
||||
if (items.length > 0) {
|
||||
const inputs = items.map(item => path.join(cfg.paths.t, `${item.id}.webp`));
|
||||
await fs.mkdir(CACHE_DIR, { recursive: true });
|
||||
await execFile('magick', [
|
||||
...inputs, '+append', '-background', 'none',
|
||||
'-resize', '600x300^', '-gravity', 'center', '-extent', '600x300', cachePath
|
||||
]);
|
||||
res.writeHead(200, { 'Content-Type': 'image/webp', 'Cache-Control': 'public, max-age=3600' });
|
||||
return res.end(await fs.readFile(cachePath));
|
||||
const inputs = [];
|
||||
for (const item of items) {
|
||||
const filePath = path.join(cfg.paths.t, `${item.id}.webp`);
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
inputs.push(`${filePath}[0]`);
|
||||
} catch {
|
||||
// Ignore missing thumbnail
|
||||
}
|
||||
}
|
||||
|
||||
if (inputs.length > 0) {
|
||||
await fs.mkdir(CACHE_DIR, { recursive: true });
|
||||
await execFile('magick', [
|
||||
...inputs, '+append', '-background', 'none',
|
||||
'-resize', '600x300^', '-gravity', 'center', '-extent', '600x300', cachePath
|
||||
]);
|
||||
res.writeHead(200, { 'Content-Type': 'image/webp', 'Cache-Control': 'public, max-age=3600' });
|
||||
return res.end(await fs.readFile(cachePath));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[USER_HALL_IMAGE]', e);
|
||||
|
||||
Reference in New Issue
Block a user