torrent-online 1.6.3 → 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 -45
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "torrent-online",
3
- "version": "1.6.3",
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,17 +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
- const segTpl = si.length ? 'seg-%s-%d.ts' : 'seg-%d.ts';
875
- const plTpl = si.length ? 'pl-%s.m3u8' : 'pl.m3u8';
876
- 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', vsm.join(','), '-hls_segment_filename', dir + '/' + segTpl, dir + '/' + plTpl);
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');
877
881
  const proc = spawn(ffmpeg, args, { stdio: ['ignore', 'ignore', 'pipe'] });
878
882
  let ffErr = 0;
879
883
  proc.stderr.on('data', d => { if (ffErr < 3) { console.error('hls:', String(d).trim().slice(0, 200)); ffErr++; } });
880
884
  proc.on('exit', () => { const cur = hlsProcs.get(key); if (cur && cur.proc === proc) hlsProcs.delete(key); });
881
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
+ }
882
894
  }
883
895
  for (let i = 0; i < 60; i++) {
884
896
  let b = null;
@@ -897,43 +909,48 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
897
909
  ensureSel(e, Number(mV[2]));
898
910
  const webFriendly = /\.(mp4|m4v|webm|m4a)$/i.test(f.name);
899
911
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
900
- res.end('<!doctype html><meta charset=utf-8><title>' + esc(f.name) + '</title>' +
901
- '<style>body{margin:0;background:#000;color:#ddd;font:14px system-ui;display:flex;flex-direction:column;height:100vh;box-sizing:border-box}' +
902
- '.bar{display:flex;gap:10px;align-items:center;padding:6px 10px;flex-wrap:wrap}' +
903
- 'select{background:#1b1b1f;color:#ddd;border:1px solid #333;border-radius:6px;padding:4px;max-width:340px}' +
904
- 'video{flex:1;width:100%;min-height:0;background:#000}' +
905
- '#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>' +
906
- '<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>' +
907
- '<video id=v controls autoplay playsinline src="' + base + '/s/' + mV[1] + '/' + mV[2] + '"></video>' +
908
- '<div id=ov>▶</div>' +
909
- '<script src="https://cdn.jsdelivr.net/npm/hls.js@1.5.17/dist/hls.min.js"></script>' +
910
- '<script>var B=' + JSON.stringify(base) + ',ID=' + mV[1] + ',I=' + mV[2] + ',MF=' + (webFriendly ? 1 : 0) + ';' +
911
- 'var v=document.getElementById("v"),aS=document.getElementById("a"),sS=document.getElementById("s"),n=document.getElementById("n"),ov=document.getElementById("ov");' +
912
- 'ov.onclick=function(){v.play().catch(function(){});ov.style.display="none"};' +
913
- 'var tries=0,hls=null;' +
914
-
915
-
916
- 'var play=function(u,seek){if(hls){try{hls.destroy()}catch(e){}hls=null}' +
917
- 'if(window.Hls&&Hls.isSupported()){hls=new Hls({startPosition:0,enableWorker:true,lowLatencyMode:false});hls.loadSource(u);hls.attachMedia(v);' +
918
- '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)}});' +
919
- 'hls.on(Hls.Events.ERROR,function(ev,d){if(d.fatal)n.textContent="⚠ HLS: "+d.type})}' +
920
- 'else{v.src=u;v.play().catch(function(){ov.style.display="flex"})}};' +
921
- 'var load=function(){fetch(B+"/api/tracks/"+ID+"/"+I).then(function(r){return r.json()}).then(function(d){' +
922
- 'if(d.retry&&tries++<15){n.textContent="⏳ готовлю файл к стриму…";setTimeout(load,4000);return;}' +
923
- 'if(d.error)n.textContent="⚠ "+d.error;' +
924
- 'var A=d.tracks||[],S=d.subs||[];' +
925
- 'if(!d.ffmpeg){return;}' +
926
- '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});' +
927
- '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)});' +
928
- 'if(A.length>1){aS.value=A[0].a;aS.style.display=""}' +
929
- '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;play(B+"/hls/"+ID+"/"+I+"/master.m3u8?a="+a+(s!==""?"&si="+s:""),t)};' +
930
- 'if(SI.length){var o0=document.createElement("option");o0.value="";o0.textContent="без субтитров";sS.appendChild(o0);' +
931
- '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=""}' +
932
- 'aS.onchange=go;sS.onchange=go;go();' +
933
- '}).catch(function(){if(tries++<15)setTimeout(load,4000)})};' +
934
- 'v.addEventListener("playing",function(){n.textContent="";ov.style.display="none"});' +
935
- '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);' +
936
- '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>`);
937
954
  return;
938
955
  }
939
956
  res.statusCode = 404; res.end('not found');