import './Playlist.css'; import html from './Playlist.html'; import { getItem, getPlaylistItems } from '../../api/embyApi'; import { createParty } from '../../api/party'; import { joinMusicParty } from '../../musicBar/MusicBar'; import { navigate } from '../../router'; import type { RouteParams } from '../../router'; import { escapeHtml, formatDuration } from '../../tiles'; import { LOADING_HINT } from '../../loading'; import type { ItemDto } from '../../types'; const TICKS_PER_SECOND = 10000000; // Emby/.NET convention: 1 tick = 100ns, same as everywhere else this app touches durations /** * Route `/playlist/:id` - a playlist's own contents, read-only (see * docs/architecture.md's "Scope" note: no per-item start/add here, only * views/library's poster tiles and views/album's per-track rows do * that - a playlist item carries no back-reference to "which playlist * contains me" the way an Episode's SeriesId does, so "start from item * N, queue the rest of the playlist" isn't something this page can * reconstruct the way views/series does). The one action here is * "Lancer cette playlist" - launches the whole thing as a party, * pre-queued server-side (see index.ts's createPartyFromPlaylist). */ export async function renderPlaylist(root: HTMLElement, params: RouteParams): Promise { const playlistId = params.id as string; root.innerHTML = LOADING_HINT; let playlist: ItemDto; let items: ItemDto[]; try { [playlist, items] = await Promise.all([getItem(playlistId), getPlaylistItems(playlistId)]); } catch { root.innerHTML = '

Impossible de charger cette playlist.

'; return; } root.innerHTML = html; root.querySelector('#back')!.addEventListener('click', () => history.back()); (root.querySelector('#playlistCover') as HTMLImageElement).src = playlist.imageUrl; root.querySelector('#playlistTitle')!.textContent = playlist.name; root.querySelector('#playlistMeta')!.textContent = items.length ? `${items.length} élément${items.length > 1 ? 's' : ''}` : 'Playlist vide'; (root.querySelector('#embyLink') as HTMLAnchorElement).href = playlist.embyUrl; const listEl = root.querySelector('#itemList')!; const launchBtn = root.querySelector('#launchBtn') as HTMLButtonElement; if (!items.length) { listEl.innerHTML = '

Cette playlist ne contient rien pour le moment.

'; launchBtn.disabled = true; return; } listEl.innerHTML = items.map((item, i) => { const meta = item.overview || [item.artistName, item.albumName].filter(Boolean).join(' · '); const duration = item.durationTicks ? formatDuration(item.durationTicks / TICKS_PER_SECOND) : ''; return `
${i + 1}. ${escapeHtml(item.name)}${duration ? ` (${escapeHtml(duration)})` : ''}

${escapeHtml(meta)}

`; }).join(''); launchBtn.addEventListener('click', async () => { launchBtn.disabled = true; launchBtn.textContent = 'Démarrage...'; try { const party = await createParty(playlistId); if (party.mediaType === 'music') { // Same "no dedicated page for music" rule as everywhere else // (views/album, views/player's own dispatcher) - the persistent // bottom bar attaches and this page just goes back to being a // normal page underneath it. joinMusicParty(party.partyId, party); navigate('/home'); } else { navigate(`/party/${party.partyId}`); } } catch (err) { alert((err as Error).message); launchBtn.disabled = false; launchBtn.textContent = 'Lancer cette playlist'; } }); }