This commit is contained in:
2026-04-18 23:08:52 +12:00
commit e68a8b1622
20 changed files with 4207 additions and 0 deletions
+295
View File
@@ -0,0 +1,295 @@
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
export let config = { embyUrl: '', apiKey: '', dbPath: '' };
let localConfig = { ...config };
let status = '';
let statusType = ''; // 'ok' | 'error' | 'info'
let busy = false;
function setStatus(msg, type = 'info') {
status = msg;
statusType = type;
}
async function saveConfig() {
busy = true;
setStatus('Saving…');
try {
const res = await fetch('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(localConfig)
});
if (!res.ok) throw new Error(await res.text());
config = { ...localConfig };
dispatch('configSaved', { ...localConfig });
setStatus('Settings saved.', 'ok');
} catch (e) {
setStatus(`Save failed: ${e.message}`, 'error');
} finally {
busy = false;
}
}
async function testConnection() {
if (!localConfig.embyUrl || !localConfig.apiKey) {
setStatus('Enter Emby URL and API key first.', 'error');
return;
}
busy = true;
setStatus('Connecting to Emby…');
try {
// Save config first so the server endpoint can read it
await fetch('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(localConfig)
});
const res = await fetch('/api/emby-users');
if (!res.ok) {
const err = await res.json().catch(() => ({ message: res.statusText }));
throw new Error(err.message || res.statusText);
}
const users = await res.json();
setStatus(`Connected — ${users.length} users found.`, 'ok');
} catch (e) {
setStatus(`Connection failed: ${e.message}`, 'error');
} finally {
busy = false;
}
}
async function refreshNames() {
busy = true;
setStatus('Fetching user names from Emby…');
try {
const res = await fetch('/api/emby-users');
if (!res.ok) {
const err = await res.json().catch(() => ({ message: res.statusText }));
throw new Error(err.message || res.statusText);
}
const embyUsers = await res.json();
dispatch('namesRefreshed', embyUsers);
setStatus(`Names refreshed — ${embyUsers.length} users from Emby.`, 'ok');
} catch (e) {
setStatus(`Failed: ${e.message}`, 'error');
} finally {
busy = false;
}
}
async function loadFromDb() {
if (!localConfig.dbPath) {
setStatus('Enter the DB path first.', 'error');
return;
}
busy = true;
setStatus('Reading database…');
try {
const res = await fetch('/api/db-read', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ dbPath: localConfig.dbPath })
});
if (!res.ok) {
const err = await res.json().catch(() => ({ message: res.statusText }));
throw new Error(err.message || res.statusText);
}
const { users, validation } = await res.json();
dispatch('usersLoaded', { users, validation });
if (validation?.mismatchedUsers > 0 || validation?.missingSectionUserIds > 0) {
setStatus(
`Loaded ${users.length} users from ${validation.userSource}. ` +
`${validation.mismatchedUsers} user(s) had mismatched section UserIds and ` +
`${validation.normalizedUsers} user(s) were normalized on load.`,
'ok'
);
} else {
setStatus(
`Loaded ${users.length} users from ${validation?.userSource || 'database'}. ` +
'UserSettings IDs and section UserIds match.',
'ok'
);
}
} catch (e) {
setStatus(`Failed: ${e.message}`, 'error');
} finally {
busy = false;
}
}
</script>
<div class="panel">
<h3>Settings</h3>
<section>
<h4>Emby connection</h4>
<label>
<span>Server URL</span>
<input type="text" bind:value={localConfig.embyUrl} placeholder="http://localhost:8096" />
</label>
<label>
<span>API key</span>
<input type="password" bind:value={localConfig.apiKey} placeholder="Paste your API key" />
</label>
<p class="hint">
Get your API key from Emby: Dashboard → Advanced → API Keys → New API Key.
</p>
<div class="row">
<button class="btn ghost" on:click={saveConfig} disabled={busy}>Save</button>
<button class="btn ghost" on:click={testConnection} disabled={busy}>Test connection</button>
<button class="btn ghost" on:click={refreshNames} disabled={busy || !config.apiKey}>
Refresh user names
</button>
</div>
</section>
<section>
<h4>Database file</h4>
<label>
<span>Path to users.db</span>
<input
type="text"
bind:value={localConfig.dbPath}
placeholder="C:\ProgramData\Emby-Server\data\users.db"
/>
</label>
<p class="hint">
Stop Emby before loading or writing to avoid corruption. Typical path:
<code>C:\ProgramData\Emby-Server\data\users.db</code>
</p>
<div class="row">
<button class="btn ghost" on:click={saveConfig} disabled={busy}>Save path</button>
<button class="btn accent" on:click={loadFromDb} disabled={busy || !localConfig.dbPath}>
Load from DB
</button>
</div>
</section>
{#if status}
<div class="status" class:ok={statusType === 'ok'} class:error={statusType === 'error'}>
{status}
</div>
{/if}
</div>
<style>
.panel {
padding: 0 4px;
}
h3 {
font-size: 15px;
font-weight: 700;
margin: 0 0 16px;
color: var(--text);
}
h4 {
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-muted);
margin: 0 0 10px;
}
section {
margin-bottom: 24px;
padding-bottom: 20px;
border-bottom: 1px solid var(--border);
}
section:last-of-type {
border-bottom: none;
}
label {
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: 10px;
}
label span {
font-size: 12px;
color: var(--text-muted);
font-weight: 500;
}
input {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
padding: 7px 10px;
font-size: 13px;
color: var(--text);
font-family: inherit;
width: 100%;
box-sizing: border-box;
}
input:focus {
outline: none;
border-color: var(--accent);
}
.hint {
font-size: 11px;
color: var(--text-muted);
margin: 0 0 10px;
line-height: 1.5;
}
code {
font-size: 11px;
background: var(--bg);
padding: 1px 4px;
border-radius: 3px;
}
.row {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.btn {
all: unset;
cursor: pointer;
padding: 6px 14px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
font-family: inherit;
transition: all 0.12s;
}
.btn.ghost {
color: var(--text-muted);
border: 1px solid var(--border);
}
.btn.ghost:hover:not(:disabled) {
background: var(--surface-hover);
color: var(--text);
}
.btn.accent {
background: var(--accent);
color: #fff;
}
.btn.accent:hover:not(:disabled) {
opacity: 0.9;
}
.btn:disabled {
opacity: 0.4;
cursor: default;
}
.status {
margin-top: 8px;
padding: 8px 12px;
border-radius: 6px;
font-size: 12px;
background: var(--surface-hover);
color: var(--text-muted);
border-left: 3px solid var(--border);
}
.status.ok {
border-left-color: #22c55e;
color: #86efac;
}
.status.error {
border-left-color: var(--danger);
color: #fca5a5;
}
</style>