Kompletter Video-Konverter mit Web-UI, GPU-Beschleunigung (Intel VAAPI), Video-Bibliothek mit Serien/Film-Erkennung und TVDB-Integration. Features: - AV1/HEVC/H.264 Encoding (GPU + CPU) - Video-Bibliothek mit ffprobe-Analyse und Filtern - TVDB-Integration mit Review-Modal und Sprachkonfiguration - Film-Scanning und TVDB-Zuordnung - Import- und Clean-Service (Grundgeruest) - WebSocket Live-Updates, Queue-Management - Docker mit GPU/CPU-Profilen Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
342 lines
15 KiB
HTML
342 lines
15 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Einstellungen - VideoKonverter{% endblock %}
|
|
|
|
{% block content %}
|
|
<section class="admin-section">
|
|
<h2>Einstellungen</h2>
|
|
|
|
<form hx-post="/htmx/settings" hx-target="#save-result" hx-swap="innerHTML">
|
|
|
|
<!-- Encoding -->
|
|
<fieldset>
|
|
<legend>Encoding</legend>
|
|
<div class="form-grid">
|
|
<div class="form-group">
|
|
<label for="encoding_mode">Modus</label>
|
|
<select name="encoding_mode" id="encoding_mode">
|
|
<option value="cpu" {% if settings.encoding.mode == 'cpu' %}selected{% endif %}>CPU</option>
|
|
<option value="gpu" {% if settings.encoding.mode == 'gpu' %}selected{% endif %}>GPU (Intel VAAPI)</option>
|
|
<option value="auto" {% if settings.encoding.mode == 'auto' %}selected{% endif %}>Auto-Erkennung</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="gpu_device">GPU Device</label>
|
|
<select name="gpu_device" id="gpu_device">
|
|
{% for device in gpu_devices %}
|
|
<option value="{{ device }}" {% if device == settings.encoding.gpu_device %}selected{% endif %}>{{ device }}</option>
|
|
{% endfor %}
|
|
{% if not gpu_devices %}
|
|
<option value="/dev/dri/renderD128">Keine GPU erkannt</option>
|
|
{% endif %}
|
|
</select>
|
|
{% if gpu_available %}
|
|
<span class="status-badge ok">GPU verfuegbar</span>
|
|
{% else %}
|
|
<span class="status-badge warn">Keine GPU</span>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="default_preset">Standard-Preset</label>
|
|
<select name="default_preset" id="default_preset">
|
|
{% for key, preset in presets.items() %}
|
|
<option value="{{ key }}" {% if key == settings.encoding.default_preset %}selected{% endif %}>{{ preset.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="max_parallel_jobs">Max. parallele Jobs</label>
|
|
<input type="number" name="max_parallel_jobs" id="max_parallel_jobs"
|
|
value="{{ settings.encoding.max_parallel_jobs }}" min="1" max="8">
|
|
</div>
|
|
</div>
|
|
</fieldset>
|
|
|
|
<!-- Dateien -->
|
|
<fieldset>
|
|
<legend>Dateien</legend>
|
|
<div class="form-grid">
|
|
<div class="form-group">
|
|
<label for="target_container">Ziel-Container</label>
|
|
<select name="target_container" id="target_container">
|
|
<option value="webm" {% if settings.files.target_container == 'webm' %}selected{% endif %}>WebM (AV1/Opus)</option>
|
|
<option value="mkv" {% if settings.files.target_container == 'mkv' %}selected{% endif %}>MKV (Matroska)</option>
|
|
<option value="mp4" {% if settings.files.target_container == 'mp4' %}selected{% endif %}>MP4</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="target_folder">Ziel-Ordner</label>
|
|
<input type="text" name="target_folder" id="target_folder"
|
|
value="{{ settings.files.target_folder }}"
|
|
placeholder="same = gleicher Ordner">
|
|
</div>
|
|
|
|
<div class="form-group checkbox-group">
|
|
<label>
|
|
<input type="checkbox" name="delete_source"
|
|
{% if settings.files.delete_source %}checked{% endif %}>
|
|
Quelldatei nach Konvertierung loeschen
|
|
</label>
|
|
</div>
|
|
|
|
<div class="form-group checkbox-group">
|
|
<label>
|
|
<input type="checkbox" name="recursive_scan"
|
|
{% if settings.files.recursive_scan %}checked{% endif %}>
|
|
Unterordner rekursiv scannen
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</fieldset>
|
|
|
|
<!-- Cleanup -->
|
|
<fieldset>
|
|
<legend>Cleanup</legend>
|
|
<div class="form-grid">
|
|
<div class="form-group checkbox-group">
|
|
<label>
|
|
<input type="checkbox" name="cleanup_enabled"
|
|
{% if settings.cleanup.enabled %}checked{% endif %}>
|
|
Auto-Cleanup aktivieren
|
|
</label>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Zu loeschende Extensions</label>
|
|
<input type="text" name="cleanup_extensions"
|
|
value="{{ settings.cleanup.delete_extensions | join(', ') }}"
|
|
placeholder=".avi, .wmv, .nfo, .txt, .jpg">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Ausnahmen (Muster)</label>
|
|
<input type="text" name="cleanup_exclude"
|
|
value="{{ settings.cleanup.exclude_patterns | join(', ') }}"
|
|
placeholder="readme*, *.md">
|
|
</div>
|
|
</div>
|
|
</fieldset>
|
|
|
|
<!-- Audio -->
|
|
<fieldset>
|
|
<legend>Audio</legend>
|
|
<div class="form-grid">
|
|
<div class="form-group">
|
|
<label for="audio_languages">Sprachen</label>
|
|
<input type="text" name="audio_languages" id="audio_languages"
|
|
value="{{ settings.audio.languages | join(', ') }}"
|
|
placeholder="ger, eng, und">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="audio_codec">Codec</label>
|
|
<select name="audio_codec" id="audio_codec">
|
|
<option value="libopus" {% if settings.audio.default_codec == 'libopus' %}selected{% endif %}>Opus</option>
|
|
<option value="aac" {% if settings.audio.default_codec == 'aac' %}selected{% endif %}>AAC</option>
|
|
<option value="copy" {% if settings.audio.default_codec == 'copy' %}selected{% endif %}>Stream Copy</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group checkbox-group">
|
|
<label>
|
|
<input type="checkbox" name="keep_channels"
|
|
{% if settings.audio.keep_channels %}checked{% endif %}>
|
|
Kanalanzahl beibehalten (kein Downmix)
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</fieldset>
|
|
|
|
<!-- Untertitel -->
|
|
<fieldset>
|
|
<legend>Untertitel</legend>
|
|
<div class="form-grid">
|
|
<div class="form-group">
|
|
<label for="subtitle_languages">Sprachen</label>
|
|
<input type="text" name="subtitle_languages" id="subtitle_languages"
|
|
value="{{ settings.subtitle.languages | join(', ') }}"
|
|
placeholder="ger, eng">
|
|
</div>
|
|
</div>
|
|
</fieldset>
|
|
|
|
<!-- TVDB / Bibliothek -->
|
|
<fieldset>
|
|
<legend>Bibliothek / TVDB</legend>
|
|
<div class="form-grid">
|
|
<div class="form-group">
|
|
<label for="tvdb_api_key">TVDB API Key</label>
|
|
<input type="text" name="tvdb_api_key" id="tvdb_api_key"
|
|
value="{{ settings.library.tvdb_api_key if settings.library else '' }}"
|
|
placeholder="API Key von thetvdb.com">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="tvdb_pin">TVDB PIN</label>
|
|
<input type="text" name="tvdb_pin" id="tvdb_pin"
|
|
value="{{ settings.library.tvdb_pin if settings.library else '' }}"
|
|
placeholder="Subscriber PIN (optional)">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="tvdb_language">TVDB Sprache</label>
|
|
<select name="tvdb_language" id="tvdb_language">
|
|
{% set lang = settings.library.tvdb_language if settings.library and settings.library.tvdb_language else 'deu' %}
|
|
<option value="deu" {% if lang == 'deu' %}selected{% endif %}>Deutsch</option>
|
|
<option value="eng" {% if lang == 'eng' %}selected{% endif %}>English</option>
|
|
<option value="fra" {% if lang == 'fra' %}selected{% endif %}>Francais</option>
|
|
<option value="spa" {% if lang == 'spa' %}selected{% endif %}>Espanol</option>
|
|
<option value="ita" {% if lang == 'ita' %}selected{% endif %}>Italiano</option>
|
|
<option value="jpn" {% if lang == 'jpn' %}selected{% endif %}>Japanese</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</fieldset>
|
|
|
|
<!-- Logging -->
|
|
<fieldset>
|
|
<legend>Logging</legend>
|
|
<div class="form-grid">
|
|
<div class="form-group">
|
|
<label for="log_level">Log-Level</label>
|
|
<select name="log_level" id="log_level">
|
|
{% for level in ['DEBUG', 'INFO', 'WARNING', 'ERROR'] %}
|
|
<option value="{{ level }}" {% if level == settings.logging.level %}selected{% endif %}>{{ level }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</fieldset>
|
|
|
|
<div class="form-actions">
|
|
<button type="submit" class="btn-primary">Speichern</button>
|
|
</div>
|
|
<div id="save-result"></div>
|
|
</form>
|
|
</section>
|
|
|
|
<!-- Scan-Pfade -->
|
|
<section class="admin-section">
|
|
<h2>Bibliothek - Scan-Pfade</h2>
|
|
<div id="library-paths">
|
|
<div class="loading-msg">Lade Pfade...</div>
|
|
</div>
|
|
<div class="form-grid" style="margin-top:1rem">
|
|
<div class="form-group">
|
|
<label>Name</label>
|
|
<input type="text" id="new-path-name" placeholder="z.B. Serien">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Pfad</label>
|
|
<input type="text" id="new-path-path" placeholder="/mnt/30 - Media/Serien">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Typ</label>
|
|
<select id="new-path-type">
|
|
<option value="series">Serien</option>
|
|
<option value="movie">Filme</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group" style="justify-content:flex-end">
|
|
<button class="btn-primary" onclick="addLibraryPath()">Pfad hinzufuegen</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Presets -->
|
|
<section class="admin-section">
|
|
<h2>Encoding-Presets</h2>
|
|
<div class="presets-grid">
|
|
{% for key, preset in presets.items() %}
|
|
<div class="preset-card">
|
|
<h3>{{ preset.name }}</h3>
|
|
<div class="preset-details">
|
|
<span class="tag">{{ preset.video_codec }}</span>
|
|
<span class="tag">{{ preset.container }}</span>
|
|
<span class="tag">{{ preset.quality_param }}={{ preset.quality_value }}</span>
|
|
{% if preset.hw_init %}<span class="tag gpu">GPU</span>{% else %}<span class="tag cpu">CPU</span>{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</section>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
// Scan-Pfade Verwaltung
|
|
function loadLibraryPaths() {
|
|
fetch("/api/library/paths")
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
const container = document.getElementById("library-paths");
|
|
const paths = data.paths || [];
|
|
if (!paths.length) {
|
|
container.innerHTML = '<div class="loading-msg">Keine Scan-Pfade konfiguriert</div>';
|
|
return;
|
|
}
|
|
container.innerHTML = paths.map(p => `
|
|
<div class="preset-card" style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.5rem">
|
|
<div>
|
|
<strong>${p.name}</strong>
|
|
<span class="tag">${p.media_type === 'series' ? 'Serien' : 'Filme'}</span>
|
|
<br><span style="font-size:0.8rem;color:#888">${p.path}</span>
|
|
${p.last_scan ? '<br><span style="font-size:0.75rem;color:#666">Letzter Scan: ' + p.last_scan + '</span>' : ''}
|
|
</div>
|
|
<div style="display:flex;gap:0.3rem">
|
|
<button class="btn-small btn-secondary" onclick="scanPath(${p.id})">Scannen</button>
|
|
<button class="btn-small btn-danger" onclick="deletePath(${p.id})">Loeschen</button>
|
|
</div>
|
|
</div>
|
|
`).join("");
|
|
})
|
|
.catch(() => {
|
|
document.getElementById("library-paths").innerHTML =
|
|
'<div style="text-align:center;color:#666;padding:1rem">Fehler beim Laden</div>';
|
|
});
|
|
}
|
|
|
|
function addLibraryPath() {
|
|
const name = document.getElementById("new-path-name").value.trim();
|
|
const path = document.getElementById("new-path-path").value.trim();
|
|
const mediaType = document.getElementById("new-path-type").value;
|
|
if (!name || !path) {
|
|
alert("Name und Pfad erforderlich");
|
|
return;
|
|
}
|
|
fetch("/api/library/paths", {
|
|
method: "POST",
|
|
headers: {"Content-Type": "application/json"},
|
|
body: JSON.stringify({name: name, path: path, media_type: mediaType}),
|
|
})
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.error) {
|
|
alert("Fehler: " + data.error);
|
|
} else {
|
|
document.getElementById("new-path-name").value = "";
|
|
document.getElementById("new-path-path").value = "";
|
|
loadLibraryPaths();
|
|
}
|
|
})
|
|
.catch(e => alert("Fehler: " + e));
|
|
}
|
|
|
|
function deletePath(pathId) {
|
|
if (!confirm("Scan-Pfad und alle zugehoerigen Daten loeschen?")) return;
|
|
fetch("/api/library/paths/" + pathId, {method: "DELETE"})
|
|
.then(r => r.json())
|
|
.then(() => loadLibraryPaths())
|
|
.catch(e => alert("Fehler: " + e));
|
|
}
|
|
|
|
function scanPath(pathId) {
|
|
fetch("/api/library/scan/" + pathId, {method: "POST"})
|
|
.then(r => r.json())
|
|
.then(data => alert(data.message || "Scan gestartet"))
|
|
.catch(e => alert("Fehler: " + e));
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", loadLibraryPaths);
|
|
</script>
|
|
{% endblock %}
|