torrent-online 1.5.9 → 1.6.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 +94 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "torrent-online",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.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
|
@@ -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);
|
|
@@ -776,7 +780,20 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
|
|
|
776
780
|
if (s.codec_type === 'audio') tracks.push({ a: ai++, lang, title, codec: s.codec_name });
|
|
777
781
|
else if (s.codec_type === 'subtitle') subs.push({ s: si++, lang, title, codec: s.codec_name });
|
|
778
782
|
}
|
|
779
|
-
|
|
783
|
+
const vs = (j.streams || []).find(s => s.codec_type === 'video');
|
|
784
|
+
let vc = '';
|
|
785
|
+
if (vs) {
|
|
786
|
+
if (vs.codec_name === 'h264') {
|
|
787
|
+
const P = { Baseline: '42', Main: '4D', Extended: '58', High: '64', 'High 10': '6E', 'High 4:2:2': '76', 'High 4:4:4': 'F4' };
|
|
788
|
+
const idc = P[vs.profile] || '64';
|
|
789
|
+
const lvl = Number.isFinite(vs.level) ? ('0' + Math.round(vs.level / 10).toString(16)).slice(-2) : '28';
|
|
790
|
+
vc = 'avc1.' + idc + '00' + lvl;
|
|
791
|
+
} else if (vs.codec_name === 'hevc') {
|
|
792
|
+
vc = Number.isFinite(vs.level) ? 'hvc1.1.6.L' + Math.round(vs.level / 10) + '.B0' : 'hvc1.1.6.L120.B0';
|
|
793
|
+
} else if (vs.codec_name === 'av1') vc = 'av01.0.04M.08';
|
|
794
|
+
else if (vs.codec_name === 'vp9') vc = 'vp09.00.10.08';
|
|
795
|
+
}
|
|
796
|
+
return json(200, { ffmpeg: true, tracks, subs, vc });
|
|
780
797
|
} catch (e2) { return json(200, { ffmpeg: true, tracks: [], subs: [], retry: true }); }
|
|
781
798
|
}
|
|
782
799
|
const mR = /^\/remux\/(\d+)\/(\d+)$/.exec(p);
|
|
@@ -823,6 +840,53 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
|
|
|
823
840
|
res.on('close', () => { try { proc.kill('SIGKILL'); } catch {} });
|
|
824
841
|
return;
|
|
825
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, 8);
|
|
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
|
+
}
|
|
826
890
|
const mV = /^\/view\/(\d+)\/(\d+)$/.exec(p);
|
|
827
891
|
if (mV) {
|
|
828
892
|
const e = st.torrents.get(Number(mV[1]));
|
|
@@ -835,28 +899,42 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
|
|
|
835
899
|
'<style>body{margin:0;background:#000;color:#ddd;font:14px system-ui;display:flex;flex-direction:column;height:100vh;box-sizing:border-box}' +
|
|
836
900
|
'.bar{display:flex;gap:10px;align-items:center;padding:6px 10px;flex-wrap:wrap}' +
|
|
837
901
|
'select{background:#1b1b1f;color:#ddd;border:1px solid #333;border-radius:6px;padding:4px;max-width:340px}' +
|
|
838
|
-
'video{flex:1;width:100%;min-height:0;background:#000}
|
|
902
|
+
'video{flex:1;width:100%;min-height:0;background:#000}' +
|
|
903
|
+
'#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>' +
|
|
839
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>' +
|
|
840
|
-
'<video id=v controls autoplay
|
|
841
|
-
'<
|
|
842
|
-
'
|
|
843
|
-
'var MF=' + (webFriendly ? 1 : 0) + ';' +
|
|
844
|
-
'var
|
|
905
|
+
'<video id=v controls autoplay playsinline src="' + base + '/s/' + mV[1] + '/' + mV[2] + '"></video>' +
|
|
906
|
+
'<div id=ov>▶</div>' +
|
|
907
|
+
'<script src="https://cdn.jsdelivr.net/npm/hls.js@1.5.17/dist/hls.min.js"></script>' +
|
|
908
|
+
'<script>var B=' + JSON.stringify(base) + ',ID=' + mV[1] + ',I=' + mV[2] + ',MF=' + (webFriendly ? 1 : 0) + ';' +
|
|
909
|
+
'var v=document.getElementById("v"),aS=document.getElementById("a"),sS=document.getElementById("s"),n=document.getElementById("n"),ov=document.getElementById("ov");' +
|
|
910
|
+
'ov.onclick=function(){v.play().catch(function(){});ov.style.display="none"};' +
|
|
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
|
+
'var subTracks=function(){if(hls)return hls.subtitleTracks||[];return Array.prototype.slice.call(v.textTracks||[]).filter(function(t){return t.kind==="subtitles"||t.kind==="captions"})};' +
|
|
914
|
+
'var applySub=function(i){var T=subTracks();T.forEach(function(t,k){var en=(k===i);if(t.enabled!==undefined&&!!hls)t.enabled=en;else t.mode=en?"showing":"disabled"})};' +
|
|
915
|
+
'var fillSubs=function(){var T=subTracks();if(!T.length)return;sS.innerHTML="";var o0=document.createElement("option");o0.value="-1";o0.textContent="без субтитров";sS.appendChild(o0);' +
|
|
916
|
+
'T.forEach(function(t,i){var o=document.createElement("option");o.value=i;o.textContent="💬 "+(t.language&&t.language!=="und"?t.language:"субтитры "+(i+1))+(t.label?" — "+t.label:"")+(t.name?" — "+t.name:"");sS.appendChild(o)});' +
|
|
917
|
+
'sS.style.display="";sS.value="0";sS.onchange=function(){applySub(+sS.value)};applySub(0)};' +
|
|
918
|
+
'var play=function(u){if(hls){try{hls.destroy()}catch(e){}hls=null}' +
|
|
919
|
+
'if(NAT){v.src=u;v.play().catch(function(){ov.style.display="flex"});setTimeout(fillSubs,2000);setTimeout(fillSubs,5000)}' +
|
|
920
|
+
'else if(window.Hls&&Hls.isSupported()){hls=new Hls({startPosition:0,enableWorker:true,lowLatencyMode:false});hls.loadSource(u);hls.attachMedia(v);' +
|
|
921
|
+
'hls.on(Hls.Events.MANIFEST_PARSED,function(){v.play().catch(function(){ov.style.display="flex"});fillSubs()});' +
|
|
922
|
+
'hls.on(Hls.Events.ERROR,function(ev,d){if(d.fatal)n.textContent="⚠ HLS: "+d.type})}' +
|
|
923
|
+
'else{v.src=u;v.play().catch(function(){ov.style.display="flex"})}};' +
|
|
845
924
|
'var load=function(){fetch(B+"/api/tracks/"+ID+"/"+I).then(function(r){return r.json()}).then(function(d){' +
|
|
846
925
|
'if(d.retry&&tries++<15){n.textContent="⏳ готовлю файл к стриму…";setTimeout(load,4000);return;}' +
|
|
847
926
|
'if(d.error)n.textContent="⚠ "+d.error;' +
|
|
848
927
|
'var A=d.tracks||[],S=d.subs||[];' +
|
|
849
928
|
'if(!d.ffmpeg){return;}' +
|
|
850
|
-
'
|
|
929
|
+
'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});' +
|
|
851
930
|
'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)});' +
|
|
852
931
|
'if(A.length>1){aS.value=A[0].a;aS.style.display=""}' +
|
|
853
|
-
'
|
|
854
|
-
'
|
|
855
|
-
'
|
|
856
|
-
'
|
|
857
|
-
'v.addEventListener("playing",function(){n.textContent=""});v.addEventListener("volumechange",function(){if(!v.muted)n.title="звук включён"});' +
|
|
932
|
+
'var go=function(){var a=aS.style.display!==""?aS.value:0;play(B+"/hls/"+ID+"/"+I+"/master.m3u8?a="+a+(SI.length?"&si="+SI.join(","):""))};' +
|
|
933
|
+
'aS.onchange=go;go();' +
|
|
934
|
+
'}).catch(function(){if(tries++<15)setTimeout(load,4000)})};' +
|
|
935
|
+
'v.addEventListener("playing",function(){n.textContent="";ov.style.display="none"});' +
|
|
858
936
|
'setInterval(function(){fetch(B+"/api/status").then(function(r){return r.json()}).then(function(l){var t=l.find(function(x){return x.id===ID});if(t&&v.readyState<2)n.textContent="⏬ "+t.speed+" · пиры "+t.peers+(t.progress>0?" · "+t.progress+"%":"")}).catch(function(){})},2000);' +
|
|
859
|
-
'
|
|
937
|
+
'load();</script>');
|
|
860
938
|
return;
|
|
861
939
|
}
|
|
862
940
|
res.statusCode = 404; res.end('not found');
|
|
@@ -904,6 +982,8 @@ async function startWeb(opts, srcArg) {
|
|
|
904
982
|
if (cleaning) return; cleaning = true;
|
|
905
983
|
st.vlc.forEach(v => { try { v.kill(); } catch {} });
|
|
906
984
|
try { server.close(); } catch {}
|
|
985
|
+
hlsProcs.forEach(h => { try { h.proc.kill('SIGKILL'); } catch {} });
|
|
986
|
+
try { fs.rmSync(path.join(os.tmpdir(), 'wtui-hls-' + process.pid), { recursive: true, force: true }); } catch {}
|
|
907
987
|
try { await new Promise(r => client.destroy(r)); } catch {}
|
|
908
988
|
if (!opts.keepCache) await cleanCache(cacheDir);
|
|
909
989
|
process.exit(code);
|