// Talks ONLY to our own backend now - never directly to Emby. The backend // holds the real Emby session (server/src/services/AuthService.ts); 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). import type { ItemDto, LibraryDto, PlaybackOptionsDto, SeasonDto } from '../types'; async function apiFetch(path: string, options: RequestInit = {}): Promise { 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() as Promise; } class AuthState { userName: string | null = null; get isLoggedIn(): boolean { return !!this.userName; } } export const auth = new AuthState(); /** Call once on app start to find out if our session cookie is still valid. */ export async function restoreSession(): Promise { try { const data = await apiFetch<{ userName: string }>('/api/auth/me'); auth.userName = data.userName; return true; } catch { auth.userName = null; return false; } } export async function login(username: string, password: string): Promise<{ userName: string }> { const data = await apiFetch<{ userName: string }>('/api/auth/login', { method: 'POST', body: JSON.stringify({ username, password }) }); auth.userName = data.userName; return data; } export async function logout(): Promise { await apiFetch('/api/auth/logout', { method: 'POST' }).catch(() => {}); auth.userName = null; } export async function searchItems(query: string): Promise { if (!query || query.trim().length < 2) return []; const data = await apiFetch<{ items: ItemDto[] }>(`/api/catalog/search?q=${encodeURIComponent(query)}`); return data.items || []; } export function getItem(itemId: string): Promise { return apiFetch(`/api/catalog/item/${itemId}`); } export async function getLibraries(): Promise { const data = await apiFetch<{ libraries: LibraryDto[] }>('/api/catalog/libraries'); return data.libraries || []; } export async function getLibraryItems(libraryId: string): Promise { const data = await apiFetch<{ items: ItemDto[] }>(`/api/catalog/libraries/${libraryId}/items`); return data.items || []; } export async function getRecentItems(libraryId: string): Promise { const data = await apiFetch<{ items: ItemDto[] }>(`/api/catalog/libraries/${libraryId}/recent`); return data.items || []; } export async function getSeriesSeasons(seriesId: string): Promise { const data = await apiFetch<{ seasons: SeasonDto[] }>(`/api/catalog/series/${seriesId}/seasons`); return data.seasons || []; } export async function getSeriesEpisodes(seriesId: string, seasonId: string): Promise { const data = await apiFetch<{ episodes: ItemDto[] }>(`/api/catalog/series/${seriesId}/episodes?seasonId=${encodeURIComponent(seasonId)}`); return data.episodes || []; } export async function getArtists(libraryId: string): Promise { const data = await apiFetch<{ artists: ItemDto[] }>(`/api/catalog/libraries/${libraryId}/artists`); return data.artists || []; } export async function getArtistAlbums(artistId: string): Promise { const data = await apiFetch<{ albums: ItemDto[] }>(`/api/catalog/artists/${artistId}/albums`); return data.albums || []; } export async function getAlbumTracks(albumId: string): Promise { const data = await apiFetch<{ tracks: ItemDto[] }>(`/api/catalog/albums/${albumId}/tracks`); return data.tracks || []; } /** A playlist's own contents, in order - see views/playlist/Playlist.ts. * Launching the whole thing is createParty(playlistId) (api/party.ts) - * same call as any other item, the server decides what "launch a * playlist" means from the id alone (see index.ts). */ export async function getPlaylistItems(playlistId: string): Promise { const data = await apiFetch<{ items: ItemDto[] }>(`/api/catalog/playlists/${playlistId}/items`); return data.items || []; } export function getPlaybackOptions(itemId: string): Promise { return apiFetch(`/api/catalog/item/${itemId}/playback-options`); } /** `options` may carry AudioStreamIndex/MaxStreamingBitrate/mediaType - not * subtitles, which are their own separate request now (see subtitleUrl * below and Player.ts's applySubtitleTrack for why). */ export function streamUrl(itemId: string, options: Record = {}): string { const entries = Object.entries(options).filter(([, v]) => v !== '' && v != null) as [string, string][]; const qs = new URLSearchParams(entries).toString(); return `/api/catalog/stream/${itemId}/master.m3u8${qs ? '?' + qs : ''}`; } /** Same stream, tagged for a music track - see server/src/services/ * CatalogService.ts's proxyStream for why mediaType is the one thing * that matters here (its hand-built-fallback path is genuinely different * for audio). */ export function audioStreamUrl(itemId: string): string { return streamUrl(itemId, { mediaType: 'audio' }); } /** A subtitle track's own text, as WebVTT (server/src/services/ * CatalogService.ts's proxySubtitle converts it, whatever Emby's own * source format is) - for a native , not part of the video * stream at all. Only ever built for a track getPlaybackOptions already * reported as isTextBased - see Player.ts's subtitle