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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/wtui.js +33 -9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "torrent-online",
3
- "version": "1.5.2",
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:(t.files[0]?t.files[0].i:0);
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
- 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><div class=files>'+files+'</div><div class=acts><button data-act=vlc data-id='+t.id+'>▶ VLC</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>';
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
- t.files.forEach((f, i) => { if (VIDEO_EXT.has(path.extname(f.name).toLowerCase())) sel.add(i); });
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 — выход.');