Movie Monk — Media Hub





Movie Monk — M3U8 Player + Health Checker


Movie Monk — M3U8 Player

Ready


Player



No stream



Nothing playing

// ------- State / Storage ------- const KEY_M3U_TEXT='mm_m3u_text', KEY_FAVS='mm_favs', KEY_RECENT='mm_recent', KEY_STATUS='mm_status_cache_v1'; let hls=null, channels=[], view=[], favs=new Set(JSON.parse(localStorage.getItem(KEY_FAVS)||'[]')); let recent=JSON.parse(localStorage.getItem(KEY_RECENT)||'[]'); // array of urls let statusCache=JSON.parse(localStorage.getItem(KEY_STATUS)||'{}'); // {url:{s:'ok'|'cors'|'fail'|'pending', t:epoch}} let tab='all', search='', group='', country='', currentIndex=-1;

const video=$('#video'), now=$('#now'), mini=$('#mini'), miniTxt=$('#miniTxt'); const progressEl = $('#testProgress');

// ------- Parse M3U ------- const attrRe = /([a-zA-Z0-9\-_]+)="([^"]*)"/g; function parseM3U(text){ const lines=text.split(/\r?\n/).map(l=>l.trim()); const out=[]; let cur=null; for (const ln of lines){ if (!ln) continue; if (ln.startsWith('#EXTINF')){ const parts=ln.split(',',2); const attrs={}; let m; while ((m=attrRe.exec(parts[0]))!==null){ attrs[m[1].toLowerCase()]=m[2]; } const name=(parts[1]||attrs['tvg-name']||'Channel').trim(); cur = { n:name, u:'', // filled by next line g:attrs['group-title']||'Ungrouped', t:attrs['tvg-id']||'', c:attrs['tvg-country']||'', logo:attrs['tvg-logo']||'' }; } else if (!ln.startsWith('#')){ if (cur){ cur.u=ln; out.push(cur); cur=null; } else out.push({n:ln,u:ln,g:'Ungrouped',t:'',c:'',logo:''}); } } return out; }

// ------- Rendering ------- function populateFilters(list){ const groups=[...new Set(list.map(c=>c.g).filter(Boolean))].sort(); const countries=[...new Set(list.map(c=>c.c).filter(Boolean))].sort(); $('#groupSel').innerHTML = '' + groups.map(g=>``).join(''); $('#countrySel').innerHTML = '' + countries.map(c=>``).join(''); }

function compute(){ let l = channels.slice(); if (tab==='fav') l = l.filter(ch=>favs.has(ch.u)); if (tab==='recent') l = l.filter(ch=>recent.includes(ch.u)); if (search){ const q=search.toLowerCase(); l = l.filter(ch=>(ch.n||'').toLowerCase().includes(q)||(ch.g||'').toLowerCase().includes(q)); } if (group) l = l.filter(ch=> (ch.g||'').toLowerCase()===group.toLowerCase()); if (country) l = l.filter(ch=> (ch.c||'').toLowerCase()===country.toLowerCase()); view = l.sort((a,b)=> (a.n||'').localeCompare(b.n||'')); }

function indicatorFor(url){ const st = statusCache[url]?.s || 'pending'; let cls='gray', label='untested'; if (st==='ok'){ cls='green'; label='tested working'; } else if (st==='cors'){ cls='yellow'; label='working but blocked by browser/firewall (CORS)'; } else if (st==='fail'){ cls='red'; label='not working'; } return ``; }

function render(){ compute(); const cont = $('#channels'); cont.innerHTML=''; if (!view.length){ cont.innerHTML = '

No channels match.

'; return; } view.forEach((ch,i)=>{ const row=document.createElement('div'); row.className='item'; row.dataset.idx=i;

const meta=document.createElement('div'); meta.className='meta'; const num=document.createElement('div'); num.className='num'; num.textContent=(i+1).toString().padStart(3,'0');

const stat = document.createElement('span'); stat.innerHTML = indicatorFor(ch.u); stat.style.marginRight='6px';

const title=document.createElement('div'); title.className='title'; title.textContent=ch.n; const sub=document.createElement('div'); sub.className='sub'; sub.textContent=`${ch.g||'Ungrouped'} • ${ch.u}`;

meta.appendChild(num); meta.appendChild(stat); meta.appendChild(title); meta.appendChild(sub);

const actions=document.createElement('div'); actions.className='actions';

// "Find alt" appears when red const altBtn = document.createElement('a'); altBtn.textContent = 'Find alt'; altBtn.className = 'btn ghost'; altBtn.style.display = (statusCache[ch.u]?.s === 'fail') ? 'inline-block' : 'none'; altBtn.target = '_blank'; altBtn.rel = 'noopener noreferrer'; altBtn.href = duckQueryFor(ch); actions.appendChild(altBtn);

const testBtn = document.createElement('button'); testBtn.className='btn warn'; testBtn.textContent='Test'; testBtn.addEventListener('click', async ()=>{ await testOne(ch); updateRow(row, ch, i); }); actions.appendChild(testBtn);

const fav=document.createElement('button'); fav.className='btn ghost'; fav.textContent = favs.has(ch.u)?'★':'☆'; fav.addEventListener('click', ()=>{ if (favs.has(ch.u)) favs.delete(ch.u); else favs.add(ch.u); localStorage.setItem(KEY_FAVS, JSON.stringify([...favs])); render(); }); actions.appendChild(fav);

const play=document.createElement('button'); play.className='btn'; play.textContent='Play'; play.addEventListener('click', ()=>{ currentIndex=i; playUrl(ch.u,ch.n); }); actions.appendChild(play);

row.appendChild(meta); row.appendChild(actions); cont.appendChild(row); }); }

function updateRow(row, ch, i){ // Replace status dot + "Find alt" visibility const statHolder = row.querySelector('.meta > span.dot, .meta > span:not(.num)'); // safer: rebuild meta children except num/title/sub? Simpler: just rebuild whole row const newRow = row.cloneNode(true); newRow.dataset.idx = i; const meta = newRow.querySelector('.meta'); meta.innerHTML = ''; const num=document.createElement('div'); num.className='num'; num.textContent=(i+1).toString().padStart(3,'0'); const stat = document.createElement('span'); stat.innerHTML = indicatorFor(ch.u); stat.style.marginRight='6px'; const title=document.createElement('div'); title.className='title'; title.textContent=ch.n; const sub=document.createElement('div'); sub.className='sub'; sub.textContent=`${ch.g||'Ungrouped'} • ${ch.u}`; meta.appendChild(num); meta.appendChild(stat); meta.appendChild(title); meta.appendChild(sub);

const actions = newRow.querySelector('.actions'); const altBtn = actions.querySelector('a'); if (altBtn){ altBtn.style.display = (statusCache[ch.u]?.s === 'fail') ? 'inline-block' : 'none'; altBtn.href = duckQueryFor(ch); } // Rebind play/fav/test (because we cloned) const [testBtn, favBtn, playBtn] = actions.querySelectorAll('button'); testBtn.addEventListener('click', async ()=>{ await testOne(ch); updateRow(newRow, ch, i); }); favBtn.addEventListener('click', ()=>{ if (favs.has(ch.u)) favs.delete(ch.u); else favs.add(ch.u); localStorage.setItem(KEY_FAVS, JSON.stringify([...favs])); render(); }); playBtn.addEventListener('click', ()=>{ currentIndex=i; playUrl(ch.u,ch.n); });

row.replaceWith(newRow); }

// ------- Player ------- function hlsCfg(){ if ((navigator.deviceMemory||4)u!==url)].slice(0,50); localStorage.setItem(KEY_RECENT, JSON.stringify(recent));

if (/\.m3u8($|\?)/i.test(url)){ if (video.canPlayType('application/vnd.apple.mpegurl')){ video.src=url; video.play().catch(()=>{}); } else if (window.Hls && window.Hls.isSupported()){ hls = new Hls(hlsCfg()); hls.loadSource(url); hls.attachMedia(video); hls.on(Hls.Events.MANIFEST_PARSED, ()=> video.play().catch(()=>{})); } else alert('HLS not supported in this browser.'); } else { video.src=url; video.play().catch(()=>{}); } } $('#stop').addEventListener('click', stopPlayback); $('#miniStop').addEventListener('click', stopPlayback); new MutationObserver(()=>{ mini.style.display = (window.innerWidth{ const url = $('#m3uUrl').value.trim(); if (!url){ alert('Enter a URL to a .m3u'); return; } try{ const txt = await fetchText(url); ingest(txt); $('#chip').textContent='Loaded from URL'; } catch(e){ alert('Fetch failed (CORS or URL). '+e.message); } }); $('#loadFile').addEventListener('click', ()=>{ const f = $('#m3uFile').files?.[0]; if (!f){ alert('Pick a .m3u'); return; } const r = new FileReader(); r.onload=()=>ingest(r.result); r.readAsText(f); }); $('#clearLocal').addEventListener('click', ()=>{ localStorage.removeItem(KEY_M3U_TEXT); channels=[]; render(); });

// Drag & drop window.addEventListener('dragover', e=>e.preventDefault()); window.addEventListener('drop', e=>{ e.preventDefault(); const f = e.dataTransfer.files?.[0]; if (!f) return; const r = new FileReader(); r.onload=()=>ingest(r.result); r.readAsText(f); });

// ------- Filters / Tabs ------- $('#search').addEventListener('input', e=>{ search=e.target.value; render(); }); $('#groupSel').addEventListener('change', e=>{ group=e.target.value; render(); }); $('#countrySel').addEventListener('change', e=>{ country=e.target.value; render(); }); $$('.tab').forEach(b=> b.addEventListener('click', ()=>{ $$('.tab').forEach(x=>x.classList.remove('active')); b.classList.add('active'); tab=b.dataset.tab; render(); }));

// ------- Remote / Keyboard ------- function chUp(){ if (!view.length) return; currentIndex=(currentIndex+1+view.length)%view.length; const ch=view[currentIndex]; playUrl(ch.u,ch.n); scrollToCurrent(); } function chDown(){ if (!view.length) return; currentIndex=(currentIndex-1+view.length)%view.length; const ch=view[currentIndex]; playUrl(ch.u,ch.n); scrollToCurrent(); } function ok(){ if (currentIndex{}); else video.pause(); } function mute(){ video.muted = !video.muted; } function favToggle(){ if (currentIndex{ const b=e.target.closest('button'); if (!b) return; if (b.dataset.act){ ({chUp, chDown, ok, next, prev, playpause, mute, stop, fav:favToggle}[b.dataset.act]||(()=>{}))(); } else if (b.dataset.num){ bufferDigits(b.dataset.num); } });

// Numeric channel jump let numBuf='', numTimer=null; function bufferDigits(d){ numBuf += String(d); if (numTimer) clearTimeout(numTimer); numTimer = setTimeout(()=>{ const n=parseInt(numBuf,10); numBuf=''; numTimer=null; if (!isFinite(n)) return; const idx=Math.max(0, Math.min(view.length-1, n-1)); currentIndex=idx; const ch=view[idx]; playUrl(ch.u,ch.n); scrollToCurrent(); }, 600); } window.addEventListener('keydown', e=>{ if (['INPUT','TEXTAREA','SELECT'].includes((e.target.tagName||'').toUpperCase())) return; if (e.key==='ArrowUp'){ e.preventDefault(); chUp(); } else if (e.key==='ArrowDown'){ e.preventDefault(); chDown(); } else if (e.key==='Enter'){ e.preventDefault(); ok(); } else if (e.key===' '){ e.preventDefault(); playpause(); } else if (e.key.toLowerCase()==='f'){ e.preventDefault(); favToggle(); } else if (e.key==='/'){ e.preventDefault(); $('#search').focus(); } else if (/^\d$/.test(e.key)){ bufferDigits(e.key); } });

// ------- Health Checker ------- const MAX_AGE_MS = 4 * 60 * 60 * 1000; // 4 hours const CONCURRENCY = 4; const TIMEOUT_MS = 6000; let abortTests=false;

function duckQueryFor(ch){ const q = `${ch.n} m3u8 OR m3u (playlist) site:github.com OR site:raw.githubusercontent.com OR site:gitlab.com OR site:pastebin.com OR site:iptv-org.github.io`; return `https://duckduckgo.com/?q=${encodeURIComponent(q)}`; }

function cacheSet(url, s){ statusCache[url] = {s, t: Date.now()}; localStorage.setItem(KEY_STATUS, JSON.stringify(statusCache)); }

function cacheGet(url){ const v = statusCache[url]; if (!v) return null; if (Date.now() - v.t > MAX_AGE_MS) return null; return v; }

async function testOne(ch){ const cached = cacheGet(ch.u); if (cached) return cached.s; // Try to fetch the manifest/head const ctrl = new AbortController(); const timer = setTimeout(()=>ctrl.abort('timeout'), TIMEOUT_MS); let status='fail'; try{ if (/\.m3u8($|\?)/i.test(ch.u)){ const r = await fetch(ch.u, {mode:'cors', method:'GET', headers:{'Accept':'application/vnd.apple.mpegURL,application/x-mpegURL,*/*;q=0.1'}, signal: ctrl.signal}); if (r.ok){ const text = await r.text(); if (/#EXTM3U/i.test(text)) status='ok'; else status='fail'; } else { status = (r.status>=500||r.status===404)?'fail':'cors'; } } else { // For non-HLS, attempt HEAD first; if blocked, treat as CORS (yellow) let r; try{ r = await fetch(ch.u, {method:'HEAD', mode:'cors', signal: ctrl.signal}); status = r.ok ? 'ok' : (r.status ? 'fail' : 'cors'); }catch(e){ status = 'cors'; // most likely CORS/firewall } } }catch(e){ // Distinguish timeout vs generic vs CORS (fetch TypeError often = CORS) if (e && (e.name==='AbortError' || String(e).includes('timeout'))) status='fail'; else status='cors'; }finally{ clearTimeout(timer); } cacheSet(ch.u, status); return status; }

async function testQueue(list){ abortTests=false; let done=0; progressEl.textContent = `Testing ${list.length} channels…`; const work = list.slice(); let active=0; return new Promise(resolve=>{ const next = ()=>{ if (abortTests) { progressEl.textContent = 'Stopped.'; return resolve(); } if (!work.length && active===0){ progressEl.textContent = `Done. Checked ${done} channel(s).`; return resolve(); } while (active{}).catch(()=>{}).finally(()=>{ active--; done++; progressEl.textContent = `Testing… ${done}/${list.length}`; // update that row if visible const idx = view.findIndex(v=>v.u===ch.u); if (idx>-1){ const row = $('#channels').querySelector(`.item[data-idx="${idx}"]`); if (row) updateRow(row, ch, idx); } next(); }); } }; next(); }); }

$('#testVisible').addEventListener('click', async ()=>{ compute(); // ensure view is fresh await testQueue(view); }); $('#stopTests').addEventListener('click', ()=>{ abortTests=true; });

// ------- Init ------- (function init(){ const saved = localStorage.getItem(KEY_M3U_TEXT); if (saved){ ingest(saved); $('#chip').textContent='Loaded (saved)'; } })();

})();



This website uses cookies.