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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/wtui.js +31 -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.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:(t.files[0]?t.files[0].i:0);
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
- 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>';
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
- t.files.forEach((f, i) => { if (VIDEO_EXT.has(path.extname(f.name).toLowerCase())) sel.add(i); });
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 — выход.');