torrent-online 1.6.4 → 1.6.6
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 +44 -38
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "torrent-online",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.6",
|
|
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,42 @@ 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
|
|
878
|
-
const
|
|
879
|
-
const
|
|
880
|
-
args
|
|
864
|
+
for (const junk of ['file.mp4', 'sub.vtt']) { try { fs.rmSync(path.join(dir, junk)); } catch {} }
|
|
865
|
+
for (const [k, h] of hlsProcs) if (k.startsWith(mF[1] + '-' + mF[2] + '-')) { try { h.proc.kill('SIGKILL'); } catch {} hlsProcs.delete(k); }
|
|
866
|
+
const url = 'http://127.0.0.1:' + st.port + base + '/s/' + mF[1] + '/' + mF[2];
|
|
867
|
+
const args = ['-y', '-hide_banner', '-loglevel', 'error', '-rw_timeout', '30000000', '-reconnect', '1', '-reconnect_streamed', '1', '-reconnect_delay_max', '5'];
|
|
868
|
+
if (t > 0) args.push('-ss', String(t));
|
|
869
|
+
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
870
|
const proc = spawn(ffmpeg, args, { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
882
871
|
let ffErr = 0;
|
|
883
|
-
proc.stderr.on('data', d => { if (ffErr < 3) { console.error('
|
|
872
|
+
proc.stderr.on('data', d => { if (ffErr < 3) { console.error('fmp4:', String(d).trim().slice(0, 200)); ffErr++; } });
|
|
884
873
|
proc.on('exit', () => { const cur = hlsProcs.get(key); if (cur && cur.proc === proc) hlsProcs.delete(key); });
|
|
885
874
|
hlsProcs.set(key, { proc, dir, touched: Date.now() });
|
|
886
875
|
if (si >= 0) {
|
|
887
|
-
const subArgs = ['-hide_banner', '-loglevel', 'error', '-rw_timeout', '30000000', '-reconnect', '1', '-reconnect_streamed', '1', '-reconnect_delay_max', '5'
|
|
876
|
+
const subArgs = ['-y', '-hide_banner', '-loglevel', 'error', '-rw_timeout', '30000000', '-reconnect', '1', '-reconnect_streamed', '1', '-reconnect_delay_max', '5'];
|
|
877
|
+
if (t > 0) subArgs.push('-ss', String(t));
|
|
878
|
+
subArgs.push('-i', url, '-map', '0:s:' + si, '-c:s', 'webvtt', '-f', 'webvtt', dir + '/sub.vtt');
|
|
888
879
|
const sp = spawn(ffmpeg, subArgs, { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
889
880
|
let sf = 0;
|
|
890
881
|
sp.stderr.on('data', d => { if (sf < 2) { console.error('vtt:', String(d).trim().slice(0, 150)); sf++; } });
|
|
@@ -892,13 +883,33 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
|
|
|
892
883
|
hlsProcs.set(key + '-vtt', { proc: sp, dir, touched: Date.now() });
|
|
893
884
|
}
|
|
894
885
|
}
|
|
895
|
-
|
|
886
|
+
if (name === 'sub.vtt') {
|
|
896
887
|
let b = null;
|
|
897
888
|
try { b = fs.readFileSync(fp); } catch {}
|
|
898
|
-
|
|
899
|
-
|
|
889
|
+
res.setHeader('Content-Type', 'text/vtt; charset=utf-8');
|
|
890
|
+
res.end(b || 'WEBVTT\n\n');
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
let size = 0;
|
|
894
|
+
for (let i = 0; i < 60 && size === 0; i++) {
|
|
895
|
+
try { size = fs.statSync(fp).size; } catch {}
|
|
896
|
+
if (size === 0) await new Promise(r => setTimeout(r, 500));
|
|
900
897
|
}
|
|
901
|
-
res.statusCode = 504; res.end('ffmpeg timeout');
|
|
898
|
+
if (size === 0) { res.statusCode = 504; res.end('ffmpeg timeout'); return; }
|
|
899
|
+
res.setHeader('Accept-Ranges', 'bytes');
|
|
900
|
+
const rm = /^bytes=(\d*)-(\d*)$/.exec((req.headers.range || '').trim());
|
|
901
|
+
if (rm) {
|
|
902
|
+
let startB, endB;
|
|
903
|
+
if (rm[1] === '') { const n = Number(rm[2]); startB = Math.max(0, size - n); endB = size - 1; }
|
|
904
|
+
else { startB = Number(rm[1]); endB = rm[2] === '' ? size - 1 : Math.min(Number(rm[2]), size - 1); }
|
|
905
|
+
if (startB >= size || startB > endB) { res.writeHead(416, { 'Content-Range': 'bytes */' + size }); res.end(); return; }
|
|
906
|
+
res.writeHead(206, { 'Content-Range': 'bytes ' + startB + '-' + endB + '/' + size, 'Content-Length': endB - startB + 1, 'Content-Type': 'video/mp4' });
|
|
907
|
+
fs.createReadStream(fp, { start: startB, end: endB }).pipe(res);
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
res.setHeader('Content-Type', 'video/mp4');
|
|
911
|
+
res.setHeader('Content-Length', size);
|
|
912
|
+
fs.createReadStream(fp).pipe(res);
|
|
902
913
|
return;
|
|
903
914
|
}
|
|
904
915
|
const mV = /^\/view\/(\d+)\/(\d+)$/.exec(p);
|
|
@@ -918,7 +929,6 @@ video{flex:1;width:100%;min-height:0;background:#000}
|
|
|
918
929
|
<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
930
|
<video id=v controls autoplay playsinline src="${base}/s/${mV[1]}/${mV[2]}"></video>
|
|
920
931
|
<div id=ov>▶</div>
|
|
921
|
-
<script src="https://cdn.jsdelivr.net/npm/hls.js@1.5.17/dist/hls.min.js"></script>
|
|
922
932
|
<script>
|
|
923
933
|
var B=${JSON.stringify(base)},ID=${mV[1]},I=${mV[2]};
|
|
924
934
|
var v=document.getElementById("v"),aS=document.getElementById("a"),sS=document.getElementById("s"),n=document.getElementById("n"),ov=document.getElementById("ov");
|
|
@@ -929,11 +939,7 @@ var parseVtt=function(txt){var NL=String.fromCharCode(10);var C=[];txt.split(NL+
|
|
|
929
939
|
var pollSub=function(){if(!subUrl)return;fetch(subUrl).then(function(r){return r.text()}).then(parseVtt).catch(function(){})};
|
|
930
940
|
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
941
|
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"})}};
|
|
942
|
+
var play=function(u){v.src=u;v.currentTime=0;v.play().catch(function(){ov.style.display="flex"})};
|
|
937
943
|
var load=function(){fetch(B+"/api/tracks/"+ID+"/"+I).then(function(r){return r.json()}).then(function(d){
|
|
938
944
|
if(d.retry&&tries++<15){n.textContent="⏳ готовлю файл к стриму…";setTimeout(load,4000);return;}
|
|
939
945
|
if(d.error)n.textContent="⚠ "+d.error;
|
|
@@ -944,7 +950,7 @@ A.forEach(function(t){var o=document.createElement("option");o.value=t.a;o.textC
|
|
|
944
950
|
if(A.length>1){aS.value=A[0].a;aS.style.display=""}
|
|
945
951
|
if(SI.length){var o0=document.createElement("option");o0.value="";o0.textContent="без субтитров";sS.appendChild(o0);
|
|
946
952
|
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=
|
|
953
|
+
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
954
|
aS.onchange=go;sS.onchange=go;go();
|
|
949
955
|
}).catch(function(){if(tries++<15)setTimeout(load,4000)})};
|
|
950
956
|
v.addEventListener("playing",function(){n.textContent="";ov.style.display="none"});
|