torrent-online 1.6.6 → 1.7.1

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 +61 -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.1",
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,49 @@ 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 isSeg = /^seg-\d+\.ts$/.test(name);
862
+ const need = !isSeg && (!hlsProcs.has(key) || Math.abs((hlsProcs.get(key).start || 0) - t) > 3);
863
+ if (need) {
864
+ 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); }
865
+ await fsp.rm(dir, { recursive: true, force: true });
863
866
  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];
867
+ const url = 'http://127.0.0.1:' + st.port + base + '/s/' + mH[1] + '/' + mH[2];
868
+ const pre = t > 12 ? t - 10 : t;
869
+ const post = t > 12 ? 10 : 0;
867
870
  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');
871
+ if (pre > 0) args.push('-ss', String(pre));
872
+ args.push('-i', url);
873
+ if (post > 0) args.push('-ss', String(post));
874
+ 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
875
  const proc = spawn(ffmpeg, args, { stdio: ['ignore', 'ignore', 'pipe'] });
871
876
  let ffErr = 0;
872
- proc.stderr.on('data', d => { if (ffErr < 3) { console.error('fmp4:', String(d).trim().slice(0, 200)); ffErr++; } });
877
+ proc.stderr.on('data', d => { if (ffErr < 3) { console.error('hls:', String(d).trim().slice(0, 200)); ffErr++; } });
873
878
  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() });
879
+ hlsProcs.set(key, { proc, dir, touched: Date.now(), start: t });
875
880
  if (si >= 0) {
876
881
  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');
882
+ if (pre > 0) subArgs.push('-ss', String(pre));
883
+ subArgs.push('-i', url);
884
+ if (post > 0) subArgs.push('-ss', String(post));
885
+ subArgs.push('-map', '0:s:' + si, '-c:s', 'webvtt', '-f', 'webvtt', dir + '/sub.vtt');
879
886
  const sp = spawn(ffmpeg, subArgs, { stdio: ['ignore', 'ignore', 'pipe'] });
880
887
  let sf = 0;
881
888
  sp.stderr.on('data', d => { if (sf < 2) { console.error('vtt:', String(d).trim().slice(0, 150)); sf++; } });
@@ -883,33 +890,39 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
883
890
  hlsProcs.set(key + '-vtt', { proc: sp, dir, touched: Date.now() });
884
891
  }
885
892
  }
893
+ const h = hlsProcs.get(key);
894
+ if (h) h.touched = Date.now();
886
895
  if (name === 'sub.vtt') {
887
- let b = null;
888
- try { b = fs.readFileSync(fp); } catch {}
896
+ let b = '';
897
+ try { b = fs.readFileSync(path.join(dir, 'sub.vtt'), 'utf8'); } catch {}
898
+ const base0 = h ? (h.start || 0) : 0;
899
+ 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; };
900
+ const outv = b.replace(/(\d[\d:.]*) --> (\d[\d:.]*)/g, (all, x, y) => shift(x.trim()) + ' --> ' + shift(y.trim()));
889
901
  res.setHeader('Content-Type', 'text/vtt; charset=utf-8');
890
- res.end(b || 'WEBVTT\n\n');
902
+ res.end(outv || 'WEBVTT\n\n');
891
903
  return;
892
904
  }
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);
905
+ if (/^seg-\d+\.ts$/.test(name)) {
906
+ const fp = path.join(dir, name);
907
+ let b = null;
908
+ try { b = fs.readFileSync(fp); } catch {}
909
+ if (!b) { res.statusCode = 404; res.end('no segment'); return; }
910
+ res.setHeader('Content-Type', 'video/mp2t');
911
+ res.setHeader('Cache-Control', 'no-store');
912
+ res.end(b);
908
913
  return;
909
914
  }
910
- res.setHeader('Content-Type', 'video/mp4');
911
- res.setHeader('Content-Length', size);
912
- fs.createReadStream(fp).pipe(res);
915
+ let segs = [];
916
+ try { segs = fs.readdirSync(dir).filter(x => /^seg-\d+\.ts$/.test(x)).sort((x, y) => (+x.slice(4)) - (+y.slice(4))); } catch {}
917
+ segs = segs.slice(0, -1); // последний сегмент ещё пишется
918
+ const durs = segs.map(() => 4);
919
+ const maxD = 6;
920
+ 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';
921
+ const qs = '?a=' + encodeURIComponent(a) + (si >= 0 ? '&si=' + si : '&si=') + '&t=' + t;
922
+ for (let i = 0; i < segs.length; i++) pl += '#EXTINF:' + durs[i].toFixed(3) + ',\n' + segs[i] + qs + '\n';
923
+ res.setHeader('Content-Type', 'application/vnd.apple.mpegurl');
924
+ res.setHeader('Cache-Control', 'no-store');
925
+ res.end(pl);
913
926
  return;
914
927
  }
915
928
  const mV = /^\/view\/(\d+)\/(\d+)$/.exec(p);
@@ -927,8 +940,9 @@ select{background:#1b1b1f;color:#ddd;border:1px solid #333;border-radius:6px;pad
927
940
  video{flex:1;width:100%;min-height:0;background:#000}
928
941
  #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
942
  <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>
943
+ <video id=v controls autoplay playsinline></video>
931
944
  <div id=ov>▶</div>
945
+ <script src="https://cdn.jsdelivr.net/npm/hls.js@1.5.17/dist/hls.min.js"></script>
932
946
  <script>
933
947
  var B=${JSON.stringify(base)},ID=${mV[1]},I=${mV[2]};
934
948
  var v=document.getElementById("v"),aS=document.getElementById("a"),sS=document.getElementById("s"),n=document.getElementById("n"),ov=document.getElementById("ov");
@@ -939,7 +953,11 @@ var parseVtt=function(txt){var NL=String.fromCharCode(10);var C=[];txt.split(NL+
939
953
  var pollSub=function(){if(!subUrl)return;fetch(subUrl).then(function(r){return r.text()}).then(parseVtt).catch(function(){})};
940
954
  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
955
  setInterval(pollSub,20000);
942
- var play=function(u){v.src=u;v.currentTime=0;v.play().catch(function(){ov.style.display="flex"})};
956
+ var play=function(u){if(hls){try{hls.destroy()}catch(e){}hls=null}
957
+ if(window.Hls&&Hls.isSupported()){hls=new Hls({enableWorker:true,lowLatencyMode:false,liveSyncDurationCount:3});hls.loadSource(u);hls.attachMedia(v);
958
+ hls.on(Hls.Events.MANIFEST_PARSED,function(){v.currentTime=0;v.play().catch(function(){ov.style.display="flex"})});
959
+ hls.on(Hls.Events.ERROR,function(ev,d){if(d.fatal)n.textContent="⚠ HLS: "+d.type})}
960
+ else{v.src=u;v.play().catch(function(){ov.style.display="flex"})}};
943
961
  var load=function(){fetch(B+"/api/tracks/"+ID+"/"+I).then(function(r){return r.json()}).then(function(d){
944
962
  if(d.retry&&tries++<15){n.textContent="⏳ готовлю файл к стриму…";setTimeout(load,4000);return;}
945
963
  if(d.error)n.textContent="⚠ "+d.error;
@@ -950,7 +968,7 @@ A.forEach(function(t){var o=document.createElement("option");o.value=t.a;o.textC
950
968
  if(A.length>1){aS.value=A[0].a;aS.style.display=""}
951
969
  if(SI.length){var o0=document.createElement("option");o0.value="";o0.textContent="без субтитров";sS.appendChild(o0);
952
970
  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:""))};
971
+ 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
972
  aS.onchange=go;sS.onchange=go;go();
955
973
  }).catch(function(){if(tries++<15)setTimeout(load,4000)})};
956
974
  v.addEventListener("playing",function(){n.textContent="";ov.style.display="none"});