feat: Jellyfin-Integration — ARIA steuert die Wiedergabe ueber die API
Statt sich durch die Fire-TV-Oberflaeche zu klicken (fehleranfaellig: ARIA ist am Titel vorbeigescrollt und auf Home gelandet) spricht ARIA den Jellyfin-Server direkt an: suchen -> Sitzung des Abspielgeraets finden -> abspielen. - Diagnostic (Einstellungen > Integrationen): Jellyfin-Abschnitt mit MEHREREN Servern (+-Button; eigener, vom Kumpel ...), einer davon Standard. Pro Server Name/URL/API-Key + Test-Knopf (prueft /System/Info, meldet Name und Version). Gespeichert in /shared/config/jellyfin.json mit Datei-Rechten 0600 (API-Keys). - Brain: jellyfin_search / jellyfin_devices / jellyfin_play / jellyfin_control. Serverwahl per Name oder Standard. 'resume' ist echt implementiert (liest PlaybackPositionTicks und setzt startPositionTicks) statt nur zu existieren. Fehlertexte sind handlungsleitend: 401 -> API-Key pruefen; kein Geraet -> die Jellyfin-App muss offen sein, sonst erst host_app_launch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -961,6 +961,33 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Jellyfin (Medien) -->
|
||||
<div class="settings-section">
|
||||
<h2>Jellyfin (Filme & Serien)</h2>
|
||||
<div style="font-size:11px;color:#8888AA;margin-bottom:8px;">
|
||||
Damit steuert ARIA die Wiedergabe direkt über die Jellyfin-Schnittstelle statt
|
||||
sich durch die Fire-TV-Oberfläche zu klicken: Film suchen → Abspielgerät finden →
|
||||
starten. Mehrere Server möglich (eigener, vom Kumpel …) — einer davon ist der
|
||||
Standard, wenn ARIA keinen nennt.
|
||||
<br>API-Key: Jellyfin → <em>Dashboard → API-Schlüssel → +</em>.
|
||||
<strong>ARIAs Brain muss den Server erreichen können</strong> (URL aus Sicht des Servers,
|
||||
nicht des Browsers).
|
||||
</div>
|
||||
<div class="card" style="max-width:780px;">
|
||||
<div id="jellyfin-list" style="display:flex;flex-direction:column;gap:10px;">
|
||||
<div style="color:#555570;font-style:italic;">Lade …</div>
|
||||
</div>
|
||||
<div style="margin-top:14px;display:flex;gap:8px;align-items:center;">
|
||||
<button class="btn secondary" onclick="addJellyfinServer()" style="padding:6px 14px;font-size:12px;">+ Server</button>
|
||||
<button class="btn" onclick="saveJellyfinConfig()" style="padding:6px 14px;font-size:12px;">Speichern</button>
|
||||
<span id="jellyfin-status" style="color:#8888AA;font-size:11px;"></span>
|
||||
</div>
|
||||
<div style="color:#666680;font-size:10px;margin-top:8px;">
|
||||
API-Keys werden mit Datei-Rechten 0600 gespeichert (/shared/config/jellyfin.json).
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FLUX Bildgenerierung -->
|
||||
<div class="settings-section">
|
||||
<h2>FLUX Bildgenerierung</h2>
|
||||
@@ -4537,6 +4564,7 @@
|
||||
loadRuntimeConfig();
|
||||
loadOnboardingQR();
|
||||
loadOAuthServices();
|
||||
loadJellyfinConfig();
|
||||
refreshVoiceIdStatus();
|
||||
} else if (tab === 'brain') {
|
||||
loadBrainStatus();
|
||||
@@ -4743,6 +4771,120 @@
|
||||
send({ action: 'host_forget', hostId });
|
||||
}
|
||||
|
||||
// ── Jellyfin-Server (Medien-Integration) ─────────────────────────
|
||||
let jellyfinServers = [];
|
||||
|
||||
async function loadJellyfinConfig() {
|
||||
try {
|
||||
const r = await fetch('/api/jellyfin-config');
|
||||
const d = await r.json();
|
||||
jellyfinServers = d.servers || [];
|
||||
} catch (e) {
|
||||
jellyfinServers = [];
|
||||
}
|
||||
renderJellyfin();
|
||||
}
|
||||
|
||||
function addJellyfinServer() {
|
||||
jellyfinServers.push({
|
||||
id: 'jf_' + Date.now().toString(36),
|
||||
name: '', url: '', apiKey: '',
|
||||
isDefault: jellyfinServers.length === 0,
|
||||
});
|
||||
renderJellyfin();
|
||||
}
|
||||
|
||||
function removeJellyfinServer(i) {
|
||||
const s = jellyfinServers[i];
|
||||
if (!confirm('Server "' + (s.name || s.url || 'neu') + '" entfernen?')) return;
|
||||
jellyfinServers.splice(i, 1);
|
||||
if (jellyfinServers.length && !jellyfinServers.some(x => x.isDefault)) {
|
||||
jellyfinServers[0].isDefault = true;
|
||||
}
|
||||
renderJellyfin();
|
||||
}
|
||||
|
||||
function setJellyfinDefault(i) {
|
||||
jellyfinServers.forEach((s, k) => { s.isDefault = (k === i); });
|
||||
renderJellyfin();
|
||||
}
|
||||
|
||||
function updateJellyfinField(i, field, value) {
|
||||
if (jellyfinServers[i]) jellyfinServers[i][field] = value;
|
||||
}
|
||||
|
||||
function renderJellyfin() {
|
||||
const box = document.getElementById('jellyfin-list');
|
||||
if (!box) return;
|
||||
if (!jellyfinServers.length) {
|
||||
box.innerHTML = '<div style="color:#555570;font-style:italic;">' +
|
||||
'Noch kein Server. Mit „+ Server" einen anlegen.</div>';
|
||||
return;
|
||||
}
|
||||
box.innerHTML = jellyfinServers.map((s, i) =>
|
||||
'<div style="border:1px solid #1E1E2E;border-radius:8px;padding:10px;">' +
|
||||
'<div style="display:flex;gap:8px;align-items:center;margin-bottom:8px;">' +
|
||||
'<input type="radio" name="jf-default" ' + (s.isDefault ? 'checked' : '') +
|
||||
' onchange="setJellyfinDefault(' + i + ')" title="Standard-Server">' +
|
||||
'<span style="color:#8888AA;font-size:11px;">Standard</span>' +
|
||||
'<input type="text" value="' + escapeHtml(s.name || '') + '" placeholder="Name (z.B. Zuhause, Kumpel Tim)"' +
|
||||
' oninput="updateJellyfinField(' + i + ',\'name\',this.value)"' +
|
||||
' style="flex:1;background:#1E1E2E;color:#fff;border:1px solid #2A2A3E;border-radius:6px;padding:5px 9px;font-size:12px;">' +
|
||||
'<button class="btn secondary" onclick="testJellyfinServer(' + i + ')" style="padding:4px 10px;font-size:11px;">Test</button>' +
|
||||
'<button class="btn secondary" onclick="removeJellyfinServer(' + i + ')" style="padding:4px 9px;font-size:11px;">✕</button>' +
|
||||
'</div>' +
|
||||
'<div style="display:flex;gap:8px;">' +
|
||||
'<input type="text" value="' + escapeHtml(s.url || '') + '" placeholder="http://192.168.0.10:8096"' +
|
||||
' oninput="updateJellyfinField(' + i + ',\'url\',this.value)"' +
|
||||
' style="flex:2;background:#1E1E2E;color:#fff;border:1px solid #2A2A3E;border-radius:6px;padding:5px 9px;font-size:12px;">' +
|
||||
'<input type="password" value="' + escapeHtml(s.apiKey || '') + '" placeholder="API-Key"' +
|
||||
' oninput="updateJellyfinField(' + i + ',\'apiKey\',this.value)"' +
|
||||
' style="flex:1;background:#1E1E2E;color:#fff;border:1px solid #2A2A3E;border-radius:6px;padding:5px 9px;font-size:12px;">' +
|
||||
'</div>' +
|
||||
'<div id="jf-test-' + i + '" style="font-size:11px;color:#8888AA;margin-top:6px;"></div>' +
|
||||
'</div>'
|
||||
).join('');
|
||||
}
|
||||
|
||||
async function testJellyfinServer(i) {
|
||||
const s = jellyfinServers[i];
|
||||
const out = document.getElementById('jf-test-' + i);
|
||||
if (!s || !out) return;
|
||||
out.textContent = 'prüfe …';
|
||||
try {
|
||||
const r = await fetch('/api/jellyfin-test', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url: s.url, apiKey: s.apiKey }),
|
||||
});
|
||||
const d = await r.json();
|
||||
out.innerHTML = d.ok
|
||||
? '<span style="color:#34C759;">✓ erreichbar — ' + escapeHtml(d.name || '') +
|
||||
' (Version ' + escapeHtml(d.version || '?') + ')</span>'
|
||||
: '<span style="color:#FF6E6E;">✗ ' + escapeHtml(d.error || 'nicht erreichbar') + '</span>';
|
||||
} catch (e) {
|
||||
out.innerHTML = '<span style="color:#FF6E6E;">✗ ' + escapeHtml(e.message) + '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
async function saveJellyfinConfig() {
|
||||
const el = document.getElementById('jellyfin-status');
|
||||
el.textContent = 'speichere …';
|
||||
try {
|
||||
const r = await fetch('/api/jellyfin-config', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ servers: jellyfinServers }),
|
||||
});
|
||||
const d = await r.json();
|
||||
if (!d.ok) throw new Error(d.error || 'Fehler');
|
||||
jellyfinServers = d.config.servers || [];
|
||||
renderJellyfin();
|
||||
el.innerHTML = '<span style="color:#34C759;">✓ gespeichert (' + jellyfinServers.length + ')</span>';
|
||||
setTimeout(() => { el.textContent = ''; }, 4000);
|
||||
} catch (e) {
|
||||
el.innerHTML = '<span style="color:#FF6E6E;">✗ ' + escapeHtml(e.message) + '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// ── APK-Rollout auf Android/Fire TV (per Satellit + ADB) ─────────
|
||||
let apkTarget = null; // {satId, ip, name}
|
||||
let apkRequestId = null;
|
||||
|
||||
Reference in New Issue
Block a user