torrent-online 1.5.5 → 1.5.7
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 +41 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "torrent-online",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.7",
|
|
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
|
@@ -565,6 +565,7 @@ button:hover{background:#2a3c58}
|
|
|
565
565
|
.fin .play{margin-left:6px;padding:0 8px;font-size:12px}
|
|
566
566
|
.picks{margin:6px 0 2px}
|
|
567
567
|
.picks button{font-size:12px;padding:3px 10px}
|
|
568
|
+
body.drop{outline:3px dashed #3b82f6;outline-offset:-10px;background:rgba(59,130,246,.06)}
|
|
568
569
|
.fin .play{margin-left:6px;padding:0 8px;font-size:12px}
|
|
569
570
|
.picks{margin:6px 0 2px}
|
|
570
571
|
.picks button{font-size:12px;padding:3px 10px}
|
|
@@ -575,7 +576,7 @@ video{width:100vw;height:100vh;background:#000;display:block;margin:0}
|
|
|
575
576
|
</style></head><body>
|
|
576
577
|
<header>🦀 <b>TorrentOnline</b></header>
|
|
577
578
|
<div class=wrap>
|
|
578
|
-
<div class=search><input id=q placeholder="Поиск: пацаны 4 сезон · breaking bad · magnet:…" autocomplete=off><button id=go>Искать</button><button id=mag>+ magnet</button><button id=tpath>+ .torrent файл</button></div>
|
|
579
|
+
<div class=search><input id=q placeholder="Поиск: пацаны 4 сезон · breaking bad · magnet:…" autocomplete=off><button id=go>Искать</button><button id=mag>+ magnet</button><button id=tpath>+ .torrent файл</button><input type=file id=tt accept=".torrent,application/x-bittorrent" style=display:none></div>
|
|
579
580
|
<div id=notes></div>
|
|
580
581
|
<div id=results></div>
|
|
581
582
|
<h3>Торренты</h3>
|
|
@@ -596,7 +597,18 @@ box.innerHTML=h;
|
|
|
596
597
|
document.getElementById('go').onclick=doSearch;
|
|
597
598
|
document.getElementById('q').addEventListener('keydown',function(e){if(e.key==='Enter')doSearch();});
|
|
598
599
|
document.getElementById('mag').onclick=function(){var m=prompt('magnet:');if(m&&m.indexOf('magnet:')===0){api('/api/add',{source:m}).then(function(){poll();toTorrents();});}};
|
|
599
|
-
document.getElementById('tpath').onclick=function(){
|
|
600
|
+
document.getElementById('tpath').onclick=function(){document.getElementById('tt').click();};
|
|
601
|
+
document.getElementById('tt').onchange=function(){if(this.files&&this.files[0])uploadFile(this.files[0]);this.value='';};
|
|
602
|
+
function uploadFile(f){var nb=document.getElementById('notes');nb.innerHTML='<div class=note>⏳ Загружаю '+esc(f.name)+'…</div>';
|
|
603
|
+
fetch(BASE+'/api/add-file',{method:'POST',headers:{'content-type':'application/octet-stream'},body:f}).then(function(r){return r.json()}).then(function(d){nb.innerHTML=d.error?'<div class=note>⚠ '+esc(d.error)+'</div>':'';poll();toTorrents();}).catch(function(){nb.innerHTML='<div class=note>⚠ Ошибка загрузки</div>';});}
|
|
604
|
+
document.addEventListener('dragover',function(e){e.preventDefault();document.body.classList.add('drop');});
|
|
605
|
+
document.addEventListener('dragleave',function(e){if(e.target===document.body||e.relatedTarget===null)document.body.classList.remove('drop');});
|
|
606
|
+
document.addEventListener('drop',function(e){e.preventDefault();document.body.classList.remove('drop');
|
|
607
|
+
var fs=e.dataTransfer.files||[];
|
|
608
|
+
for(var i=0;i<fs.length;i++){if(/\.torrent$/i.test(fs[i].name)){uploadFile(fs[i]);return;}}
|
|
609
|
+
var t=(e.dataTransfer.getData('text/uri-list')||'')+(e.dataTransfer.getData('text/plain')||'');
|
|
610
|
+
var mm=t.match(/magnet:\?[^\s,]+/);if(mm){api('/api/add',{source:'magnet:'+mm[0]}).then(function(){poll();toTorrents();});}
|
|
611
|
+
});
|
|
600
612
|
function render(list){var box=document.getElementById('torrents');
|
|
601
613
|
if(!list.length){box.innerHTML='<div class=note>Пусто. Найди торрент и кликни по строке.</div>';return;}
|
|
602
614
|
box.innerHTML=list.map(function(t){
|
|
@@ -641,6 +653,7 @@ function makeWebServer(client, st, opts, cacheDir, base) {
|
|
|
641
653
|
|
|
642
654
|
async function webReq(req, res, client, st, opts, cacheDir, base) {
|
|
643
655
|
const u = new URL(req.url, 'http://127.0.0.1');
|
|
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 {} } };
|
|
644
657
|
let p = u.pathname;
|
|
645
658
|
if (base) {
|
|
646
659
|
if (p !== base && !p.startsWith(base + '/')) { res.statusCode = 404; res.end('not found'); return; }
|
|
@@ -658,6 +671,22 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
|
|
|
658
671
|
const { items, notes } = await searchIndexers(q);
|
|
659
672
|
return json(200, { items, notes });
|
|
660
673
|
}
|
|
674
|
+
if (p === '/api/add-file' && req.method === 'POST') {
|
|
675
|
+
const chunks = [];
|
|
676
|
+
let size = 0;
|
|
677
|
+
await new Promise((resolve, reject) => {
|
|
678
|
+
req.on('data', c => { chunks.push(c); size += c.length; if (size > 5e6) { req.destroy(); reject(new Error('файл слишком большой')); } });
|
|
679
|
+
req.on('end', resolve);
|
|
680
|
+
req.on('error', reject);
|
|
681
|
+
});
|
|
682
|
+
const buf = Buffer.concat(chunks);
|
|
683
|
+
if (!buf.length) return json(400, { error: 'пустой файл' });
|
|
684
|
+
const id = st.nextId++;
|
|
685
|
+
st.pending.push(id);
|
|
686
|
+
try { client.add(buf, { path: cacheDir }); }
|
|
687
|
+
catch (e) { st.pending.pop(); return json(400, { error: String((e && e.message) || e) }); }
|
|
688
|
+
return json(200, { id });
|
|
689
|
+
}
|
|
661
690
|
if (p === '/api/add' && req.method === 'POST') {
|
|
662
691
|
const b = await readBody(req);
|
|
663
692
|
const source = String(b.source || '');
|
|
@@ -724,6 +753,7 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
|
|
|
724
753
|
const e = st.torrents.get(Number(mS[1]));
|
|
725
754
|
const f = e?.t.files[Number(mS[2])];
|
|
726
755
|
if (!f) { res.statusCode = 404; res.end('not found'); return; }
|
|
756
|
+
ensureSel(e, Number(mS[2]));
|
|
727
757
|
return serveFile(req, res, f);
|
|
728
758
|
}
|
|
729
759
|
const mT = /^\/api\/tracks\/(\d+)\/(\d+)$/.exec(p);
|
|
@@ -731,6 +761,7 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
|
|
|
731
761
|
const e = st.torrents.get(Number(mT[1]));
|
|
732
762
|
const f = e?.t.files[Number(mT[2])];
|
|
733
763
|
if (!f) return json(404, { error: 'нет файла' });
|
|
764
|
+
ensureSel(e, Number(mT[2]));
|
|
734
765
|
const ffprobe = detectFFBin('ffprobe');
|
|
735
766
|
if (!ffprobe) return json(200, { ffmpeg: false, tracks: [] });
|
|
736
767
|
const url = 'http://127.0.0.1:' + st.port + base + '/s/' + mT[1] + '/' + mT[2];
|
|
@@ -746,13 +777,14 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
|
|
|
746
777
|
else if (s.codec_type === 'subtitle') subs.push({ s: si++, lang, title, codec: s.codec_name });
|
|
747
778
|
}
|
|
748
779
|
return json(200, { ffmpeg: true, tracks, subs });
|
|
749
|
-
} catch (e2) { return json(200, { ffmpeg: true, tracks: [], subs: [],
|
|
780
|
+
} catch (e2) { return json(200, { ffmpeg: true, tracks: [], subs: [], retry: true }); }
|
|
750
781
|
}
|
|
751
782
|
const mR = /^\/remux\/(\d+)\/(\d+)$/.exec(p);
|
|
752
783
|
if (mR) {
|
|
753
784
|
const e = st.torrents.get(Number(mR[1]));
|
|
754
785
|
const f = e?.t.files[Number(mR[2])];
|
|
755
786
|
if (!f) { res.statusCode = 404; res.end('not found'); return; }
|
|
787
|
+
ensureSel(e, Number(mR[2]));
|
|
756
788
|
const ffmpeg = detectFFBin('ffmpeg');
|
|
757
789
|
if (!ffmpeg) { res.statusCode = 500; res.end('ffmpeg не найден'); return; }
|
|
758
790
|
const a = u.searchParams.get('a') || '0';
|
|
@@ -774,6 +806,7 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
|
|
|
774
806
|
const e = st.torrents.get(Number(mV[1]));
|
|
775
807
|
const f = e?.t.files[Number(mV[2])];
|
|
776
808
|
if (!f) { res.statusCode = 404; res.end('not found'); return; }
|
|
809
|
+
ensureSel(e, Number(mV[2]));
|
|
777
810
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
778
811
|
res.end('<!doctype html><meta charset=utf-8><title>' + esc(f.name) + '</title>' +
|
|
779
812
|
'<style>body{margin:0;background:#000;color:#ddd;font:14px system-ui;display:flex;flex-direction:column;height:100vh;box-sizing:border-box}' +
|
|
@@ -784,7 +817,9 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
|
|
|
784
817
|
'<video id=v controls autoplay src="' + base + '/s/' + mV[1] + '/' + mV[2] + '"></video>' +
|
|
785
818
|
'<script>var B=' + JSON.stringify(base) + ',ID=' + mV[1] + ',I=' + mV[2] + ';' +
|
|
786
819
|
'var v=document.getElementById("v"),aS=document.getElementById("a"),sS=document.getElementById("s"),n=document.getElementById("n");' +
|
|
787
|
-
'
|
|
820
|
+
'var tries=0;' +
|
|
821
|
+
'var load=function(){fetch(B+"/api/tracks/"+ID+"/"+I).then(function(r){return r.json()}).then(function(d){' +
|
|
822
|
+
'if(d.retry&&tries++<15){n.textContent="⏳ готовлю файл к стриму…";setTimeout(load,4000);return;}' +
|
|
788
823
|
'if(d.error)n.textContent="⚠ "+d.error;' +
|
|
789
824
|
'var A=d.tracks||[],S=d.subs||[];' +
|
|
790
825
|
'if(!d.ffmpeg||A.length<2){return;}' +
|
|
@@ -794,7 +829,7 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
|
|
|
794
829
|
'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
830
|
'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
831
|
'aS.onchange=go;sS.onchange=go;go();' +
|
|
797
|
-
'}).catch(function(){})
|
|
832
|
+
'}).catch(function(){if(tries++<15)setTimeout(load,4000)})};load();</script>');
|
|
798
833
|
return;
|
|
799
834
|
}
|
|
800
835
|
res.statusCode = 404; res.end('not found');
|
|
@@ -814,6 +849,7 @@ async function startWeb(opts, srcArg) {
|
|
|
814
849
|
const dup = [...st.torrents.values()].find(e => e.t.infoHash === t.infoHash);
|
|
815
850
|
if (dup) { try { t.destroy(); } catch {} return; }
|
|
816
851
|
t.files.forEach(f => f.deselect()); // webtorrent по умолчанию выбирает все файлы
|
|
852
|
+
try { t.fileSelector = (tt, index, file) => (file._selections && file._selections.length ? 2 : 0); } catch {}
|
|
817
853
|
const sel = new Set();
|
|
818
854
|
const vids = t.files.map((f, i) => ({ f, i })).filter(x => VIDEO_EXT.has(path.extname(x.f.name).toLowerCase()));
|
|
819
855
|
if (st.autoSelect) {
|