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.
- package/package.json +1 -1
- package/wtui.js +58 -43
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "torrent-online",
|
|
3
|
-
"version": "1.
|
|
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
|
|
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 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
|
-
|
|
865
|
-
|
|
866
|
-
const
|
|
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 (
|
|
869
|
-
args.push('-i', url
|
|
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('
|
|
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 (
|
|
878
|
-
subArgs.push('-i', url
|
|
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 =
|
|
888
|
-
try { b = fs.readFileSync(
|
|
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(
|
|
901
|
+
res.end(outv || 'WEBVTT\n\n');
|
|
891
902
|
return;
|
|
892
903
|
}
|
|
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);
|
|
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
|
-
|
|
911
|
-
|
|
912
|
-
|
|
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
|
|
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){
|
|
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
|
|
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"});
|