1const site = document.body.dataset.site || ''; 2const base = `/${site}`; 3const catalogNode = document.querySelector('#catalog-data'); 4const catalog = catalogNode ? JSON.parse(catalogNode.textContent || '[]') : []; 5const esc = (value = '') => String(value).replace(/[&<>\"]/g, (character) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[character]); 6const slugify = (value = '') => String(value).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''); 7const listKey = `${site}:watchlist:v3`; 8const readList = () => { 9 try { 10 const ids = JSON.parse(localStorage.getItem(listKey) || '[]'); 11 return Array.isArray(ids) ? [...new Set(ids.filter(id => typeof id === 'string' && id))] : []; 12 } catch { return []; } 13}; 14let saveNoticeTimer; 15function showSaveError() { 16 let notice = document.querySelector('[data-save-notice]'); 17 if (!notice) { 18 notice = document.createElement('div'); 19 notice.dataset.saveNotice = ''; 20 notice.setAttribute('role', 'status'); 21 notice.style.cssText = 'position:fixed;bottom:16px;left:16px;right:16px;max-width:460px;margin:auto;padding:14px 18px;background:#182127;color:#fff;border:1px solid #73818a;border-radius:8px;z-index:10000;font:16px/1.5 sans-serif'; 22 document.body.append(notice); 23 } 24 notice.textContent = 'Your browser could not save this change. Check its storage settings.'; 25 notice.hidden = false; 26 clearTimeout(saveNoticeTimer); 27 saveNoticeTimer = setTimeout(() => { notice.hidden = true; }, 6000); 28} 29const writeList = (ids) => { 30 try { localStorage.setItem(listKey, JSON.stringify([...new Set(ids)])); return true; }
31 catch { showSaveError(); return false; } 32}; 33 34const menu = document.querySelector('[data-menu]'); 35const menuNav = document.querySelector('.navlinks'); 36function closeNavigation() { 37 menu?.setAttribute('aria-expanded', 'false'); 38 menu?.setAttribute('aria-label', 'Open navigation'); 39 menuNav?.classList.remove('open'); 40} 41menu?.addEventListener('click', () => { 42 const open = menu.getAttribute('aria-expanded') !== 'true'; 43 menu.setAttribute('aria-expanded', String(open)); 44 menu.setAttribute('aria-label', open ? 'Close navigation' : 'Open navigation'); 45 menuNav?.classList.toggle('open', open); 46}); 47document.addEventListener('keydown', (event) => { 48 if (event.key === 'Escape' && menu?.getAttribute('aria-expanded') === 'true') { 49 closeNavigation(); 50 menu.focus(); 51 } 52}); 53document.addEventListener('click', (event) => { 54 if (!menu?.contains(event.target) && !menuNav?.contains(event.target)) closeNavigation(); 55}); 56menuNav?.addEventListener('click', (event) => { if (event.target.closest('a')) closeNavigation(); }); 57window.addEventListener('resize', () => { if (menu && getComputedStyle(menu).display === 'none') closeNavigation(); }); 58 59document.querySelectorAll('[data-search]').forEach((form) => form.addEventListener('submit', (event) => { 60 event.preventDefault(); 61 const query = String(new FormData(form).get('q') || '').trim(); 62 location.href = `${base}/search/?q=${encodeURIComponent(query)}`; 63})); 64 65function toolCard(title, label = '') { 66 const rating = Number(title.rating) > 0 && Number(title.voteCount) > 0 ? ` · â ${Number(title.rating).toFixed(1)}` : ''; 67 const active = readList().includes(title.id); 68 return `<article class="tool-card"><a href="${title.path}"><img src="${esc(title.poster)}" alt="${esc(title.title)} poster" width="172" height="258"><div>${label ? `<small>${esc(label)}</small>` : ''}<h3>${esc(title.title)}</h3><p>${esc(title.year)} · ${esc(title.genre)}${rating}</p></div></a><button type="button" data-watch-id="${esc(title.id)}" class="${active ? 'saved' : ''}" aria-pressed="${active}" aria-label="${active ? 'Remove' : 'Save'} ${esc(title.title)} ${active ? 'from' : 'to'} My List">${active ? 'â Saved' : 'ï¼ Save'}</button></article>`; 69} 70 71const output = document.querySelector('[data-tool-output]'); 72const tool = document.querySelector('[data-tool]'); 73let toolTurn = 0; 74function runTool() { 75 if (!output || !catalog.length) return; 76 const values = tool ? new FormData(tool) : new FormData(); 77 const mood = String(values.get('mood') || ''); 78 const format = String(values.get('format') || ''); 79 const genre = String(values.get('genre') || ''); 80 const windowValue = String(values.get('window') || ''); 81 const mode = tool?.dataset.toolMode || 'mix'; 82 const genreMap = { bold: ['Action', 'Adventure', 'Thriller'], warm: ['Comedy', 'Family', 'Romance'], strange: ['Fantasy', 'Science Fiction', 'Mystery'], serious: ['Drama', 'History', 'Crime'] }; 83 let pool = catalog.filter((title) => !format || title.type === format).filter((title) => !mood || title.genres.some((item) => genreMap[mood]?.includes(item))).filter((title) => !genre || title.genres.some((item) => slugify(item) === genre)); 84 if (windowValue || ['today', 'broadcast'].includes(mode)) pool.sort((a, b) => String(b.releaseDate || '').localeCompare(String(a.releaseDate || ''))); 85 if (mode === 'radar' || mode === 'route') pool.sort((a, b) => Number(b.popularity || 0) - Number(a.popularity || 0)); 86 if (mode === 'compare') pool.sort((a, b) => (Number(b.rating || 0) * Math.log10(Number(b.voteCount || 0) + 1)) - (Number(a.rating || 0) * Math.log10(Number(a.voteCount || 0) + 1))); 87 if (!pool.length) pool = catalog; 88 const count = mode === 'double' ? 2 : 3; 89 const selected = Array.from({ length: count }, (_, index) => pool[(toolTurn + index * 5) % pool.length]); 90 const labels = mode === 'double' ? ['Feature presentation', 'Second half'] : mode === 'compare' ? ['Strongest signal', 'Different angle', 'Wildcard'] : mode === 'queue' ? ['Tonight', 'Weekend', 'Later'] : mode === 'three' ? ['Choice 1', 'Choice 2', 'Choice 3'] : ['First choice', 'Second angle', 'Keep for later']; 91 output.innerHTML = selected.map((title, index) => toolCard(title, labels[index])).join(''); 92 toolTurn += 1; 93} 94tool?.addEventListener('submit', (event) => { event.preventDefault(); runTool(); }); 95tool?.addEventListener('change', runTool); 96document.querySelector('[data-run-tool]')?.addEventListener('click', runTool); 97runTool(); 98 99const instant = document.querySelector('[data-instant]'); 100if (instant && output) { 101 const renderInstant = () => { 102 const query = instant.value.trim().toLowerCase(); 103 const results = (query ? catalog.filter((title) => [title.title, title.year, title.originalLanguage, ...title.genres].join(' ').toLowerCase().includes(query)) : catalog).slice(0, 12); 104 output.innerHTML = results.map((title) => toolCard(title)).join(''); 105 }; 106 instant.addEventListener('input', renderInstant); 107 renderInstant(); 108} 109 110const filter = document.querySelector('[data-filter]'); 111if (filter) { 112 const initial = new URLSearchParams(location.search); 113 for (const name of ['q', 'genre', 'year']) { 114 const field = filter.elements.namedItem(name); 115 const value = initial.get(name); 116 if (field && value !== null && (name === 'q' || [...field.options].some(option => option.value === value))) field.value = value; 117 } 118 const apply = () => { 119 const values = new FormData(filter); 120 const query = String(values.get('q') || '').toLowerCase().trim(); 121 const genre = String(values.get('genre') || ''); 122 const year = String(values.get('year') || '');
123 let count = 0; 124 document.querySelectorAll('[data-catalog] > div').forEach((node) => { 125 const match = (!query || node.dataset.title.includes(query)) && (!genre || node.dataset.genre.split('|').includes(genre)) && (!year || node.dataset.year === year); 126 node.hidden = !match; 127 if (match) count += 1; 128 }); 129 const countNode = document.querySelector('[data-result-count]'); 130 if (countNode) countNode.textContent = `${count} matching title${count === 1 ? '' : 's'}`; 131 const empty = document.querySelector('[data-empty]'); 132 if (empty) empty.hidden = count > 0; 133 const params = new URLSearchParams(location.search); 134 for (const name of ['q', 'genre', 'year']) { 135 const value = String(values.get(name) || '').trim(); 136 if (value) params.set(name, value); else params.delete(name); 137 } 138 history.replaceState(null, '', `${location.pathname}${params.size ? `?${params}` : ''}${location.hash}`); 139 }; 140 filter.addEventListener('input', apply); 141 filter.addEventListener('change', apply); 142 filter.addEventListener('submit', (event) => { event.preventDefault(); apply(); }); 143 apply(); 144} 145 146const searchForm = document.querySelector('[data-search-page]'); 147const searchResults = document.querySelector('[data-search-results]'); 148if (searchForm && searchResults) { 149 const field = searchForm.elements.q; 150 field.value = new URLSearchParams(location.search).get('q') || ''; 151 const render = () => { 152 const query = field.value.trim().toLowerCase(); 153 const results = query ? catalog.filter((title) => [title.title, title.originalTitle, title.year, title.originalLanguage, ...title.genres].join(' ').toLowerCase().includes(query)).slice(0, 80) : []; 154 searchResults.innerHTML = results.map((title) => toolCard(title)).join(''); 155 const count = document.querySelector('[data-search-count]'); 156 if (count) count.textContent = query ? `${results.length} result${results.length === 1 ? '' : 's'} for â${field.value.trim()}â` : `Search ${catalog.length} movies and TV shows.`; 157 }; 158 const updateSearch = () => { 159 const query = field.value.trim(); 160 history.replaceState(null, '', `${location.pathname}${query ? `?q=${encodeURIComponent(query)}` : ''}${location.hash}`); 161 render(); 162 }; 163 searchForm.addEventListener('submit', (event) => { event.preventDefault(); updateSearch(); }); 164 searchForm.addEventListener('input', updateSearch); 165 render(); 166} 167 168const providerNode = document.querySelector('#provider-data'); 169const region = document.querySelector('[data-region]'); 170const providerOutput = document.querySelector('[data-providers]'); 171if (providerNode && region && providerOutput) { 172 const providerData = JSON.parse(providerNode.textContent || '{}'); 173 const labels = { free: 'Free', ads: 'With ads', subscription: 'Subscription', rent: 'Rent', buy: 'Buy' }; 174 const renderProviders = () => { 175 const selected = providerData.providers?.[region.value]; 176 const selectedCountry = providerData.countries?.[region.value] || 'us'; 177 const country = selectedCountry === 'gb' ? 'uk' : selectedCountry; 178 const justWatchUrl = `https://gomoviestv.cyou/home/${country}/search?q=${encodeURIComponent(providerData.title || '')}`; 179 if (!selected) { providerOutput.innerHTML = `<div class="provider-empty"><p>Check current viewing options for this title in your country.</p><a class="action" href="${esc(justWatchUrl)}" rel="nofollow noopener" target="_blank">Search on JustWatch â</a></div>`; return; } 180 const groups = Object.entries(labels).filter(([key]) => selected[key]?.length); 181 providerOutput.innerHTML = groups.length ? `${groups.map(([key, label]) => `<section class="provider-group"><h3>${label}</h3><div>${selected[key].map((provider) => `<a href="${esc(justWatchUrl)}" rel="nofollow noopener" target="_blank">${provider.logo ? `<img src="${esc(provider.logo)}" alt="${esc(provider.name)} logo" width="46" height="46">` : ''}<span>${esc(provider.name)}</span><b>View on JustWatch â</b></a>`).join('')}</div></section>`).join('')}<p><a class="action" href="${esc(justWatchUrl)}" rel="nofollow noopener" target="_blank">See every option on JustWatch â</a></p>` : `<div class="provider-empty"><p>Check current viewing options for your country.</p><a class="action" href="${esc(justWatchUrl)}" rel="nofollow noopener" target="_blank">Search on JustWatch â</a></div>`; 182 }; 183 region.addEventListener('change', renderProviders); 184} 185 186function refreshSaveButtons() { 187 const saved = new Set(readList()); 188 document.querySelectorAll('[data-watch-id]').forEach((button) => { 189 const active = saved.has(button.dataset.watchId); 190 button.classList.toggle('saved', active); 191 button.textContent = active ? 'â Saved' : 'ï¼ Save'; 192 button.setAttribute('aria-pressed', String(active)); 193 const title = catalog.find(item => item.id === button.dataset.watchId)?.title || 'title'; 194 button.setAttribute('aria-label', `${active ? 'Remove' : 'Save'} ${title} ${active ? 'from' : 'to'} My List`); 195 }); 196} 197document.addEventListener('click', (event) => { 198 const button = event.target.closest('[data-watch-id]'); 199 if (!button) return; 200 const saved = new Set(readList()); 201 saved.has(button.dataset.watchId) ? saved.delete(button.dataset.watchId) : saved.add(button.dataset.watchId); 202 if (!writeList([...saved])) return; 203 refreshSaveButtons(); 204 renderWatchlist(); 205}); 206const watchlistOutput = document.querySelector('[data-watchlist-output]'); 207function renderWatchlist() { 208 if (!watchlistOutput) return; 209 const saved = new Set(readList()); 210 const matches = catalog.filter((title) => saved.has(title.id)); 211 watchlistOutput.innerHTML = matches.map((title) =>
211 toolCard(title, 'Saved on this device')).join(''); 212 const empty = document.querySelector('[data-watchlist-empty]'); 213 if (empty) empty.hidden = matches.length > 0; 214} 215document.querySelector('[data-watchlist-clear]')?.addEventListener('click', () => { if (writeList([])) { refreshSaveButtons(); renderWatchlist(); } }); 216window.addEventListener('storage', (event) => { if (event.key === listKey || event.key === null) { refreshSaveButtons(); renderWatchlist(); } }); 217refreshSaveButtons(); 218renderWatchlist(); 219 220let deferredInstall; 221window.addEventListener('beforeinstallprompt', (event) => { event.preventDefault(); deferredInstall = event; document.querySelectorAll('[data-install-app]').forEach((button) => button.hidden = false); }); 222document.querySelectorAll('[data-install-app]').forEach((button) => button.addEventListener('click', async () => { 223 if (deferredInstall) { deferredInstall.prompt(); await deferredInstall.userChoice; deferredInstall = null; } 224 else alert('Use your browser menu and choose Install app or Add to Home Screen.'); 225})); 226if ('serviceWorker' in navigator) window.addEventListener('load', () => navigator.serviceWorker.register(`${base}/sw.js`).catch(() => {}));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.