torrent-online 1.6.0 → 1.6.1

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 +71 -18
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "torrent-online",
3
- "version": "1.6.0",
3
+ "version": "1.6.1",
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
@@ -654,6 +654,9 @@ function makeWebServer(client, st, opts, cacheDir, base) {
654
654
  async function webReq(req, res, client, st, opts, cacheDir, base) {
655
655
  const u = new URL(req.url, 'http://127.0.0.1');
656
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 {} } };
657
+ const hlsProcs = new Map();
658
+ const HLS_ROOT = path.join(os.tmpdir(), 'wtui-hls-' + process.pid);
659
+ setInterval(() => { const now = Date.now(); for (const [k, h] of hlsProcs) if (now - h.touched > 180000) { try { h.proc.kill('SIGKILL'); } catch {} hlsProcs.delete(k); try { fs.rmSync(h.dir, { recursive: true, force: true }); } catch {} } } , 30000).unref();
657
660
  let p = u.pathname;
658
661
  if (base) {
659
662
  if (p !== base && !p.startsWith(base + '/')) { res.statusCode = 404; res.end('not found'); return; }
@@ -746,6 +749,7 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
746
749
  const id = Number(b.id);
747
750
  const e = st.torrents.get(id);
748
751
  if (e) { st.torrents.delete(id); try { await new Promise(r => e.t.destroy(r)); } catch {} }
752
+ for (const [k, h] of hlsProcs) if (k.startsWith(id + '-')) { try { h.proc.kill('SIGKILL'); } catch {} hlsProcs.delete(k); try { fs.rmSync(h.dir, { recursive: true, force: true }); } catch {} }
749
753
  return json(200, { ok: true });
750
754
  }
751
755
  const mS = /^\/s\/(\d+)\/(\d+)$/.exec(p);
@@ -836,6 +840,53 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
836
840
  res.on('close', () => { try { proc.kill('SIGKILL'); } catch {} });
837
841
  return;
838
842
  }
843
+ const mH = /^\/hls\/(\d+)\/(\d+)\/([\w.%-]+)$/.exec(p);
844
+ if (mH) {
845
+ const e = st.torrents.get(Number(mH[1]));
846
+ const f = e?.t.files[Number(mH[2])];
847
+ if (!f) { res.statusCode = 404; res.end('not found'); return; }
848
+ ensureSel(e, Number(mH[2]));
849
+ const ffmpeg = detectFFBin('ffmpeg');
850
+ if (!ffmpeg) { res.statusCode = 500; res.end('ffmpeg не найден'); return; }
851
+ const name = mH[3];
852
+ if (name.includes('..')) { res.statusCode = 400; res.end('bad'); return; }
853
+ const a = u.searchParams.get('a') || '0';
854
+ const si = (u.searchParams.get('si') || '').split(',').map(Number).filter(n => Number.isFinite(n)).slice(0, 8);
855
+ const key = mH[1] + '-' + mH[2] + '-' + a + (si.length ? '-s' + si.join('.') : '');
856
+ const dir = path.join(HLS_ROOT, key);
857
+ const fp = path.join(dir, name);
858
+ const hit0 = hlsProcs.get(key); if (hit0) hit0.touched = Date.now();
859
+ if (name !== 'master.m3u8') {
860
+ let b = null;
861
+ try { b = fs.readFileSync(fp); } catch {}
862
+ if (b) { res.setHeader('Content-Type', name.endsWith('.m3u8') ? 'application/x-mpegURL' : name.endsWith('.ts') ? 'video/mp2t' : name.endsWith('.vtt') ? 'text/vtt; charset=utf-8' : 'application/octet-stream'); res.end(b); }
863
+ else { res.statusCode = 404; res.end('no segment'); }
864
+ return;
865
+ }
866
+ if (!hlsProcs.has(key)) {
867
+ await ensureDir(dir);
868
+ for (const [k, h] of hlsProcs) if (k.startsWith(mH[1] + '-' + mH[2] + '-')) { try { h.proc.kill('SIGKILL'); } catch {} hlsProcs.delete(k); }
869
+ const url = 'http://127.0.0.1:' + st.port + base + '/s/' + mH[1] + '/' + mH[2];
870
+ 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'];
871
+ for (const i of si) args.push('-map', '0:s:' + i);
872
+ if (si.length) args.push('-c:s', 'webvtt');
873
+ const vsm = ['v:0', 'a:0']; for (let i = 0; i < si.length; i++) vsm.push('s:' + i);
874
+ args.push('-f', 'hls', '-hls_time', '4', '-hls_list_size', '12', '-hls_flags', 'delete_segments+independent_segments', '-hls_segment_type', 'mpegts', '-master_pl_name', 'master.m3u8', '-var_stream_map', vsm.join(','), '-hls_segment_filename', dir + '/seg-%v%a%s-%d.ts', dir + '/pl-%v%a%s.m3u8');
875
+ const proc = spawn(ffmpeg, args, { stdio: ['ignore', 'ignore', 'pipe'] });
876
+ let ffErr = 0;
877
+ proc.stderr.on('data', d => { if (ffErr < 3) { console.error('hls:', String(d).trim().slice(0, 200)); ffErr++; } });
878
+ proc.on('exit', () => { const cur = hlsProcs.get(key); if (cur && cur.proc === proc) hlsProcs.delete(key); });
879
+ hlsProcs.set(key, { proc, dir, touched: Date.now() });
880
+ }
881
+ for (let i = 0; i < 60; i++) {
882
+ let b = null;
883
+ try { b = fs.readFileSync(fp); } catch {}
884
+ if (b) { res.setHeader('Content-Type', 'application/x-mpegURL'); res.end(b); return; }
885
+ await new Promise(r => setTimeout(r, 500));
886
+ }
887
+ res.statusCode = 504; res.end('ffmpeg timeout');
888
+ return;
889
+ }
839
890
  const mV = /^\/view\/(\d+)\/(\d+)$/.exec(p);
840
891
  if (mV) {
841
892
  const e = st.torrents.get(Number(mV[1]));
@@ -853,33 +904,33 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
853
904
  '<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>' +
854
905
  '<video id=v controls autoplay playsinline src="' + base + '/s/' + mV[1] + '/' + mV[2] + '"></video>' +
855
906
  '<div id=ov>▶</div>' +
907
+ '<script src="https://cdn.jsdelivr.net/npm/hls.js@1.5.17/dist/hls.min.js"></script>' +
856
908
  '<script>var B=' + JSON.stringify(base) + ',ID=' + mV[1] + ',I=' + mV[2] + ',MF=' + (webFriendly ? 1 : 0) + ';' +
857
909
  'var v=document.getElementById("v"),aS=document.getElementById("a"),sS=document.getElementById("s"),n=document.getElementById("n"),ov=document.getElementById("ov");' +
858
910
  'ov.onclick=function(){v.play().catch(function(){});ov.style.display="none"};' +
859
- 'var tries=0,ms=null,VC="";' +
860
- 'var MSEOK=function(){return ("MediaSource" in window)&&VC&&MediaSource.isTypeSupported("video/mp4;codecs=\'" + VC + ",mp4a.40.2\'")};' +
861
- 'var stopMse=function(){if(ms){try{ms.endOfStream()}catch(e){}ms=null}};' +
862
- 'var mse=function(u){stopMse();var m=new MediaSource();v.src=URL.createObjectURL(m);' +
863
- 'm.addEventListener("sourceopen",function(){var sb=m.addSourceBuffer("video/mp4;codecs=\'" + VC + ",mp4a.40.2\'");sb.mode="sequence";var q=[];' +
864
- 'sb.addEventListener("updateend",function(){if(q.length){try{sb.appendBuffer(q.shift())}catch(e){}}});' +
865
- 'fetch(u).then(function(r){var rd=r.body.getReader();var pump=function(){rd.read().then(function(res){' +
866
- 'if(res.done){try{if(!sb.updating&&!q.length)m.endOfStream()}catch(e){}return;}' +
867
- 'q.push(res.value);if(q.length<400&&!sb.updating){try{sb.appendBuffer(q.shift())}catch(e){}}pump()}).catch(function(){})};pump();});' +
868
- 'v.play().catch(function(){ov.style.display="flex"})})};' +
911
+ 'var tries=0,hls=null;' +
912
+ 'var NAT=v.canPlayType("application/vnd.apple.mpegurl")!==""&&(!window.Hls||!Hls.isSupported()||/Apple Computer/.test(navigator.vendor));' +
913
+ 'var subTracks=function(){if(hls)return hls.subtitleTracks||[];return Array.prototype.slice.call(v.textTracks||[]).filter(function(t){return t.kind==="subtitles"||t.kind==="captions"})};' +
914
+ 'var applySub=function(i){var T=subTracks();T.forEach(function(t,k){var en=(k===i);if(t.enabled!==undefined&&!!hls)t.enabled=en;else t.mode=en?"showing":"disabled"})};' +
915
+ 'var fillSubs=function(){var T=subTracks();if(!T.length)return;sS.innerHTML="";var o0=document.createElement("option");o0.value="-1";o0.textContent="без субтитров";sS.appendChild(o0);' +
916
+ 'T.forEach(function(t,i){var o=document.createElement("option");o.value=i;o.textContent="💬 "+(t.language&&t.language!=="und"?t.language:"субтитры "+(i+1))+(t.label?" — "+t.label:"")+(t.name?" — "+t.name:"");sS.appendChild(o)});' +
917
+ 'sS.style.display="";sS.value="0";sS.onchange=function(){applySub(+sS.value)};applySub(0)};' +
918
+ 'var play=function(u){if(hls){try{hls.destroy()}catch(e){}hls=null}' +
919
+ 'if(NAT){v.src=u;v.play().catch(function(){ov.style.display="flex"});setTimeout(fillSubs,2000);setTimeout(fillSubs,5000)}' +
920
+ 'else if(window.Hls&&Hls.isSupported()){hls=new Hls({startPosition:0,enableWorker:true,lowLatencyMode:false});hls.loadSource(u);hls.attachMedia(v);' +
921
+ 'hls.on(Hls.Events.MANIFEST_PARSED,function(){v.play().catch(function(){ov.style.display="flex"});fillSubs()});' +
922
+ 'hls.on(Hls.Events.ERROR,function(ev,d){if(d.fatal)n.textContent="⚠ HLS: "+d.type})}' +
923
+ 'else{v.src=u;v.play().catch(function(){ov.style.display="flex"})}};' +
869
924
  'var load=function(){fetch(B+"/api/tracks/"+ID+"/"+I).then(function(r){return r.json()}).then(function(d){' +
870
925
  'if(d.retry&&tries++<15){n.textContent="⏳ готовлю файл к стриму…";setTimeout(load,4000);return;}' +
871
926
  'if(d.error)n.textContent="⚠ "+d.error;' +
872
- 'var A=d.tracks||[],S=d.subs||[];VC=d.vc||"";' +
927
+ 'var A=d.tracks||[],S=d.subs||[];' +
873
928
  'if(!d.ffmpeg){return;}' +
874
- 'if(A.length<2&&!S.length&&MF){return;}' +
929
+ 'var SI=S.filter(function(t){return ["subrip","ass","ssa","srt","mov_text","webvtt","text"].indexOf(t.codec)>=0}).map(function(t){return t.s});' +
875
930
  'A.forEach(function(t){var o=document.createElement("option");o.value=t.a;o.textContent="♪ "+(t.lang&&t.lang!=="und"?t.lang:"дорожка "+(t.a+1))+(t.title?" — "+t.title:"")+" ["+t.codec+"]";aS.appendChild(o)});' +
876
931
  'if(A.length>1){aS.value=A[0].a;aS.style.display=""}' +
877
- 'if(S.length){var o0=document.createElement("option");o0.value="";o0.textContent="без субтитров";sS.appendChild(o0);' +
878
- '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=""}' +
879
- 'var go=function(){var t=v.currentTime;var s=sS.value;' +
880
- 'var u=B+"/remux/"+ID+"/"+I+"?a="+(aS.style.display!==""?aS.value:0)+(s!==""?"&s="+s:"");' +
881
- 'if(s===""&&MSEOK()){mse(u)}else{stopMse();v.src=u;v.load();v.addEventListener("loadedmetadata",function once(){v.currentTime=t;v.removeEventListener("loadedmetadata",once)});v.play().catch(function(){ov.style.display="flex"})}};' +
882
- 'aS.onchange=go;sS.onchange=go;go();' +
932
+ 'var go=function(){var a=aS.style.display!==""?aS.value:0;play(B+"/hls/"+ID+"/"+I+"/master.m3u8?a="+a+(SI.length?"&si="+SI.join(","):""))};' +
933
+ 'aS.onchange=go;go();' +
883
934
  '}).catch(function(){if(tries++<15)setTimeout(load,4000)})};' +
884
935
  'v.addEventListener("playing",function(){n.textContent="";ov.style.display="none"});' +
885
936
  '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);' +
@@ -931,6 +982,8 @@ async function startWeb(opts, srcArg) {
931
982
  if (cleaning) return; cleaning = true;
932
983
  st.vlc.forEach(v => { try { v.kill(); } catch {} });
933
984
  try { server.close(); } catch {}
985
+ hlsProcs.forEach(h => { try { h.proc.kill('SIGKILL'); } catch {} });
986
+ try { fs.rmSync(path.join(os.tmpdir(), 'wtui-hls-' + process.pid), { recursive: true, force: true }); } catch {}
934
987
  try { await new Promise(r => client.destroy(r)); } catch {}
935
988
  if (!opts.keepCache) await cleanCache(cacheDir);
936
989
  process.exit(code);