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.
- package/package.json +1 -1
- package/wtui.js +61 -43
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "torrent-online",
|
|
3
|
-
"version": "1.
|
|
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
|
|
844
|
-
if (
|
|
845
|
-
const e = st.torrents.get(Number(
|
|
846
|
-
const f = e?.t.files[Number(
|
|
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(
|
|
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 =
|
|
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(
|
|
857
|
-
const key =
|
|
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
|
-
|
|
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
|
-
|
|
865
|
-
|
|
866
|
-
const
|
|
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 (
|
|
869
|
-
args.push('-i', url
|
|
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('
|
|
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 (
|
|
878
|
-
subArgs.push('-i', url
|
|
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 =
|
|
888
|
-
try { b = fs.readFileSync(
|
|
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(
|
|
902
|
+
res.end(outv || 'WEBVTT\n\n');
|
|
891
903
|
return;
|
|
892
904
|
}
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
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
|
-
|
|
911
|
-
|
|
912
|
-
|
|
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
|
|
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){
|
|
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
|
|
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"});
|