torrent-online 1.5.5 → 1.5.7

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 +41 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "torrent-online",
3
- "version": "1.5.5",
3
+ "version": "1.5.7",
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){
@@ -641,6 +653,7 @@ function makeWebServer(client, st, opts, cacheDir, base) {
641
653
 
642
654
  async function webReq(req, res, client, st, opts, cacheDir, base) {
643
655
  const u = new URL(req.url, 'http://127.0.0.1');
656
+ const ensureSel = (e, i) => { const f = e?.t.files[i]; if (f && !e.sel.has(i)) { e.sel.add(i); try { f.select(); } catch {} } };
644
657
  let p = u.pathname;
645
658
  if (base) {
646
659
  if (p !== base && !p.startsWith(base + '/')) { res.statusCode = 404; res.end('not found'); return; }
@@ -658,6 +671,22 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
658
671
  const { items, notes } = await searchIndexers(q);
659
672
  return json(200, { items, notes });
660
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
+ }
661
690
  if (p === '/api/add' && req.method === 'POST') {
662
691
  const b = await readBody(req);
663
692
  const source = String(b.source || '');
@@ -724,6 +753,7 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
724
753
  const e = st.torrents.get(Number(mS[1]));
725
754
  const f = e?.t.files[Number(mS[2])];
726
755
  if (!f) { res.statusCode = 404; res.end('not found'); return; }
756
+ ensureSel(e, Number(mS[2]));
727
757
  return serveFile(req, res, f);
728
758
  }
729
759
  const mT = /^\/api\/tracks\/(\d+)\/(\d+)$/.exec(p);
@@ -731,6 +761,7 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
731
761
  const e = st.torrents.get(Number(mT[1]));
732
762
  const f = e?.t.files[Number(mT[2])];
733
763
  if (!f) return json(404, { error: 'нет файла' });
764
+ ensureSel(e, Number(mT[2]));
734
765
  const ffprobe = detectFFBin('ffprobe');
735
766
  if (!ffprobe) return json(200, { ffmpeg: false, tracks: [] });
736
767
  const url = 'http://127.0.0.1:' + st.port + base + '/s/' + mT[1] + '/' + mT[2];
@@ -746,13 +777,14 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
746
777
  else if (s.codec_type === 'subtitle') subs.push({ s: si++, lang, title, codec: s.codec_name });
747
778
  }
748
779
  return json(200, { ffmpeg: true, tracks, subs });
749
- } catch (e2) { return json(200, { ffmpeg: true, tracks: [], subs: [], error: String((e2 && e2.message) || e2).slice(0, 160) }); }
780
+ } catch (e2) { return json(200, { ffmpeg: true, tracks: [], subs: [], retry: true }); }
750
781
  }
751
782
  const mR = /^\/remux\/(\d+)\/(\d+)$/.exec(p);
752
783
  if (mR) {
753
784
  const e = st.torrents.get(Number(mR[1]));
754
785
  const f = e?.t.files[Number(mR[2])];
755
786
  if (!f) { res.statusCode = 404; res.end('not found'); return; }
787
+ ensureSel(e, Number(mR[2]));
756
788
  const ffmpeg = detectFFBin('ffmpeg');
757
789
  if (!ffmpeg) { res.statusCode = 500; res.end('ffmpeg не найден'); return; }
758
790
  const a = u.searchParams.get('a') || '0';
@@ -774,6 +806,7 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
774
806
  const e = st.torrents.get(Number(mV[1]));
775
807
  const f = e?.t.files[Number(mV[2])];
776
808
  if (!f) { res.statusCode = 404; res.end('not found'); return; }
809
+ ensureSel(e, Number(mV[2]));
777
810
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
778
811
  res.end('<!doctype html><meta charset=utf-8><title>' + esc(f.name) + '</title>' +
779
812
  '<style>body{margin:0;background:#000;color:#ddd;font:14px system-ui;display:flex;flex-direction:column;height:100vh;box-sizing:border-box}' +
@@ -784,7 +817,9 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
784
817
  '<video id=v controls autoplay src="' + base + '/s/' + mV[1] + '/' + mV[2] + '"></video>' +
785
818
  '<script>var B=' + JSON.stringify(base) + ',ID=' + mV[1] + ',I=' + mV[2] + ';' +
786
819
  'var v=document.getElementById("v"),aS=document.getElementById("a"),sS=document.getElementById("s"),n=document.getElementById("n");' +
787
- 'fetch(B+"/api/tracks/"+ID+"/"+I).then(function(r){return r.json()}).then(function(d){' +
820
+ 'var tries=0;' +
821
+ 'var load=function(){fetch(B+"/api/tracks/"+ID+"/"+I).then(function(r){return r.json()}).then(function(d){' +
822
+ 'if(d.retry&&tries++<15){n.textContent="⏳ готовлю файл к стриму…";setTimeout(load,4000);return;}' +
788
823
  'if(d.error)n.textContent="⚠ "+d.error;' +
789
824
  'var A=d.tracks||[],S=d.subs||[];' +
790
825
  'if(!d.ffmpeg||A.length<2){return;}' +
@@ -794,7 +829,7 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
794
829
  '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=""}' +
795
830
  '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()};' +
796
831
  'aS.onchange=go;sS.onchange=go;go();' +
797
- '}).catch(function(){})</script>');
832
+ '}).catch(function(){if(tries++<15)setTimeout(load,4000)})};load();</script>');
798
833
  return;
799
834
  }
800
835
  res.statusCode = 404; res.end('not found');
@@ -814,6 +849,7 @@ async function startWeb(opts, srcArg) {
814
849
  const dup = [...st.torrents.values()].find(e => e.t.infoHash === t.infoHash);
815
850
  if (dup) { try { t.destroy(); } catch {} return; }
816
851
  t.files.forEach(f => f.deselect()); // webtorrent по умолчанию выбирает все файлы
852
+ try { t.fileSelector = (tt, index, file) => (file._selections && file._selections.length ? 2 : 0); } catch {}
817
853
  const sel = new Set();
818
854
  const vids = t.files.map((f, i) => ({ f, i })).filter(x => VIDEO_EXT.has(path.extname(x.f.name).toLowerCase()));
819
855
  if (st.autoSelect) {