/* Lazy classic chunk; its executing singleton stays pinned until reload. */ (window.YT ||= {}).Share = (() => { const appLink = (id, t) => `${location.origin}/?v=${encodeURIComponent(id)}${t >= 1 ? `&t=${Math.floor(t)}` : ''}`; const ytLink = (id, t) => `https://youtu.be/${encodeURIComponent(id)}${t >= 1 ? `?t=${Math.floor(t)}` : ''}`; async function copy(text, label) { try { await navigator.clipboard.writeText(text); toast(`${label} copied ✓`); } catch { toast('Copy blocked — long-press the link to copy it'); } } function open() { const meta = current && current.meta; if (!meta || !meta.id || meta.custom) { toast('Play a YouTube video first'); return; } const t0 = Math.floor(Player.master.currentTime || 0); const body = document.createElement('div'); body.className = 'share-moment'; body.innerHTML = `
`; const timeIn = body.querySelector('.sm-time'); const cur = () => { const v = LyricsCore.parseStamp(timeIn.value); return v === null ? t0 : v; }; const refresh = () => { body.querySelector('.sm-app').textContent = appLink(meta.id, cur()); const yt = body.querySelector('.sm-yt'); if (yt) yt.textContent = ytLink(meta.id, cur()); }; timeIn.addEventListener('input', refresh); timeIn.addEventListener('keydown', (e) => e.stopPropagation()); refresh(); // Soundbite range: the A-B loop when one is set, else the next 20 s. const bStart = abA !== null ? abA : t0; const bEnd = abB !== null && abB > bStart ? abB : bStart + 20; body.querySelector('.sm-b-start').value = LyricsCore.fmtClock(bStart); body.querySelector('.sm-b-end').value = LyricsCore.fmtClock(bEnd); body.querySelectorAll('.sm-bite input').forEach((i) => i.addEventListener('keydown', (e) => e.stopPropagation())); let biteBlob = null; let gifBlob = null; body.addEventListener('click', async (e) => { const act = e.target.closest('[data-act]'); if (!act) return; if (act.dataset.act === 'app') copy(appLink(meta.id, cur()), 'App link'); else if (act.dataset.act === 'yt') copy(ytLink(meta.id, cur()), 'YouTube link'); else if (act.dataset.act === 'gif') { const out = body.querySelector('.sm-gif-out'); out.innerHTML = '
Making the GIF…
'; act.disabled = true; try { const d = body.querySelector('.sm-len').value; const res = await fetch(`/api/media/${encodeURIComponent(meta.id)}/gif?t=${cur()}&d=${d}&w=480`); if (!res.ok) { const j = await res.json().catch(() => ({})); throw new Error(j.error || `HTTP ${res.status}`); } gifBlob = await res.blob(); const url = URL.createObjectURL(gifBlob); const name = `${(meta.title || meta.id).replace(/[^\w\- ]+/g, '').trim().slice(0, 40) || meta.id}-${Math.floor(cur())}s.gif`; out.innerHTML = `GIF preview
${MediaIcons.svg('download')} Download ${navigator.canShare && navigator.canShare({ files: [new File([gifBlob], name, { type: 'image/gif' })] }) ? '' : ''}
`; out.dataset.name = name; } catch (err) { out.innerHTML = `
⚠ ${escapeHtml(err.message)}
`; } finally { act.disabled = false; } } else if (act.dataset.act === 'bite') { const out = body.querySelector('.sm-bite-out'); const a = LyricsCore.parseStamp(body.querySelector('.sm-b-start').value); const b = LyricsCore.parseStamp(body.querySelector('.sm-b-end').value); const fmt = body.querySelector('.sm-b-fmt').value; const max = fmt === 'm4r' ? 40 : 60; if (a === null || b === null || b <= a) { out.innerHTML = '
⚠ Times must look like 1:23, end after start
'; return; } if (b - a > max) { out.innerHTML = `
⚠ Max ${max} s for ${fmt === 'm4r' ? 'an iPhone ringtone' : 'a soundbite'}
`; return; } out.innerHTML = '
Cutting the soundbite…
'; act.disabled = true; try { const res = await fetch(`/api/media/${encodeURIComponent(meta.id)}/clip?start=${a}&end=${b}&fmt=${fmt}`); if (!res.ok) { const j = await res.json().catch(() => ({})); throw new Error(j.error || `HTTP ${res.status}`); } biteBlob = await res.blob(); const url = URL.createObjectURL(biteBlob); const name = `${(meta.title || meta.id).replace(/[^\w\- ]+/g, '').trim().slice(0, 40) || meta.id}-${Math.floor(a)}s.${fmt}`; out.dataset.name = name; out.dataset.type = fmt === 'mp3' ? 'audio/mpeg' : 'audio/mp4'; out.innerHTML = `
${MediaIcons.svg('download')} Download ${navigator.canShare && navigator.canShare({ files: [new File([biteBlob], name, { type: out.dataset.type })] }) ? '' : ''}
${fmt === 'm4r' ? '

iPhone: share it to GarageBand (or drag into Finder with the phone connected) to set it as a ringtone.

' : ''}`; } catch (err) { out.innerHTML = `
⚠ ${escapeHtml(err.message)}
`; } finally { act.disabled = false; } } else if (act.dataset.act === 'sharebite' && biteBlob) { const o = body.querySelector('.sm-bite-out'); try { await navigator.share({ files: [new File([biteBlob], o.dataset.name, { type: o.dataset.type })], title: meta.title }); } catch { /* dismissed */ } } else if (act.dataset.act === 'sharegif' && gifBlob) { const name = body.querySelector('.sm-gif-out').dataset.name; try { await navigator.share({ files: [new File([gifBlob], name, { type: 'image/gif' })], title: meta.title }); } catch { /* dismissed */ } } }); const actions = [{ label: 'Done', onClick: closeModal }]; if (navigator.share) { actions.push({ label: '↗ Share link', primary: true, onClick: async () => { try { await navigator.share({ title: meta.title, text: `${meta.title} @ ${LyricsCore.fmtClock(cur())}`, url: appLink(meta.id, cur()) }); } catch { /* dismissed */ } } }); } else { actions.push({ label: 'Copy app link', primary: true, onClick: () => copy(appLink(meta.id, cur()), 'App link') }); } showModal('🔗 Share this moment', body, actions); } // /?v=&t= — open that video at that moment. async function bootFromUrl() { let id = '', t = 0; try { const q = new URLSearchParams(location.search); id = (q.get('v') || '').trim(); t = Math.max(0, parseFloat(q.get('t')) || 0); } catch { return; } if (!isMediaId(id)) return; try { const u = new URL(location.href); u.searchParams.delete('v'); u.searchParams.delete('t'); history.replaceState(null, '', u.pathname + u.search + u.hash); } catch { /* keep */ } await playVideoAt(data.history.find((x) => x.id === id) || { id, title: '' }, t); } return { open, bootFromUrl }; })(); /* Lazy classic chunk; its executing singleton stays pinned until reload. */ (window.YT ||= {}).External = (() => { const isAndroid = /Android/i.test(navigator.userAgent); const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); const streamUrl = (id) => `${location.origin}${isUploadId(id) ? '/api/uploads/' : '/api/media/'}${encodeURIComponent(id)}`; async function cached(id) { if (isUploadId(id)) return true; // uploads are always a single file on the server try { const j = await (await fetch(`/api/media/${encodeURIComponent(id)}/status`)).json(); return j && j.status === 'ready'; } catch { return false; } } const intent = (url, title, pkg) => `intent:${url}#Intent;action=android.intent.action.VIEW;type=video/*;${pkg ? `package=${pkg};` : ''}S.title=${encodeURIComponent(title || '')};end`; function m3u(items) { return ['#EXTM3U', ...items.flatMap((v) => [`#EXTINF:${Math.round(v.duration || -1)},${(v.title || v.id).replace(/[\r\n]/g, ' ')}`, streamUrl(v.id)])].join('\n') + '\n'; } function download(name, text) { const a = document.createElement('a'); a.href = URL.createObjectURL(new Blob([text], { type: 'audio/x-mpegurl' })); a.download = name; a.click(); setTimeout(() => URL.revokeObjectURL(a.href), 2000); } async function open() { const meta = current && current.meta; if (!meta || !meta.id || meta.custom) { toast('Play a YouTube video first'); return; } const body = document.createElement('div'); body.className = 'ext-panel'; body.innerHTML = '
Checking the server copy…
'; showModal('📺 Open in another player', body, [{ label: 'Close', primary: true, onClick: closeModal }]); const ok = await cached(meta.id); if (!ok) { body.innerHTML = ``; return; } const url = streamUrl(meta.id); const title = meta.title || meta.id; const inList = queue.length > 1 && queueIndex >= 0; body.innerHTML = `
${isAndroid ? ` ▶ VLC ▶ MX Player ▶ Open with…` : isIOS ? ` ▶ VLC ▶ Infuse` : `▶ VLC`} ${inList ? `` : ''}

${isAndroid ? 'VLC / MX Player must be installed; “Open with…” shows every video app.' : isIOS ? 'Needs the VLC (or Infuse) app. The link streams from your server; nothing is downloaded first.' : 'Desktop VLC may not register vlc:// links — the .m3u file opens in VLC, mpv, IINA or any media player.'}

`; body.addEventListener('click', async (e) => { const act = e.target.closest('[data-act]'); if (!act) return; if (act.dataset.act === 'copy') { try { await navigator.clipboard.writeText(url); toast('Stream URL copied ✓'); } catch { toast('Copy blocked'); } } else if (act.dataset.act === 'm3u') { download(`${title.replace(/[^\w\- ]+/g, '').trim().slice(0, 50) || meta.id}.m3u`, m3u([meta])); } else if (act.dataset.act === 'm3u-list') { act.disabled = true; const items = queue.slice(Math.max(0, queueIndex)); const flags = await Promise.all(items.map((v) => (v.custom ? false : cached(v.id)))); const ready = items.filter((_, i) => flags[i]); download('ytplayer-queue.m3u', m3u(ready)); toast(`Playlist with ${ready.length} of ${items.length} songs${ready.length < items.length ? ' (the rest aren’t cached on the server yet)' : ''}`, { duration: 5000 }); act.disabled = false; } }); } return { open, m3u, intent }; })();