Files
Viz/worker.mjs
T

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