|
1 | 1 | import { BTR_BASE, getToken, connect, verify, disconnect } from './bittorrented.js'; |
2 | 2 |
|
3 | | -const $ = (id) => document.getElementById(id); |
4 | | -const el = (t, c, x) => { const e = document.createElement(t); if (c) e.className = c; if (x != null) e.textContent = x; return e; }; |
| 3 | +const el = (id) => document.getElementById(id); |
| 4 | +function escapeHtml(s) { const d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML; } |
| 5 | +function escAttr(s) { return String(s || '').replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>'); } |
| 6 | +function setStatus(t, k) { const s = el('status'); s.textContent = t; s.className = 'status ' + (k || ''); } |
5 | 7 |
|
6 | | -let activeTab = 'favorites'; |
7 | | - |
8 | | -function setStatus(text, kind) { const s = $('status'); s.textContent = text; s.className = 'status ' + (kind || ''); } |
| 8 | +let tokenCache = ''; |
9 | 9 |
|
10 | 10 | function showConnected(email) { |
11 | | - $('connect').classList.remove('active'); |
12 | | - $('content').classList.add('active'); |
13 | | - $('acct').textContent = email || 'connected'; |
14 | | - $('disc').classList.remove('hidden'); |
15 | | - loadTab(activeTab); |
| 11 | + el('connect').classList.remove('active'); |
| 12 | + el('content').classList.add('active'); |
| 13 | + el('acct').textContent = email || 'connected'; |
| 14 | + el('disc').classList.remove('hidden'); |
| 15 | + loadFavorites(); |
16 | 16 | } |
17 | | - |
18 | 17 | function showDisconnected() { |
19 | | - $('content').classList.remove('active'); |
20 | | - $('connect').classList.add('active'); |
21 | | - $('acct').textContent = ''; |
22 | | - $('disc').classList.add('hidden'); |
23 | | -} |
24 | | - |
25 | | -// bittorrented item shapes vary by section — render defensively from common fields. |
26 | | -function fieldOf(o, keys) { for (const k of keys) if (o && o[k] != null && o[k] !== '') return o[k]; return ''; } |
27 | | - |
28 | | -function renderItems(items) { |
29 | | - const list = $('list'); list.innerHTML = ''; |
30 | | - if (!Array.isArray(items) || items.length === 0) { list.appendChild(el('div', 'muted', 'Nothing here yet.')); return; } |
31 | | - for (const it of items) { |
32 | | - const title = fieldOf(it, ['title', 'name', 'label']) || '(untitled)'; |
33 | | - const url = fieldOf(it, ['url', 'stream', 'streamUrl', 'link', 'magnet', 'href']); |
34 | | - const sub = fieldOf(it, ['group', 'category', 'genre', 'author', 'description']); |
35 | | - const row = el('div', 'msg'); |
36 | | - const n = el('span', 'n', '▶'); |
37 | | - const a = url ? el('a', null, title) : el('span', null, title); |
38 | | - if (url) { a.href = url; a.target = '_blank'; a.rel = 'noreferrer'; } |
39 | | - row.append(n, a); |
40 | | - if (sub) { const s = el('span', 't', ' ' + sub); row.append(s); } |
41 | | - list.appendChild(row); |
42 | | - } |
| 18 | + el('content').classList.remove('active'); |
| 19 | + el('connect').classList.add('active'); |
| 20 | + el('acct').textContent = ''; |
| 21 | + el('disc').classList.add('hidden'); |
43 | 22 | } |
44 | 23 |
|
45 | | -async function loadTab(tab) { |
46 | | - activeTab = tab; |
47 | | - document.querySelectorAll('#media-tabs .tab').forEach((b) => b.classList.toggle('active', b.dataset.tab === tab)); |
48 | | - const list = $('list'); list.innerHTML = ''; list.appendChild(el('div', 'muted', 'Loading…')); |
| 24 | +// --- favorites (Live TV / Podcasts / Radio / Music / Books / Movies) --------- |
| 25 | +async function loadFavorites() { |
49 | 26 | const token = await getToken(); |
50 | | - // Map UI tabs → bittorrented.com /api/v1 endpoints. |
51 | | - const path = { favorites: 'favorites', livetv: 'livetv', radio: 'radio', podcasts: 'podcasts' }[tab] || 'favorites'; |
| 27 | + tokenCache = token || ''; |
| 28 | + el('btr').innerHTML = '<p class="muted">Loading favorites…</p>'; |
| 29 | + let data; |
52 | 30 | try { |
53 | | - const r = await fetch(`${BTR_BASE}/api/v1/${path}`, { headers: { authorization: `Bearer ${token}` } }); |
| 31 | + const r = await fetch(`${BTR_BASE}/api/v1/favorites`, { headers: { authorization: `Bearer ${token}` } }); |
54 | 32 | if (r.status === 401) { showDisconnected(); setStatus('Session expired — connect again.', 'err'); return; } |
55 | | - const data = await r.json().catch(() => ([])); |
56 | | - renderItems(Array.isArray(data) ? data : (data.items || data.results || data[path] || [])); |
57 | | - } catch (e) { |
58 | | - list.innerHTML = ''; |
59 | | - list.appendChild(el('div', 'muted', 'Could not load ' + path + '. ' + ((e && e.message) || ''))); |
| 33 | + if (!r.ok) { el('btr').innerHTML = `<p class="muted">Couldn’t load favorites (${r.status}).</p>`; return; } |
| 34 | + data = await r.json(); |
| 35 | + } catch (e) { el('btr').innerHTML = `<p class="muted">Couldn’t load favorites. ${escapeHtml((e && e.message) || '')}</p>`; return; } |
| 36 | + |
| 37 | + const tv = data.tv || [], radio = data.radio || [], pods = data.podcasts || [], movies = data.movies || []; |
| 38 | + |
| 39 | + const tile = (player, url, img, ph, label, sub, played) => |
| 40 | + `<a class="btr-item" data-label="${escAttr(String(label || '').toLowerCase())}" ${player ? `href="#" data-player="${escAttr(player)}"` : `href="${escAttr(url)}" target="_blank" rel="noreferrer"`}>` + |
| 41 | + (img ? `<img src="${escAttr(img)}" alt="" loading="lazy" referrerpolicy="no-referrer" />` : `<span class="btr-ph">${ph}</span>`) + |
| 42 | + (played ? '<span class="btr-played" title="Played">✓</span>' : '') + |
| 43 | + `<span class="btr-label">${escapeHtml(label)}</span>` + |
| 44 | + (sub ? `<span class="btr-sub">${escapeHtml(sub)}</span>` : '') + '</a>'; |
| 45 | + const group = (title, items) => items.length |
| 46 | + ? `<div class="btr-group" data-group><h3>${title} <span class="btr-count">${items.length}</span></h3><div class="btr-items">${items.join('')}</div></div>` : ''; |
| 47 | + |
| 48 | + if (!tv.length && !radio.length && !pods.length && !movies.length) { |
| 49 | + el('btr').innerHTML = '<p class="muted">Connected — add Live TV, radio, podcast or movie favorites on bittorrented.com and they’ll show up here.</p>'; |
| 50 | + return; |
60 | 51 | } |
| 52 | + const music = movies.filter((m) => m.contentType === 'music'); |
| 53 | + const books = movies.filter((m) => m.contentType === 'book'); |
| 54 | + const shows = movies.filter((m) => !['music', 'book'].includes(m.contentType)); |
| 55 | + |
| 56 | + el('btr').innerHTML = '<div id="btr-groups">' + |
| 57 | + group('Live TV', tv.map((c) => tile(c.player, c.url, c.logo, '📺', c.name))) + |
| 58 | + group('Podcasts', pods.map((p) => { |
| 59 | + const ep = (p.episodes || [])[0]; |
| 60 | + const played = !!(ep && ep.progress && ep.progress.completed); |
| 61 | + const sub = ep ? (played ? `✓ ${ep.title}` : `▶ ${ep.title}`) : 'no recent episodes'; |
| 62 | + return tile(ep && ep.player, p.url, p.image, '🎙', p.title, sub, played); |
| 63 | + })) + |
| 64 | + group('Radio', radio.map((s) => tile(s.player, s.url, s.logo, '📻', s.name))) + |
| 65 | + group('Music', music.map((m) => tile(m.player, m.url, m.poster, '🎵', m.title))) + |
| 66 | + group('Books', books.map((m) => tile(m.player, m.url, m.poster, '📖', m.title))) + |
| 67 | + group('Movies & Shows', shows.map((m) => tile(m.player, m.url, m.poster, '🎬', m.title))) + |
| 68 | + '</div>'; |
61 | 69 | } |
62 | 70 |
|
63 | | -document.querySelectorAll('#media-tabs .tab').forEach((b) => b.addEventListener('click', () => loadTab(b.dataset.tab))); |
| 71 | +// live filter |
| 72 | +el('btr-filter').addEventListener('input', (e) => { |
| 73 | + const q = e.target.value.trim().toLowerCase(); |
| 74 | + document.querySelectorAll('#btr-groups [data-group]').forEach((g) => { |
| 75 | + let any = false; |
| 76 | + g.querySelectorAll('.btr-item').forEach((it) => { |
| 77 | + const show = !q || (it.getAttribute('data-label') || '').includes(q); |
| 78 | + it.style.display = show ? '' : 'none'; |
| 79 | + if (show) any = true; |
| 80 | + }); |
| 81 | + g.style.display = any ? '' : 'none'; |
| 82 | + }); |
| 83 | +}); |
64 | 84 |
|
65 | | -$('go').addEventListener('click', async () => { |
| 85 | +// --- player modal (bittorrented /api/player in a themed iframe) --------------- |
| 86 | +function openPlayer(url) { |
| 87 | + const sep = url.includes('?') ? '&' : '?'; |
| 88 | + const tk = tokenCache ? `&token=${encodeURIComponent(tokenCache)}` : ''; |
| 89 | + const type = (url.match(/[?&]type=([^&]+)/) || [])[1] || ''; |
| 90 | + const isAudio = ['audio', 'podcast', 'music', 'radio'].includes(type); |
| 91 | + el('player-modal').classList.toggle('audio', isAudio); |
| 92 | + el('player-frame').src = `${url}${sep}theme=dark&bg=${encodeURIComponent('#05070d')}&accent=${encodeURIComponent('#34e7ff')}${tk}&_=${Date.now()}`; |
| 93 | + el('player-modal').hidden = false; |
| 94 | +} |
| 95 | +function closePlayer() { el('player-modal').hidden = true; el('player-frame').src = 'about:blank'; } |
| 96 | +el('btr').addEventListener('click', (e) => { |
| 97 | + const a = e.target.closest('[data-player]'); |
| 98 | + if (a) { e.preventDefault(); openPlayer(a.getAttribute('data-player')); } |
| 99 | +}); |
| 100 | +el('player-close').addEventListener('click', closePlayer); |
| 101 | +el('player-modal').addEventListener('click', (e) => { if (e.target === el('player-modal') && !el('player-modal').classList.contains('audio')) closePlayer(); }); |
| 102 | + |
| 103 | +// --- connect / disconnect ----------------------------------------------------- |
| 104 | +el('go').addEventListener('click', async () => { |
66 | 105 | setStatus('Opening bittorrented.com… finish signing in & approving in the new tab.'); |
67 | 106 | try { |
68 | 107 | const res = await connect(); |
69 | 108 | if (res.connected) { setStatus('Connected ✓', 'ok'); showConnected(res.email); } |
70 | 109 | else setStatus('Connection not completed.', 'err'); |
71 | 110 | } catch (e) { setStatus((e && e.message) || 'connect failed', 'err'); } |
72 | 111 | }); |
| 112 | +el('disc').addEventListener('click', async () => { await disconnect(); showDisconnected(); }); |
73 | 113 |
|
74 | | -$('disc').addEventListener('click', async () => { await disconnect(); showDisconnected(); }); |
75 | | - |
76 | | -// On load: if already connected, jump straight to content. |
77 | | -(async () => { |
78 | | - const res = await verify(); |
79 | | - if (res.connected) showConnected(res.email); |
80 | | -})(); |
| 114 | +(async () => { const res = await verify(); if (res.connected) showConnected(res.email); })(); |
0 commit comments