torrent-online 1.5.1 → 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 +47 -35
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "torrent-online",
3
- "version": "1.5.1",
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
@@ -275,7 +275,7 @@ function serveFile(req, res, file) {
275
275
  const TRANSLIT = { а:'a',б:'b',в:'v',г:'g',д:'d',е:'e',ё:'e',ж:'zh',з:'z',и:'i',й:'y',к:'k',л:'l',м:'m',н:'n',о:'o',п:'p',р:'r',с:'s',т:'t',у:'u',ф:'f',х:'kh',ц:'ts',ч:'ch',ш:'sh',щ:'shch',ъ:'',ы:'y',ь:'',э:'e',ю:'yu',я:'ya' };
276
276
  function translit(s) { return s.toLowerCase().split('').map(c => TRANSLIT[c] ?? c).join(''); }
277
277
 
278
- // RU->EN словарь популярных названий для TPB/1337x
278
+ // RU->EN словарь популярных названий для TPB
279
279
  const RU_EN = {
280
280
  'пацаны': 'the boys', 'во все тяжкие': 'breaking bad', 'лучше звонка сола': 'better call saul',
281
281
  'игра престолов': 'game of thrones', 'странные дела': 'stranger things', 'ведьмак': 'the witcher',
@@ -309,31 +309,21 @@ function ruToEn(q) {
309
309
  }
310
310
  function btihKey(mag) { const m = /btih:([0-9a-fA-F]{40}|[0-9a-fA-F]{32})/.exec(mag); return m ? m[1].toLowerCase() : mag.slice(0, 90); }
311
311
 
312
- async function search1337x(q) {
313
- const r = await fetch(`https://1337x.st/api/v1/search/${encodeURIComponent(q)}/1/1/1/0/`, {
314
- headers: { 'x-api-key': 'sk1337x73871873371873', 'user-agent': UA, accept: 'application/json' },
315
- signal: AbortSignal.timeout(9000),
316
- });
317
- const text = await r.text();
318
- if (!r.ok) throw new Error(`1337x: HTTP ${r.status}${/Just a moment/.test(text) ? ' (Cloudflare)' : ''}`);
319
- let j;
320
- try { j = JSON.parse(text); } catch { throw new Error('1337x: не JSON'); }
321
- const out = [];
322
- for (const it of j.data || []) {
323
- const mag = it.magnetLink || it.magnet_link || it.torrent_magnet;
324
- if (mag && mag.startsWith('magnet:'))
325
- out.push({ name: String(it.name || ''), mag, size: String(it.size || ''), seeds: Number(it.seeders) || 0, src: '1337x' });
326
- }
327
- return out;
328
- }
329
312
 
330
- async function searchTPB(q, host = 'https://tpb.party') {
331
- const r = await fetch(`${host}/search/${encodeURIComponent(q)}/0/99/0/`, {
332
- headers: { 'user-agent': UA },
333
- signal: AbortSignal.timeout(9000),
334
- });
335
- if (!r.ok) throw new Error(`TPB: HTTP ${r.status}`);
336
- const html = await r.text();
313
+ async function searchTPB(q, host = '') {
314
+ const hosts = ['https://tpb.party', 'https://piratebay.live'];
315
+ let html = '', lastErr = new Error('TPB: нет ответа');
316
+ for (const h of (host ? [host] : hosts)) {
317
+ try {
318
+ const r = await fetch(`${h}/search/${encodeURIComponent(q)}/0/99/0/`, {
319
+ headers: { 'user-agent': UA },
320
+ signal: AbortSignal.timeout(9000),
321
+ });
322
+ if (r.ok) { html = await r.text(); lastErr = null; break; }
323
+ lastErr = new Error(`TPB: HTTP ${r.status}`);
324
+ } catch (e) { lastErr = e; }
325
+ }
326
+ if (lastErr) throw lastErr;
337
327
  const out = [];
338
328
  for (const row of html.split('<tr>').slice(1)) {
339
329
  const magRaw = /href="(magnet:[^"]+)"/.exec(row)?.[1];
@@ -376,7 +366,6 @@ async function searchIndexers(q) {
376
366
  jobs.push(searchRutor(queries[0]).then(rs => results.push(...rs), e => notes.push(e.message)));
377
367
  for (const qq of queries) {
378
368
  jobs.push(searchTPB(qq).then(rs => results.push(...rs), e => notes.push(e.message)));
379
- jobs.push(search1337x(qq).then(rs => results.push(...rs), e => notes.push(e.message)));
380
369
  }
381
370
  await Promise.allSettled(jobs);
382
371
  const seen = new Set();
@@ -562,6 +551,12 @@ button:hover{background:#2a3c58}
562
551
  .bar div{height:100%;background:linear-gradient(90deg,#3b82f6,#7fd4ff);border-radius:3px}
563
552
  .fin{display:block;padding:4px 2px;cursor:pointer;color:var(--mut);font-size:14px}
564
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}
565
560
  .acts{margin-top:10px;display:flex;gap:8px;flex-wrap:wrap}
566
561
  .acts .del{margin-left:auto;color:#ff8a8a;border-color:#4a2a33;background:#2a1c22}
567
562
  h3{color:var(--mut);font-size:14px;margin:18px 0 4px;font-weight:500}
@@ -569,7 +564,7 @@ video{width:100vw;height:100vh;background:#000;display:block;margin:0}
569
564
  </style></head><body>
570
565
  <header>🦀 <b>TorrentOnline</b></header>
571
566
  <div class=wrap>
572
- <div class=search><input id=q placeholder="Поиск: пацаны 4 сезон · breaking bad · magnet:…" autocomplete=off><button id=go>Искать</button><button id=mag>+ magnet</button></div>
567
+ <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>
573
568
  <div id=notes></div>
574
569
  <div id=results></div>
575
570
  <h3>Торренты</h3>
@@ -585,28 +580,33 @@ var h='';(d.notes||[]).forEach(function(n){h+='<div class=note>⚠ '+esc(n)+'</d
585
580
  if(!d.items.length){box.innerHTML=h+'<div class=note>Ничего не найдено</div>';return;}
586
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('');
587
582
  box.innerHTML=h;
588
- [].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();});};});
589
584
  }).catch(function(){box.innerHTML='<div class=note>Ошибка поиска</div>';});}
590
585
  document.getElementById('go').onclick=doSearch;
591
586
  document.getElementById('q').addEventListener('keydown',function(e){if(e.key==='Enter')doSearch();});
592
- document.getElementById('mag').onclick=function(){var m=prompt('magnet:');if(m&&m.indexOf('magnet:')===0){api('/api/add',{source:m}).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();});}};
593
589
  function render(list){var box=document.getElementById('torrents');
594
590
  if(!list.length){box.innerHTML='<div class=note>Пусто. Найди торрент и кликни по строке.</div>';return;}
595
591
  box.innerHTML=list.map(function(t){
596
- 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('');
597
593
  var first=t.files.filter(function(f){return f.selected;})[0];
598
- 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;
599
596
  var bar=t.ready?t.progress:0;
600
- 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>';
601
599
  }).join('');
602
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});};});
603
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');
604
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);});}
605
603
  if(act==='web'){window.open(BASE+'/view/'+id+'/'+b.getAttribute('data-i'));}
606
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);}
607
606
  };});
608
607
  }
609
608
  function poll(){api('/api/status').then(render).catch(function(){});}
609
+ function toTorrents(){var el=document.getElementById('torrents');el.scrollIntoView({behavior:'smooth',block:'start'});}
610
610
  poll();setInterval(poll,1500);
611
611
  </script></body></html>`;
612
612
 
@@ -651,9 +651,15 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
651
651
  const b = await readBody(req);
652
652
  const source = String(b.source || '');
653
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
+ }
654
659
  const id = st.nextId++;
655
660
  st.pending.push(id);
656
- 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) }); }
657
663
  return json(200, { id });
658
664
  }
659
665
  if (p === '/api/status') {
@@ -729,12 +735,18 @@ async function startWeb(opts, srcArg) {
729
735
  const base = token ? '/t/' + token : '';
730
736
  const st = { torrents: new Map(), nextId: 1, vlc: [], port: 0, pending: [], autoSelect: !!srcArg };
731
737
  client.on('torrent', t => {
738
+ t.on('error', () => {});
732
739
  const id = st.pending.shift();
733
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; }
734
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()));
735
745
  if (st.autoSelect) {
736
746
  st.autoSelect = false;
737
- 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); // фильм один — авто-старт
738
750
  }
739
751
  sel.forEach(i => t.files[i]?.select());
740
752
  st.torrents.set(id, { id, t, sel });
@@ -761,7 +773,7 @@ async function startWeb(opts, srcArg) {
761
773
  }
762
774
  process.on('SIGINT', () => shutdown(0));
763
775
  process.on('SIGTERM', () => shutdown(0));
764
- 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;
765
777
  process.on('unhandledRejection', e => { const m = (e && (e.message || e)) + ''; if (!NET_NOISE.test(m)) console.error('unhandledRejection:', m); });
766
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')); });
767
779
  console.log('Ctrl+C — выход.');