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();