// Talks ONLY to our own backend now - never directly to Emby. The backend // holds the real Emby session (server/src/embyAuth.js); this module just // wraps fetch calls to our own /api/* routes. See docs/architecture.md for // why this changed from the first version (CORS, plus not wanting the // browser to hold an Emby token at all). async function apiFetch(path, options = {}) { const res = await fetch(path, { credentials: 'same-origin', ...options, headers: { 'Content-Type': 'application/json', ...(options.headers || {}) } }); if (!res.ok) { const body = await res.json().catch(() => ({})); throw new Error(body.error || `Request failed: ${res.status}`); } return res.json(); } export const auth = { userName: null, get isLoggedIn() { return !!this.userName; } }; /** Call once on app start to find out if our session cookie is still valid. */ export async function restoreSession() { try { const data = await apiFetch('/api/auth/me'); auth.userName = data.userName; return true; } catch (err) { auth.userName = null; return false; } } export async function login(username, password) { const data = await apiFetch('/api/auth/login', { method: 'POST', body: JSON.stringify({ username, password }) }); auth.userName = data.userName; return data; } export async function logout() { await apiFetch('/api/auth/logout', { method: 'POST' }).catch(() => {}); auth.userName = null; } export async function searchItems(query) { if (!query || query.trim().length < 2) return []; const data = await apiFetch(`/api/catalog/search?q=${encodeURIComponent(query)}`); return data.items || []; } export async function getItem(itemId) { return apiFetch(`/api/catalog/item/${itemId}`); } export async function getLibraries() { const data = await apiFetch('/api/catalog/libraries'); return data.libraries || []; } export async function getLibraryItems(libraryId) { const data = await apiFetch(`/api/catalog/libraries/${libraryId}/items`); return data.items || []; } export async function getRecentItems(libraryId) { const data = await apiFetch(`/api/catalog/libraries/${libraryId}/recent`); return data.items || []; } export async function getSeriesSeasons(seriesId) { const data = await apiFetch(`/api/catalog/series/${seriesId}/seasons`); return data.seasons || []; } export async function getSeriesEpisodes(seriesId, seasonId) { const data = await apiFetch(`/api/catalog/series/${seriesId}/episodes?seasonId=${encodeURIComponent(seasonId)}`); return data.episodes || []; } export async function getArtists(libraryId) { const data = await apiFetch(`/api/catalog/libraries/${libraryId}/artists`); return data.artists || []; } export async function getArtistAlbums(artistId) { const data = await apiFetch(`/api/catalog/artists/${artistId}/albums`); return data.albums || []; } export async function getAlbumTracks(albumId) { const data = await apiFetch(`/api/catalog/albums/${albumId}/tracks`); return data.tracks || []; } export async function getPlaybackOptions(itemId) { return apiFetch(`/api/catalog/item/${itemId}/playback-options`); } /** `options` may carry AudioStreamIndex/SubtitleStreamIndex/MaxStreamingBitrate/mediaType. */ export function streamUrl(itemId, options = {}) { const qs = new URLSearchParams( Object.fromEntries(Object.entries(options).filter(([, v]) => v !== '' && v != null)) ).toString(); return `/api/catalog/stream/${itemId}/master.m3u8${qs ? '?' + qs : ''}`; } /** Same stream, tagged for a music track - see server/src/index.js's * /master.m3u8 route for why mediaType is the one thing that matters here * (its hand-built-fallback path is genuinely different for audio). */ export function audioStreamUrl(itemId) { return streamUrl(itemId, { mediaType: 'audio' }); }