torrent-online 1.6.0 → 1.6.2
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 +68 -17
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "torrent-online",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.2",
|
|
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
|
@@ -654,6 +654,9 @@ function makeWebServer(client, st, opts, cacheDir, base) {
|
|
|
654
654
|
async function webReq(req, res, client, st, opts, cacheDir, base) {
|
|
655
655
|
const u = new URL(req.url, 'http://127.0.0.1');
|
|
656
656
|
const ensureSel = (e, i) => { const f = e?.t.files[i]; if (f && !e.sel.has(i)) { e.sel.add(i); try { f.select(); } catch {} } };
|
|
657
|
+
const hlsProcs = new Map();
|
|
658
|
+
const HLS_ROOT = path.join(os.tmpdir(), 'wtui-hls-' + process.pid);
|
|
659
|
+
setInterval(() => { const now = Date.now(); for (const [k, h] of hlsProcs) if (now - h.touched > 180000) { try { h.proc.kill('SIGKILL'); } catch {} hlsProcs.delete(k); try { fs.rmSync(h.dir, { recursive: true, force: true }); } catch {} } } , 30000).unref();
|
|
657
660
|
let p = u.pathname;
|
|
658
661
|
if (base) {
|
|
659
662
|
if (p !== base && !p.startsWith(base + '/')) { res.statusCode = 404; res.end('not found'); return; }
|
|
@@ -746,6 +749,7 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
|
|
|
746
749
|
const id = Number(b.id);
|
|
747
750
|
const e = st.torrents.get(id);
|
|
748
751
|
if (e) { st.torrents.delete(id); try { await new Promise(r => e.t.destroy(r)); } catch {} }
|
|
752
|
+
for (const [k, h] of hlsProcs) if (k.startsWith(id + '-')) { try { h.proc.kill('SIGKILL'); } catch {} hlsProcs.delete(k); try { fs.rmSync(h.dir, { recursive: true, force: true }); } catch {} }
|
|
749
753
|
return json(200, { ok: true });
|
|
750
754
|
}
|
|
751
755
|
const mS = /^\/s\/(\d+)\/(\d+)$/.exec(p);
|
|
@@ -836,6 +840,53 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
|
|
|
836
840
|
res.on('close', () => { try { proc.kill('SIGKILL'); } catch {} });
|
|
837
841
|
return;
|
|
838
842
|
}
|
|
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
|
+
if (!f) { res.statusCode = 404; res.end('not found'); return; }
|
|
848
|
+
ensureSel(e, Number(mH[2]));
|
|
849
|
+
const ffmpeg = detectFFBin('ffmpeg');
|
|
850
|
+
if (!ffmpeg) { res.statusCode = 500; res.end('ffmpeg не найден'); return; }
|
|
851
|
+
const name = mH[3];
|
|
852
|
+
if (name.includes('..')) { res.statusCode = 400; res.end('bad'); return; }
|
|
853
|
+
const a = u.searchParams.get('a') || '0';
|
|
854
|
+
const si = (u.searchParams.get('si') || '').split(',').map(Number).filter(n => Number.isFinite(n)).slice(0, 1);
|
|
855
|
+
const key = mH[1] + '-' + mH[2] + '-' + a + (si.length ? '-s' + si.join('.') : '');
|
|
856
|
+
const dir = path.join(HLS_ROOT, key);
|
|
857
|
+
const fp = path.join(dir, name);
|
|
858
|
+
const hit0 = hlsProcs.get(key); if (hit0) hit0.touched = Date.now();
|
|
859
|
+
if (name !== 'master.m3u8') {
|
|
860
|
+
let b = null;
|
|
861
|
+
try { b = fs.readFileSync(fp); } catch {}
|
|
862
|
+
if (b) { res.setHeader('Content-Type', name.endsWith('.m3u8') ? 'application/x-mpegURL' : name.endsWith('.ts') ? 'video/mp2t' : name.endsWith('.vtt') ? 'text/vtt; charset=utf-8' : 'application/octet-stream'); res.end(b); }
|
|
863
|
+
else { res.statusCode = 404; res.end('no segment'); }
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
if (!hlsProcs.has(key)) {
|
|
867
|
+
await ensureDir(dir);
|
|
868
|
+
for (const [k, h] of hlsProcs) if (k.startsWith(mH[1] + '-' + mH[2] + '-')) { try { h.proc.kill('SIGKILL'); } catch {} hlsProcs.delete(k); }
|
|
869
|
+
const url = 'http://127.0.0.1:' + st.port + base + '/s/' + mH[1] + '/' + mH[2];
|
|
870
|
+
const args = ['-hide_banner', '-loglevel', 'error', '-rw_timeout', '30000000', '-reconnect', '1', '-reconnect_streamed', '1', '-reconnect_delay_max', '5', '-i', url, '-map', '0:v:0', '-map', '0:a:' + a, '-c:v', 'copy', '-c:a', 'aac', '-b:a', '192k'];
|
|
871
|
+
for (const i of si) args.push('-map', '0:s:' + i);
|
|
872
|
+
if (si.length) args.push('-c:s', 'webvtt');
|
|
873
|
+
const vsm = ['v:0', 'a:0']; for (let i = 0; i < si.length; i++) vsm.push('s:' + i);
|
|
874
|
+
args.push('-f', 'hls', '-hls_time', '4', '-hls_list_size', '12', '-hls_flags', 'delete_segments+independent_segments', '-hls_segment_type', 'mpegts', '-master_pl_name', 'master.m3u8', '-var_stream_map', vsm.join(','), '-hls_segment_filename', dir + '/seg-%v%a%s-%d.ts', dir + '/pl-%v%a%s.m3u8');
|
|
875
|
+
const proc = spawn(ffmpeg, args, { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
876
|
+
let ffErr = 0;
|
|
877
|
+
proc.stderr.on('data', d => { if (ffErr < 3) { console.error('hls:', String(d).trim().slice(0, 200)); ffErr++; } });
|
|
878
|
+
proc.on('exit', () => { const cur = hlsProcs.get(key); if (cur && cur.proc === proc) hlsProcs.delete(key); });
|
|
879
|
+
hlsProcs.set(key, { proc, dir, touched: Date.now() });
|
|
880
|
+
}
|
|
881
|
+
for (let i = 0; i < 60; i++) {
|
|
882
|
+
let b = null;
|
|
883
|
+
try { b = fs.readFileSync(fp); } catch {}
|
|
884
|
+
if (b) { res.setHeader('Content-Type', 'application/x-mpegURL'); res.end(b); return; }
|
|
885
|
+
await new Promise(r => setTimeout(r, 500));
|
|
886
|
+
}
|
|
887
|
+
res.statusCode = 504; res.end('ffmpeg timeout');
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
839
890
|
const mV = /^\/view\/(\d+)\/(\d+)$/.exec(p);
|
|
840
891
|
if (mV) {
|
|
841
892
|
const e = st.torrents.get(Number(mV[1]));
|
|
@@ -853,32 +904,30 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
|
|
|
853
904
|
'<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>' +
|
|
854
905
|
'<video id=v controls autoplay playsinline src="' + base + '/s/' + mV[1] + '/' + mV[2] + '"></video>' +
|
|
855
906
|
'<div id=ov>▶</div>' +
|
|
907
|
+
'<script src="https://cdn.jsdelivr.net/npm/hls.js@1.5.17/dist/hls.min.js"></script>' +
|
|
856
908
|
'<script>var B=' + JSON.stringify(base) + ',ID=' + mV[1] + ',I=' + mV[2] + ',MF=' + (webFriendly ? 1 : 0) + ';' +
|
|
857
909
|
'var v=document.getElementById("v"),aS=document.getElementById("a"),sS=document.getElementById("s"),n=document.getElementById("n"),ov=document.getElementById("ov");' +
|
|
858
910
|
'ov.onclick=function(){v.play().catch(function(){});ov.style.display="none"};' +
|
|
859
|
-
'var tries=0,
|
|
860
|
-
'var
|
|
861
|
-
|
|
862
|
-
'var
|
|
863
|
-
'
|
|
864
|
-
'
|
|
865
|
-
'
|
|
866
|
-
'
|
|
867
|
-
'
|
|
868
|
-
'v.play().catch(function(){ov.style.display="flex"})})};' +
|
|
911
|
+
'var tries=0,hls=null;' +
|
|
912
|
+
'var NAT=v.canPlayType("application/vnd.apple.mpegurl")!==""&&(!window.Hls||!Hls.isSupported()||/Apple Computer/.test(navigator.vendor));' +
|
|
913
|
+
|
|
914
|
+
'var play=function(u){if(hls){try{hls.destroy()}catch(e){}hls=null}' +
|
|
915
|
+
'if(NAT){v.src=u;v.play().catch(function(){ov.style.display="flex"})}' +
|
|
916
|
+
'else if(window.Hls&&Hls.isSupported()){hls=new Hls({startPosition:0,enableWorker:true,lowLatencyMode:false});hls.loadSource(u);hls.attachMedia(v);' +
|
|
917
|
+
'hls.on(Hls.Events.MANIFEST_PARSED,function(){v.play().catch(function(){ov.style.display="flex"})});' +
|
|
918
|
+
'hls.on(Hls.Events.ERROR,function(ev,d){if(d.fatal)n.textContent="⚠ HLS: "+d.type})}' +
|
|
919
|
+
'else{v.src=u;v.play().catch(function(){ov.style.display="flex"})}};' +
|
|
869
920
|
'var load=function(){fetch(B+"/api/tracks/"+ID+"/"+I).then(function(r){return r.json()}).then(function(d){' +
|
|
870
921
|
'if(d.retry&&tries++<15){n.textContent="⏳ готовлю файл к стриму…";setTimeout(load,4000);return;}' +
|
|
871
922
|
'if(d.error)n.textContent="⚠ "+d.error;' +
|
|
872
|
-
'var A=d.tracks||[],S=d.subs||[];
|
|
923
|
+
'var A=d.tracks||[],S=d.subs||[];' +
|
|
873
924
|
'if(!d.ffmpeg){return;}' +
|
|
874
|
-
'
|
|
925
|
+
'var SI=S.filter(function(t){return ["subrip","ass","ssa","srt","mov_text","webvtt","text"].indexOf(t.codec)>=0}).map(function(t){return t.s});' +
|
|
875
926
|
'A.forEach(function(t){var o=document.createElement("option");o.value=t.a;o.textContent="♪ "+(t.lang&&t.lang!=="und"?t.lang:"дорожка "+(t.a+1))+(t.title?" — "+t.title:"")+" ["+t.codec+"]";aS.appendChild(o)});' +
|
|
876
927
|
'if(A.length>1){aS.value=A[0].a;aS.style.display=""}' +
|
|
877
|
-
'
|
|
878
|
-
'
|
|
879
|
-
'
|
|
880
|
-
'var u=B+"/remux/"+ID+"/"+I+"?a="+(aS.style.display!==""?aS.value:0)+(s!==""?"&s="+s:"");' +
|
|
881
|
-
'if(s===""&&MSEOK()){mse(u)}else{stopMse();v.src=u;v.load();v.addEventListener("loadedmetadata",function once(){v.currentTime=t;v.removeEventListener("loadedmetadata",once)});v.play().catch(function(){ov.style.display="flex"})}};' +
|
|
928
|
+
'var go=function(){var a=aS.style.display!==""?aS.value:0;var s=sS.style.display!==""?sS.value:"";play(B+"/hls/"+ID+"/"+I+"/master.m3u8?a="+a+(s!==""?"&si="+s:""))};' +
|
|
929
|
+
'if(SI.length){var o0=document.createElement("option");o0.value="";o0.textContent="без субтитров";sS.appendChild(o0);' +
|
|
930
|
+
'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=""}' +
|
|
882
931
|
'aS.onchange=go;sS.onchange=go;go();' +
|
|
883
932
|
'}).catch(function(){if(tries++<15)setTimeout(load,4000)})};' +
|
|
884
933
|
'v.addEventListener("playing",function(){n.textContent="";ov.style.display="none"});' +
|
|
@@ -931,6 +980,8 @@ async function startWeb(opts, srcArg) {
|
|
|
931
980
|
if (cleaning) return; cleaning = true;
|
|
932
981
|
st.vlc.forEach(v => { try { v.kill(); } catch {} });
|
|
933
982
|
try { server.close(); } catch {}
|
|
983
|
+
hlsProcs.forEach(h => { try { h.proc.kill('SIGKILL'); } catch {} });
|
|
984
|
+
try { fs.rmSync(path.join(os.tmpdir(), 'wtui-hls-' + process.pid), { recursive: true, force: true }); } catch {}
|
|
934
985
|
try { await new Promise(r => client.destroy(r)); } catch {}
|
|
935
986
|
if (!opts.keepCache) await cleanCache(cacheDir);
|
|
936
987
|
process.exit(code);
|