Git Repository
Mini Video Library
Mini Video Library scannt lokale Medienordner, verwaltet Metadaten und Vorschaubilder und streamt kompatible Videos direkt per HTTP an den Browser.
HTTPS
https://zanvex.de/git/mini-video-library.gitinternal/web/templates/settings.html
Zum Verzeichnis{{template "head" .}}<link rel="stylesheet" href="/static/progress.css"><link rel="stylesheet" href="/static/settings.css"><link rel="stylesheet" href="/static/data.css">
<section class="settings-shell">
<aside class="settings-tabs"><p class="eyebrow">{{t "nav.settings"}}</p><button data-tab="library">{{t "settings.library"}}</button><button data-tab="appearance">{{t "settings.appearance"}}</button><button data-tab="tracks">{{t "settings.tracks"}}</button><button data-tab="ffmpeg">{{t "settings.ffmpeg"}}</button><button data-tab="scan">{{t "settings.scan"}}</button><button data-tab="plugins">{{t "settings.plugins"}}</button><button data-tab="diagnostics">Medienprüfung</button><button data-tab="problems">{{t "settings.problems"}}</button><button data-tab="stats">{{t "settings.stats"}}</button><button data-tab="duplicates">{{t "settings.duplicates"}}</button><button data-tab="logs">{{t "settings.logs"}} <i id="error-count"></i></button></aside>
<div class="settings settings-content">
<section data-panel="tracks" hidden><h1>{{t "settings.tracks"}}</h1><form method="post" action="/api/preferences"><label>{{t "settings.audio_language"}}<select name="preferred_audio_language"><option value="">{{t "settings.language_auto"}}</option><option value="de" {{if eq .Config.PreferredAudioLanguage "de"}}selected{{end}}>Deutsch</option><option value="en" {{if eq .Config.PreferredAudioLanguage "en"}}selected{{end}}>English</option><option value="ja" {{if eq .Config.PreferredAudioLanguage "ja"}}selected{{end}}>日本語</option></select></label><label>{{t "settings.subtitle_language"}}<select name="preferred_subtitle_language"><option value="">{{t "settings.language_auto"}}</option><option value="de" {{if eq .Config.PreferredSubtitleLanguage "de"}}selected{{end}}>Deutsch</option><option value="en" {{if eq .Config.PreferredSubtitleLanguage "en"}}selected{{end}}>English</option><option value="ja" {{if eq .Config.PreferredSubtitleLanguage "ja"}}selected{{end}}>日本語</option></select></label><button class="play">{{t "settings.save"}}</button></form></section>
<section data-panel="library"><h1>{{t "settings.library"}}</h1>{{if .Saved}}<p class="notice">{{t "settings.saved"}}</p>{{end}}<form method="post"><label>{{t "settings.folders"}} <small>{{t "settings.folder_help"}}</small><textarea name="libraries" rows="7">{{range .Config.Libraries}}{{.}}
{{end}}</textarea></label><div class="twocol"><label>{{t "settings.thumbnail_width"}}<input name="thumbnail_width" type="number" min="160" max="1920" value="{{.Config.ThumbnailWidth}}"></label><label>{{t "settings.scan_interval"}}<input name="scan_interval" value="{{.Config.ScanInterval}}"></label><label>{{t "settings.ffmpeg_path"}}<input name="ffmpeg_path" value="{{.Config.FFmpegPath}}"></label><label>{{t "settings.ffprobe_path"}}<input name="ffprobe_path" value="{{.Config.FFprobePath}}"></label></div><button class="play">{{t "settings.save"}}</button></form></section>
<section data-panel="appearance" hidden><h1>{{t "settings.appearance"}}</h1><form method="post" action="/api/preferences"><label>{{t "settings.language"}}<select name="language"><option value="de" {{if eq .Config.Language "de"}}selected{{end}}>Deutsch</option><option value="en" {{if eq .Config.Language "en"}}selected{{end}}>English</option></select></label><label>{{t "settings.theme"}}<select name="theme"><option value="">{{t "settings.system"}}</option>{{range .Themes}}<option value="{{.ID}}" {{if eq $.Config.ActiveTheme .ID}}selected{{end}}>{{.Name}} · {{.Version}}</option>{{end}}</select></label><button class="play">{{t "settings.save"}}</button></form><hr><h2>{{t "settings.install_theme"}}</h2><p class="muted">{{t "settings.theme_help"}}</p><form method="post" action="/api/themes/install" enctype="multipart/form-data"><input type="file" name="theme" accept=".zip,application/zip" required><button>{{t "settings.install_theme"}}</button></form>{{if .Themes}}<h2>{{t "settings.installed_themes"}}</h2><div class="data-list">{{range .Themes}}<article><strong>{{.Name}} · {{.Version}}</strong><span class="muted">{{.Author}} · {{.Description}}</span></article>{{end}}</div>{{end}}</section>
<section data-panel="ffmpeg" hidden><h1>{{t "settings.ffmpeg"}}</h1><p class="muted">Lädt den empfohlenen Windows Essentials Build herunter, prüft die SHA-256-Prüfsumme und installiert ffmpeg sowie ffprobe lokal in <code>./ffmpeg</code>.</p><button id="install">{{t "settings.install_ffmpeg"}}</button><p id="install-status"></p></section>
<section data-panel="scan" hidden><h1>{{t "settings.scan"}}</h1><p class="muted">Neue und geänderte Videos erfassen, Metadaten lesen und Vorschaubilder erzeugen.</p><button id="scan" {{if .Scanning}}disabled{{end}}>{{t "settings.scan_now"}}</button> <button id="cancel-scan" {{if not .Scanning}}disabled{{end}}>{{t "settings.cancel"}}</button> <button onclick="fetch('/api/scan/full',{method:'POST'})">{{t "settings.full_scan"}}</button> <button onclick="fetch('/api/scan/thumbnails',{method:'POST'})">{{t "settings.rebuild_thumbnails"}}</button><div id="scan-progress" class="scan-progress" {{if not .Scanning}}hidden{{end}}><div class="progress-track"><i id="progress-bar"></i></div><strong id="progress-title">Scan wird vorbereitet …</strong><p id="status"></p></div></section>
<section data-panel="problems" hidden><h1>{{t "settings.problem_files"}}</h1><p class="muted">Dateien, die ffprobe nicht lesen konnte. Unveränderte Fehler werden sechs Stunden lang nicht erneut analysiert.</p><button onclick="fetch('/api/problems/retry',{method:'POST'})">{{t "settings.retry_all"}}</button><div id="problems" class="data-list"></div></section>
<section data-panel="stats" hidden><h1>{{t "settings.stats"}}</h1><div id="stats" class="stat-grid"></div></section>
<section data-panel="duplicates" hidden><h1>{{t "settings.duplicates"}}</h1><p class="muted">Wahrscheinlich identische Dateien anhand eines schnellen Inhalts-Hashes.</p><div id="duplicates" class="data-list"></div></section>
<section data-panel="diagnostics" hidden><h1>Medienprüfung</h1><p class="muted">Prüft Dateien, Laufzeit, Spuren, Container, Artwork und NFO-Dateien.</p><button id="run-diagnostics">Jetzt prüfen</button><div id="diagnostics" class="data-list"></div></section>
<section data-panel="logs" hidden><div class="log-heading"><div><h1>Logs</h1><p class="muted">Die letzten 500 Meldungen dieser Programmsitzung.</p></div><button id="refresh-logs">Aktualisieren</button></div><div class="log-filters"><button data-level="all">Alle</button><button data-level="ERROR">Fehler</button><button data-level="WARN">Warnungen</button><button data-level="INFO">Info</button></div><div id="logs" class="logs"><p class="muted">Logs werden geladen …</p></div></section>
<section data-panel="plugins" hidden><h1>Plugins</h1><article class="plugin-card"><h2>AniDB / TMDB</h2><p class="muted">Zugangsdaten und Suchergebnisse werden dauerhaft in der lokalen SQLite-Datenbank gespeichert.</p><form id="online-settings"><label>TMDB Read Access Token <small>{{if .Plugin.TMDBConfigured}}Token ist gespeichert – leer lassen, um ihn beizubehalten{{end}}</small><input name="tmdb_token" type="password" placeholder="{{if .Plugin.TMDBConfigured}}•••••••• (gespeichert){{end}}"></label><label>AniDB Clientname<input name="anidb_client" value="{{.Plugin.AniDBClient}}" required></label><label>AniDB Clientversion<input name="anidb_clientver" type="number" min="1" value="{{.Plugin.AniDBClientVer}}" required></label><button>Konfiguration speichern</button><span id="online-settings-status" role="status"></span></form></article><article class="plugin-card"><h2>NFO-Dateien</h2><p class="muted">Einzelne NFO-Dateien werden auf der jeweiligen Videoseite gelesen oder geschrieben.</p><button id="nfo-all">NFO für gesamte Bibliothek schreiben</button><span id="nfo-status"></span></article><article class="plugin-card"><h2>Metadaten-Editor</h2><p class="muted">Der Editor ist auf jeder Videodetailseite verfügbar.</p></article></section></div></section>
<script>
const tabs=[...document.querySelectorAll('[data-tab]')],panels=[...document.querySelectorAll('[data-panel]')];function selectTab(name){tabs.forEach(x=>x.classList.toggle('active',x.dataset.tab===name));panels.forEach(x=>x.hidden=x.dataset.panel!==name);location.hash=name;if(name==='logs')loadLogs();if(name==='problems')loadProblems();if(name==='stats')loadStats();if(name==='duplicates')loadDuplicates();if(name==='diagnostics')loadDiagnostics()}tabs.forEach(x=>x.onclick=()=>selectTab(x.dataset.tab));selectTab(location.hash.slice(1)||'library');
const scanButton=document.getElementById('scan'),scanBox=document.getElementById('scan-progress'),scanBar=document.getElementById('progress-bar'),scanTitle=document.getElementById('progress-title'),scanStatus=document.getElementById('status');function duration(s){if(!s)return '–';const h=Math.floor(s/3600),m=Math.floor((s%3600)/60),sec=Math.floor(s%60);return h?h+' Std. '+m+' Min.':m?m+' Min. '+sec+' Sek.':sec+' Sek.'}async function updateScan(){const x=await(await fetch('/api/scan/status')).json();scanButton.disabled=x.running;scanBox.hidden=!x.running&&x.started_at===0;if(x.phase==='discovering'){scanTitle.textContent='Videodateien werden gesucht …';scanBar.style.width='100%';scanBar.classList.add('indeterminate');scanStatus.textContent=x.discovered+' Videos gefunden · Laufzeit '+duration(x.elapsed_seconds)}else{scanBar.classList.remove('indeterminate');scanBar.style.width=Math.min(100,x.percent)+'%';scanTitle.textContent=x.running?x.percent.toFixed(1)+' % verarbeitet':'Scan abgeschlossen';scanStatus.textContent=x.processed+' von '+x.discovered+' verarbeitet · '+x.updated+' aktualisiert · '+x.errors+' Fehler · Laufzeit '+duration(x.elapsed_seconds)+(x.running?' · Restzeit ca. '+duration(x.eta_seconds):'')}if(x.running)setTimeout(updateScan,1000)}scanButton.onclick=async function(){this.disabled=true;scanBox.hidden=false;scanTitle.textContent='Scan wird gestartet …';const r=await fetch('/api/scan',{method:'POST'});if(r.ok)setTimeout(updateScan,300);else updateScan()};{{if .Scanning}}updateScan();{{end}}
document.getElementById('install').onclick=async function(){const b=this,s=document.getElementById('install-status');b.disabled=true;s.textContent='Download und Installation laufen …';const res=await fetch('/api/ffmpeg/install',{method:'POST'});if(!res.ok){s.textContent='Installation läuft bereits oder konnte nicht gestartet werden.';return}const timer=setInterval(async()=>{const x=await(await fetch('/api/ffmpeg/status')).json();if(x.installing)return;clearInterval(timer);if(x.installed){s.textContent='Installiert und sofort aktiviert.'}else{s.textContent='Fehler: '+x.error;b.disabled=false}},1500)};
let logEntries=[],logLevel='all';function renderLogs(){const box=document.getElementById('logs');box.replaceChildren();const rows=logEntries.filter(x=>logLevel==='all'||x.level===logLevel).reverse();if(!rows.length){const p=document.createElement('p');p.className='muted';p.textContent='Keine passenden Meldungen.';box.append(p);return}for(const x of rows){const row=document.createElement('article');row.className='log-row '+x.level.toLowerCase();const head=document.createElement('div');head.innerHTML='<time></time><b></b><strong></strong>';head.querySelector('time').textContent=new Date(x.time).toLocaleTimeString();head.querySelector('b').textContent=x.level;head.querySelector('strong').textContent=x.message;row.append(head);if(x.attributes){const pre=document.createElement('pre');pre.textContent=Object.entries(x.attributes).map(([k,v])=>k+'='+v).join('\n');row.append(pre)}box.append(row)}}async function loadLogs(){logEntries=await(await fetch('/api/logs?limit=500')).json();document.getElementById('error-count').textContent=logEntries.filter(x=>x.level==='ERROR').length||'';renderLogs()}document.getElementById('refresh-logs').onclick=loadLogs;document.querySelectorAll('[data-level]').forEach(b=>b.onclick=function(){logLevel=this.dataset.level;renderLogs()});loadLogs();setInterval(()=>{if(!document.querySelector('[data-panel="logs"]').hidden)loadLogs()},3000);
document.getElementById('cancel-scan').onclick=()=>fetch('/api/scan/cancel',{method:'POST'});function node(tag,text,cls){const n=document.createElement(tag);n.textContent=text;if(cls)n.className=cls;return n}async function loadProblems(){const data=await(await fetch('/api/problems')).json(),box=document.getElementById('problems');box.replaceChildren();if(!data.length){box.append(node('p','Keine problematischen Dateien.','muted'));return}for(const x of data){const row=node('article','',x.ignored?'ignored':'');row.append(node('strong',x.path),node('pre',x.error));const b=node('button','Ausblenden');b.onclick=async()=>{await fetch('/api/problems/ignore',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:new URLSearchParams({path:x.path})});loadProblems()};row.append(b);box.append(row)}}async function loadStats(){const x=await(await fetch('/api/stats')).json(),items=[['Videos',x.videos],['Gesamtgröße',(x.bytes/1073741824).toFixed(1)+' GiB'],['Gesamtdauer',duration(x.duration)],['Gesehen',x.watched],['Favoriten',x.favorites],['Problemdateien',x.problems],['4K',x.four_k],['Full HD',x.full_hd]],box=document.getElementById('stats');box.replaceChildren();for(const i of items){const n=node('article','');n.append(node('strong',i[1]),node('span',i[0]));box.append(n)}}async function loadDuplicates(){const data=await(await fetch('/api/duplicates')).json(),box=document.getElementById('duplicates');box.replaceChildren();if(!data.length){box.append(node('p','Keine identischen Dateien gefunden.','muted'));return}for(const x of data){const n=node('article','');n.append(node('strong',x.count+' identische Dateien · '+(x.bytes/1073741824).toFixed(2)+' GiB'),node('pre',x.paths));box.append(n)}}
document.getElementById('online-settings').onsubmit=async function(e){e.preventDefault();const s=document.getElementById('online-settings-status'),b=this.querySelector('button');b.disabled=true;s.textContent=' Wird gespeichert …';try{const r=await fetch('/api/plugins/online/settings',{method:'POST',body:new FormData(this)});s.textContent=r.ok?' Gespeichert.':' Fehler: '+await r.text()}catch(e){s.textContent=' Fehler: '+e.message}finally{b.disabled=false}};document.getElementById('nfo-all').onclick=async function(){if(!confirm('Für alle Videos NFO-Dateien schreiben? Vorhandene Dateien werden überschrieben.'))return;const s=document.getElementById('nfo-status');s.textContent=' NFO-Export läuft …';const r=await fetch('/api/plugins/nfo/write-all',{method:'POST'});s.textContent=r.ok?' '+(await r.json()).written+' Dateien geschrieben':' Fehler beim Export'};
async function loadDiagnostics(){const box=document.getElementById('diagnostics');box.replaceChildren(node('p','Bibliothek wird geprüft …','muted'));const data=await(await fetch('/api/diagnostics')).json();box.replaceChildren();if(!data.length){box.append(node('p','Keine Auffälligkeiten gefunden.','notice'));return}for(const x of data){const n=node('article','',x.level);n.append(node('strong',x.title+' · '+x.message),node('span',x.path,'muted'));box.append(n)}}document.getElementById('run-diagnostics').onclick=loadDiagnostics;
</script>{{template "foot" .}}