torrent-online 1.6.3 → 1.6.5

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 +88 -66
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "torrent-online",
3
- "version": "1.6.3",
3
+ "version": "1.6.5",
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
@@ -840,53 +840,75 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
840
840
  res.on('close', () => { try { proc.kill('SIGKILL'); } catch {} });
841
841
  return;
842
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])];
843
+ const mF = /^\/fmp4\/(\d+)\/(\d+)\/([\w.-]+)$/.exec(p);
844
+ if (mF) {
845
+ const e = st.torrents.get(Number(mF[1]));
846
+ const f = e?.t.files[Number(mF[2])];
847
847
  if (!f) { res.statusCode = 404; res.end('not found'); return; }
848
- ensureSel(e, Number(mH[2]));
848
+ ensureSel(e, Number(mF[2]));
849
849
  const ffmpeg = detectFFBin('ffmpeg');
850
850
  if (!ffmpeg) { res.statusCode = 500; res.end('ffmpeg не найден'); return; }
851
- const name = mH[3];
851
+ const name = mF[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 t = Math.max(0, Math.round((Number(u.searchParams.get('t')) || 0) / 5) * 5);
857
+ const key = mF[1] + '-' + mF[2] + '-' + a + '-' + (si >= 0 ? 's' + si : 'n') + '-' + t;
856
858
  const dir = path.join(HLS_ROOT, key);
857
859
  const fp = path.join(dir, name);
858
860
  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
- }
861
+ const hit1 = hlsProcs.get(key + '-vtt'); if (hit1) hit1.touched = Date.now();
866
862
  if (!hlsProcs.has(key)) {
867
863
  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
- 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);
864
+ for (const [k, h] of hlsProcs) if (k.startsWith(mF[1] + '-' + mF[2] + '-')) { try { h.proc.kill('SIGKILL'); } catch {} hlsProcs.delete(k); }
865
+ const url = 'http://127.0.0.1:' + st.port + base + '/s/' + mF[1] + '/' + mF[2];
866
+ const args = ['-hide_banner', '-loglevel', 'error', '-rw_timeout', '30000000', '-reconnect', '1', '-reconnect_streamed', '1', '-reconnect_delay_max', '5'];
867
+ if (t > 0) args.push('-ss', String(t));
868
+ args.push('-i', url, '-map', '0:v:0', '-map', '0:a:' + a, '-c:v', 'copy', '-c:a', 'aac', '-b:a', '192k', '-max_muxing_queue_size', '9999', '-f', 'mp4', '-movflags', 'frag_keyframe+empty_moov+default_base_moof', dir + '/file.mp4');
877
869
  const proc = spawn(ffmpeg, args, { stdio: ['ignore', 'ignore', 'pipe'] });
878
870
  let ffErr = 0;
879
- proc.stderr.on('data', d => { if (ffErr < 3) { console.error('hls:', String(d).trim().slice(0, 200)); ffErr++; } });
871
+ proc.stderr.on('data', d => { if (ffErr < 3) { console.error('fmp4:', String(d).trim().slice(0, 200)); ffErr++; } });
880
872
  proc.on('exit', () => { const cur = hlsProcs.get(key); if (cur && cur.proc === proc) hlsProcs.delete(key); });
881
873
  hlsProcs.set(key, { proc, dir, touched: Date.now() });
874
+ if (si >= 0) {
875
+ const subArgs = ['-hide_banner', '-loglevel', 'error', '-rw_timeout', '30000000', '-reconnect', '1', '-reconnect_streamed', '1', '-reconnect_delay_max', '5'];
876
+ if (t > 0) subArgs.push('-ss', String(t));
877
+ subArgs.push('-i', url, '-map', '0:s:' + si, '-c:s', 'webvtt', '-f', 'webvtt', dir + '/sub.vtt');
878
+ const sp = spawn(ffmpeg, subArgs, { stdio: ['ignore', 'ignore', 'pipe'] });
879
+ let sf = 0;
880
+ sp.stderr.on('data', d => { if (sf < 2) { console.error('vtt:', String(d).trim().slice(0, 150)); sf++; } });
881
+ sp.on('exit', () => { const cur = hlsProcs.get(key + '-vtt'); if (cur && cur.proc === sp) hlsProcs.delete(key + '-vtt'); });
882
+ hlsProcs.set(key + '-vtt', { proc: sp, dir, touched: Date.now() });
883
+ }
882
884
  }
883
- for (let i = 0; i < 60; i++) {
885
+ if (name === 'sub.vtt') {
884
886
  let b = null;
885
887
  try { b = fs.readFileSync(fp); } catch {}
886
- if (b) { res.setHeader('Content-Type', 'application/x-mpegURL'); res.end(b); return; }
887
- await new Promise(r => setTimeout(r, 500));
888
+ res.setHeader('Content-Type', 'text/vtt; charset=utf-8');
889
+ res.end(b || 'WEBVTT\n\n');
890
+ return;
888
891
  }
889
- res.statusCode = 504; res.end('ffmpeg timeout');
892
+ let size = 0;
893
+ for (let i = 0; i < 60 && size === 0; i++) {
894
+ try { size = fs.statSync(fp).size; } catch {}
895
+ if (size === 0) await new Promise(r => setTimeout(r, 500));
896
+ }
897
+ if (size === 0) { res.statusCode = 504; res.end('ffmpeg timeout'); return; }
898
+ res.setHeader('Accept-Ranges', 'bytes');
899
+ const rm = /^bytes=(\d*)-(\d*)$/.exec((req.headers.range || '').trim());
900
+ if (rm) {
901
+ let startB, endB;
902
+ if (rm[1] === '') { const n = Number(rm[2]); startB = Math.max(0, size - n); endB = size - 1; }
903
+ else { startB = Number(rm[1]); endB = rm[2] === '' ? size - 1 : Math.min(Number(rm[2]), size - 1); }
904
+ if (startB >= size || startB > endB) { res.writeHead(416, { 'Content-Range': 'bytes */' + size }); res.end(); return; }
905
+ res.writeHead(206, { 'Content-Range': 'bytes ' + startB + '-' + endB + '/' + size, 'Content-Length': endB - startB + 1, 'Content-Type': 'video/mp4' });
906
+ fs.createReadStream(fp, { start: startB, end: endB }).pipe(res);
907
+ return;
908
+ }
909
+ res.setHeader('Content-Type', 'video/mp4');
910
+ res.setHeader('Content-Length', size);
911
+ fs.createReadStream(fp).pipe(res);
890
912
  return;
891
913
  }
892
914
  const mV = /^\/view\/(\d+)\/(\d+)$/.exec(p);
@@ -897,43 +919,43 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
897
919
  ensureSel(e, Number(mV[2]));
898
920
  const webFriendly = /\.(mp4|m4v|webm|m4a)$/i.test(f.name);
899
921
  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>');
922
+ res.end(`<!doctype html><meta charset=utf-8><title>${esc(f.name)}</title>
923
+ <style>body{margin:0;background:#000;color:#ddd;font:14px system-ui;display:flex;flex-direction:column;height:100vh;box-sizing:border-box}
924
+ .bar{display:flex;gap:10px;align-items:center;padding:6px 10px;flex-wrap:wrap}
925
+ select{background:#1b1b1f;color:#ddd;border:1px solid #333;border-radius:6px;padding:4px;max-width:340px}
926
+ video{flex:1;width:100%;min-height:0;background:#000}
927
+ #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>
928
+ <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>
929
+ <video id=v controls autoplay playsinline src="${base}/s/${mV[1]}/${mV[2]}"></video>
930
+ <div id=ov>▶</div>
931
+ <script>
932
+ var B=${JSON.stringify(base)},ID=${mV[1]},I=${mV[2]};
933
+ var v=document.getElementById("v"),aS=document.getElementById("a"),sS=document.getElementById("s"),n=document.getElementById("n"),ov=document.getElementById("ov");
934
+ ov.onclick=function(){v.play().catch(function(){});ov.style.display="none"};
935
+ var tries=0,hls=null,cues=[],subUrl="";
936
+ 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);
937
+ 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};
938
+ var pollSub=function(){if(!subUrl)return;fetch(subUrl).then(function(r){return r.text()}).then(parseVtt).catch(function(){})};
939
+ 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});
940
+ setInterval(pollSub,20000);
941
+ var play=function(u){v.src=u;v.currentTime=0;v.play().catch(function(){ov.style.display="flex"})};
942
+ var load=function(){fetch(B+"/api/tracks/"+ID+"/"+I).then(function(r){return r.json()}).then(function(d){
943
+ if(d.retry&&tries++<15){n.textContent="⏳ готовлю файл к стриму…";setTimeout(load,4000);return;}
944
+ if(d.error)n.textContent="⚠ "+d.error;
945
+ var A=d.tracks||[],S=d.subs||[];
946
+ if(!d.ffmpeg){return;}
947
+ 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});
948
+ 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)});
949
+ if(A.length>1){aS.value=A[0].a;aS.style.display=""}
950
+ if(SI.length){var o0=document.createElement("option");o0.value="";o0.textContent="без субтитров";sS.appendChild(o0);
951
+ 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=""}
952
+ var go=function(){var a=aS.style.display!==""?aS.value:0;var s=sS.style.display!==""?sS.value:"";var t=Math.max(0,Math.round(v.currentTime/5)*5-5);cues=[];cueDiv.innerHTML="";subUrl=s!==""?B+"/fmp4/"+ID+"/"+I+"/sub.vtt?a="+a+"&si="+s+"&t="+t:"";if(subUrl)pollSub();play(B+"/fmp4/"+ID+"/"+I+"/file.mp4?a="+a+(s!==""?"&si="+s:"")+(t>0?"&t="+t:""))};
953
+ aS.onchange=go;sS.onchange=go;go();
954
+ }).catch(function(){if(tries++<15)setTimeout(load,4000)})};
955
+ v.addEventListener("playing",function(){n.textContent="";ov.style.display="none"});
956
+ 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);
957
+ load();
958
+ </script>`);
937
959
  return;
938
960
  }
939
961
  res.statusCode = 404; res.end('not found');