torrent-online 1.7.12 → 1.7.13
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/CHANGELOG.md +4 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/wtui.js +43 -19
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
Формат — [Keep a Changelog](https://keepachangelog.com/ru/1.1.0/), версии — [Semantic Versioning](https://semver.org/lang/ru/).
|
|
4
4
|
|
|
5
|
+
## [1.7.13] — 2026-10-04
|
|
6
|
+
**RU:** Повторное открытие больше не гасит сервер. Старая вкладка «сервер остановлен» его не трогает, а сам сервер не умирает вместе с запускалкой.
|
|
7
|
+
**EN:** Opening the app again no longer stops the server. The old «server stopped» tab leaves it alone, and the server stays up after the launcher exits.
|
|
8
|
+
|
|
5
9
|
## [1.7.12] — 2026-10-04
|
|
6
10
|
**RU:** Если сервер только что поднялся, браузер открывает новую страницу, а не старую вкладку «сервер остановлен».
|
|
7
11
|
**EN:** When the server has just started, the browser opens a fresh page instead of the old «server stopped» tab.
|
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ node wtui.js "https://example.com/file.torrent"
|
|
|
63
63
|
```bash
|
|
64
64
|
node wtui.js --web
|
|
65
65
|
```
|
|
66
|
-
Тот же процесс, тот же порт. В браузере: поиск, поле magnet, кнопка файла `.torrent`, карточки с прогрессом, у каждого файла кнопки VLC и Браузер. VLC играет исходник. Браузер играет тот же поток со звуком: обычная шкала, перемотка и смена дорожки. Кнопки RU и EN меняют язык страницы и плеера. «Закрыть» останавливает сервер и не трогает кэш. Следующий двойной щелчок
|
|
66
|
+
Тот же процесс, тот же порт. В браузере: поиск, поле magnet, кнопка файла `.torrent`, карточки с прогрессом, у каждого файла кнопки VLC и Браузер. VLC играет исходник. Браузер играет тот же поток со звуком: обычная шкала, перемотка и смена дорожки. Кнопки RU и EN меняют язык страницы и плеера. «Закрыть» останавливает сервер и не трогает кэш. Следующий двойной щелчок открывает новую вкладку, старую с надписью «сервер остановлен» можно закрыть. С телефона в Wi-Fi — через `--lan` (токен в URL).
|
|
67
67
|
|
|
68
68
|
## Нативное приложение (Tauri, macOS)
|
|
69
69
|
Окошко с иконкой, без терминала: над тем же веб-GUI.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "torrent-online",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.13",
|
|
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
|
@@ -142,12 +142,25 @@ function pickRandomPort() {
|
|
|
142
142
|
|
|
143
143
|
async function listenOnFreePort(server, host, fixedPort) {
|
|
144
144
|
if (fixedPort) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
145
|
+
const deadline = Date.now() + 8000;
|
|
146
|
+
let lastErr;
|
|
147
|
+
while (Date.now() < deadline) {
|
|
148
|
+
try {
|
|
149
|
+
await new Promise((resolve, reject) => {
|
|
150
|
+
const onError = (err) => { server.off('listening', onListening); reject(err); };
|
|
151
|
+
const onListening = () => { server.off('error', onError); resolve(); };
|
|
152
|
+
server.once('error', onError);
|
|
153
|
+
server.once('listening', onListening);
|
|
154
|
+
server.listen(fixedPort, host);
|
|
155
|
+
});
|
|
156
|
+
return fixedPort;
|
|
157
|
+
} catch (err) {
|
|
158
|
+
lastErr = err;
|
|
159
|
+
if (err?.code !== 'EADDRINUSE') throw err;
|
|
160
|
+
await sleep(250);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
throw lastErr || new Error('Порт занят: ' + fixedPort);
|
|
151
164
|
}
|
|
152
165
|
let port = pickRandomPort();
|
|
153
166
|
for (let i = 0; i < 30; i++) {
|
|
@@ -608,8 +621,8 @@ video{width:100vw;height:100vh;background:#000;display:block;margin:0}
|
|
|
608
621
|
var BASE="__BASE__";
|
|
609
622
|
var BOOT="__BOOT__";
|
|
610
623
|
var DICT={
|
|
611
|
-
ru:{phSearch:'Поиск: пацаны 4 сезон · breaking bad',search:'Искать',phMag:'Вставь magnet:?xt=urn:btih:…',addMag:'Добавить magnet',fileBtn:'Файл .torrent',lead:'Поток идёт сразу, целиком ждать не нужно. У каждого файла кнопки VLC и Браузер. Кэш при выходе не удаляется — только кнопкой «Очистить кэш». «Закрыть» останавливает сервер и не трогает скачанное.',clear:'Очистить кэш',quit:'Закрыть',torrents:'Торренты',adding:'добавляю в клиент…',torrent:'торрент',searching:'Ищу…',emptyFind:'Ничего не найдено',searchErr:'Ошибка поиска',needMag:'Нужна ссылка, которая начинается с magnet:',clearAsk:'Удалить скачанные файлы из кэша? Список торрентов тоже очистится.',cleared:'Кэш очищен',clearFail:'Не удалось очистить кэш',uploading:'Загружаю',uploadErr:'Ошибка загрузки',empty:'Пусто. Найди торрент и кликни по строке.',browser:'Браузер',all:'выбрать все',none:'снять все',waitMeta:'ждём метаданные…',peers:'пиры',hint:'Отметь серии галочками — ▶ VLC / ▶ Браузер запустят выбранное. Одна серия стартует автоматически.',del:'✕ удалить',quitAsk:'Остановить сервер? Скачанные файлы останутся в кэше.',quitDone:'Сервер остановлен. Открой TorrentOnline ещё раз —
|
|
612
|
-
en:{phSearch:'Search: the boys season 4 · breaking bad',search:'Search',phMag:'Paste magnet:?xt=urn:btih:…',addMag:'Add magnet',fileBtn:'.torrent file',lead:'Playback starts right away. You do not wait for the full download. Each file has VLC and Browser buttons. Exit does not delete the cache. Only Clear cache does. Quit stops the server and leaves downloads on disk.',clear:'Clear cache',quit:'Quit',torrents:'Torrents',adding:'adding to the client…',torrent:'torrent',searching:'Searching…',emptyFind:'Nothing found',searchErr:'Search failed',needMag:'The link must start with magnet:',clearAsk:'Delete downloaded files from the cache? The torrent list will be cleared too.',cleared:'Cache cleared',clearFail:'Could not clear the cache',uploading:'Uploading',uploadErr:'Upload failed',empty:'Nothing here yet. Search for a torrent and click a row.',browser:'Browser',all:'select all',none:'select none',waitMeta:'waiting for metadata…',peers:'peers',hint:'Tick the episodes. VLC and Browser play the selection. A single episode starts on its own.',del:'✕ remove',quitAsk:'Stop the server? Downloaded files stay in the cache.',quitDone:'Server stopped. Open TorrentOnline again and
|
|
624
|
+
ru:{phSearch:'Поиск: пацаны 4 сезон · breaking bad',search:'Искать',phMag:'Вставь magnet:?xt=urn:btih:…',addMag:'Добавить magnet',fileBtn:'Файл .torrent',lead:'Поток идёт сразу, целиком ждать не нужно. У каждого файла кнопки VLC и Браузер. Кэш при выходе не удаляется — только кнопкой «Очистить кэш». «Закрыть» останавливает сервер и не трогает скачанное.',clear:'Очистить кэш',quit:'Закрыть',torrents:'Торренты',adding:'добавляю в клиент…',torrent:'торрент',searching:'Ищу…',emptyFind:'Ничего не найдено',searchErr:'Ошибка поиска',needMag:'Нужна ссылка, которая начинается с magnet:',clearAsk:'Удалить скачанные файлы из кэша? Список торрентов тоже очистится.',cleared:'Кэш очищен',clearFail:'Не удалось очистить кэш',uploading:'Загружаю',uploadErr:'Ошибка загрузки',empty:'Пусто. Найди торрент и кликни по строке.',browser:'Браузер',all:'выбрать все',none:'снять все',waitMeta:'ждём метаданные…',peers:'пиры',hint:'Отметь серии галочками — ▶ VLC / ▶ Браузер запустят выбранное. Одна серия стартует автоматически.',del:'✕ удалить',quitAsk:'Остановить сервер? Скачанные файлы останутся в кэше.',quitDone:'Сервер остановлен. Открой TorrentOnline ещё раз — откроется новая вкладка. Эту можно закрыть. Скачанное на месте.',quitFail:'Сервер всё ещё отвечает.',quitSoon:'Сервер только открылся. Нажми «Закрыть» ещё раз через пару секунд.',addErr:'Ошибка /api/add','empty-file':'пустой файл','need-source':'нужна ссылка или файл','no-torrent':'нет торрента','no-file':'нет файла','pick-files':'выбери файлы','no-vlc':'VLC не найден'},
|
|
625
|
+
en:{phSearch:'Search: the boys season 4 · breaking bad',search:'Search',phMag:'Paste magnet:?xt=urn:btih:…',addMag:'Add magnet',fileBtn:'.torrent file',lead:'Playback starts right away. You do not wait for the full download. Each file has VLC and Browser buttons. Exit does not delete the cache. Only Clear cache does. Quit stops the server and leaves downloads on disk.',clear:'Clear cache',quit:'Quit',torrents:'Torrents',adding:'adding to the client…',torrent:'torrent',searching:'Searching…',emptyFind:'Nothing found',searchErr:'Search failed',needMag:'The link must start with magnet:',clearAsk:'Delete downloaded files from the cache? The torrent list will be cleared too.',cleared:'Cache cleared',clearFail:'Could not clear the cache',uploading:'Uploading',uploadErr:'Upload failed',empty:'Nothing here yet. Search for a torrent and click a row.',browser:'Browser',all:'select all',none:'select none',waitMeta:'waiting for metadata…',peers:'peers',hint:'Tick the episodes. VLC and Browser play the selection. A single episode starts on its own.',del:'✕ remove',quitAsk:'Stop the server? Downloaded files stay in the cache.',quitDone:'Server stopped. Open TorrentOnline again and a new tab will open. You can close this one. Downloads stay on disk.',quitFail:'The server is still running.',quitSoon:'The server just opened. Press Quit again in a couple of seconds.',addErr:'/api/add error','empty-file':'empty file','need-source':'a link or a file is required','no-torrent':'no such torrent','no-file':'no such file','pick-files':'pick files first','no-vlc':'VLC was not found'}
|
|
613
626
|
};
|
|
614
627
|
function lang(){try{return localStorage.getItem('to-lang')==='en'?'en':'ru'}catch(e){return 'ru'}}
|
|
615
628
|
function tr(k){var d=DICT[lang()]||DICT.ru;if(d&&d[k]!=null)return d[k];return (DICT.ru&&DICT.ru[k]!=null)?DICT.ru[k]:k}
|
|
@@ -638,8 +651,8 @@ document.getElementById('q').addEventListener('keydown',function(e){if(e.key==='
|
|
|
638
651
|
document.getElementById('mag').onclick=function(){var m=document.getElementById('magin').value.trim();var nb=document.getElementById('notes');if(m.indexOf('magnet:')!==0){nb.innerHTML='<div class=note>'+esc(tr('needMag'))+'</div>';return;}nb.innerHTML='';addTorrent(m);document.getElementById('magin').value='';};
|
|
639
652
|
document.getElementById('magin').addEventListener('keydown',function(e){if(e.key==='Enter')document.getElementById('mag').click();});
|
|
640
653
|
document.getElementById('clr').onclick=function(){if(!confirm(tr('clearAsk')))return;api('/api/clear-cache',{ok:1}).then(function(){document.getElementById('notes').innerHTML='<div class=note>'+esc(tr('cleared'))+'</div>';poll();}).catch(function(){document.getElementById('notes').innerHTML='<div class=note>'+esc(tr('clearFail'))+'</div>';});};
|
|
641
|
-
function showStopped(){if(pollTimer){clearInterval(pollTimer);pollTimer=null;}document.body.innerHTML='<p style="margin:0;padding:28px;font:16px/1.45 -apple-system,sans-serif;color:#e8ecf1">'+esc(tr('quitDone'))+'</p>';document.body.style.background='#0f1115';
|
|
642
|
-
document.getElementById('quit').onclick=function(){if(!confirm(tr('quitAsk')))return;fetch(BASE+'/api/quit',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({boot:BOOT})}).then(function(r){if(r.status===409){document.getElementById('notes').innerHTML='<div class=note>'+esc(tr('
|
|
654
|
+
function showStopped(){if(pollTimer){clearInterval(pollTimer);pollTimer=null;}document.body.innerHTML='<p style="margin:0;padding:28px;font:16px/1.45 -apple-system,sans-serif;color:#e8ecf1">'+esc(tr('quitDone'))+'</p>';document.body.style.background='#0f1115';}
|
|
655
|
+
document.getElementById('quit').onclick=function(ev){if(ev&&ev.isTrusted===false)return;if(!confirm(tr('quitAsk')))return;var send=function(){fetch(BASE+'/api/quit',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({boot:BOOT})}).then(function(r){if(r.status===409){return r.json().then(function(d){if(d&&d.error==='starting'){if(!send.retried){send.retried=1;setTimeout(send,2000);return;}var n=document.getElementById('notes');if(n)n.innerHTML='<div class=note>'+esc(tr('quitSoon'))+'</div>';return;}var n2=document.getElementById('notes');if(n2)n2.innerHTML='<div class=note>'+esc(tr('quitFail'))+'</div>';});}showStopped();}).catch(function(){showStopped();});};send();};
|
|
643
656
|
function setLang(code){try{localStorage.setItem('to-lang',code)}catch(e){}applyLang();poll();}
|
|
644
657
|
document.getElementById('lang-ru').onclick=function(){setLang('ru');};
|
|
645
658
|
document.getElementById('lang-en').onclick=function(){setLang('en');};
|
|
@@ -951,8 +964,16 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
|
|
|
951
964
|
if (p === '/api/health') return json(200, { ok: true, version: PKG_VERSION });
|
|
952
965
|
if (p === '/api/quit' && req.method === 'POST') {
|
|
953
966
|
const b = await readBody(req).catch(() => ({}));
|
|
954
|
-
//
|
|
955
|
-
if (
|
|
967
|
+
// Сразу после старта старая вкладка ещё может прислать «Закрыть». Это не клик пользователя.
|
|
968
|
+
if (Date.now() - (st.startedAt || 0) < 8000) {
|
|
969
|
+
console.log('quit ignored: starting');
|
|
970
|
+
return json(409, { error: 'starting' });
|
|
971
|
+
}
|
|
972
|
+
if (!b || b.boot !== st.boot) {
|
|
973
|
+
console.log('quit ignored: stale');
|
|
974
|
+
return json(409, { error: 'stale' });
|
|
975
|
+
}
|
|
976
|
+
console.log('quit accepted');
|
|
956
977
|
res.statusCode = 200;
|
|
957
978
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
958
979
|
res.setHeader('Connection', 'close');
|
|
@@ -1370,6 +1391,7 @@ async function startWeb(opts, srcArg) {
|
|
|
1370
1391
|
const server = makeWebServer(client, st, opts, cacheDir, base);
|
|
1371
1392
|
const host = opts.lan ? '0.0.0.0' : '127.0.0.1';
|
|
1372
1393
|
st.port = await listenOnFreePort(server, host, opts.port);
|
|
1394
|
+
st.startedAt = Date.now();
|
|
1373
1395
|
if (process.env.WTUI_PIDFILE) {
|
|
1374
1396
|
try { fs.writeFileSync(process.env.WTUI_PIDFILE, String(process.pid)); } catch {}
|
|
1375
1397
|
}
|
|
@@ -1382,20 +1404,22 @@ async function startWeb(opts, srcArg) {
|
|
|
1382
1404
|
}
|
|
1383
1405
|
if (srcArg) { st.pending.push(st.nextId++); client.add(srcArg, { path: cacheDir }); }
|
|
1384
1406
|
let cleaning = false;
|
|
1385
|
-
async function shutdown(code = 0) {
|
|
1407
|
+
async function shutdown(reason, code = 0) {
|
|
1386
1408
|
if (cleaning) return; cleaning = true;
|
|
1409
|
+
console.log('выход: ' + reason);
|
|
1387
1410
|
st.vlc.forEach(v => { try { v.kill(); } catch {} });
|
|
1388
1411
|
try { server.close(); } catch {}
|
|
1412
|
+
try { server.closeAllConnections(); } catch {}
|
|
1389
1413
|
if (st.hls) for (const h of st.hls.procs.values()) { try { h.proc.kill('SIGKILL'); } catch {} }
|
|
1390
1414
|
try { fs.rmSync(path.join(os.tmpdir(), 'wtui-hls-' + process.pid), { recursive: true, force: true }); } catch {}
|
|
1391
|
-
// Порт
|
|
1392
|
-
|
|
1393
|
-
bye.unref();
|
|
1415
|
+
// Порт отдаём сразу. Кэш не стираем. destroy не ждём: на macOS он может повиснуть и держать процесс.
|
|
1416
|
+
setTimeout(() => process.exit(code), 100);
|
|
1394
1417
|
try { client.destroy(() => process.exit(code)); } catch { process.exit(code); }
|
|
1395
1418
|
}
|
|
1396
|
-
st.quit = () => { shutdown(0); };
|
|
1397
|
-
process.on('
|
|
1398
|
-
process.on('
|
|
1419
|
+
st.quit = () => { shutdown('quit', 0); };
|
|
1420
|
+
process.on('SIGHUP', () => { console.log('SIGHUP ignored'); });
|
|
1421
|
+
process.on('SIGINT', () => shutdown('SIGINT', 0));
|
|
1422
|
+
process.on('SIGTERM', () => shutdown('SIGTERM', 0));
|
|
1399
1423
|
const NET_NOISE = /premature|aborted|EPIPE|ECONNRESET|ETIMEDOUT|socket|stream|duplicate torrent/i;
|
|
1400
1424
|
process.on('unhandledRejection', e => { const m = (e && (e.message || e)) + ''; if (!NET_NOISE.test(m)) console.error('unhandledRejection:', m); });
|
|
1401
1425
|
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')); });
|