torrent-online 1.6.5 → 1.7.0

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 +60 -44
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "torrent-online",
3
- "version": "1.6.5",
3
+ "version": "1.7.0",
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,41 +840,48 @@ 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 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])];
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])];
847
847
  if (!f) { res.statusCode = 404; res.end('not found'); return; }
848
- ensureSel(e, Number(mF[2]));
848
+ ensureSel(e, Number(mH[2]));
849
849
  const ffmpeg = detectFFBin('ffmpeg');
850
850
  if (!ffmpeg) { res.statusCode = 500; res.end('ffmpeg не найден'); return; }
851
- const name = mF[3];
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
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 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
+ const t = Math.max(0, Math.round(Number(u.searchParams.get('t')) || 0));
857
+ const key = mH[1] + '-' + mH[2] + '-' + a + '-' + (si >= 0 ? 's' + si : 'n');
858
858
  const dir = path.join(HLS_ROOT, key);
859
- const fp = path.join(dir, name);
860
859
  const hit0 = hlsProcs.get(key); if (hit0) hit0.touched = Date.now();
861
860
  const hit1 = hlsProcs.get(key + '-vtt'); if (hit1) hit1.touched = Date.now();
862
- if (!hlsProcs.has(key)) {
861
+ const need = !hlsProcs.has(key) || Math.abs((hlsProcs.get(key).start || 0) - t) > 3;
862
+ if (need) {
863
+ for (const [k, h] of hlsProcs) if (k.startsWith(key) || k.startsWith(mH[1] + '-' + mH[2] + '-')) { try { h.proc.kill('SIGKILL'); } catch {} hlsProcs.delete(k); }
864
+ await fsp.rm(dir, { recursive: true, force: true });
863
865
  await ensureDir(dir);
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');
866
+ const url = 'http://127.0.0.1:' + st.port + base + '/s/' + mH[1] + '/' + mH[2];
867
+ const pre = t > 12 ? t - 10 : t;
868
+ const post = t > 12 ? 10 : 0;
869
+ const args = ['-y', '-hide_banner', '-loglevel', 'error', '-rw_timeout', '30000000', '-reconnect', '1', '-reconnect_streamed', '1', '-reconnect_delay_max', '5'];
870
+ if (pre > 0) args.push('-ss', String(pre));
871
+ args.push('-i', url);
872
+ if (post > 0) args.push('-ss', String(post));
873
+ args.push('-map', '0:v:0', '-map', '0:a:' + a, '-c:v', 'copy', '-c:a', 'aac', '-b:a', '192k', '-af', 'aresample=async=1:first_pts=0', '-max_muxing_queue_size', '9999', '-f', 'segment', '-segment_time', '4', '-reset_timestamps', '1', '-segment_start_number', '0', dir + '/seg-%03d.ts');
869
874
  const proc = spawn(ffmpeg, args, { stdio: ['ignore', 'ignore', 'pipe'] });
870
875
  let ffErr = 0;
871
- proc.stderr.on('data', d => { if (ffErr < 3) { console.error('fmp4:', String(d).trim().slice(0, 200)); ffErr++; } });
876
+ proc.stderr.on('data', d => { if (ffErr < 3) { console.error('hls:', String(d).trim().slice(0, 200)); ffErr++; } });
872
877
  proc.on('exit', () => { const cur = hlsProcs.get(key); if (cur && cur.proc === proc) hlsProcs.delete(key); });
873
- hlsProcs.set(key, { proc, dir, touched: Date.now() });
878
+ hlsProcs.set(key, { proc, dir, touched: Date.now(), start: t });
874
879
  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');
880
+ const subArgs = ['-y', '-hide_banner', '-loglevel', 'error', '-rw_timeout', '30000000', '-reconnect', '1', '-reconnect_streamed', '1', '-reconnect_delay_max', '5'];
881
+ if (pre > 0) subArgs.push('-ss', String(pre));
882
+ subArgs.push('-i', url);
883
+ if (post > 0) subArgs.push('-ss', String(post));
884
+ subArgs.push('-map', '0:s:' + si, '-c:s', 'webvtt', '-f', 'webvtt', dir + '/sub.vtt');
878
885
  const sp = spawn(ffmpeg, subArgs, { stdio: ['ignore', 'ignore', 'pipe'] });
879
886
  let sf = 0;
880
887
  sp.stderr.on('data', d => { if (sf < 2) { console.error('vtt:', String(d).trim().slice(0, 150)); sf++; } });
@@ -882,33 +889,37 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
882
889
  hlsProcs.set(key + '-vtt', { proc: sp, dir, touched: Date.now() });
883
890
  }
884
891
  }
892
+ const h = hlsProcs.get(key);
893
+ if (h) h.touched = Date.now();
885
894
  if (name === 'sub.vtt') {
886
- let b = null;
887
- try { b = fs.readFileSync(fp); } catch {}
895
+ let b = '';
896
+ try { b = fs.readFileSync(path.join(dir, 'sub.vtt'), 'utf8'); } catch {}
897
+ const base0 = h ? (h.start || 0) : 0;
898
+ const shift = s => { const m = /^(\d+):(\d+):([0-9.]+)$/.exec(s); if (!m) return s; let x = (+m[1]) * 3600 + (+m[2]) * 60 + parseFloat(m[3]) - base0; if (x < 0) x = 0; const hh = Math.floor(x / 3600), mm = Math.floor((x % 3600) / 60), ss = (x % 60).toFixed(3).padStart(6, '0'); return (hh ? hh + ':' : '') + String(mm).padStart(2, '0') + ':' + ss; };
899
+ const outv = b.replace(/(\d[\d:.]*) --> (\d[\d:.]*)/g, (all, x, y) => shift(x.trim()) + ' --> ' + shift(y.trim()));
888
900
  res.setHeader('Content-Type', 'text/vtt; charset=utf-8');
889
- res.end(b || 'WEBVTT\n\n');
901
+ res.end(outv || 'WEBVTT\n\n');
890
902
  return;
891
903
  }
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);
904
+ if (/^seg-\d+\.ts$/.test(name)) {
905
+ const fp = path.join(dir, name);
906
+ let b = null;
907
+ try { b = fs.readFileSync(fp); } catch {}
908
+ if (!b) { res.statusCode = 404; res.end('no segment'); return; }
909
+ res.setHeader('Content-Type', 'video/mp2t');
910
+ res.setHeader('Cache-Control', 'no-store');
911
+ res.end(b);
907
912
  return;
908
913
  }
909
- res.setHeader('Content-Type', 'video/mp4');
910
- res.setHeader('Content-Length', size);
911
- fs.createReadStream(fp).pipe(res);
914
+ let segs = [];
915
+ try { segs = fs.readdirSync(dir).filter(x => /^seg-\d+\.ts$/.test(x)).sort((x, y) => (+x.slice(4)) - (+y.slice(4))); } catch {}
916
+ const durs = segs.map(() => 4);
917
+ const maxD = 6;
918
+ let pl = '#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:' + maxD + '\n#EXT-X-PLAYLIST-TYPE:EVENT\n#EXT-X-MEDIA-SEQUENCE:0\n';
919
+ for (let i = 0; i < segs.length; i++) pl += '#EXTINF:' + durs[i].toFixed(3) + ',\n' + segs[i] + '\n';
920
+ res.setHeader('Content-Type', 'application/vnd.apple.mpegurl');
921
+ res.setHeader('Cache-Control', 'no-store');
922
+ res.end(pl);
912
923
  return;
913
924
  }
914
925
  const mV = /^\/view\/(\d+)\/(\d+)$/.exec(p);
@@ -926,8 +937,9 @@ select{background:#1b1b1f;color:#ddd;border:1px solid #333;border-radius:6px;pad
926
937
  video{flex:1;width:100%;min-height:0;background:#000}
927
938
  #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
939
  <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>
940
+ <video id=v controls autoplay playsinline></video>
930
941
  <div id=ov>▶</div>
942
+ <script src="https://cdn.jsdelivr.net/npm/hls.js@1.5.17/dist/hls.min.js"></script>
931
943
  <script>
932
944
  var B=${JSON.stringify(base)},ID=${mV[1]},I=${mV[2]};
933
945
  var v=document.getElementById("v"),aS=document.getElementById("a"),sS=document.getElementById("s"),n=document.getElementById("n"),ov=document.getElementById("ov");
@@ -938,7 +950,11 @@ var parseVtt=function(txt){var NL=String.fromCharCode(10);var C=[];txt.split(NL+
938
950
  var pollSub=function(){if(!subUrl)return;fetch(subUrl).then(function(r){return r.text()}).then(parseVtt).catch(function(){})};
939
951
  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
952
  setInterval(pollSub,20000);
941
- var play=function(u){v.src=u;v.currentTime=0;v.play().catch(function(){ov.style.display="flex"})};
953
+ var play=function(u){if(hls){try{hls.destroy()}catch(e){}hls=null}
954
+ if(window.Hls&&Hls.isSupported()){hls=new Hls({enableWorker:true,lowLatencyMode:false,liveSyncDurationCount:3});hls.loadSource(u);hls.attachMedia(v);
955
+ hls.on(Hls.Events.MANIFEST_PARSED,function(){v.currentTime=0;v.play().catch(function(){ov.style.display="flex"})});
956
+ hls.on(Hls.Events.ERROR,function(ev,d){if(d.fatal)n.textContent="⚠ HLS: "+d.type})}
957
+ else{v.src=u;v.play().catch(function(){ov.style.display="flex"})}};
942
958
  var load=function(){fetch(B+"/api/tracks/"+ID+"/"+I).then(function(r){return r.json()}).then(function(d){
943
959
  if(d.retry&&tries++<15){n.textContent="⏳ готовлю файл к стриму…";setTimeout(load,4000);return;}
944
960
  if(d.error)n.textContent="⚠ "+d.error;
@@ -949,7 +965,7 @@ A.forEach(function(t){var o=document.createElement("option");o.value=t.a;o.textC
949
965
  if(A.length>1){aS.value=A[0].a;aS.style.display=""}
950
966
  if(SI.length){var o0=document.createElement("option");o0.value="";o0.textContent="без субтитров";sS.appendChild(o0);
951
967
  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:""))};
968
+ 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));cues=[];cueDiv.innerHTML="";subUrl=s!==""?B+"/hls/"+ID+"/"+I+"/sub.vtt?a="+a+"&si="+s+"&t="+t:"";if(subUrl)pollSub();play(B+"/hls/"+ID+"/"+I+"/master.m3u8?a="+a+(s!==""?"&si="+s:"")+(t>0?"&t="+t:""))};
953
969
  aS.onchange=go;sS.onchange=go;go();
954
970
  }).catch(function(){if(tries++<15)setTimeout(load,4000)})};
955
971
  v.addEventListener("playing",function(){n.textContent="";ov.style.display="none"});