torrent-online 1.5.4 → 1.5.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 +74 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "torrent-online",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.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
|
@@ -5,7 +5,9 @@ import { promises as fsp } from 'fs';
|
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import os from 'os';
|
|
7
7
|
import crypto from 'crypto';
|
|
8
|
-
import { spawn, spawnSync } from 'child_process';
|
|
8
|
+
import { spawn, spawnSync, execFile } from 'child_process';
|
|
9
|
+
import { promisify } from 'util';
|
|
10
|
+
const execFileP = promisify(execFile);
|
|
9
11
|
import { fileURLToPath } from 'url';
|
|
10
12
|
import { select, checkbox, input, confirm, Separator } from '@inquirer/prompts';
|
|
11
13
|
import WebTorrent from 'webtorrent';
|
|
@@ -159,6 +161,14 @@ async function listenOnFreePort(server, host, fixedPort) {
|
|
|
159
161
|
}
|
|
160
162
|
|
|
161
163
|
// ---- VLC ----
|
|
164
|
+
function detectFFBin(name) {
|
|
165
|
+
const cands = ['/opt/homebrew/bin/' + name, '/usr/local/bin/' + name, '/usr/bin/' + name];
|
|
166
|
+
for (const c of cands) if (fs.existsSync(c)) return c;
|
|
167
|
+
const r = spawnSync('which', [name], { encoding: 'utf8', timeout: 3000 });
|
|
168
|
+
const p = (r.stdout || '').trim();
|
|
169
|
+
return p && fs.existsSync(p) ? p : null;
|
|
170
|
+
}
|
|
171
|
+
|
|
162
172
|
function detectVLCPath() {
|
|
163
173
|
const macVLC = '/Applications/VLC.app/Contents/MacOS/VLC';
|
|
164
174
|
if (fs.existsSync(macVLC)) return macVLC;
|
|
@@ -716,13 +726,75 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
|
|
|
716
726
|
if (!f) { res.statusCode = 404; res.end('not found'); return; }
|
|
717
727
|
return serveFile(req, res, f);
|
|
718
728
|
}
|
|
729
|
+
const mT = /^\/api\/tracks\/(\d+)\/(\d+)$/.exec(p);
|
|
730
|
+
if (mT) {
|
|
731
|
+
const e = st.torrents.get(Number(mT[1]));
|
|
732
|
+
const f = e?.t.files[Number(mT[2])];
|
|
733
|
+
if (!f) return json(404, { error: 'нет файла' });
|
|
734
|
+
const ffprobe = detectFFBin('ffprobe');
|
|
735
|
+
if (!ffprobe) return json(200, { ffmpeg: false, tracks: [] });
|
|
736
|
+
const url = 'http://127.0.0.1:' + st.port + base + '/s/' + mT[1] + '/' + mT[2];
|
|
737
|
+
try {
|
|
738
|
+
const { stdout } = await execFileP(ffprobe, ['-hide_banner', '-loglevel', 'error', '-print_format', 'json', '-show_streams', url], { maxBuffer: 8 * 1048576, timeout: 25000 });
|
|
739
|
+
const j = JSON.parse(stdout);
|
|
740
|
+
let ai = 0, si = 0;
|
|
741
|
+
const tracks = [], subs = [];
|
|
742
|
+
for (const s of (j.streams || [])) {
|
|
743
|
+
const lang = (s.tags && s.tags.language) || '';
|
|
744
|
+
const title = (s.tags && s.tags.title) || '';
|
|
745
|
+
if (s.codec_type === 'audio') tracks.push({ a: ai++, lang, title, codec: s.codec_name });
|
|
746
|
+
else if (s.codec_type === 'subtitle') subs.push({ s: si++, lang, title, codec: s.codec_name });
|
|
747
|
+
}
|
|
748
|
+
return json(200, { ffmpeg: true, tracks, subs });
|
|
749
|
+
} catch (e2) { return json(200, { ffmpeg: true, tracks: [], subs: [], error: String((e2 && e2.message) || e2).slice(0, 160) }); }
|
|
750
|
+
}
|
|
751
|
+
const mR = /^\/remux\/(\d+)\/(\d+)$/.exec(p);
|
|
752
|
+
if (mR) {
|
|
753
|
+
const e = st.torrents.get(Number(mR[1]));
|
|
754
|
+
const f = e?.t.files[Number(mR[2])];
|
|
755
|
+
if (!f) { res.statusCode = 404; res.end('not found'); return; }
|
|
756
|
+
const ffmpeg = detectFFBin('ffmpeg');
|
|
757
|
+
if (!ffmpeg) { res.statusCode = 500; res.end('ffmpeg не найден'); return; }
|
|
758
|
+
const a = u.searchParams.get('a') || '0';
|
|
759
|
+
const sb = u.searchParams.get('s');
|
|
760
|
+
const url = 'http://127.0.0.1:' + st.port + base + '/s/' + mR[1] + '/' + mR[2];
|
|
761
|
+
const args = ['-hide_banner', '-loglevel', 'error', '-i', url, '-map', '0:v:0', '-map', '0:a:' + a, '-c:v', 'copy', '-c:a', 'aac', '-b:a', '192k', '-max_muxing_queue_size', '9999'];
|
|
762
|
+
if (sb !== null && sb !== '') args.push('-map', '0:s:' + sb, '-c:s', 'mov_text');
|
|
763
|
+
args.push('-f', 'mp4', '-movflags', 'frag_keyframe+empty_moov+default_base_moof', '-');
|
|
764
|
+
res.setHeader('Content-Type', 'video/mp4');
|
|
765
|
+
const proc = spawn(ffmpeg, args, { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
766
|
+
proc.stdout.pipe(res);
|
|
767
|
+
proc.on('error', () => { try { res.end(); } catch {} });
|
|
768
|
+
req.on('close', () => { try { proc.kill('SIGKILL'); } catch {} });
|
|
769
|
+
res.on('close', () => { try { proc.kill('SIGKILL'); } catch {} });
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
719
772
|
const mV = /^\/view\/(\d+)\/(\d+)$/.exec(p);
|
|
720
773
|
if (mV) {
|
|
721
774
|
const e = st.torrents.get(Number(mV[1]));
|
|
722
775
|
const f = e?.t.files[Number(mV[2])];
|
|
723
776
|
if (!f) { res.statusCode = 404; res.end('not found'); return; }
|
|
724
777
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
725
|
-
res.end('<!doctype html><meta charset=utf-8><title>' + esc(f.name) + '</title
|
|
778
|
+
res.end('<!doctype html><meta charset=utf-8><title>' + esc(f.name) + '</title>' +
|
|
779
|
+
'<style>body{margin:0;background:#000;color:#ddd;font:14px system-ui;display:flex;flex-direction:column;height:100vh;box-sizing:border-box}' +
|
|
780
|
+
'.bar{display:flex;gap:10px;align-items:center;padding:6px 10px;flex-wrap:wrap}' +
|
|
781
|
+
'select{background:#1b1b1f;color:#ddd;border:1px solid #333;border-radius:6px;padding:4px;max-width:340px}' +
|
|
782
|
+
'video{flex:1;width:100%;min-height:0;background:#000}</style>' +
|
|
783
|
+
'<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>' +
|
|
784
|
+
'<video id=v controls autoplay src="' + base + '/s/' + mV[1] + '/' + mV[2] + '"></video>' +
|
|
785
|
+
'<script>var B=' + JSON.stringify(base) + ',ID=' + mV[1] + ',I=' + mV[2] + ';' +
|
|
786
|
+
'var v=document.getElementById("v"),aS=document.getElementById("a"),sS=document.getElementById("s"),n=document.getElementById("n");' +
|
|
787
|
+
'fetch(B+"/api/tracks/"+ID+"/"+I).then(function(r){return r.json()}).then(function(d){' +
|
|
788
|
+
'if(d.error)n.textContent="⚠ "+d.error;' +
|
|
789
|
+
'var A=d.tracks||[],S=d.subs||[];' +
|
|
790
|
+
'if(!d.ffmpeg||A.length<2){return;}' +
|
|
791
|
+
'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)});' +
|
|
792
|
+
'aS.value=A[0].a;aS.style.display="";' +
|
|
793
|
+
'if(S.length){var o0=document.createElement("option");o0.value="";o0.textContent="без субтитров";sS.appendChild(o0);' +
|
|
794
|
+
'S.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.style.display=""}' +
|
|
795
|
+
'var go=function(){var t=v.currentTime;v.src=B+"/remux/"+ID+"/"+I+"?a="+aS.value+(sS.value!==""?"&s="+sS.value:"");v.load();v.addEventListener("loadedmetadata",function once(){v.currentTime=t;v.removeEventListener("loadedmetadata",once)});v.play()};' +
|
|
796
|
+
'aS.onchange=go;sS.onchange=go;go();' +
|
|
797
|
+
'}).catch(function(){})</script>');
|
|
726
798
|
return;
|
|
727
799
|
}
|
|
728
800
|
res.statusCode = 404; res.end('not found');
|