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 { showToast } from '../../toast'; 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. Two actions: * "Lancer cette playlist" (always from the first item) and, per row, * "Lire à partir d'ici" - both launch a party pre-queued server-side * (see index.ts's createPartyFromPlaylist), the second just also * passing which item to start from. That second one could only ever * be built here, on this exact page: unlike an Episode's own SeriesId, * a playlist item carries no back-reference to "which playlist * contains me", so resolving "item N of playlist P" needs playlist * P's own already-fetched item list in hand, not just the clicked * item's bare id - views/library's tiles and views/album's per-track * rows have no such list to offer, only this page does. */ 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(''); /** * Shared by "Lancer cette playlist" (startAtItemId omitted - the * server then just starts from the playlist's own first item, same * as always) and every row's own "Lire à partir d'ici" - only the * button clicked, its busy label, and whether a starting point is * passed differ; everything after that (create, dispatch on * mediaType, error handling) is identical either way. */ async function launch(btn: HTMLButtonElement, busyLabel: string, idleLabel: string, startAtItemId?: string) { btn.disabled = true; btn.textContent = busyLabel; try { const party = await createParty(playlistId, startAtItemId); 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) { showToast((err as Error).message, { variant: 'error' }); btn.disabled = false; btn.textContent = idleLabel; } } launchBtn.addEventListener('click', () => launch(launchBtn, 'Démarrage...', 'Lancer cette playlist')); listEl.querySelectorAll('.startFromBtn').forEach((btn) => { btn.addEventListener('click', () => launch(btn, 'Démarrage...', "Lire à partir d'ici", btn.dataset.itemId)); }); }