torrent-online 1.6.4 → 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 +43 -38
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "torrent-online",
3
- "version": "1.6.4",
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,51 +840,41 @@ 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
854
  const s1 = u.searchParams.get('si');
855
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
+ 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;
857
858
  const dir = path.join(HLS_ROOT, key);
858
859
  const fp = path.join(dir, name);
859
860
  const hit0 = hlsProcs.get(key); if (hit0) hit0.touched = Date.now();
860
861
  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
- }
868
- if (name !== 'master.m3u8') {
869
- let b = null;
870
- try { b = fs.readFileSync(fp); } catch {}
871
- 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); }
872
- else { res.statusCode = 404; res.end('no segment'); }
873
- return;
874
- }
875
862
  if (!hlsProcs.has(key)) {
876
863
  await ensureDir(dir);
877
- for (const [k, h] of hlsProcs) if (k.startsWith(mH[1] + '-' + mH[2] + '-')) { try { h.proc.kill('SIGKILL'); } catch {} hlsProcs.delete(k); }
878
- const url = 'http://127.0.0.1:' + st.port + base + '/s/' + mH[1] + '/' + mH[2];
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'];
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');
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');
881
869
  const proc = spawn(ffmpeg, args, { stdio: ['ignore', 'ignore', 'pipe'] });
882
870
  let ffErr = 0;
883
- 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++; } });
884
872
  proc.on('exit', () => { const cur = hlsProcs.get(key); if (cur && cur.proc === proc) hlsProcs.delete(key); });
885
873
  hlsProcs.set(key, { proc, dir, touched: Date.now() });
886
874
  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'];
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');
888
878
  const sp = spawn(ffmpeg, subArgs, { stdio: ['ignore', 'ignore', 'pipe'] });
889
879
  let sf = 0;
890
880
  sp.stderr.on('data', d => { if (sf < 2) { console.error('vtt:', String(d).trim().slice(0, 150)); sf++; } });
@@ -892,13 +882,33 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
892
882
  hlsProcs.set(key + '-vtt', { proc: sp, dir, touched: Date.now() });
893
883
  }
894
884
  }
895
- for (let i = 0; i < 60; i++) {
885
+ if (name === 'sub.vtt') {
896
886
  let b = null;
897
887
  try { b = fs.readFileSync(fp); } catch {}
898
- if (b) { res.setHeader('Content-Type', 'application/x-mpegURL'); res.end(b); return; }
899
- 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;
891
+ }
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));
900
896
  }
901
- res.statusCode = 504; res.end('ffmpeg timeout');
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);
902
912
  return;
903
913
  }
904
914
  const mV = /^\/view\/(\d+)\/(\d+)$/.exec(p);
@@ -918,7 +928,6 @@ video{flex:1;width:100%;min-height:0;background:#000}
918
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>
919
929
  <video id=v controls autoplay playsinline src="${base}/s/${mV[1]}/${mV[2]}"></video>
920
930
  <div id=ov>▶</div>
921
- <script src="https://cdn.jsdelivr.net/npm/hls.js@1.5.17/dist/hls.min.js"></script>
922
931
  <script>
923
932
  var B=${JSON.stringify(base)},ID=${mV[1]},I=${mV[2]};
924
933
  var v=document.getElementById("v"),aS=document.getElementById("a"),sS=document.getElementById("s"),n=document.getElementById("n"),ov=document.getElementById("ov");
@@ -929,11 +938,7 @@ var parseVtt=function(txt){var NL=String.fromCharCode(10);var C=[];txt.split(NL+
929
938
  var pollSub=function(){if(!subUrl)return;fetch(subUrl).then(function(r){return r.text()}).then(parseVtt).catch(function(){})};
930
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});
931
940
  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"})}};
941
+ var play=function(u){v.src=u;v.currentTime=0;v.play().catch(function(){ov.style.display="flex"})};
937
942
  var load=function(){fetch(B+"/api/tracks/"+ID+"/"+I).then(function(r){return r.json()}).then(function(d){
938
943
  if(d.retry&&tries++<15){n.textContent="⏳ готовлю файл к стриму…";setTimeout(load,4000);return;}
939
944
  if(d.error)n.textContent="⚠ "+d.error;
@@ -944,7 +949,7 @@ A.forEach(function(t){var o=document.createElement("option");o.value=t.a;o.textC
944
949
  if(A.length>1){aS.value=A[0].a;aS.style.display=""}
945
950
  if(SI.length){var o0=document.createElement("option");o0.value="";o0.textContent="без субтитров";sS.appendChild(o0);
946
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=""}
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)};
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:""))};
948
953
  aS.onchange=go;sS.onchange=go;go();
949
954
  }).catch(function(){if(tries++<15)setTimeout(load,4000)})};
950
955
  v.addEventListener("playing",function(){n.textContent="";ov.style.display="none"});