All checks were successful
Publier l'image Docker / publish (push) Successful in 6s
Three changes requested:
1. Server proxies Emby, browser never talks to it directly. Login is
now POST /api/auth/login {username,password} on our own backend;
the backend calls Emby's AuthenticateByName server-side and mints
its own opaque session (httpOnly cookie), storing
{embyToken,userId,userName} in memory. The browser never sees or
holds a real Emby access token. Every other call - search, item
details, images, HLS streaming, party actions - goes to our own
/api/* origin, authenticated by that cookie; server/src/embyProxy.js
forwards to Emby server-side and streams the response back
(including a playlist-rewrite pass for HLS .m3u8 so hls.js never
gets handed a URL pointing at Emby directly). This also sidesteps
the CORS problem the previous direct-to-Emby design had.
2. EMBY_SERVER_URL is backend-only config (.env.example); the login
form no longer asks for a server address at all.
3. client/css/app.css now uses colors read directly off the live Emby
server's own dark theme (--theme-primary-color-* etc, fetched via a
JS probe against the running client, not guessed): accent green
#52b54b/#6ccf65, near-black background #151216.
Tested for real: ran the server locally against the actual
EMBY_SERVER_URL and submitted intentionally-wrong credentials through
the UI - confirmed the full round trip (browser -> our backend -> real
Emby -> real 401 -> our error message) with no CORS issue, since it's
all server-to-server now. Login itself hasn't been exercised with real
credentials, so catalog/streaming remain unverified - see
docs/architecture.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
58 lines
1.9 KiB
JavaScript
58 lines
1.9 KiB
JavaScript
import { getItem } from '../embyApi.js';
|
|
import { createParty, joinParty } from '../party.js';
|
|
import { navigate } from '../router.js';
|
|
|
|
export function renderItemDetail(root, params) {
|
|
root.innerHTML = '<main class="wrap"><p class="hint">Chargement...</p></main>';
|
|
|
|
getItem(params.id).then((item) => {
|
|
root.innerHTML = `
|
|
<main class="wrap">
|
|
<button class="linklike" id="back">← Retour</button>
|
|
<div class="detail">
|
|
<img src="${item.imageUrl}" alt="" />
|
|
<div>
|
|
<h1>${escapeHtml(item.name)}</h1>
|
|
<p>${escapeHtml(item.overview || '')}</p>
|
|
|
|
<button id="createBtn">Démarrer une watch party</button>
|
|
<div class="or">ou</div>
|
|
<label>Code de la party
|
|
<input id="joinCode" maxlength="6" style="text-transform:uppercase" />
|
|
</label>
|
|
<button id="joinBtn">Rejoindre</button>
|
|
<p id="error" class="error"></p>
|
|
</div>
|
|
</div>
|
|
</main>`;
|
|
|
|
root.querySelector('#back').addEventListener('click', () => history.back());
|
|
|
|
root.querySelector('#createBtn').addEventListener('click', async () => {
|
|
try {
|
|
const party = await createParty(item.id);
|
|
navigate(`/party/${party.partyId}`);
|
|
} catch (err) {
|
|
root.querySelector('#error').textContent = err.message;
|
|
}
|
|
});
|
|
|
|
root.querySelector('#joinBtn').addEventListener('click', async () => {
|
|
const code = root.querySelector('#joinCode').value.trim();
|
|
try {
|
|
const party = await joinParty(code);
|
|
navigate(`/party/${party.partyId}`);
|
|
} catch (err) {
|
|
root.querySelector('#error').textContent = err.message;
|
|
}
|
|
});
|
|
}).catch(() => {
|
|
root.innerHTML = '<main class="wrap"><p class="error">Titre introuvable.</p></main>';
|
|
});
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
const div = document.createElement('div');
|
|
div.textContent = str;
|
|
return div.innerHTML;
|
|
}
|