torrent-online 1.6.4 → 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 +43 -38
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,51 +840,41 @@ 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
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
|
|
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;
|
|
857
858
|
const dir = path.join(HLS_ROOT, key);
|
|
858
859
|
const fp = path.join(dir, name);
|
|
859
860
|
const hit0 = hlsProcs.get(key); if (hit0) hit0.touched = Date.now();
|
|
860
861
|
const hit1 = hlsProcs.get(key + '-vtt'); if (hit1) hit1.touched = Date.now();
|
|
861
|
-
if (name === 'sub.vtt') {
|
|
862
|
-
let b = null;
|
|
863
|
-
try { b = fs.readFileSync(fp); } catch {}
|
|
864
|
-
res.setHeader('Content-Type', 'text/vtt; charset=utf-8');
|
|
865
|
-
res.end(b || 'WEBVTT\n\n');
|
|
866
|
-
return;
|
|
867
|
-
}
|
|
868
|
-
if (name !== 'master.m3u8') {
|
|
869
|
-
let b = null;
|
|
870
|
-
try { b = fs.readFileSync(fp); } catch {}
|
|
871
|
-
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); }
|
|
872
|
-
else { res.statusCode = 404; res.end('no segment'); }
|
|
873
|
-
return;
|
|
874
|
-
}
|
|
875
862
|
if (!hlsProcs.has(key)) {
|
|
876
863
|
await ensureDir(dir);
|
|
877
|
-
for (const [k, h] of hlsProcs) if (k.startsWith(
|
|
878
|
-
const url = 'http://127.0.0.1:' + st.port + base + '/s/' +
|
|
879
|
-
const args = ['-hide_banner', '-loglevel', 'error', '-rw_timeout', '30000000', '-reconnect', '1', '-reconnect_streamed', '1', '-reconnect_delay_max', '5'
|
|
880
|
-
|
|
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');
|
|
881
869
|
const proc = spawn(ffmpeg, args, { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
882
870
|
let ffErr = 0;
|
|
883
|
-
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++; } });
|
|
884
872
|
proc.on('exit', () => { const cur = hlsProcs.get(key); if (cur && cur.proc === proc) hlsProcs.delete(key); });
|
|
885
873
|
hlsProcs.set(key, { proc, dir, touched: Date.now() });
|
|
886
874
|
if (si >= 0) {
|
|
887
|
-
const subArgs = ['-hide_banner', '-loglevel', 'error', '-rw_timeout', '30000000', '-reconnect', '1', '-reconnect_streamed', '1', '-reconnect_delay_max', '5'
|
|
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');
|
|
888
878
|
const sp = spawn(ffmpeg, subArgs, { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
889
879
|
let sf = 0;
|
|
890
880
|
sp.stderr.on('data', d => { if (sf < 2) { console.error('vtt:', String(d).trim().slice(0, 150)); sf++; } });
|
|
@@ -892,13 +882,33 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
|
|
|
892
882
|
hlsProcs.set(key + '-vtt', { proc: sp, dir, touched: Date.now() });
|
|
893
883
|
}
|
|
894
884
|
}
|
|
895
|
-
|
|
885
|
+
if (name === 'sub.vtt') {
|
|
896
886
|
let b = null;
|
|
897
887
|
try { b = fs.readFileSync(fp); } catch {}
|
|
898
|
-
|
|
899
|
-
|
|
888
|
+
res.setHeader('Content-Type', 'text/vtt; charset=utf-8');
|
|
889
|
+
res.end(b || 'WEBVTT\n\n');
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
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));
|
|
900
896
|
}
|
|
901
|
-
res.statusCode = 504; res.end('ffmpeg timeout');
|
|
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);
|
|
902
912
|
return;
|
|
903
913
|
}
|
|
904
914
|
const mV = /^\/view\/(\d+)\/(\d+)$/.exec(p);
|
|
@@ -918,7 +928,6 @@ video{flex:1;width:100%;min-height:0;background:#000}
|
|
|
918
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>
|
|
919
929
|
<video id=v controls autoplay playsinline src="${base}/s/${mV[1]}/${mV[2]}"></video>
|
|
920
930
|
<div id=ov>▶</div>
|
|
921
|
-
<script src="https://cdn.jsdelivr.net/npm/hls.js@1.5.17/dist/hls.min.js"></script>
|
|
922
931
|
<script>
|
|
923
932
|
var B=${JSON.stringify(base)},ID=${mV[1]},I=${mV[2]};
|
|
924
933
|
var v=document.getElementById("v"),aS=document.getElementById("a"),sS=document.getElementById("s"),n=document.getElementById("n"),ov=document.getElementById("ov");
|
|
@@ -929,11 +938,7 @@ var parseVtt=function(txt){var NL=String.fromCharCode(10);var C=[];txt.split(NL+
|
|
|
929
938
|
var pollSub=function(){if(!subUrl)return;fetch(subUrl).then(function(r){return r.text()}).then(parseVtt).catch(function(){})};
|
|
930
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});
|
|
931
940
|
setInterval(pollSub,20000);
|
|
932
|
-
var play=function(u
|
|
933
|
-
if(window.Hls&&Hls.isSupported()){hls=new Hls({startPosition:0,enableWorker:true,lowLatencyMode:false});hls.loadSource(u);hls.attachMedia(v);
|
|
934
|
-
hls.on(Hls.Events.MANIFEST_PARSED,function(){v.play().catch(function(){ov.style.display="flex"});if(seek>1){var iv=setInterval(function(){if(v.duration>seek+3){v.currentTime=seek;clearInterval(iv)}},1000);setTimeout(function(){clearInterval(iv)},120000)}});
|
|
935
|
-
hls.on(Hls.Events.ERROR,function(ev,d){if(d.fatal)n.textContent="⚠ HLS: "+d.type})}
|
|
936
|
-
else{v.src=u;v.play().catch(function(){ov.style.display="flex"})}};
|
|
941
|
+
var play=function(u){v.src=u;v.currentTime=0;v.play().catch(function(){ov.style.display="flex"})};
|
|
937
942
|
var load=function(){fetch(B+"/api/tracks/"+ID+"/"+I).then(function(r){return r.json()}).then(function(d){
|
|
938
943
|
if(d.retry&&tries++<15){n.textContent="⏳ готовлю файл к стриму…";setTimeout(load,4000);return;}
|
|
939
944
|
if(d.error)n.textContent="⚠ "+d.error;
|
|
@@ -944,7 +949,7 @@ A.forEach(function(t){var o=document.createElement("option");o.value=t.a;o.textC
|
|
|
944
949
|
if(A.length>1){aS.value=A[0].a;aS.style.display=""}
|
|
945
950
|
if(SI.length){var o0=document.createElement("option");o0.value="";o0.textContent="без субтитров";sS.appendChild(o0);
|
|
946
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=""}
|
|
947
|
-
var go=function(){var a=aS.style.display!==""?aS.value:0;var s=sS.style.display!==""?sS.value:"";var t=
|
|
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:""))};
|
|
948
953
|
aS.onchange=go;sS.onchange=go;go();
|
|
949
954
|
}).catch(function(){if(tries++<15)setTimeout(load,4000)})};
|
|
950
955
|
v.addEventListener("playing",function(){n.textContent="";ov.style.display="none"});
|