0.12.0-15 Fix DVD Review
This commit is contained in:
1 parent
3e3f3231be
commit
76b020d5c0
30 files changed
+5148
-44
No files matched your search
@@ -0,0 +1,556 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { getDb } = require('../db/database');
|
||||
const settingsService = require('./settingsService');
|
||||
const wsService = require('./websocketService');
|
||||
const logger = require('./logger').child('CONVERTER_SCAN');
|
||||
const {
|
||||
defaultConverterRawDir
|
||||
} = require('../config');
|
||||
|
||||
const VIDEO_EXTENSIONS = new Set(['mkv', 'mp4', 'm2ts', 'avi', 'mov', 'm4v', 'wmv', 'ts']);
|
||||
const AUDIO_EXTENSIONS = new Set(['flac', 'mp3', 'aac', 'wav', 'm4a', 'ogg', 'opus', 'wma', 'ape']);
|
||||
const ISO_EXTENSIONS = new Set(['iso']);
|
||||
|
||||
let _pollingTimer = null;
|
||||
let _pollingEnabled = false;
|
||||
let _pollingInterval = 300;
|
||||
|
||||
function detectMediaType(fileName) {
|
||||
const ext = path.extname(String(fileName || '')).slice(1).toLowerCase();
|
||||
if (ISO_EXTENSIONS.has(ext)) return 'iso';
|
||||
if (VIDEO_EXTENSIONS.has(ext)) return 'video';
|
||||
if (AUDIO_EXTENSIONS.has(ext)) return 'audio';
|
||||
return null;
|
||||
}
|
||||
|
||||
function detectFormat(fileName) {
|
||||
return path.extname(String(fileName || '')).slice(1).toLowerCase() || null;
|
||||
}
|
||||
|
||||
function getConfiguredExtensions(settings) {
|
||||
const raw = String(settings?.converter_scan_extensions || '').trim();
|
||||
if (!raw) {
|
||||
return new Set([...VIDEO_EXTENSIONS, ...AUDIO_EXTENSIONS, ...ISO_EXTENSIONS]);
|
||||
}
|
||||
return new Set(
|
||||
raw.split(',')
|
||||
.map((ext) => ext.trim().toLowerCase())
|
||||
.filter(Boolean)
|
||||
);
|
||||
}
|
||||
|
||||
function getFileSize(fullPath) {
|
||||
try {
|
||||
return fs.statSync(fullPath).size;
|
||||
} catch (_err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const SCAN_MAX_DEPTH = 4;
|
||||
|
||||
/**
|
||||
* Rekursiv alle Dateien und direkten Unterordner im rawDir scannen.
|
||||
* Gibt ein flaches Array von { relPath, entryType, fileSize, detectedMediaType, detectedFormat } zurück.
|
||||
* Maximale Tiefe: SCAN_MAX_DEPTH (verhindert unendliche Rekursion bei geschachtelten Ordnern).
|
||||
*/
|
||||
function scanDirectory(rawDir, allowedExtensions, parentRelPath = '', depth = 0) {
|
||||
const entries = [];
|
||||
|
||||
if (depth >= SCAN_MAX_DEPTH) {
|
||||
return entries;
|
||||
}
|
||||
|
||||
let dirEntries;
|
||||
try {
|
||||
dirEntries = fs.readdirSync(rawDir, { withFileTypes: true });
|
||||
} catch (_err) {
|
||||
return entries;
|
||||
}
|
||||
|
||||
for (const dirent of dirEntries) {
|
||||
const relPath = parentRelPath ? `${parentRelPath}/${dirent.name}` : dirent.name;
|
||||
const fullPath = path.join(rawDir, relPath);
|
||||
|
||||
if (dirent.isDirectory()) {
|
||||
entries.push({
|
||||
relPath,
|
||||
entryType: 'directory',
|
||||
fileSize: null,
|
||||
detectedMediaType: null,
|
||||
detectedFormat: null
|
||||
});
|
||||
// Rekursiv in Unterordner (mit Tiefenbegrenzung)
|
||||
const subEntries = scanDirectory(rawDir, allowedExtensions, relPath, depth + 1);
|
||||
entries.push(...subEntries);
|
||||
} else if (dirent.isFile()) {
|
||||
const ext = path.extname(dirent.name).slice(1).toLowerCase();
|
||||
if (!allowedExtensions.has(ext)) {
|
||||
continue;
|
||||
}
|
||||
entries.push({
|
||||
relPath,
|
||||
entryType: 'file',
|
||||
fileSize: getFileSize(fullPath),
|
||||
detectedMediaType: detectMediaType(dirent.name),
|
||||
detectedFormat: detectFormat(dirent.name)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan-Ergebnisse in die DB schreiben (INSERT OR REPLACE) und
|
||||
* nicht mehr vorhandene Einträge ohne Job entfernen.
|
||||
*/
|
||||
async function persistScanResults(rawDir, entries) {
|
||||
const db = await getDb();
|
||||
|
||||
if (entries.length > 0) {
|
||||
const stmt = await db.prepare(`
|
||||
INSERT INTO converter_scan_entries (rel_path, entry_type, file_size, detected_media_type, detected_format, last_seen_at)
|
||||
VALUES (?, ?, ?, ?, ?, datetime('now'))
|
||||
ON CONFLICT(rel_path) DO UPDATE SET
|
||||
entry_type = excluded.entry_type,
|
||||
file_size = excluded.file_size,
|
||||
detected_media_type = excluded.detected_media_type,
|
||||
detected_format = excluded.detected_format,
|
||||
last_seen_at = excluded.last_seen_at
|
||||
`);
|
||||
|
||||
for (const entry of entries) {
|
||||
await stmt.run(
|
||||
entry.relPath,
|
||||
entry.entryType,
|
||||
entry.fileSize,
|
||||
entry.detectedMediaType,
|
||||
entry.detectedFormat
|
||||
);
|
||||
}
|
||||
await stmt.finalize();
|
||||
}
|
||||
|
||||
// Einträge ohne zugewiesenen Job entfernen, wenn Datei nicht mehr vorhanden
|
||||
const existingEntries = await db.all(
|
||||
`SELECT rel_path FROM converter_scan_entries WHERE job_id IS NULL`
|
||||
);
|
||||
const currentRelPaths = new Set(entries.map((e) => e.relPath));
|
||||
|
||||
for (const row of existingEntries) {
|
||||
if (!currentRelPaths.has(row.rel_path)) {
|
||||
const fullPath = path.join(rawDir, row.rel_path);
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
await db.run(
|
||||
`DELETE FROM converter_scan_entries WHERE rel_path = ? AND job_id IS NULL`,
|
||||
[row.rel_path]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hauptmethode: rawDir scannen, DB aktualisieren, WebSocket-Event senden.
|
||||
*/
|
||||
async function scan() {
|
||||
const settings = await settingsService.getSettingsMap();
|
||||
const rawDir = String(settings?.converter_raw_dir || defaultConverterRawDir || '').trim();
|
||||
|
||||
if (!rawDir) {
|
||||
logger.warn('converter:scan:no-raw-dir');
|
||||
return { rawDir: null, entryCount: 0 };
|
||||
}
|
||||
|
||||
if (!fs.existsSync(rawDir)) {
|
||||
logger.warn('converter:scan:dir-missing', { rawDir });
|
||||
return { rawDir, entryCount: 0 };
|
||||
}
|
||||
|
||||
const allowedExtensions = getConfiguredExtensions(settings);
|
||||
logger.info('converter:scan:start', { rawDir, allowedExtensions: [...allowedExtensions] });
|
||||
|
||||
const entries = scanDirectory(rawDir, allowedExtensions);
|
||||
await persistScanResults(rawDir, entries);
|
||||
|
||||
logger.info('converter:scan:done', { rawDir, entryCount: entries.length });
|
||||
|
||||
wsService.broadcast('CONVERTER_SCAN_UPDATE', { entryCount: entries.length });
|
||||
|
||||
return { rawDir, entryCount: entries.length };
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle Einträge für den File-Explorer zurückgeben.
|
||||
* Optionaler parentRelPath filtert auf Kinder eines bestimmten Verzeichnisses.
|
||||
*/
|
||||
async function getEntries(parentRelPath = null) {
|
||||
const db = await getDb();
|
||||
|
||||
let rows;
|
||||
if (!parentRelPath) {
|
||||
// Root-Ebene: nur Einträge ohne '/' im rel_path
|
||||
rows = await db.all(`
|
||||
SELECT e.*, j.status AS job_status, j.title AS job_title
|
||||
FROM converter_scan_entries e
|
||||
LEFT JOIN jobs j ON j.id = e.job_id
|
||||
ORDER BY e.entry_type DESC, e.rel_path ASC
|
||||
`);
|
||||
// Nur direkte Kinder (kein '/' im rel_path)
|
||||
rows = rows.filter((r) => !String(r.rel_path).includes('/'));
|
||||
} else {
|
||||
const prefix = parentRelPath.endsWith('/') ? parentRelPath : `${parentRelPath}/`;
|
||||
rows = await db.all(`
|
||||
SELECT e.*, j.status AS job_status, j.title AS job_title
|
||||
FROM converter_scan_entries e
|
||||
LEFT JOIN jobs j ON j.id = e.job_id
|
||||
ORDER BY e.entry_type DESC, e.rel_path ASC
|
||||
`);
|
||||
// Direkte Kinder des angegebenen Verzeichnisses
|
||||
rows = rows.filter((r) => {
|
||||
const rel = String(r.rel_path);
|
||||
if (!rel.startsWith(prefix)) return false;
|
||||
const remainder = rel.slice(prefix.length);
|
||||
return !remainder.includes('/');
|
||||
});
|
||||
}
|
||||
|
||||
return rows.map((r) => ({
|
||||
id: r.id,
|
||||
relPath: r.rel_path,
|
||||
entryType: r.entry_type,
|
||||
fileSize: r.file_size,
|
||||
detectedMediaType: r.detected_media_type,
|
||||
detectedFormat: r.detected_format,
|
||||
jobId: r.job_id,
|
||||
jobStatus: r.job_status || null,
|
||||
jobTitle: r.job_title || null,
|
||||
lastSeenAt: r.last_seen_at
|
||||
}));
|
||||
}
|
||||
|
||||
async function getEntryById(id) {
|
||||
const db = await getDb();
|
||||
const row = await db.get(
|
||||
`SELECT * FROM converter_scan_entries WHERE id = ?`,
|
||||
[Number(id)]
|
||||
);
|
||||
return row || null;
|
||||
}
|
||||
|
||||
async function getEntryByRelPath(relPath) {
|
||||
const db = await getDb();
|
||||
const row = await db.get(
|
||||
`SELECT * FROM converter_scan_entries WHERE rel_path = ?`,
|
||||
[relPath]
|
||||
);
|
||||
return row || null;
|
||||
}
|
||||
|
||||
async function markEntryAsJob(relPath, jobId) {
|
||||
const db = await getDb();
|
||||
await db.run(
|
||||
`UPDATE converter_scan_entries SET job_id = ? WHERE rel_path = ?`,
|
||||
[jobId, relPath]
|
||||
);
|
||||
}
|
||||
|
||||
async function getRawDir() {
|
||||
const settings = await settingsService.getSettingsMap();
|
||||
return String(settings?.converter_raw_dir || defaultConverterRawDir || '').trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Polling-Loop starten.
|
||||
*/
|
||||
async function startPolling() {
|
||||
const settings = await settingsService.getSettingsMap();
|
||||
_pollingEnabled = String(settings?.converter_polling_enabled || 'false').toLowerCase() === 'true';
|
||||
_pollingInterval = Math.max(30, Number(settings?.converter_polling_interval || 300)) * 1000;
|
||||
|
||||
stopPolling();
|
||||
|
||||
if (!_pollingEnabled) {
|
||||
logger.info('converter:polling:disabled');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('converter:polling:start', { intervalMs: _pollingInterval });
|
||||
|
||||
const tick = async () => {
|
||||
try {
|
||||
await scan();
|
||||
} catch (error) {
|
||||
logger.error('converter:polling:error', { error: error?.message });
|
||||
}
|
||||
if (_pollingEnabled) {
|
||||
_pollingTimer = setTimeout(tick, _pollingInterval);
|
||||
}
|
||||
};
|
||||
|
||||
_pollingTimer = setTimeout(tick, _pollingInterval);
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (_pollingTimer) {
|
||||
clearTimeout(_pollingTimer);
|
||||
_pollingTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function restartPolling() {
|
||||
await startPolling();
|
||||
}
|
||||
|
||||
// ── Datei-Operationen (Löschen, Umbenennen, Verschieben, Ordner erstellen) ──
|
||||
|
||||
/**
|
||||
* Relativen Pfad normalisieren und auf Path-Traversal prüfen.
|
||||
* Gibt null zurück wenn der Pfad ungültig ist.
|
||||
*/
|
||||
function normalizeRelPath(input) {
|
||||
if (input === null || input === undefined) return '';
|
||||
const raw = String(input).replace(/\\/g, '/').trim();
|
||||
if (!raw || raw === '.') return '';
|
||||
if (raw.startsWith('/')) return null;
|
||||
const normalized = path.posix.normalize(raw);
|
||||
if (normalized.startsWith('..')) return null;
|
||||
if (normalized === '.') return '';
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function makeError(msg, code) {
|
||||
const err = new Error(msg);
|
||||
err.statusCode = code;
|
||||
return err;
|
||||
}
|
||||
|
||||
/**
|
||||
* Datei oder Ordner löschen. Aktualisiert DB-Einträge.
|
||||
*/
|
||||
async function deleteEntry(relPath) {
|
||||
const rawDir = await getRawDir();
|
||||
if (!rawDir) throw makeError('Kein RAW-Verzeichnis konfiguriert.', 400);
|
||||
|
||||
const rel = normalizeRelPath(relPath);
|
||||
if (rel === null || rel === '') throw makeError('Ungültiger oder leerer Pfad (Root kann nicht gelöscht werden).', 400);
|
||||
|
||||
const absPath = path.join(rawDir, rel);
|
||||
// Traversal-Schutz
|
||||
if (!absPath.startsWith(rawDir + path.sep)) throw makeError('Pfad außerhalb des RAW-Verzeichnisses.', 400);
|
||||
|
||||
const db = await getDb();
|
||||
const entry = await db.get(`SELECT job_id FROM converter_scan_entries WHERE rel_path = ?`, [rel]);
|
||||
if (entry?.job_id) throw makeError('Eintrag ist einem Job zugewiesen und kann nicht gelöscht werden.', 409);
|
||||
|
||||
if (!fs.existsSync(absPath)) throw makeError(`Pfad existiert nicht: ${rel}`, 404);
|
||||
|
||||
fs.rmSync(absPath, { recursive: true, force: true });
|
||||
|
||||
await db.run(
|
||||
`DELETE FROM converter_scan_entries WHERE rel_path = ? OR rel_path LIKE ?`,
|
||||
[rel, `${rel}/%`]
|
||||
);
|
||||
|
||||
return { deleted: rel };
|
||||
}
|
||||
|
||||
/**
|
||||
* Datei oder Ordner umbenennen. Aktualisiert DB-Einträge.
|
||||
*/
|
||||
async function renameEntry(relPath, newName) {
|
||||
const rawDir = await getRawDir();
|
||||
if (!rawDir) throw makeError('Kein RAW-Verzeichnis konfiguriert.', 400);
|
||||
|
||||
const rel = normalizeRelPath(relPath);
|
||||
if (rel === null || rel === '') throw makeError('Ungültiger Quellpfad.', 400);
|
||||
|
||||
const safeName = String(newName || '').trim();
|
||||
if (!safeName || safeName.includes('/') || safeName.includes('\\') || safeName === '.' || safeName === '..') {
|
||||
throw makeError('Ungültiger Name.', 400);
|
||||
}
|
||||
|
||||
const parentRel = path.posix.dirname(rel);
|
||||
const newRel = (parentRel === '.' || parentRel === '') ? safeName : `${parentRel}/${safeName}`;
|
||||
|
||||
const absOld = path.join(rawDir, rel);
|
||||
const absNew = path.join(rawDir, newRel);
|
||||
|
||||
if (!absOld.startsWith(rawDir + path.sep)) throw makeError('Quellpfad außerhalb des RAW-Verzeichnisses.', 400);
|
||||
if (!absNew.startsWith(rawDir + path.sep)) throw makeError('Zielpfad außerhalb des RAW-Verzeichnisses.', 400);
|
||||
|
||||
const db = await getDb();
|
||||
const entry = await db.get(`SELECT job_id FROM converter_scan_entries WHERE rel_path = ?`, [rel]);
|
||||
if (entry?.job_id) throw makeError('Eintrag ist einem Job zugewiesen und kann nicht umbenannt werden.', 409);
|
||||
|
||||
if (!fs.existsSync(absOld)) throw makeError('Quelle nicht gefunden.', 404);
|
||||
if (fs.existsSync(absNew)) throw makeError('Ziel existiert bereits.', 409);
|
||||
|
||||
fs.renameSync(absOld, absNew);
|
||||
|
||||
const rows = await db.all(
|
||||
`SELECT rel_path FROM converter_scan_entries WHERE rel_path = ? OR rel_path LIKE ?`,
|
||||
[rel, `${rel}/%`]
|
||||
);
|
||||
for (const row of rows) {
|
||||
const updatedRel = newRel + row.rel_path.slice(rel.length);
|
||||
await db.run(`UPDATE converter_scan_entries SET rel_path = ? WHERE rel_path = ?`, [updatedRel, row.rel_path]);
|
||||
}
|
||||
|
||||
return { oldRelPath: rel, newRelPath: newRel };
|
||||
}
|
||||
|
||||
/**
|
||||
* Datei oder Ordner in ein anderes Verzeichnis verschieben. Aktualisiert DB-Einträge.
|
||||
*/
|
||||
async function moveEntry(relPath, targetParentRelPath) {
|
||||
const rawDir = await getRawDir();
|
||||
if (!rawDir) throw makeError('Kein RAW-Verzeichnis konfiguriert.', 400);
|
||||
|
||||
const rel = normalizeRelPath(relPath);
|
||||
if (rel === null || rel === '') throw makeError('Ungültiger Quellpfad.', 400);
|
||||
|
||||
const targetParentRel = normalizeRelPath(targetParentRelPath != null ? targetParentRelPath : '');
|
||||
if (targetParentRel === null) throw makeError('Ungültiger Zielpfad.', 400);
|
||||
|
||||
const name = path.posix.basename(rel);
|
||||
const newRel = targetParentRel === '' ? name : `${targetParentRel}/${name}`;
|
||||
|
||||
if (rel === newRel) throw makeError('Quelle und Ziel sind identisch.', 400);
|
||||
if (newRel.startsWith(`${rel}/`)) throw makeError('Kann nicht in eigenen Unterordner verschoben werden.', 400);
|
||||
|
||||
const absOld = path.join(rawDir, rel);
|
||||
const absNew = path.join(rawDir, newRel);
|
||||
|
||||
if (!absOld.startsWith(rawDir + path.sep)) throw makeError('Quellpfad außerhalb des RAW-Verzeichnisses.', 400);
|
||||
if (!absNew.startsWith(rawDir + path.sep) && absNew !== rawDir) throw makeError('Zielpfad außerhalb des RAW-Verzeichnisses.', 400);
|
||||
|
||||
const db = await getDb();
|
||||
const entry = await db.get(`SELECT job_id FROM converter_scan_entries WHERE rel_path = ?`, [rel]);
|
||||
if (entry?.job_id) throw makeError('Eintrag ist einem Job zugewiesen und kann nicht verschoben werden.', 409);
|
||||
|
||||
if (!fs.existsSync(absOld)) throw makeError('Quelle nicht gefunden.', 404);
|
||||
if (fs.existsSync(absNew)) throw makeError('Ziel existiert bereits.', 409);
|
||||
|
||||
fs.renameSync(absOld, absNew);
|
||||
|
||||
const rows = await db.all(
|
||||
`SELECT rel_path FROM converter_scan_entries WHERE rel_path = ? OR rel_path LIKE ?`,
|
||||
[rel, `${rel}/%`]
|
||||
);
|
||||
for (const row of rows) {
|
||||
const updatedRel = newRel + row.rel_path.slice(rel.length);
|
||||
await db.run(`UPDATE converter_scan_entries SET rel_path = ? WHERE rel_path = ?`, [updatedRel, row.rel_path]);
|
||||
}
|
||||
|
||||
return { oldRelPath: rel, newRelPath: newRel, targetParentRelPath: targetParentRel };
|
||||
}
|
||||
|
||||
/**
|
||||
* Neuen Ordner erstellen (kein DB-Eintrag — erscheint beim nächsten Scan).
|
||||
*/
|
||||
async function createFolder(parentRelPath, name) {
|
||||
const rawDir = await getRawDir();
|
||||
if (!rawDir) throw makeError('Kein RAW-Verzeichnis konfiguriert.', 400);
|
||||
|
||||
const parentRel = normalizeRelPath(parentRelPath != null ? parentRelPath : '');
|
||||
if (parentRel === null) throw makeError('Ungültiger übergeordneter Pfad.', 400);
|
||||
|
||||
const safeName = String(name || '').trim();
|
||||
if (!safeName || safeName.includes('/') || safeName.includes('\\') || safeName === '.' || safeName === '..') {
|
||||
throw makeError('Ungültiger Ordnername.', 400);
|
||||
}
|
||||
|
||||
const newRel = parentRel === '' ? safeName : `${parentRel}/${safeName}`;
|
||||
const absPath = path.join(rawDir, newRel);
|
||||
|
||||
if (!absPath.startsWith(rawDir + path.sep)) throw makeError('Pfad außerhalb des RAW-Verzeichnisses.', 400);
|
||||
if (fs.existsSync(absPath)) throw makeError('Ordner existiert bereits.', 409);
|
||||
|
||||
fs.mkdirSync(absPath, { recursive: true });
|
||||
|
||||
return { relPath: newRel };
|
||||
}
|
||||
|
||||
// ── Reines FS-Baum-Listing (keine DB) ─────────────────────────────────────
|
||||
|
||||
const TREE_MAX_DEPTH = 8;
|
||||
|
||||
function buildRawTree(rawDir, relPath, depth) {
|
||||
if (depth >= TREE_MAX_DEPTH) return [];
|
||||
const absDir = relPath ? path.join(rawDir, relPath) : rawDir;
|
||||
let dirents;
|
||||
try {
|
||||
dirents = fs.readdirSync(absDir, { withFileTypes: true });
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
|
||||
dirents.sort((a, b) => {
|
||||
const ad = a.isDirectory() ? 0 : 1;
|
||||
const bd = b.isDirectory() ? 0 : 1;
|
||||
if (ad !== bd) return ad - bd;
|
||||
return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });
|
||||
});
|
||||
|
||||
const nodes = [];
|
||||
for (const dirent of dirents) {
|
||||
// Versteckte Einträge überspringen
|
||||
if (dirent.name.startsWith('.')) continue;
|
||||
const childRel = relPath ? `${relPath}/${dirent.name}` : dirent.name;
|
||||
if (dirent.isDirectory()) {
|
||||
const children = buildRawTree(rawDir, childRel, depth + 1);
|
||||
const size = children.reduce((s, c) => s + (c.size || 0), 0);
|
||||
nodes.push({ name: dirent.name, type: 'folder', path: childRel, size, children });
|
||||
} else if (dirent.isFile()) {
|
||||
let size = 0;
|
||||
try { size = fs.statSync(path.join(rawDir, childRel)).size; } catch (_) {}
|
||||
nodes.push({
|
||||
name: dirent.name,
|
||||
type: 'file',
|
||||
path: childRel,
|
||||
size,
|
||||
detectedMediaType: detectMediaType(dirent.name),
|
||||
detectedFormat: detectFormat(dirent.name)
|
||||
});
|
||||
}
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
async function getTree() {
|
||||
const rawDir = await getRawDir();
|
||||
if (!rawDir || !fs.existsSync(rawDir)) {
|
||||
return { rawDir: rawDir || null, tree: null };
|
||||
}
|
||||
const children = buildRawTree(rawDir, '', 0);
|
||||
const size = children.reduce((s, c) => s + (c.size || 0), 0);
|
||||
return {
|
||||
rawDir,
|
||||
tree: { name: path.basename(rawDir) || 'raw', type: 'folder', path: '', size, children }
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
scan,
|
||||
getEntries,
|
||||
getEntryById,
|
||||
getEntryByRelPath,
|
||||
markEntryAsJob,
|
||||
getRawDir,
|
||||
normalizeRelPath,
|
||||
getTree,
|
||||
startPolling,
|
||||
stopPolling,
|
||||
restartPolling,
|
||||
detectMediaType,
|
||||
detectFormat,
|
||||
deleteEntry,
|
||||
renameEntry,
|
||||
moveEntry,
|
||||
createFolder
|
||||
};
|
||||
Reference in new issue
Block a user