torrent-online 1.6.6 → 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 +58 -43
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "torrent-online",
3
- "version": "1.6.6",
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,42 +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 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];
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;
867
869
  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');
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');
870
874
  const proc = spawn(ffmpeg, args, { stdio: ['ignore', 'ignore', 'pipe'] });
871
875
  let ffErr = 0;
872
- 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++; } });
873
877
  proc.on('exit', () => { const cur = hlsProcs.get(key); if (cur && cur.proc === proc) hlsProcs.delete(key); });
874
- hlsProcs.set(key, { proc, dir, touched: Date.now() });
878
+ hlsProcs.set(key, { proc, dir, touched: Date.now(), start: t });
875
879
  if (si >= 0) {
876
880
  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');
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');
879
885
  const sp = spawn(ffmpeg, subArgs, { stdio: ['ignore', 'ignore', 'pipe'] });
880
886
  let sf = 0;
881
887
  sp.stderr.on('data', d => { if (sf < 2) { console.error('vtt:', String(d).trim().slice(0, 150)); sf++; } });
@@ -883,33 +889,37 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
883
889
  hlsProcs.set(key + '-vtt', { proc: sp, dir, touched: Date.now() });
884
890
  }
885
891
  }
892
+ const h = hlsProcs.get(key);
893
+ if (h) h.touched = Date.now();
886
894
  if (name === 'sub.vtt') {
887
- let b = null;
888
- 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()));
889
900
  res.setHeader('Content-Type', 'text/vtt; charset=utf-8');
890
- res.end(b || 'WEBVTT\n\n');
901
+ res.end(outv || 'WEBVTT\n\n');
891
902
  return;
892
903
  }
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));
897
- }
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);
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);
908
912
  return;
909
913
  }
910
- res.setHeader('Content-Type', 'video/mp4');
911
- res.setHeader('Content-Length', size);
912
- 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);
913
923
  return;
914
924
  }
915
925
  const mV = /^\/view\/(\d+)\/(\d+)$/.exec(p);
@@ -927,8 +937,9 @@ select{background:#1b1b1f;color:#ddd;border:1px solid #333;border-radius:6px;pad
927
937
  video{flex:1;width:100%;min-height:0;background:#000}
928
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>
929
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>
930
- <video id=v controls autoplay playsinline src="${base}/s/${mV[1]}/${mV[2]}"></video>
940
+ <video id=v controls autoplay playsinline></video>
931
941
  <div id=ov>▶</div>
942
+ <script src="https://cdn.jsdelivr.net/npm/hls.js@1.5.17/dist/hls.min.js"></script>
932
943
  <script>
933
944
  var B=${JSON.stringify(base)},ID=${mV[1]},I=${mV[2]};
934
945
  var v=document.getElementById("v"),aS=document.getElementById("a"),sS=document.getElementById("s"),n=document.getElementById("n"),ov=document.getElementById("ov");
@@ -939,7 +950,11 @@ var parseVtt=function(txt){var NL=String.fromCharCode(10);var C=[];txt.split(NL+
939
950
  var pollSub=function(){if(!subUrl)return;fetch(subUrl).then(function(r){return r.text()}).then(parseVtt).catch(function(){})};
940
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});
941
952
  setInterval(pollSub,20000);
942
- 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"})}};
943
958
  var load=function(){fetch(B+"/api/tracks/"+ID+"/"+I).then(function(r){return r.json()}).then(function(d){
944
959
  if(d.retry&&tries++<15){n.textContent="⏳ готовлю файл к стриму…";setTimeout(load,4000);return;}
945
960
  if(d.error)n.textContent="⚠ "+d.error;
@@ -950,7 +965,7 @@ A.forEach(function(t){var o=document.createElement("option");o.value=t.a;o.textC
950
965
  if(A.length>1){aS.value=A[0].a;aS.style.display=""}
951
966
  if(SI.length){var o0=document.createElement("option");o0.value="";o0.textContent="без субтитров";sS.appendChild(o0);
952
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=""}
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:""))};
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:""))};
954
969
  aS.onchange=go;sS.onchange=go;go();
955
970
  }).catch(function(){if(tries++<15)setTimeout(load,4000)})};
956
971
  v.addEventListener("playing",function(){n.textContent="";ov.style.display="none"});