torrent-online 1.5.2 → 1.5.3
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 +31 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "torrent-online",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.3",
|
|
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
|
@@ -551,6 +551,12 @@ button:hover{background:#2a3c58}
|
|
|
551
551
|
.bar div{height:100%;background:linear-gradient(90deg,#3b82f6,#7fd4ff);border-radius:3px}
|
|
552
552
|
.fin{display:block;padding:4px 2px;cursor:pointer;color:var(--mut);font-size:14px}
|
|
553
553
|
.fin input{margin-right:8px;accent-color:#3b82f6}
|
|
554
|
+
.fin .play{margin-left:6px;padding:0 8px;font-size:12px}
|
|
555
|
+
.picks{margin:6px 0 2px}
|
|
556
|
+
.picks button{font-size:12px;padding:3px 10px}
|
|
557
|
+
.fin .play{margin-left:6px;padding:0 8px;font-size:12px}
|
|
558
|
+
.picks{margin:6px 0 2px}
|
|
559
|
+
.picks button{font-size:12px;padding:3px 10px}
|
|
554
560
|
.acts{margin-top:10px;display:flex;gap:8px;flex-wrap:wrap}
|
|
555
561
|
.acts .del{margin-left:auto;color:#ff8a8a;border-color:#4a2a33;background:#2a1c22}
|
|
556
562
|
h3{color:var(--mut);font-size:14px;margin:18px 0 4px;font-weight:500}
|
|
@@ -574,29 +580,33 @@ var h='';(d.notes||[]).forEach(function(n){h+='<div class=note>⚠ '+esc(n)+'</d
|
|
|
574
580
|
if(!d.items.length){box.innerHTML=h+'<div class=note>Ничего не найдено</div>';return;}
|
|
575
581
|
h+=d.items.map(function(it){return '<div class=row data-mag="'+esc(it.mag)+'"><span class=tag>'+esc(it.src)+'</span><span class=rname>'+esc(it.name)+'</span>'+(it.size?'<span class=meta>'+esc(it.size)+'</span>':'')+(it.seeds?'<span class=meta>🌱 '+it.seeds+'</span>':'')+'</div>';}).join('');
|
|
576
582
|
box.innerHTML=h;
|
|
577
|
-
[].forEach.call(box.querySelectorAll('.row'),function(r){r.onclick=function(){api('/api/add',{source:r.getAttribute('data-mag')}).then(poll);};});
|
|
583
|
+
[].forEach.call(box.querySelectorAll('.row'),function(r){r.onclick=function(){api('/api/add',{source:r.getAttribute('data-mag')}).then(function(){poll();toTorrents();});};});
|
|
578
584
|
}).catch(function(){box.innerHTML='<div class=note>Ошибка поиска</div>';});}
|
|
579
585
|
document.getElementById('go').onclick=doSearch;
|
|
580
586
|
document.getElementById('q').addEventListener('keydown',function(e){if(e.key==='Enter')doSearch();});
|
|
581
|
-
document.getElementById('mag').onclick=function(){var m=prompt('magnet:');if(m&&m.indexOf('magnet:')===0){api('/api/add',{source:m}).then(poll);}};
|
|
582
|
-
document.getElementById('tpath').onclick=function(){var p=prompt('Путь к .torrent, напр. /Users/you/Downloads/torrent.torrent');if(p&&p.trim()){api('/api/add',{source:p.trim()}).then(poll);}};
|
|
587
|
+
document.getElementById('mag').onclick=function(){var m=prompt('magnet:');if(m&&m.indexOf('magnet:')===0){api('/api/add',{source:m}).then(function(){poll();toTorrents();});}};
|
|
588
|
+
document.getElementById('tpath').onclick=function(){var p=prompt('Путь к .torrent, напр. /Users/you/Downloads/torrent.torrent');if(p&&p.trim()){api('/api/add',{source:p.trim()}).then(function(){poll();toTorrents();});}};
|
|
583
589
|
function render(list){var box=document.getElementById('torrents');
|
|
584
590
|
if(!list.length){box.innerHTML='<div class=note>Пусто. Найди торрент и кликни по строке.</div>';return;}
|
|
585
591
|
box.innerHTML=list.map(function(t){
|
|
586
|
-
var files=t.files.map(function(f){return '<label class=fin><input type=checkbox data-id='+t.id+' data-i='+f.i+(f.selected?' checked':'')+'> '+esc(f.name)+' <span class=meta>'+esc(f.size)+'</span></label>';}).join('');
|
|
592
|
+
var files=t.files.map(function(f){return '<label class=fin><input type=checkbox data-id='+t.id+' data-i='+f.i+(f.selected?' checked':'')+'> '+esc(f.name)+' <span class=meta>'+esc(f.size)+'</span> <button class=play data-act=web data-id='+t.id+' data-i='+f.i+'>▶</button></label>';}).join('');
|
|
587
593
|
var first=t.files.filter(function(f){return f.selected;})[0];
|
|
588
|
-
var wi=first?first.i:
|
|
594
|
+
var wi=first?first.i:0;
|
|
595
|
+
var nSel=t.files.filter(function(f){return f.selected;}).length;
|
|
589
596
|
var bar=t.ready?t.progress:0;
|
|
590
|
-
|
|
597
|
+
var hint=(t.ready&&nSel===0)?'<div class=note>Отметь серии галочками — ▶ VLC / ▶ Браузер запустят выбранное. Одна серия стартует автоматически.</div>':'';
|
|
598
|
+
return '<div class=card><div class=tname>'+esc(t.name)+'</div><div class=tmeta>'+(t.ready?'⏬ '+t.progress+'% · '+esc(t.speed)+' · пиры '+t.peers:'⏳ ждём метаданные…')+'</div><div class=bar><div style=width:'+bar+'%></div></div>'+(t.files.length>1?'<div class=picks><button data-act=all data-id='+t.id+'>выбрать все</button><button data-act=none data-id='+t.id+'>снять все</button></div>':'')+'<div class=files>'+files+'</div>'+hint+'<div class=acts><button data-act=vlc data-id='+t.id+'>▶ VLC'+(nSel?' ('+nSel+')':'')+'</button><button data-act=web data-id='+t.id+' data-i='+wi+'>▶ Браузер</button><button class=del data-act=del data-id='+t.id+'>✕ удалить</button></div></div>';
|
|
591
599
|
}).join('');
|
|
592
600
|
[].forEach.call(box.querySelectorAll('.fin input'),function(cb){cb.onchange=function(){var id=+cb.getAttribute('data-id');var idx=[].filter.call(box.querySelectorAll('.fin input[data-id="'+id+'"]'),function(x){return x.checked;}).map(function(x){return +x.getAttribute('data-i');});api('/api/select',{id:id,indices:idx});};});
|
|
593
601
|
[].forEach.call(box.querySelectorAll('button[data-act]'),function(b){b.onclick=function(){var id=+b.getAttribute('data-id');var act=b.getAttribute('data-act');
|
|
594
602
|
if(act==='vlc'){b.textContent='…';api('/api/vlc',{id:id}).then(function(d){b.textContent=d.error?('⚠ '+d.error):'▶ VLC';setTimeout(function(){b.textContent='▶ VLC';},2500);});}
|
|
595
603
|
if(act==='web'){window.open(BASE+'/view/'+id+'/'+b.getAttribute('data-i'));}
|
|
596
604
|
if(act==='del'){api('/api/remove',{id:id}).then(poll);}
|
|
605
|
+
if(act==='all'||act==='none'){var idx=act==='all'?[].map.call(box.querySelectorAll('.fin input[data-id="'+id+'"]'),function(x){return +x.getAttribute('data-i');}):[];api('/api/select',{id:id,indices:idx}).then(poll);}
|
|
597
606
|
};});
|
|
598
607
|
}
|
|
599
608
|
function poll(){api('/api/status').then(render).catch(function(){});}
|
|
609
|
+
function toTorrents(){var el=document.getElementById('torrents');el.scrollIntoView({behavior:'smooth',block:'start'});}
|
|
600
610
|
poll();setInterval(poll,1500);
|
|
601
611
|
</script></body></html>`;
|
|
602
612
|
|
|
@@ -641,9 +651,15 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
|
|
|
641
651
|
const b = await readBody(req);
|
|
642
652
|
const source = String(b.source || '');
|
|
643
653
|
if (!source) return json(400, { error: 'нужен source' });
|
|
654
|
+
const btih = (source.match(/btih:([0-9a-f]{40})/i) || [])[1];
|
|
655
|
+
if (btih) {
|
|
656
|
+
const ex = [...st.torrents.values()].find(e => e.t.infoHash === btih.toLowerCase());
|
|
657
|
+
if (ex) return json(200, { id: ex.id, dup: true });
|
|
658
|
+
}
|
|
644
659
|
const id = st.nextId++;
|
|
645
660
|
st.pending.push(id);
|
|
646
|
-
client.add(source, { path: cacheDir });
|
|
661
|
+
try { client.add(source, { path: cacheDir }); }
|
|
662
|
+
catch (e) { st.pending.pop(); return json(400, { error: String((e && e.message) || e) }); }
|
|
647
663
|
return json(200, { id });
|
|
648
664
|
}
|
|
649
665
|
if (p === '/api/status') {
|
|
@@ -719,12 +735,18 @@ async function startWeb(opts, srcArg) {
|
|
|
719
735
|
const base = token ? '/t/' + token : '';
|
|
720
736
|
const st = { torrents: new Map(), nextId: 1, vlc: [], port: 0, pending: [], autoSelect: !!srcArg };
|
|
721
737
|
client.on('torrent', t => {
|
|
738
|
+
t.on('error', () => {});
|
|
722
739
|
const id = st.pending.shift();
|
|
723
740
|
if (id == null) return;
|
|
741
|
+
const dup = [...st.torrents.values()].find(e => e.t.infoHash === t.infoHash);
|
|
742
|
+
if (dup) { try { t.destroy(); } catch {} return; }
|
|
724
743
|
const sel = new Set();
|
|
744
|
+
const vids = t.files.map((f, i) => ({ f, i })).filter(x => VIDEO_EXT.has(path.extname(x.f.name).toLowerCase()));
|
|
725
745
|
if (st.autoSelect) {
|
|
726
746
|
st.autoSelect = false;
|
|
727
|
-
|
|
747
|
+
vids.forEach(x => sel.add(x.i));
|
|
748
|
+
} else if (vids.length === 1) {
|
|
749
|
+
sel.add(vids[0].i); // фильм один — авто-старт
|
|
728
750
|
}
|
|
729
751
|
sel.forEach(i => t.files[i]?.select());
|
|
730
752
|
st.torrents.set(id, { id, t, sel });
|
|
@@ -751,7 +773,7 @@ async function startWeb(opts, srcArg) {
|
|
|
751
773
|
}
|
|
752
774
|
process.on('SIGINT', () => shutdown(0));
|
|
753
775
|
process.on('SIGTERM', () => shutdown(0));
|
|
754
|
-
const NET_NOISE = /premature|aborted|EPIPE|ECONNRESET|ETIMEDOUT|socket|stream/i;
|
|
776
|
+
const NET_NOISE = /premature|aborted|EPIPE|ECONNRESET|ETIMEDOUT|socket|stream|duplicate torrent/i;
|
|
755
777
|
process.on('unhandledRejection', e => { const m = (e && (e.message || e)) + ''; if (!NET_NOISE.test(m)) console.error('unhandledRejection:', m); });
|
|
756
778
|
process.on('uncaughtException', e => { const m = (e && (e.message || e)) + ''; if (NET_NOISE.test(m)) return; console.error('uncaughtException:', m); console.error((e && e.stack || '').split('\\n').slice(0, 8).join('\\n')); });
|
|
757
779
|
console.log('Ctrl+C — выход.');
|