torrent-online 1.6.2 → 1.6.4

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 +62 -44
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "torrent-online",
3
- "version": "1.6.2",
3
+ "version": "1.6.4",
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
@@ -851,11 +851,20 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
851
851
  const name = mH[3];
852
852
  if (name.includes('..')) { res.statusCode = 400; res.end('bad'); return; }
853
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, 1);
855
- const key = mH[1] + '-' + mH[2] + '-' + a + (si.length ? '-s' + si.join('.') : '');
854
+ const s1 = u.searchParams.get('si');
855
+ const si = (s1 === null || s1 === '' || !Number.isFinite(Number(s1))) ? -1 : Math.max(0, Math.min(7, Number(s1)));
856
+ const key = mH[1] + '-' + mH[2] + '-' + a + '-' + (si >= 0 ? 's' + si : 'n');
856
857
  const dir = path.join(HLS_ROOT, key);
857
858
  const fp = path.join(dir, name);
858
859
  const hit0 = hlsProcs.get(key); if (hit0) hit0.touched = Date.now();
860
+ const hit1 = hlsProcs.get(key + '-vtt'); if (hit1) hit1.touched = Date.now();
861
+ if (name === 'sub.vtt') {
862
+ let b = null;
863
+ try { b = fs.readFileSync(fp); } catch {}
864
+ res.setHeader('Content-Type', 'text/vtt; charset=utf-8');
865
+ res.end(b || 'WEBVTT\n\n');
866
+ return;
867
+ }
859
868
  if (name !== 'master.m3u8') {
860
869
  let b = null;
861
870
  try { b = fs.readFileSync(fp); } catch {}
@@ -868,15 +877,20 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
868
877
  for (const [k, h] of hlsProcs) if (k.startsWith(mH[1] + '-' + mH[2] + '-')) { try { h.proc.kill('SIGKILL'); } catch {} hlsProcs.delete(k); }
869
878
  const url = 'http://127.0.0.1:' + st.port + base + '/s/' + mH[1] + '/' + mH[2];
870
879
  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');
880
+ args.push('-f', 'hls', '-hls_time', '4', '-hls_list_size', '0', '-hls_playlist_type', 'event', '-hls_flags', 'independent_segments', '-hls_segment_type', 'mpegts', '-master_pl_name', 'master.m3u8', '-var_stream_map', 'v:0,a:0', '-hls_segment_filename', dir + '/seg-%d.ts', dir + '/pl.m3u8');
875
881
  const proc = spawn(ffmpeg, args, { stdio: ['ignore', 'ignore', 'pipe'] });
876
882
  let ffErr = 0;
877
883
  proc.stderr.on('data', d => { if (ffErr < 3) { console.error('hls:', String(d).trim().slice(0, 200)); ffErr++; } });
878
884
  proc.on('exit', () => { const cur = hlsProcs.get(key); if (cur && cur.proc === proc) hlsProcs.delete(key); });
879
885
  hlsProcs.set(key, { proc, dir, touched: Date.now() });
886
+ if (si >= 0) {
887
+ const subArgs = ['-hide_banner', '-loglevel', 'error', '-rw_timeout', '30000000', '-reconnect', '1', '-reconnect_streamed', '1', '-reconnect_delay_max', '5', '-i', url, '-map', '0:s:' + si, '-c:s', 'webvtt', '-f', 'webvtt', dir + '/sub.vtt'];
888
+ const sp = spawn(ffmpeg, subArgs, { stdio: ['ignore', 'ignore', 'pipe'] });
889
+ let sf = 0;
890
+ sp.stderr.on('data', d => { if (sf < 2) { console.error('vtt:', String(d).trim().slice(0, 150)); sf++; } });
891
+ sp.on('exit', () => { const cur = hlsProcs.get(key + '-vtt'); if (cur && cur.proc === sp) hlsProcs.delete(key + '-vtt'); });
892
+ hlsProcs.set(key + '-vtt', { proc: sp, dir, touched: Date.now() });
893
+ }
880
894
  }
881
895
  for (let i = 0; i < 60; i++) {
882
896
  let b = null;
@@ -895,44 +909,48 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
895
909
  ensureSel(e, Number(mV[2]));
896
910
  const webFriendly = /\.(mp4|m4v|webm|m4a)$/i.test(f.name);
897
911
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
898
- res.end('<!doctype html><meta charset=utf-8><title>' + esc(f.name) + '</title>' +
899
- '<style>body{margin:0;background:#000;color:#ddd;font:14px system-ui;display:flex;flex-direction:column;height:100vh;box-sizing:border-box}' +
900
- '.bar{display:flex;gap:10px;align-items:center;padding:6px 10px;flex-wrap:wrap}' +
901
- 'select{background:#1b1b1f;color:#ddd;border:1px solid #333;border-radius:6px;padding:4px;max-width:340px}' +
902
- 'video{flex:1;width:100%;min-height:0;background:#000}' +
903
- '#ov{position:fixed;inset:0;display:none;align-items:center;justify-content:center;background:rgba(0,0,0,.45);cursor:pointer;font-size:72px;color:#fff}</style>' +
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>' +
905
- '<video id=v controls autoplay playsinline src="' + base + '/s/' + mV[1] + '/' + mV[2] + '"></video>' +
906
- '<div id=ov>▶</div>' +
907
- '<script src="https://cdn.jsdelivr.net/npm/hls.js@1.5.17/dist/hls.min.js"></script>' +
908
- '<script>var B=' + JSON.stringify(base) + ',ID=' + mV[1] + ',I=' + mV[2] + ',MF=' + (webFriendly ? 1 : 0) + ';' +
909
- 'var v=document.getElementById("v"),aS=document.getElementById("a"),sS=document.getElementById("s"),n=document.getElementById("n"),ov=document.getElementById("ov");' +
910
- 'ov.onclick=function(){v.play().catch(function(){});ov.style.display="none"};' +
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
-
914
- 'var play=function(u){if(hls){try{hls.destroy()}catch(e){}hls=null}' +
915
- 'if(NAT){v.src=u;v.play().catch(function(){ov.style.display="flex"})}' +
916
- 'else if(window.Hls&&Hls.isSupported()){hls=new Hls({startPosition:0,enableWorker:true,lowLatencyMode:false});hls.loadSource(u);hls.attachMedia(v);' +
917
- 'hls.on(Hls.Events.MANIFEST_PARSED,function(){v.play().catch(function(){ov.style.display="flex"})});' +
918
- 'hls.on(Hls.Events.ERROR,function(ev,d){if(d.fatal)n.textContent="⚠ HLS: "+d.type})}' +
919
- 'else{v.src=u;v.play().catch(function(){ov.style.display="flex"})}};' +
920
- 'var load=function(){fetch(B+"/api/tracks/"+ID+"/"+I).then(function(r){return r.json()}).then(function(d){' +
921
- 'if(d.retry&&tries++<15){n.textContent="⏳ готовлю файл к стриму…";setTimeout(load,4000);return;}' +
922
- 'if(d.error)n.textContent="⚠ "+d.error;' +
923
- 'var A=d.tracks||[],S=d.subs||[];' +
924
- 'if(!d.ffmpeg){return;}' +
925
- '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});' +
926
- '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)});' +
927
- 'if(A.length>1){aS.value=A[0].a;aS.style.display=""}' +
928
- 'var go=function(){var a=aS.style.display!==""?aS.value:0;var s=sS.style.display!==""?sS.value:"";play(B+"/hls/"+ID+"/"+I+"/master.m3u8?a="+a+(s!==""?"&si="+s:""))};' +
929
- 'if(SI.length){var o0=document.createElement("option");o0.value="";o0.textContent="без субтитров";sS.appendChild(o0);' +
930
- 'S.filter(function(t){return SI.indexOf(t.s)>=0}).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.value="";sS.style.display=""}' +
931
- 'aS.onchange=go;sS.onchange=go;go();' +
932
- '}).catch(function(){if(tries++<15)setTimeout(load,4000)})};' +
933
- 'v.addEventListener("playing",function(){n.textContent="";ov.style.display="none"});' +
934
- '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);' +
935
- 'load();</script>');
912
+ res.end(`<!doctype html><meta charset=utf-8><title>${esc(f.name)}</title>
913
+ <style>body{margin:0;background:#000;color:#ddd;font:14px system-ui;display:flex;flex-direction:column;height:100vh;box-sizing:border-box}
914
+ .bar{display:flex;gap:10px;align-items:center;padding:6px 10px;flex-wrap:wrap}
915
+ select{background:#1b1b1f;color:#ddd;border:1px solid #333;border-radius:6px;padding:4px;max-width:340px}
916
+ video{flex:1;width:100%;min-height:0;background:#000}
917
+ #ov{position:fixed;inset:0;display:none;align-items:center;justify-content:center;background:rgba(0,0,0,.45);cursor:pointer;font-size:72px;color:#fff}</style>
918
+ <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>
919
+ <video id=v controls autoplay playsinline src="${base}/s/${mV[1]}/${mV[2]}"></video>
920
+ <div id=ov>▶</div>
921
+ <script src="https://cdn.jsdelivr.net/npm/hls.js@1.5.17/dist/hls.min.js"></script>
922
+ <script>
923
+ var B=${JSON.stringify(base)},ID=${mV[1]},I=${mV[2]};
924
+ var v=document.getElementById("v"),aS=document.getElementById("a"),sS=document.getElementById("s"),n=document.getElementById("n"),ov=document.getElementById("ov");
925
+ ov.onclick=function(){v.play().catch(function(){});ov.style.display="none"};
926
+ var tries=0,hls=null,cues=[],subUrl="";
927
+ var cueDiv=document.createElement("div");cueDiv.style.cssText="position:fixed;left:5%;right:5%;bottom:8%;text-align:center;color:#fff;font-size:clamp(14px,2.5vw,28px);text-shadow:0 1px 4px #000;pointer-events:none;z-index:5";document.body.appendChild(cueDiv);
928
+ var parseVtt=function(txt){var NL=String.fromCharCode(10);var C=[];txt.split(NL+NL).forEach(function(b){var m=b.match(/([0-9]+):([0-9]+):([0-9]+).([0-9]+) --> ([0-9]+):([0-9]+):([0-9]+).([0-9]+)/);if(!m)return;var t=function(h,mi,s,ms){return h*3600+mi*60+s+parseFloat("0."+ms)};var x=b.split(NL).filter(function(l){return l&&l.indexOf("-->")<0&&l.indexOf("WEBVTT")<0&&!/^[a-zA-Z-]+:/.test(l)}).join("<br>");C.push([t(+m[1],+m[2],+m[3],m[4]),t(+m[5],+m[6],+m[7],m[8]),x])});cues=C};
929
+ var pollSub=function(){if(!subUrl)return;fetch(subUrl).then(function(r){return r.text()}).then(parseVtt).catch(function(){})};
930
+ v.addEventListener("timeupdate",function(){var t=v.currentTime,a="";for(var i=cues.length-1;i>=0;i--){if(t>=cues[i][0]&&t<=cues[i][1]){a=cues[i][2];break}}cueDiv.innerHTML=a});
931
+ setInterval(pollSub,20000);
932
+ var play=function(u,seek){if(hls){try{hls.destroy()}catch(e){}hls=null}
933
+ if(window.Hls&&Hls.isSupported()){hls=new Hls({startPosition:0,enableWorker:true,lowLatencyMode:false});hls.loadSource(u);hls.attachMedia(v);
934
+ hls.on(Hls.Events.MANIFEST_PARSED,function(){v.play().catch(function(){ov.style.display="flex"});if(seek>1){var iv=setInterval(function(){if(v.duration>seek+3){v.currentTime=seek;clearInterval(iv)}},1000);setTimeout(function(){clearInterval(iv)},120000)}});
935
+ hls.on(Hls.Events.ERROR,function(ev,d){if(d.fatal)n.textContent="⚠ HLS: "+d.type})}
936
+ else{v.src=u;v.play().catch(function(){ov.style.display="flex"})}};
937
+ var load=function(){fetch(B+"/api/tracks/"+ID+"/"+I).then(function(r){return r.json()}).then(function(d){
938
+ if(d.retry&&tries++<15){n.textContent="⏳ готовлю файл к стриму…";setTimeout(load,4000);return;}
939
+ if(d.error)n.textContent="⚠ "+d.error;
940
+ var A=d.tracks||[],S=d.subs||[];
941
+ if(!d.ffmpeg){return;}
942
+ 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});
943
+ 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)});
944
+ if(A.length>1){aS.value=A[0].a;aS.style.display=""}
945
+ if(SI.length){var o0=document.createElement("option");o0.value="";o0.textContent="без субтитров";sS.appendChild(o0);
946
+ S.filter(function(t){return SI.indexOf(t.s)>=0}).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.value="";sS.style.display=""}
947
+ var go=function(){var a=aS.style.display!==""?aS.value:0;var s=sS.style.display!==""?sS.value:"";var t=v.currentTime>1?v.currentTime:0;cues=[];cueDiv.innerHTML="";subUrl=s!==""?B+"/hls/"+ID+"/"+I+"/sub.vtt?a="+a+"&si="+s:"";if(subUrl)pollSub();play(B+"/hls/"+ID+"/"+I+"/master.m3u8?a="+a+(s!==""?"&si="+s:""),t)};
948
+ aS.onchange=go;sS.onchange=go;go();
949
+ }).catch(function(){if(tries++<15)setTimeout(load,4000)})};
950
+ v.addEventListener("playing",function(){n.textContent="";ov.style.display="none"});
951
+ 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);
952
+ load();
953
+ </script>`);
936
954
  return;
937
955
  }
938
956
  res.statusCode = 404; res.end('not found');