torrent-online 1.5.6 → 1.5.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/wtui.js +38 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "torrent-online",
3
- "version": "1.5.6",
3
+ "version": "1.5.8",
4
4
  "type": "module",
5
5
  "description": "Stream .torrent / magnet to VLC or browser — torrent streaming for macOS/Linux without a torrent client. Стрим торрентов в VLC или браузер.",
6
6
  "keywords": [
package/wtui.js CHANGED
@@ -565,6 +565,7 @@ button:hover{background:#2a3c58}
565
565
  .fin .play{margin-left:6px;padding:0 8px;font-size:12px}
566
566
  .picks{margin:6px 0 2px}
567
567
  .picks button{font-size:12px;padding:3px 10px}
568
+ body.drop{outline:3px dashed #3b82f6;outline-offset:-10px;background:rgba(59,130,246,.06)}
568
569
  .fin .play{margin-left:6px;padding:0 8px;font-size:12px}
569
570
  .picks{margin:6px 0 2px}
570
571
  .picks button{font-size:12px;padding:3px 10px}
@@ -575,7 +576,7 @@ video{width:100vw;height:100vh;background:#000;display:block;margin:0}
575
576
  </style></head><body>
576
577
  <header>🦀 <b>TorrentOnline</b></header>
577
578
  <div class=wrap>
578
- <div class=search><input id=q placeholder="Поиск: пацаны 4 сезон · breaking bad · magnet:…" autocomplete=off><button id=go>Искать</button><button id=mag>+ magnet</button><button id=tpath>+ .torrent файл</button></div>
579
+ <div class=search><input id=q placeholder="Поиск: пацаны 4 сезон · breaking bad · magnet:…" autocomplete=off><button id=go>Искать</button><button id=mag>+ magnet</button><button id=tpath>+ .torrent файл</button><input type=file id=tt accept=".torrent,application/x-bittorrent" style=display:none></div>
579
580
  <div id=notes></div>
580
581
  <div id=results></div>
581
582
  <h3>Торренты</h3>
@@ -596,7 +597,18 @@ box.innerHTML=h;
596
597
  document.getElementById('go').onclick=doSearch;
597
598
  document.getElementById('q').addEventListener('keydown',function(e){if(e.key==='Enter')doSearch();});
598
599
  document.getElementById('mag').onclick=function(){var m=prompt('magnet:');if(m&&m.indexOf('magnet:')===0){api('/api/add',{source:m}).then(function(){poll();toTorrents();});}};
599
- document.getElementById('tpath').onclick=function(){var p=prompt('Путь к .torrent, напр. /Users/you/Downloads/torrent.torrent');if(p&&p.trim()){api('/api/add',{source:p.trim()}).then(function(){poll();toTorrents();});}};
600
+ document.getElementById('tpath').onclick=function(){document.getElementById('tt').click();};
601
+ document.getElementById('tt').onchange=function(){if(this.files&&this.files[0])uploadFile(this.files[0]);this.value='';};
602
+ function uploadFile(f){var nb=document.getElementById('notes');nb.innerHTML='<div class=note>⏳ Загружаю '+esc(f.name)+'…</div>';
603
+ fetch(BASE+'/api/add-file',{method:'POST',headers:{'content-type':'application/octet-stream'},body:f}).then(function(r){return r.json()}).then(function(d){nb.innerHTML=d.error?'<div class=note>⚠ '+esc(d.error)+'</div>':'';poll();toTorrents();}).catch(function(){nb.innerHTML='<div class=note>⚠ Ошибка загрузки</div>';});}
604
+ document.addEventListener('dragover',function(e){e.preventDefault();document.body.classList.add('drop');});
605
+ document.addEventListener('dragleave',function(e){if(e.target===document.body||e.relatedTarget===null)document.body.classList.remove('drop');});
606
+ document.addEventListener('drop',function(e){e.preventDefault();document.body.classList.remove('drop');
607
+ var fs=e.dataTransfer.files||[];
608
+ for(var i=0;i<fs.length;i++){if(/\.torrent$/i.test(fs[i].name)){uploadFile(fs[i]);return;}}
609
+ var t=(e.dataTransfer.getData('text/uri-list')||'')+(e.dataTransfer.getData('text/plain')||'');
610
+ var mm=t.match(/magnet:\?[^\s,]+/);if(mm){api('/api/add',{source:'magnet:'+mm[0]}).then(function(){poll();toTorrents();});}
611
+ });
600
612
  function render(list){var box=document.getElementById('torrents');
601
613
  if(!list.length){box.innerHTML='<div class=note>Пусто. Найди торрент и кликни по строке.</div>';return;}
602
614
  box.innerHTML=list.map(function(t){
@@ -659,6 +671,22 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
659
671
  const { items, notes } = await searchIndexers(q);
660
672
  return json(200, { items, notes });
661
673
  }
674
+ if (p === '/api/add-file' && req.method === 'POST') {
675
+ const chunks = [];
676
+ let size = 0;
677
+ await new Promise((resolve, reject) => {
678
+ req.on('data', c => { chunks.push(c); size += c.length; if (size > 5e6) { req.destroy(); reject(new Error('файл слишком большой')); } });
679
+ req.on('end', resolve);
680
+ req.on('error', reject);
681
+ });
682
+ const buf = Buffer.concat(chunks);
683
+ if (!buf.length) return json(400, { error: 'пустой файл' });
684
+ const id = st.nextId++;
685
+ st.pending.push(id);
686
+ try { client.add(buf, { path: cacheDir }); }
687
+ catch (e) { st.pending.pop(); return json(400, { error: String((e && e.message) || e) }); }
688
+ return json(200, { id });
689
+ }
662
690
  if (p === '/api/add' && req.method === 'POST') {
663
691
  const b = await readBody(req);
664
692
  const source = String(b.source || '');
@@ -762,11 +790,13 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
762
790
  const a = u.searchParams.get('a') || '0';
763
791
  const sb = u.searchParams.get('s');
764
792
  const url = 'http://127.0.0.1:' + st.port + base + '/s/' + mR[1] + '/' + mR[2];
765
- const args = ['-hide_banner', '-loglevel', 'error', '-i', url, '-map', '0:v:0', '-map', '0:a:' + a, '-c:v', 'copy', '-c:a', 'aac', '-b:a', '192k', '-max_muxing_queue_size', '9999'];
793
+ const args = ['-hide_banner', '-loglevel', 'error', '-rw_timeout', '30000000', '-reconnect', '1', '-reconnect_streamed', '1', '-reconnect_delay_max', '5', '-i', url, '-map', '0:v:0', '-map', '0:a:' + a, '-c:v', 'copy', '-c:a', 'aac', '-b:a', '192k', '-max_muxing_queue_size', '9999'];
766
794
  if (sb !== null && sb !== '') args.push('-map', '0:s:' + sb, '-c:s', 'mov_text');
767
795
  args.push('-f', 'mp4', '-movflags', 'frag_keyframe+empty_moov+default_base_moof', '-');
768
796
  res.setHeader('Content-Type', 'video/mp4');
769
- const proc = spawn(ffmpeg, args, { stdio: ['ignore', 'pipe', 'ignore'] });
797
+ const proc = spawn(ffmpeg, args, { stdio: ['ignore', 'pipe', 'pipe'] });
798
+ let ffErr = 0;
799
+ proc.stderr.on('data', d => { if (ffErr < 3) { console.error('ffm:', String(d).trim().slice(0, 200)); ffErr++; } });
770
800
  proc.stdout.pipe(res);
771
801
  proc.on('error', () => { try { res.end(); } catch {} });
772
802
  req.on('close', () => { try { proc.kill('SIGKILL'); } catch {} });
@@ -786,7 +816,7 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
786
816
  'select{background:#1b1b1f;color:#ddd;border:1px solid #333;border-radius:6px;padding:4px;max-width:340px}' +
787
817
  'video{flex:1;width:100%;min-height:0;background:#000}</style>' +
788
818
  '<div class=bar><b>' + esc(f.name) + '</b><select id=a style=display:none></select><select id=s style=display:none></select><span id=n></span></div>' +
789
- '<video id=v controls autoplay src="' + base + '/s/' + mV[1] + '/' + mV[2] + '"></video>' +
819
+ '<video id=v controls autoplay muted playsinline src="' + base + '/s/' + mV[1] + '/' + mV[2] + '"></video>' +
790
820
  '<script>var B=' + JSON.stringify(base) + ',ID=' + mV[1] + ',I=' + mV[2] + ';' +
791
821
  'var v=document.getElementById("v"),aS=document.getElementById("a"),sS=document.getElementById("s"),n=document.getElementById("n");' +
792
822
  'var tries=0;' +
@@ -799,8 +829,10 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
799
829
  'aS.value=A[0].a;aS.style.display="";' +
800
830
  'if(S.length){var o0=document.createElement("option");o0.value="";o0.textContent="без субтитров";sS.appendChild(o0);' +
801
831
  'S.forEach(function(t){var o=document.createElement("option");o.value=t.s;o.textContent="💬 "+(t.lang&&t.lang!=="und"?t.lang:"субтитры "+(t.s+1))+(t.title?" — "+t.title:"");sS.appendChild(o)});sS.style.display=""}' +
802
- 'var go=function(){var t=v.currentTime;v.src=B+"/remux/"+ID+"/"+I+"?a="+aS.value+(sS.value!==""?"&s="+sS.value:"");v.load();v.addEventListener("loadedmetadata",function once(){v.currentTime=t;v.removeEventListener("loadedmetadata",once)});v.play()};' +
832
+ 'var go=function(){var t=v.currentTime;v.src=B+"/remux/"+ID+"/"+I+"?a="+aS.value+(sS.value!==""?"&s="+sS.value:"");v.load();v.addEventListener("loadedmetadata",function once(){v.currentTime=t;v.removeEventListener("loadedmetadata",once)});v.play().catch(function(){})};' +
803
833
  'aS.onchange=go;sS.onchange=go;go();' +
834
+ 'v.addEventListener("playing",function(){n.textContent=""});v.addEventListener("volumechange",function(){if(!v.muted)n.title="звук включён"});' +
835
+ 'setInterval(function(){fetch(B+"/api/status").then(function(r){return r.json()}).then(function(l){var t=l.find(function(x){return x.id===ID});if(t&&v.readyState<2)n.textContent="⏬ "+t.speed+" · пиры "+t.peers+(t.progress>0?" · "+t.progress+"%":"")}).catch(function(){})},2000);' +
804
836
  '}).catch(function(){if(tries++<15)setTimeout(load,4000)})};load();</script>');
805
837
  return;
806
838
  }