This commit is contained in:
2026-09-14 15:51:11 +02:00
parent ba5bc18b98
commit 22ffc3b51a
13 changed files with 3162 additions and 284 deletions
+12 -1
View File
@@ -49,7 +49,18 @@ export default new class {
return slug;
}
formatSize(size, i = ~~(Math.log(size) / Math.log(1024))) {
formatSize(size) {
if (size === undefined || size === null || size === '') return '0 B';
if (typeof size === 'string') {
if (/^\d+(\.\d+)?\s*(B|kB|KB|MB|GB|TB)$/i.test(size.trim())) {
return size.trim();
}
const num = Number(size);
if (isNaN(num)) return '0 B';
size = num;
}
if (isNaN(size) || size <= 0) return '0 B';
const i = Math.min(4, Math.max(0, ~~(Math.log(size) / Math.log(1024))));
return (size / Math.pow(1024, i)).toFixed(2) * 1 + " " + ["B", "kB", "MB", "GB", "TB"][i];
};
calcSpeed(b, s) {
+11 -1
View File
@@ -1492,6 +1492,16 @@ const f0cklib = {
}
}
let subSizeNum = r.size;
if (!subSizeNum || isNaN(subSizeNum) || Number(subSizeNum) <= 0) {
try {
const bFile = path.join(cfg.paths.b, r.dest);
if (fs.existsSync(bFile)) {
subSizeNum = fs.statSync(bFile).size;
}
} catch (_) {}
}
let subTags = tagsBySubId.get(r.id) || [];
if (subTags.length === 0 && (r.order_index === 0 || idx === 0)) {
subTags = tags;
@@ -1505,7 +1515,7 @@ const f0cklib = {
src: `${cfg.websrv.paths.images}/${r.dest}`,
filename: r.dest,
mime: r.mime,
size: lib.formatSize(r.size),
size: lib.formatSize(subSizeNum),
width: r.width,
height: r.height,
checksum: r.checksum,
+125
View File
@@ -348,6 +348,131 @@ export default router => {
}
});
const audioMetaCache = new Map();
group.get(/\/audio-metadata$/, async (req, res) => {
try {
const rawSrc = req.url.qs?.src || req.url.qs?.file || '';
const itemId = req.url.qs?.id || '';
if (!rawSrc && !itemId) {
return res.json({ success: false, msg: 'Missing src or id' }, 400);
}
let filename = '';
if (rawSrc) {
filename = path.basename(rawSrc.split('?')[0]);
}
if (filename && (filename.includes('/') || filename.includes('\\') || filename.includes('..'))) {
return res.json({ success: false, msg: 'Invalid filename' }, 400);
}
const cacheKey = filename || `item_${itemId}`;
if (audioMetaCache.has(cacheKey)) {
return res.json(audioMetaCache.get(cacheKey));
}
let fullPath = filename ? path.join(cfg.paths.b, filename) : null;
let originalFilename = '';
if (itemId) {
const isNumeric = /^\d+$/.test(itemId);
const rows = isNumeric
? await db`SELECT dest, original_filename, title FROM items WHERE id = ${itemId}`
: await db`SELECT dest, original_filename, title FROM items WHERE slug = ${itemId}`;
if (rows && rows[0]) {
if (!fullPath) fullPath = path.join(cfg.paths.b, rows[0].dest);
originalFilename = rows[0].original_filename || '';
} else {
const subRows = isNumeric
? await db`SELECT dest, original_filename FROM sub_items WHERE id = ${itemId}`
: await db`SELECT dest, original_filename FROM sub_items WHERE slug = ${itemId}`;
if (subRows && subRows[0]) {
if (!fullPath) fullPath = path.join(cfg.paths.b, subRows[0].dest);
originalFilename = subRows[0].original_filename || '';
}
}
}
if (!fullPath) {
return res.json({ success: false, msg: 'File not found' }, 404);
}
try {
await fs.access(fullPath);
} catch {
return res.json({ success: false, msg: 'File not found on disk' }, 404);
}
const metadata = await queue.getVideoMetadata(fullPath);
const tags = metadata || {};
// Normalize all tag keys to lowercase without punctuation (e.g. 'Title' -> 'title', 'ALBUM_ARTIST' -> 'albumartist')
const lowerTags = {};
for (const [k, v] of Object.entries(tags)) {
if (v != null && v !== '') {
const normKey = k.toLowerCase().replace(/[-_\s]/g, '');
lowerTags[normKey] = typeof v === 'string' ? v.trim() : String(v).trim();
}
}
let title = lowerTags.title ||
lowerTags.tracktitle ||
lowerTags.song ||
lowerTags.songname ||
lowerTags.name ||
'';
let artist = lowerTags.artist ||
lowerTags.albumartist ||
lowerTags.author ||
lowerTags.performer ||
lowerTags.creator ||
lowerTags.composer ||
lowerTags.byline ||
'';
let album = lowerTags.album ||
lowerTags.albumtitle ||
'';
let genre = lowerTags.genre || '';
let date = lowerTags.date || lowerTags.year || '';
if (!title && !artist) {
const nameToParse = originalFilename || path.parse(fullPath).name;
const match = nameToParse.match(/^(.+?)\s*[-–—_]\s*(.+)$/);
if (match) {
artist = match[1].replace(/^[0-9]+[\s._-]+/, '').trim();
title = match[2].trim();
} else if (nameToParse && !nameToParse.startsWith('subf0ck') && nameToParse.length < 60) {
title = nameToParse.trim();
}
}
const result = {
success: true,
artist: String(artist || '').trim(),
title: String(title || '').trim(),
album: String(album || '').trim(),
genre: String(genre || '').trim(),
date: String(date || '').trim(),
has_meta: !!(artist || title)
};
if (audioMetaCache.size > 1000) {
const firstKey = audioMetaCache.keys().next().value;
audioMetaCache.delete(firstKey);
}
audioMetaCache.set(cacheKey, result);
return res.json(result);
} catch (err) {
console.error('[API-V2-AUDIO-METADATA] Error:', err);
return res.json({ success: false, msg: 'Failed to extract audio metadata' }, 500);
}
});
// F-002 Security: Require authentication to prevent SSRF via arbitrary URL fetching.
// Guests may read from the cache (in-memory or DB); only authenticated users trigger real outbound fetches.
group.get(/\/meta\/fetch$/, async (req, res) => {
+8
View File
@@ -624,6 +624,9 @@ export default (router, tpl) => {
const itemQuery = await db`
SELECT
i.slug,
i.mime,
i.dest,
i.has_coverart,
i.xd_score,
COALESCE(i.visibility, 0) as visibility,
(SELECT ta.tag_id FROM tags_assign ta
@@ -684,6 +687,10 @@ export default (router, tpl) => {
db.notify('activity', JSON.stringify({
user_id: req.session.id,
item_id: item_id,
item_slug: (getEnableItemSlugs() && itemQuery[0]?.slug) ? itemQuery[0].slug : null,
mime: itemQuery[0]?.mime || null,
dest: itemQuery[0]?.dest || null,
has_coverart: !!itemQuery[0]?.has_coverart,
type: 'comment',
body: notifyBody,
id: commentId,
@@ -1104,6 +1111,7 @@ export default (router, tpl) => {
i.id as item_id,
i.slug as item_slug,
i.dest as item_dest,
i.has_coverart,
(SELECT ta.tag_id FROM tags_assign ta
WHERE ta.item_id = i.id AND ta.tag_id = ANY(${[1, 2, cfg.nsfl_tag_id || 3]}::int[])
ORDER BY ta.tag_id LIMIT 1) AS rating_tag_id,