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