265 lines
9.6 KiB
JavaScript
265 lines
9.6 KiB
JavaScript
import fs from 'fs';
|
|
import path from 'path';
|
|
import puppeteer from 'puppeteer';
|
|
import fetch from 'node-fetch';
|
|
import { pipeline } from 'stream/promises';
|
|
|
|
const BASE_DIR = 'D:/Google Drive/Videos/AIWentNonsense';
|
|
const VIZ_URL = 'http://localhost:3000';
|
|
const NTFY_URL = 'https://ntfy.orfel.de/viz_errors';
|
|
|
|
async function main() {
|
|
console.log(`\n🔍 Suche nach anstehenden Render-Jobs in: ${BASE_DIR}\n`);
|
|
|
|
if (!fs.existsSync(BASE_DIR)) {
|
|
console.error(`Ordner nicht gefunden: ${BASE_DIR}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const songFolders = fs.readdirSync(BASE_DIR, { withFileTypes: true })
|
|
.filter(dirent => dirent.isDirectory())
|
|
.map(dirent => dirent.name);
|
|
|
|
const pendingJobs = [];
|
|
|
|
for (const folder of songFolders) {
|
|
const vizDir = path.join(BASE_DIR, folder, 'Viz');
|
|
if (!fs.existsSync(vizDir)) continue;
|
|
|
|
const files = fs.readdirSync(vizDir);
|
|
for (const file of files) {
|
|
if (file.toLowerCase().endsWith('.json')) {
|
|
const jsonPath = path.join(vizDir, file);
|
|
|
|
try {
|
|
const job = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
|
|
|
// Zieldateiname bestimmen
|
|
const targetFormat = job.format === 'prores4444' ? 'prores' : (job.format || 'prores');
|
|
const outExt = (targetFormat === 'prores' || targetFormat === 'qtrle') ? '.mov' : (targetFormat === 'webm_alpha' ? '.webm' : '.mp4');
|
|
|
|
// Prüfen, ob bereits irgendeine Datei mit dieser Endung (z.B. .mov) im Ordner liegt
|
|
const hasVideoAlready = files.some(f => f.toLowerCase().endsWith(outExt));
|
|
|
|
// Job nur vormerken, wenn NOCH KEIN solches Video existiert
|
|
if (!hasVideoAlready) {
|
|
// Wir definieren dennoch einen finalen Dateinamen für den neuen Render
|
|
const finalTitle = job.title ? job.title.replace(/[^a-zA-Z0-9_-]/g, '_') : 'Output';
|
|
const expectedVideoPath = path.join(vizDir, `${finalTitle}${outExt}`);
|
|
pendingJobs.push({ jsonPath, job, expectedVideoPath, targetFormat });
|
|
}
|
|
} catch (e) {
|
|
console.error(`⚠️ Fehler beim Lesen von ${jsonPath}: ${e.message}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (pendingJobs.length === 0) {
|
|
console.log('✅ Keine neuen JSON-Dateien ohne zugehöriges Video gefunden. Beende.');
|
|
process.exit(0);
|
|
}
|
|
|
|
console.log(`🚀 ${pendingJobs.length} anstehende(r) Job(s) gefunden.`);
|
|
|
|
for (const jobItem of pendingJobs) {
|
|
await processJob(jobItem);
|
|
}
|
|
|
|
console.log('\n🎉 Alle Jobs abgearbeitet!');
|
|
process.exit(0);
|
|
}
|
|
|
|
async function processJob({ jsonPath, job, expectedVideoPath, targetFormat }) {
|
|
const fileName = path.basename(jsonPath);
|
|
console.log(`\n---------------------------------------------------`);
|
|
console.log(`🎬 Starte Render-Job aus Datei: ${fileName}`);
|
|
|
|
const parentDir = path.resolve(path.dirname(jsonPath), '..');
|
|
let audioFileAbs = null;
|
|
|
|
if (job.audio) {
|
|
audioFileAbs = path.resolve(path.dirname(jsonPath), job.audio);
|
|
} else {
|
|
// Versuche automatisch ein Audio-File im Parent-Ordner zu finden
|
|
const parentFiles = fs.readdirSync(parentDir);
|
|
const audioFileName = parentFiles.find(f => f.toLowerCase().endsWith('.wav') || f.toLowerCase().endsWith('.mp3') || f.toLowerCase().endsWith('.flac'));
|
|
if (audioFileName) {
|
|
audioFileAbs = path.join(parentDir, audioFileName);
|
|
}
|
|
}
|
|
|
|
if (!audioFileAbs || !fs.existsSync(audioFileAbs)) {
|
|
console.error(`❌ Audiodatei nicht gefunden für ${fileName}`);
|
|
await sendError(fileName, `Audiodatei nicht gefunden für ${fileName}`);
|
|
return;
|
|
}
|
|
|
|
console.log(`🎵 Audio geladen: ${audioFileAbs}`);
|
|
|
|
// Bestimme Titel aus Parent-Ordner falls nicht gesetzt (z.B. "999 - Catch the Fire" -> "Catch the Fire")
|
|
let autoTitle = job.title;
|
|
if (!autoTitle) {
|
|
const folderName = path.basename(parentDir);
|
|
const match = folderName.match(/^\d+\s*-\s*(.+)$/);
|
|
autoTitle = match ? match[1] : folderName;
|
|
}
|
|
|
|
const autoArtist = job.artist || '';
|
|
|
|
let browser;
|
|
try {
|
|
browser = await puppeteer.launch({
|
|
headless: 'new',
|
|
args: [
|
|
'--autoplay-policy=no-user-gesture-required',
|
|
'--use-gl=egl',
|
|
'--ignore-gpu-blocklist',
|
|
'--enable-gpu-rasterization',
|
|
'--mute-audio',
|
|
'--window-size=1920,1080'
|
|
]
|
|
});
|
|
|
|
const page = await browser.newPage();
|
|
page.on('console', msg => console.log('💻 [Browser]:', msg.text()));
|
|
await page.setViewport({ width: 1920, height: 1080 });
|
|
|
|
console.log('🌐 Öffne Viz Web-UI...');
|
|
await page.goto(VIZ_URL, { waitUntil: 'networkidle2' });
|
|
|
|
// 1. Audio Upload auslösen
|
|
await page.waitForSelector('#audioFileInput', { timeout: 10000 });
|
|
const audioInput = await page.$('#audioFileInput');
|
|
await audioInput.uploadFile(audioFileAbs);
|
|
|
|
// Warten bis audioLoaded === true im Window
|
|
await page.waitForFunction(() => window.VIZ_API && window.VIZ_API.state && window.VIZ_API.state.audioLoaded === true, { timeout: 30000 });
|
|
console.log('🔊 Audio erfolgreich im Browser geladen.');
|
|
|
|
// 2. Preset setzen
|
|
if (job.preset) {
|
|
await page.evaluate((presetName) => {
|
|
if (window.VIZ_PRESETS && window.VIZ_PRESETS[presetName]) {
|
|
window.VIZ_API.applyPreset(presetName);
|
|
} else {
|
|
throw new Error('Preset nicht gefunden: ' + presetName);
|
|
}
|
|
}, job.preset);
|
|
console.log(`🎨 Preset "${job.preset}" angewendet.`);
|
|
} else if (job.name !== undefined) {
|
|
// JSON is a direct preset config object
|
|
await page.evaluate((presetObj) => {
|
|
window.VIZ_API.applyPreset(presetObj);
|
|
}, job);
|
|
console.log(`🎨 Direct Preset Config "${job.name}" angewendet.`);
|
|
}
|
|
|
|
// 3. Titel, Artist, Aspect
|
|
await page.evaluate((title, artist, aspect, format) => {
|
|
const titleEl = document.getElementById('trackTitle');
|
|
if (titleEl) titleEl.value = title || 'Visualizer';
|
|
|
|
const artistEl = document.getElementById('trackArtist');
|
|
if (artistEl) artistEl.value = artist || '';
|
|
|
|
if (aspect) {
|
|
const btn = document.querySelector(`.aspect-btn[data-aspect="${aspect}"]`);
|
|
if (btn) btn.click();
|
|
}
|
|
|
|
if (format) {
|
|
const fmtEl = document.getElementById('exportFormat');
|
|
if (fmtEl) fmtEl.value = format;
|
|
}
|
|
|
|
window.VIZ_API.updateConfigFromUI();
|
|
}, autoTitle, autoArtist, job.aspect, targetFormat);
|
|
|
|
// 4. Render-Knopf drücken
|
|
console.log('🚀 Starte Export-Prozess...');
|
|
await page.evaluate(() => {
|
|
document.getElementById('btnStartExport').click();
|
|
});
|
|
|
|
// 5. Überwache den History API Endpunkt
|
|
console.log('⏳ Warte auf den Abschluss des Renderings (Audio-Upload an Server läuft...)');
|
|
let isDone = false;
|
|
let lastStatus = '';
|
|
|
|
// Wir pollen die History, bis ein Job auftaucht (Lokales Mezzanine-Rendering & Upload dauern!)
|
|
let history = [];
|
|
let attempts = 0;
|
|
while (history.length === 0 && attempts < 600) {
|
|
await new Promise(r => setTimeout(r, 2000));
|
|
try {
|
|
const historyRes = await fetch(`${VIZ_URL}/api/export/history`);
|
|
history = await historyRes.json();
|
|
} catch(e) {}
|
|
attempts++;
|
|
}
|
|
|
|
if (history.length === 0) {
|
|
await page.screenshot({ path: path.join(path.dirname(jsonPath), fileName + '.timeout.png') });
|
|
throw new Error('Kein Export-Job in History gefunden (Timeout nach 20 Minuten). Screenshot wurde gespeichert.');
|
|
}
|
|
const sessionId = history[0].id;
|
|
console.log(`🔗 Session ID ermittelt: ${sessionId}`);
|
|
|
|
while (!isDone) {
|
|
await new Promise(r => setTimeout(r, 2000));
|
|
|
|
const pollRes = await fetch(`${VIZ_URL}/api/export/history`);
|
|
if (!pollRes.ok) continue;
|
|
|
|
const hData = await pollRes.json();
|
|
const sess = hData.find(s => s.id === sessionId);
|
|
|
|
if (!sess) throw new Error('Session plötzlich verschwunden');
|
|
if (sess.error) throw new Error('Server Render Fehler: ' + sess.error);
|
|
|
|
if (sess.currentStatus !== lastStatus) {
|
|
lastStatus = sess.currentStatus;
|
|
console.log(` [Status] ${lastStatus}`);
|
|
}
|
|
|
|
if (sess.done && sess.formats[targetFormat] && sess.formats[targetFormat].ready) {
|
|
isDone = true;
|
|
const serverPath = sess.formats[targetFormat].path;
|
|
|
|
console.log(`\n💾 Speichere Video: ${expectedVideoPath}`);
|
|
if (fs.existsSync(serverPath)) {
|
|
fs.copyFileSync(serverPath, expectedVideoPath);
|
|
} else {
|
|
console.log(` Lokale Datei ${serverPath} nicht gefunden. Lade über HTTP herunter...`);
|
|
const res = await fetch(`${VIZ_URL}/api/export/download/${sessionId}/${targetFormat}`);
|
|
if (!res.ok) throw new Error('HTTP Download fehlgeschlagen');
|
|
await pipeline(res.body, fs.createWriteStream(expectedVideoPath));
|
|
}
|
|
console.log(`✅ Video erfolgreich erstellt!`);
|
|
} else if (sess.done) {
|
|
throw new Error(`Render abgeschlossen, aber Format ${targetFormat} nicht verfügbar.`);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error(`\n❌ Fehler bei ${fileName}:`, err.message);
|
|
await sendError(fileName, err.message);
|
|
} finally {
|
|
if (browser) await browser.close();
|
|
}
|
|
}
|
|
|
|
async function sendError(fileName, message) {
|
|
try {
|
|
await fetch(NTFY_URL, {
|
|
method: 'POST',
|
|
body: `Viz Auto-Render fehlgeschlagen für ${fileName}: ${message}`,
|
|
headers: { 'Title': 'Viz Render Error', 'Tags': 'warning,video' }
|
|
});
|
|
} catch (e) {
|
|
console.error('⚠️ Konnte Ntfy Benachrichtigung nicht senden:', e.message);
|
|
}
|
|
}
|
|
|
|
main();
|