torrent-online 1.6.4 → 1.6.6

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 +44 -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.6",
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,42 @@ 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 junk of ['file.mp4', 'sub.vtt']) { try { fs.rmSync(path.join(dir, junk)); } catch {} }
865
+ for (const [k, h] of hlsProcs) if (k.startsWith(mF[1] + '-' + mF[2] + '-')) { try { h.proc.kill('SIGKILL'); } catch {} hlsProcs.delete(k); }
866
+ const url = 'http://127.0.0.1:' + st.port + base + '/s/' + mF[1] + '/' + mF[2];
867
+ const args = ['-y', '-hide_banner', '-loglevel', 'error', '-rw_timeout', '30000000', '-reconnect', '1', '-reconnect_streamed', '1', '-reconnect_delay_max', '5'];
868
+ if (t > 0) args.push('-ss', String(t));
869
+ 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
870
  const proc = spawn(ffmpeg, args, { stdio: ['ignore', 'ignore', 'pipe'] });
882
871
  let ffErr = 0;
883
- proc.stderr.on('data', d => { if (ffErr < 3) { console.error('hls:', String(d).trim().slice(0, 200)); ffErr++; } });
872
+ proc.stderr.on('data', d => { if (ffErr < 3) { console.error('fmp4:', String(d).trim().slice(0, 200)); ffErr++; } });
884
873
  proc.on('exit', () => { const cur = hlsProcs.get(key); if (cur && cur.proc === proc) hlsProcs.delete(key); });
885
874
  hlsProcs.set(key, { proc, dir, touched: Date.now() });
886
875
  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'];
876
+ const subArgs = ['-y', '-hide_banner', '-loglevel', 'error', '-rw_timeout', '30000000', '-reconnect', '1', '-reconnect_streamed', '1', '-reconnect_delay_max', '5'];
877
+ if (t > 0) subArgs.push('-ss', String(t));
878
+ subArgs.push('-i', url, '-map', '0:s:' + si, '-c:s', 'webvtt', '-f', 'webvtt', dir + '/sub.vtt');
888
879
  const sp = spawn(ffmpeg, subArgs, { stdio: ['ignore', 'ignore', 'pipe'] });
889
880
  let sf = 0;
890
881
  sp.stderr.on('data', d => { if (sf < 2) { console.error('vtt:', String(d).trim().slice(0, 150)); sf++; } });
@@ -892,13 +883,33 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
892
883
  hlsProcs.set(key + '-vtt', { proc: sp, dir, touched: Date.now() });
893
884
  }
894
885
  }
895
- for (let i = 0; i < 60; i++) {
886
+ if (name === 'sub.vtt') {
896
887
  let b = null;
897
888
  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));
889
+ res.setHeader('Content-Type', 'text/vtt; charset=utf-8');
890
+ res.end(b || 'WEBVTT\n\n');
891
+ return;
892
+ }
893
+ let size = 0;
894
+ for (let i = 0; i < 60 && size === 0; i++) {
895
+ try { size = fs.statSync(fp).size; } catch {}
896
+ if (size === 0) await new Promise(r => setTimeout(r, 500));
900
897
  }
901
- res.statusCode = 504; res.end('ffmpeg timeout');
898
+ if (size === 0) { res.statusCode = 504; res.end('ffmpeg timeout'); return; }
899
+ res.setHeader('Accept-Ranges', 'bytes');
900
+ const rm = /^bytes=(\d*)-(\d*)$/.exec((req.headers.range || '').trim());
901
+ if (rm) {
902
+ let startB, endB;
903
+ if (rm[1] === '') { const n = Number(rm[2]); startB = Math.max(0, size - n); endB = size - 1; }
904
+ else { startB = Number(rm[1]); endB = rm[2] === '' ? size - 1 : Math.min(Number(rm[2]), size - 1); }
905
+ if (startB >= size || startB > endB) { res.writeHead(416, { 'Content-Range': 'bytes */' + size }); res.end(); return; }
906
+ res.writeHead(206, { 'Content-Range': 'bytes ' + startB + '-' + endB + '/' + size, 'Content-Length': endB - startB + 1, 'Content-Type': 'video/mp4' });
907
+ fs.createReadStream(fp, { start: startB, end: endB }).pipe(res);
908
+ return;
909
+ }
910
+ res.setHeader('Content-Type', 'video/mp4');
911
+ res.setHeader('Content-Length', size);
912
+ fs.createReadStream(fp).pipe(res);
902
913
  return;
903
914
  }
904
915
  const mV = /^\/view\/(\d+)\/(\d+)$/.exec(p);
@@ -918,7 +929,6 @@ video{flex:1;width:100%;min-height:0;background:#000}
918
929
  <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
930
  <video id=v controls autoplay playsinline src="${base}/s/${mV[1]}/${mV[2]}"></video>
920
931
  <div id=ov>▶</div>
921
- <script src="https://cdn.jsdelivr.net/npm/hls.js@1.5.17/dist/hls.min.js"></script>
922
932
  <script>
923
933
  var B=${JSON.stringify(base)},ID=${mV[1]},I=${mV[2]};
924
934
  var v=document.getElementById("v"),aS=document.getElementById("a"),sS=document.getElementById("s"),n=document.getElementById("n"),ov=document.getElementById("ov");
@@ -929,11 +939,7 @@ var parseVtt=function(txt){var NL=String.fromCharCode(10);var C=[];txt.split(NL+
929
939
  var pollSub=function(){if(!subUrl)return;fetch(subUrl).then(function(r){return r.text()}).then(parseVtt).catch(function(){})};
930
940
  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
941
  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"})}};
942
+ var play=function(u){v.src=u;v.currentTime=0;v.play().catch(function(){ov.style.display="flex"})};
937
943
  var load=function(){fetch(B+"/api/tracks/"+ID+"/"+I).then(function(r){return r.json()}).then(function(d){
938
944
  if(d.retry&&tries++<15){n.textContent="⏳ готовлю файл к стриму…";setTimeout(load,4000);return;}
939
945
  if(d.error)n.textContent="⚠ "+d.error;
@@ -944,7 +950,7 @@ A.forEach(function(t){var o=document.createElement("option");o.value=t.a;o.textC
944
950
  if(A.length>1){aS.value=A[0].a;aS.style.display=""}
945
951
  if(SI.length){var o0=document.createElement("option");o0.value="";o0.textContent="без субтитров";sS.appendChild(o0);
946
952
  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)};
953
+ 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
954
  aS.onchange=go;sS.onchange=go;go();
949
955
  }).catch(function(){if(tries++<15)setTimeout(load,4000)})};
950
956
  v.addEventListener("playing",function(){n.textContent="";ov.style.display="none"});