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 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 меняют язык страницы и плеера. «Закрыть» останавливает сервер и не трогает кэш. Следующий двойной щелчок по приложению поднимает страницу снова. С телефона в Wi-Fi — через `--lan` (токен в URL).
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.12",
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
- await new Promise((resolve, reject) => {
146
- server.once('error', reject);
147
- server.once('listening', () => { server.off('error', reject); resolve(); });
148
- server.listen(fixedPort, host);
149
- });
150
- return fixedPort;
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 ещё раз — эта вкладка обновится сама. Скачанное на месте.',quitFail:'Сервер всё ещё отвечает.',addErr:'Ошибка /api/add','empty-file':'пустой файл','need-source':'нужна ссылка или файл','no-torrent':'нет торрента','no-file':'нет файла','pick-files':'выбери файлы','no-vlc':'VLC не найден'},
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 this tab will refresh itself. Downloads stay on disk.',quitFail:'The server is still running.',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'}
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';var n=0;var watch=setInterval(function(){n++;if(n>150){clearInterval(watch);return;}fetch(BASE+'/api/health').then(function(r){if(!r.ok)return;clearInterval(watch);location.reload();}).catch(function(){});},800);}
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('quitFail'))+'</div>';return;}showStopped();}).catch(function(){showStopped();});};
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 (!b || b.boot !== st.boot) return json(409, { error: 'stale' });
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
- // Порт надо отдать сразу, иначе следующий запуск .app не встаёт. Кэш не стираем.
1392
- const bye = setTimeout(() => process.exit(code), 200);
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('SIGINT', () => shutdown(0));
1398
- process.on('SIGTERM', () => shutdown(0));
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')); });