torrent-online 1.6.3 → 1.6.5
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 +88 -66
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "torrent-online",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.5",
|
|
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,53 +840,75 @@ 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 mF = /^\/fmp4\/(\d+)\/(\d+)\/([\w.-]+)$/.exec(p);
|
|
844
|
+
if (mF) {
|
|
845
|
+
const e = st.torrents.get(Number(mF[1]));
|
|
846
|
+
const f = e?.t.files[Number(mF[2])];
|
|
847
847
|
if (!f) { res.statusCode = 404; res.end('not found'); return; }
|
|
848
|
-
ensureSel(e, Number(
|
|
848
|
+
ensureSel(e, Number(mF[2]));
|
|
849
849
|
const ffmpeg = detectFFBin('ffmpeg');
|
|
850
850
|
if (!ffmpeg) { res.statusCode = 500; res.end('ffmpeg не найден'); return; }
|
|
851
|
-
const name =
|
|
851
|
+
const name = mF[3];
|
|
852
852
|
if (name.includes('..')) { res.statusCode = 400; res.end('bad'); return; }
|
|
853
853
|
const a = u.searchParams.get('a') || '0';
|
|
854
|
-
const
|
|
855
|
-
const
|
|
854
|
+
const s1 = u.searchParams.get('si');
|
|
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((Number(u.searchParams.get('t')) || 0) / 5) * 5);
|
|
857
|
+
const key = mF[1] + '-' + mF[2] + '-' + a + '-' + (si >= 0 ? 's' + si : 'n') + '-' + t;
|
|
856
858
|
const dir = path.join(HLS_ROOT, key);
|
|
857
859
|
const fp = path.join(dir, name);
|
|
858
860
|
const hit0 = hlsProcs.get(key); if (hit0) hit0.touched = Date.now();
|
|
859
|
-
|
|
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
|
-
}
|
|
861
|
+
const hit1 = hlsProcs.get(key + '-vtt'); if (hit1) hit1.touched = Date.now();
|
|
866
862
|
if (!hlsProcs.has(key)) {
|
|
867
863
|
await ensureDir(dir);
|
|
868
|
-
for (const [k, h] of hlsProcs) if (k.startsWith(
|
|
869
|
-
const url = 'http://127.0.0.1:' + st.port + base + '/s/' +
|
|
870
|
-
const args = ['-hide_banner', '-loglevel', 'error', '-rw_timeout', '30000000', '-reconnect', '1', '-reconnect_streamed', '1', '-reconnect_delay_max', '5'
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
const vsm = ['v:0', 'a:0']; for (let i = 0; i < si.length; i++) vsm.push('s:' + i);
|
|
874
|
-
const segTpl = si.length ? 'seg-%s-%d.ts' : 'seg-%d.ts';
|
|
875
|
-
const plTpl = si.length ? 'pl-%s.m3u8' : 'pl.m3u8';
|
|
876
|
-
args.push('-f', 'hls', '-hls_time', '4', '-hls_list_size', '0', '-hls_playlist_type', 'event', '-hls_flags', 'independent_segments', '-hls_segment_type', 'mpegts', '-master_pl_name', 'master.m3u8', '-var_stream_map', vsm.join(','), '-hls_segment_filename', dir + '/' + segTpl, dir + '/' + plTpl);
|
|
864
|
+
for (const [k, h] of hlsProcs) if (k.startsWith(mF[1] + '-' + mF[2] + '-')) { try { h.proc.kill('SIGKILL'); } catch {} hlsProcs.delete(k); }
|
|
865
|
+
const url = 'http://127.0.0.1:' + st.port + base + '/s/' + mF[1] + '/' + mF[2];
|
|
866
|
+
const args = ['-hide_banner', '-loglevel', 'error', '-rw_timeout', '30000000', '-reconnect', '1', '-reconnect_streamed', '1', '-reconnect_delay_max', '5'];
|
|
867
|
+
if (t > 0) args.push('-ss', String(t));
|
|
868
|
+
args.push('-i', url, '-map', '0:v:0', '-map', '0:a:' + a, '-c:v', 'copy', '-c:a', 'aac', '-b:a', '192k', '-max_muxing_queue_size', '9999', '-f', 'mp4', '-movflags', 'frag_keyframe+empty_moov+default_base_moof', dir + '/file.mp4');
|
|
877
869
|
const proc = spawn(ffmpeg, args, { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
878
870
|
let ffErr = 0;
|
|
879
|
-
proc.stderr.on('data', d => { if (ffErr < 3) { console.error('
|
|
871
|
+
proc.stderr.on('data', d => { if (ffErr < 3) { console.error('fmp4:', String(d).trim().slice(0, 200)); ffErr++; } });
|
|
880
872
|
proc.on('exit', () => { const cur = hlsProcs.get(key); if (cur && cur.proc === proc) hlsProcs.delete(key); });
|
|
881
873
|
hlsProcs.set(key, { proc, dir, touched: Date.now() });
|
|
874
|
+
if (si >= 0) {
|
|
875
|
+
const subArgs = ['-hide_banner', '-loglevel', 'error', '-rw_timeout', '30000000', '-reconnect', '1', '-reconnect_streamed', '1', '-reconnect_delay_max', '5'];
|
|
876
|
+
if (t > 0) subArgs.push('-ss', String(t));
|
|
877
|
+
subArgs.push('-i', url, '-map', '0:s:' + si, '-c:s', 'webvtt', '-f', 'webvtt', dir + '/sub.vtt');
|
|
878
|
+
const sp = spawn(ffmpeg, subArgs, { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
879
|
+
let sf = 0;
|
|
880
|
+
sp.stderr.on('data', d => { if (sf < 2) { console.error('vtt:', String(d).trim().slice(0, 150)); sf++; } });
|
|
881
|
+
sp.on('exit', () => { const cur = hlsProcs.get(key + '-vtt'); if (cur && cur.proc === sp) hlsProcs.delete(key + '-vtt'); });
|
|
882
|
+
hlsProcs.set(key + '-vtt', { proc: sp, dir, touched: Date.now() });
|
|
883
|
+
}
|
|
882
884
|
}
|
|
883
|
-
|
|
885
|
+
if (name === 'sub.vtt') {
|
|
884
886
|
let b = null;
|
|
885
887
|
try { b = fs.readFileSync(fp); } catch {}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
+
res.setHeader('Content-Type', 'text/vtt; charset=utf-8');
|
|
889
|
+
res.end(b || 'WEBVTT\n\n');
|
|
890
|
+
return;
|
|
888
891
|
}
|
|
889
|
-
|
|
892
|
+
let size = 0;
|
|
893
|
+
for (let i = 0; i < 60 && size === 0; i++) {
|
|
894
|
+
try { size = fs.statSync(fp).size; } catch {}
|
|
895
|
+
if (size === 0) await new Promise(r => setTimeout(r, 500));
|
|
896
|
+
}
|
|
897
|
+
if (size === 0) { res.statusCode = 504; res.end('ffmpeg timeout'); return; }
|
|
898
|
+
res.setHeader('Accept-Ranges', 'bytes');
|
|
899
|
+
const rm = /^bytes=(\d*)-(\d*)$/.exec((req.headers.range || '').trim());
|
|
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);
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
res.setHeader('Content-Type', 'video/mp4');
|
|
910
|
+
res.setHeader('Content-Length', size);
|
|
911
|
+
fs.createReadStream(fp).pipe(res);
|
|
890
912
|
return;
|
|
891
913
|
}
|
|
892
914
|
const mV = /^\/view\/(\d+)\/(\d+)$/.exec(p);
|
|
@@ -897,43 +919,43 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
|
|
|
897
919
|
ensureSel(e, Number(mV[2]));
|
|
898
920
|
const webFriendly = /\.(mp4|m4v|webm|m4a)$/i.test(f.name);
|
|
899
921
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
922
|
+
res.end(`<!doctype html><meta charset=utf-8><title>${esc(f.name)}</title>
|
|
923
|
+
<style>body{margin:0;background:#000;color:#ddd;font:14px system-ui;display:flex;flex-direction:column;height:100vh;box-sizing:border-box}
|
|
924
|
+
.bar{display:flex;gap:10px;align-items:center;padding:6px 10px;flex-wrap:wrap}
|
|
925
|
+
select{background:#1b1b1f;color:#ddd;border:1px solid #333;border-radius:6px;padding:4px;max-width:340px}
|
|
926
|
+
video{flex:1;width:100%;min-height:0;background:#000}
|
|
927
|
+
#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
|
+
<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 src="${base}/s/${mV[1]}/${mV[2]}"></video>
|
|
930
|
+
<div id=ov>▶</div>
|
|
931
|
+
<script>
|
|
932
|
+
var B=${JSON.stringify(base)},ID=${mV[1]},I=${mV[2]};
|
|
933
|
+
var v=document.getElementById("v"),aS=document.getElementById("a"),sS=document.getElementById("s"),n=document.getElementById("n"),ov=document.getElementById("ov");
|
|
934
|
+
ov.onclick=function(){v.play().catch(function(){});ov.style.display="none"};
|
|
935
|
+
var tries=0,hls=null,cues=[],subUrl="";
|
|
936
|
+
var cueDiv=document.createElement("div");cueDiv.style.cssText="position:fixed;left:5%;right:5%;bottom:8%;text-align:center;color:#fff;font-size:clamp(14px,2.5vw,28px);text-shadow:0 1px 4px #000;pointer-events:none;z-index:5";document.body.appendChild(cueDiv);
|
|
937
|
+
var parseVtt=function(txt){var NL=String.fromCharCode(10);var C=[];txt.split(NL+NL).forEach(function(b){var m=b.match(/([0-9]+):([0-9]+):([0-9]+).([0-9]+) --> ([0-9]+):([0-9]+):([0-9]+).([0-9]+)/);if(!m)return;var t=function(h,mi,s,ms){return h*3600+mi*60+s+parseFloat("0."+ms)};var x=b.split(NL).filter(function(l){return l&&l.indexOf("-->")<0&&l.indexOf("WEBVTT")<0&&!/^[a-zA-Z-]+:/.test(l)}).join("<br>");C.push([t(+m[1],+m[2],+m[3],m[4]),t(+m[5],+m[6],+m[7],m[8]),x])});cues=C};
|
|
938
|
+
var pollSub=function(){if(!subUrl)return;fetch(subUrl).then(function(r){return r.text()}).then(parseVtt).catch(function(){})};
|
|
939
|
+
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
|
+
setInterval(pollSub,20000);
|
|
941
|
+
var play=function(u){v.src=u;v.currentTime=0;v.play().catch(function(){ov.style.display="flex"})};
|
|
942
|
+
var load=function(){fetch(B+"/api/tracks/"+ID+"/"+I).then(function(r){return r.json()}).then(function(d){
|
|
943
|
+
if(d.retry&&tries++<15){n.textContent="⏳ готовлю файл к стриму…";setTimeout(load,4000);return;}
|
|
944
|
+
if(d.error)n.textContent="⚠ "+d.error;
|
|
945
|
+
var A=d.tracks||[],S=d.subs||[];
|
|
946
|
+
if(!d.ffmpeg){return;}
|
|
947
|
+
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});
|
|
948
|
+
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)});
|
|
949
|
+
if(A.length>1){aS.value=A[0].a;aS.style.display=""}
|
|
950
|
+
if(SI.length){var o0=document.createElement("option");o0.value="";o0.textContent="без субтитров";sS.appendChild(o0);
|
|
951
|
+
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/5)*5-5);cues=[];cueDiv.innerHTML="";subUrl=s!==""?B+"/fmp4/"+ID+"/"+I+"/sub.vtt?a="+a+"&si="+s+"&t="+t:"";if(subUrl)pollSub();play(B+"/fmp4/"+ID+"/"+I+"/file.mp4?a="+a+(s!==""?"&si="+s:"")+(t>0?"&t="+t:""))};
|
|
953
|
+
aS.onchange=go;sS.onchange=go;go();
|
|
954
|
+
}).catch(function(){if(tries++<15)setTimeout(load,4000)})};
|
|
955
|
+
v.addEventListener("playing",function(){n.textContent="";ov.style.display="none"});
|
|
956
|
+
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);
|
|
957
|
+
load();
|
|
958
|
+
</script>`);
|
|
937
959
|
return;
|
|
938
960
|
}
|
|
939
961
|
res.statusCode = 404; res.end('not found');
|