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.
- package/package.json +1 -1
- package/wtui.js +60 -44
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,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
|
|
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
|
-
const
|
|
866
|
-
const
|
|
867
|
-
|
|
868
|
-
|
|
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('
|
|
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 (
|
|
877
|
-
subArgs.push('-i', url
|
|
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 =
|
|
887
|
-
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()));
|
|
888
900
|
res.setHeader('Content-Type', 'text/vtt; charset=utf-8');
|
|
889
|
-
res.end(
|
|
901
|
+
res.end(outv || 'WEBVTT\n\n');
|
|
890
902
|
return;
|
|
891
903
|
}
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
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
|
-
|
|
910
|
-
|
|
911
|
-
|
|
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
|
|
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){
|
|
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
|
|
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"});
|