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.
- package/package.json +1 -1
- package/wtui.js +47 -35
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
|
@@ -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
|
|
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 = '
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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 — выход.');
|