gfsd
This commit is contained in:
+12
-1
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user