torrent-online 1.7.9 → 1.7.11
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 +14 -0
- package/README.md +8 -8
- package/package.json +1 -1
- package/wtui.js +128 -49
package/CHANGELOG.md
CHANGED
|
@@ -2,10 +2,24 @@
|
|
|
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.11] — 2026-10-04
|
|
6
|
+
**RU:** После «Закрыть» повторный двойной щелчок снова открывает страницу. Старая вкладка больше не гасит новый сервер.
|
|
7
|
+
**EN:** After Quit, opening the app again brings the page back. The old tab no longer stops the new server.
|
|
8
|
+
- 🐛 Выход держал порт, и следующий запуск не вставал или попадал в уже умирающий процесс
|
|
9
|
+
- 🐛 Вкладка «сервер остановлен» при новом открытии оставалась на экране. Теперь она сама обновляется, когда сервер снова жив
|
|
10
|
+
- 🐛 Запрос «Закрыть» без метки этого запуска игнорируется
|
|
11
|
+
|
|
12
|
+
## [1.7.10] — 2026-10-04
|
|
13
|
+
**RU:** На странице есть кнопка «Закрыть»: она гасит фоновый сервер и не удаляет кэш. Язык переключается кнопками RU и EN.
|
|
14
|
+
**EN:** The page has a Quit button. It stops the background server and does not delete the cache. The language switches with the RU and EN buttons.
|
|
15
|
+
- ➕ `POST /api/quit` останавливает процесс. Скачанное в `~/Movies/WebTorrent` остаётся
|
|
16
|
+
- ➕ Переключатель языка в шапке и в плеере. Выбор запоминается в браузере. По умолчанию русский
|
|
17
|
+
|
|
5
18
|
## [1.7.9] — 2026-10-04
|
|
6
19
|
**RU:** В браузере звук больше не идёт рывками и не отстаёт от картинки.
|
|
7
20
|
**EN:** In the browser, audio no longer stutters or drifts off the picture.
|
|
8
21
|
- 🐛 Поток резался на MPEG-TS, и каждый кусок обнулял время. Браузер ставил звук не на ту секунду. Теперь фрагменты fMP4 с одной шкалой времени
|
|
22
|
+
- ➕ В релиз снова положен `TorrentOnline.app.zip`: двойной щелчок открывает страницу, окно Терминала не появляется
|
|
9
23
|
|
|
10
24
|
## [1.7.8] — 2026-10-04
|
|
11
25
|
**RU:** `npm i -g` снова доходит до конца. Зависимость `ip-set` больше не вызывает `npx only-allow pnpm` и не роняет установку.
|
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ node wtui.js "https://example.com/file.torrent"
|
|
|
47
47
|
| `--port=<N>` | фиксированный порт (иначе авто-поиск 8123..10122) |
|
|
48
48
|
| `--help` | справка |
|
|
49
49
|
|
|
50
|
-
Кэш по умолчанию: `~/Movies/WebTorrent`. В окне он остаётся после
|
|
50
|
+
Кэш по умолчанию: `~/Movies/WebTorrent`. В окне он остаётся после выхода и после кнопки «Закрыть». Стирает его только «Очистить кэш». Язык страницы — кнопки RU и EN (по умолчанию русский). В режиме `--cli` папка по-прежнему удаляется при выходе, если нет `--keep-cache`.
|
|
51
51
|
|
|
52
52
|
## Возможности (v1.4)
|
|
53
53
|
- 🔎 Поиск торрентов прямо в меню: **Rutor.info (по-русски)**, TPB.party (по-английски), опционально 1337x API
|
|
@@ -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 играет исходник. Браузер играет тот же поток со звуком: обычная шкала, перемотка и смена дорожки. С телефона в Wi-Fi — через `--lan` (токен в URL).
|
|
66
|
+
Тот же процесс, тот же порт. В браузере: поиск, поле magnet, кнопка файла `.torrent`, карточки с прогрессом, у каждого файла кнопки VLC и Браузер. VLC играет исходник. Браузер играет тот же поток со звуком: обычная шкала, перемотка и смена дорожки. Кнопки RU и EN меняют язык страницы и плеера. «Закрыть» останавливает сервер и не трогает кэш. Следующий двойной щелчок по приложению поднимает страницу снова. С телефона в Wi-Fi — через `--lan` (токен в URL).
|
|
67
67
|
|
|
68
68
|
## Нативное приложение (Tauri, macOS)
|
|
69
69
|
Окошко с иконкой, без терминала: над тем же веб-GUI.
|
|
@@ -80,7 +80,7 @@ cd desktop && ./build-mac.sh
|
|
|
80
80
|
xattr -dr com.apple.quarantine /Applications/TorrentOnline.app
|
|
81
81
|
open /Applications/TorrentOnline.app
|
|
82
82
|
```
|
|
83
|
-
|
|
83
|
+
Своя сборка (двойной щелчок открывает страницу в браузере, Terminal не нужен):
|
|
84
84
|
```bash
|
|
85
85
|
./build/app.sh
|
|
86
86
|
open dist/TorrentOnline.app
|
|
@@ -112,7 +112,7 @@ docker run -p 8123:8123 torrent-online node wtui.js "magnet:?xt=..." --lan --no-
|
|
|
112
112
|
- VLC (`/Applications/VLC.app` или в PATH) — не нужен с `--no-vlc`
|
|
113
113
|
|
|
114
114
|
## Частые вопросы
|
|
115
|
-
- **Node не виден из .app** — .app
|
|
115
|
+
- **Node не виден из .app** — .app ищет `node` в `/opt/homebrew/bin` и `/usr/local/bin`. Если нет — `brew install node`.
|
|
116
116
|
- **«Writable stream closed prematurely»** — VLC рвёт пробные коннекты; мы их игнорим.
|
|
117
117
|
- **Порт занят** — авто-поиск в диапазоне 8123..10122 или `--port=`.
|
|
118
118
|
- **Magnet без пиров** — приложение отвалится через 60 с с понятной ошибкой, не будет висеть вечно.
|
|
@@ -158,7 +158,7 @@ node wtui.js "https://example.com/file.torrent"
|
|
|
158
158
|
| `--port=<N>` | fixed port (otherwise auto-scan 8123..10122) |
|
|
159
159
|
| `--help` | help |
|
|
160
160
|
|
|
161
|
-
Default cache: `~/Movies/WebTorrent`. The window keeps it after exit
|
|
161
|
+
Default cache: `~/Movies/WebTorrent`. The window keeps it after exit and after Quit. Only Clear cache deletes it. The page language is the RU and EN buttons (Russian by default). In `--cli` mode the folder is still removed on exit unless `--keep-cache` is set.
|
|
162
162
|
|
|
163
163
|
## Features (v1.4)
|
|
164
164
|
- 🔎 Built-in torrent search: **Rutor.info (Russian)**, TPB.party (English), optional 1337x API
|
|
@@ -174,7 +174,7 @@ Default cache: `~/Movies/WebTorrent`. The window keeps it after exit; the «Оч
|
|
|
174
174
|
```bash
|
|
175
175
|
node wtui.js --web
|
|
176
176
|
```
|
|
177
|
-
Same process, same port. The browser has search, a magnet field, a `.torrent` file button, progress cards, and VLC / Browser buttons on each file. VLC plays the original. The browser plays one stream with sound: a normal timeline, seek, and an audio-track switch. From a phone on your Wi-Fi — use `--lan` (token in URL).
|
|
177
|
+
Same process, same port. The browser has search, a magnet field, a `.torrent` file button, progress cards, and VLC / Browser buttons on each file. VLC plays the original. The browser plays one stream with sound: a normal timeline, seek, and an audio-track switch. The RU and EN buttons switch the page and the player. Quit stops the server and leaves the cache. The next double-click of the app brings the page back. From a phone on your Wi-Fi — use `--lan` (token in URL).
|
|
178
178
|
|
|
179
179
|
## Native app (Tauri, macOS)
|
|
180
180
|
A window with an icon, no terminal — wraps the same web GUI.
|
|
@@ -191,7 +191,7 @@ The app is unsigned, so macOS adds a quarantine flag — clear it once:
|
|
|
191
191
|
xattr -dr com.apple.quarantine /Applications/TorrentOnline.app
|
|
192
192
|
open /Applications/TorrentOnline.app
|
|
193
193
|
```
|
|
194
|
-
Build it yourself (
|
|
194
|
+
Build it yourself (a double-click opens the page in the browser, no Terminal window):
|
|
195
195
|
```bash
|
|
196
196
|
./build/app.sh
|
|
197
197
|
open dist/TorrentOnline.app
|
|
@@ -223,7 +223,7 @@ docker run -p 8123:8123 torrent-online node wtui.js "magnet:?xt=..." --lan --no-
|
|
|
223
223
|
- VLC (`/Applications/VLC.app` or in PATH) — not needed with `--no-vlc`
|
|
224
224
|
|
|
225
225
|
## FAQ
|
|
226
|
-
- **Node not visible from the .app** — the .app
|
|
226
|
+
- **Node not visible from the .app** — the .app looks for `node` in `/opt/homebrew/bin` and `/usr/local/bin`. If it is missing, run `brew install node`.
|
|
227
227
|
- **"Writable stream closed prematurely"** — VLC drops probe connections; we ignore them.
|
|
228
228
|
- **Port busy** — auto-scan in 8123..10122 or use `--port=`.
|
|
229
229
|
- **Magnet with no peers** — the app fails after 60 s with a clear error instead of hanging forever.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "torrent-online",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.11",
|
|
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
|
@@ -14,6 +14,7 @@ import { select, checkbox, input, confirm, Separator } from '@inquirer/prompts';
|
|
|
14
14
|
import WebTorrent from 'webtorrent';
|
|
15
15
|
|
|
16
16
|
const require = createRequire(import.meta.url);
|
|
17
|
+
const PKG_VERSION = JSON.parse(fs.readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version;
|
|
17
18
|
|
|
18
19
|
/*
|
|
19
20
|
torrent-online — WebTorrent → VLC / браузер (ESM)
|
|
@@ -555,9 +556,13 @@ function watchFolder() {
|
|
|
555
556
|
const WEB_HTML = `<!doctype html><html><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>TorrentOnline</title><style>
|
|
556
557
|
:root{--bg:#0f1115;--card:#171a21;--line:#232833;--txt:#e8ecf1;--mut:#8a93a3;--acc:#7fd4ff}
|
|
557
558
|
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--txt);font:15px/1.45 -apple-system,'Segoe UI',Roboto,sans-serif}
|
|
558
|
-
header{display:flex;gap:10px;align-items:center;padding:14px 20px;border-bottom:1px solid var(--line);position:sticky;top:0;background:rgba(15,17,21,.92);backdrop-filter:blur(6px)}
|
|
559
|
+
header{display:flex;gap:10px;align-items:center;padding:14px 20px;border-bottom:1px solid var(--line);position:sticky;top:0;background:rgba(15,17,21,.92);backdrop-filter:blur(6px);flex-wrap:wrap}
|
|
559
560
|
header b{font-size:18px}
|
|
560
|
-
#
|
|
561
|
+
#langs{margin-left:auto;display:flex;gap:4px}
|
|
562
|
+
#langs button{min-width:44px;padding:6px 10px}
|
|
563
|
+
#langs button.on{background:#2a3c58;color:#fff;border-color:var(--acc)}
|
|
564
|
+
#clr{color:#ffd7a8;border-color:#5a4630;background:#2a241c}
|
|
565
|
+
#quit{color:#ffb4b4;border-color:#5a3038;background:#2a1c22}
|
|
561
566
|
.wrap{max-width:900px;margin:0 auto;padding:16px}
|
|
562
567
|
.search{display:flex;gap:8px}
|
|
563
568
|
input#q{flex:1;min-width:0;background:#1d222c;border:1px solid #2a3140;color:var(--txt);padding:10px 12px;border-radius:8px;font-size:15px}
|
|
@@ -589,45 +594,59 @@ body.drop{outline:3px dashed #3b82f6;outline-offset:-10px;background:rgba(59,130
|
|
|
589
594
|
h3{color:var(--mut);font-size:14px;margin:18px 0 4px;font-weight:500}
|
|
590
595
|
video{width:100vw;height:100vh;background:#000;display:block;margin:0}
|
|
591
596
|
</style></head><body>
|
|
592
|
-
<header>🦀 <b>TorrentOnline</b><button id=clr type=button>Очистить кэш</button></header>
|
|
597
|
+
<header>🦀 <b>TorrentOnline</b><span id=langs><button type=button id=lang-ru class=on>RU</button><button type=button id=lang-en>EN</button></span><button id=clr type=button>Очистить кэш</button><button id=quit type=button>Закрыть</button></header>
|
|
593
598
|
<div class=wrap>
|
|
594
599
|
<div class=search><input id=q placeholder="Поиск: пацаны 4 сезон · breaking bad" autocomplete=off><button id=go type=button>Искать</button></div>
|
|
595
600
|
<div class=search style="margin-top:8px"><input id=magin placeholder="Вставь magnet:?xt=urn:btih:…" autocomplete=off><button id=mag type=button>Добавить magnet</button><button id=tpath type=button>Файл .torrent</button><input type=file id=tt accept=".torrent,application/x-bittorrent" style=display:none></div>
|
|
596
|
-
<div class=note>Поток идёт сразу, целиком ждать не нужно. У каждого файла кнопки VLC и Браузер. Кэш при выходе не удаляется — только
|
|
601
|
+
<div class=note id=lead>Поток идёт сразу, целиком ждать не нужно. У каждого файла кнопки VLC и Браузер. Кэш при выходе не удаляется — только кнопкой «Очистить кэш». «Закрыть» останавливает сервер и не трогает скачанное.</div>
|
|
597
602
|
<div id=notes></div>
|
|
598
603
|
<div id=results></div>
|
|
599
|
-
<h3>Торренты</h3>
|
|
604
|
+
<h3 id=th>Торренты</h3>
|
|
600
605
|
<div id=torrents></div>
|
|
601
606
|
</div>
|
|
602
607
|
<script>
|
|
603
608
|
var BASE="__BASE__";
|
|
609
|
+
var BOOT="__BOOT__";
|
|
610
|
+
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'}
|
|
613
|
+
};
|
|
614
|
+
function lang(){try{return localStorage.getItem('to-lang')==='en'?'en':'ru'}catch(e){return 'ru'}}
|
|
615
|
+
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}
|
|
616
|
+
function errText(code){if(code==null||code==='')return '';var s=tr(String(code));return s===String(code)?String(code):s}
|
|
617
|
+
function applyLang(){var L=lang();document.documentElement.lang=L;var ruB=document.getElementById('lang-ru'),enB=document.getElementById('lang-en');if(ruB)ruB.className=L==='ru'?'on':'';if(enB)enB.className=L==='en'?'on':'';document.getElementById('clr').textContent=tr('clear');document.getElementById('quit').textContent=tr('quit');document.getElementById('go').textContent=tr('search');document.getElementById('mag').textContent=tr('addMag');document.getElementById('tpath').textContent=tr('fileBtn');document.getElementById('q').placeholder=tr('phSearch');document.getElementById('magin').placeholder=tr('phMag');document.getElementById('lead').textContent=tr('lead');document.getElementById('th').textContent=tr('torrents');}
|
|
604
618
|
function esc(s){return String(s).replace(/[&<>"']/g,function(c){return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c];});}
|
|
605
619
|
function api(p,body){var o=body?{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(body)}:undefined;return fetch(BASE+p,o).then(function(r){return r.json();});}
|
|
606
620
|
function addTorrent(mag){
|
|
607
621
|
var box=document.getElementById('torrents');
|
|
608
622
|
var dn=(/dn=([^&]*)/.exec(mag)||[,''])[1];try{dn=decodeURIComponent(dn.replace(/\\+/g,' '))}catch(e){}
|
|
609
623
|
var ph=document.createElement('div');ph.className='card';
|
|
610
|
-
ph.innerHTML='<div class=tname>⏳ '+esc(dn||'
|
|
624
|
+
ph.innerHTML='<div class=tname>⏳ '+esc(dn||tr('torrent'))+'</div><div class=tmeta>'+esc(tr('adding'))+'</div>';
|
|
611
625
|
box.prepend(ph);
|
|
612
|
-
api('/api/add',{source:mag}).then(function(d){if(d&&d.error){ph.innerHTML='<div class=tname>⚠ '+esc(d.error)+'</div>';}poll();toTorrents();}).catch(function(e){ph.innerHTML='<div class=tname>⚠
|
|
626
|
+
api('/api/add',{source:mag}).then(function(d){if(d&&d.error){ph.innerHTML='<div class=tname>⚠ '+esc(errText(d.error))+'</div>';}poll();toTorrents();}).catch(function(e){ph.innerHTML='<div class=tname>⚠ '+esc(tr('addErr'))+': '+esc(e&&e.message||e)+'</div>';});
|
|
613
627
|
}
|
|
614
|
-
function doSearch(){var q=document.getElementById('q').value.trim();if(!q)return;if(q.indexOf('magnet:')===0){addTorrent(q);document.getElementById('q').value='';return;}var box=document.getElementById('results');box.innerHTML='<div class=note
|
|
628
|
+
function doSearch(){var q=document.getElementById('q').value.trim();if(!q)return;if(q.indexOf('magnet:')===0){addTorrent(q);document.getElementById('q').value='';return;}var box=document.getElementById('results');box.innerHTML='<div class=note>'+esc(tr('searching'))+'</div>';
|
|
615
629
|
api('/api/search?q='+encodeURIComponent(q)).then(function(d){
|
|
616
630
|
var h='';(d.notes||[]).forEach(function(n){h+='<div class=note>⚠ '+esc(n)+'</div>';});
|
|
617
|
-
if(!d.items.length){box.innerHTML=h+'<div class=note
|
|
631
|
+
if(!d.items.length){box.innerHTML=h+'<div class=note>'+esc(tr('emptyFind'))+'</div>';return;}
|
|
618
632
|
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('');
|
|
619
633
|
box.innerHTML=h;
|
|
620
634
|
[].forEach.call(box.querySelectorAll('.row'),function(r){r.onclick=function(){addTorrent(r.getAttribute('data-mag'));};});
|
|
621
|
-
}).catch(function(){box.innerHTML='<div class=note
|
|
635
|
+
}).catch(function(){box.innerHTML='<div class=note>'+esc(tr('searchErr'))+'</div>';});}
|
|
622
636
|
document.getElementById('go').onclick=doSearch;
|
|
623
637
|
document.getElementById('q').addEventListener('keydown',function(e){if(e.key==='Enter')doSearch();});
|
|
624
|
-
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
|
|
638
|
+
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='';};
|
|
625
639
|
document.getElementById('magin').addEventListener('keydown',function(e){if(e.key==='Enter')document.getElementById('mag').click();});
|
|
626
|
-
document.getElementById('clr').onclick=function(){if(!confirm('
|
|
640
|
+
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();});};
|
|
643
|
+
function setLang(code){try{localStorage.setItem('to-lang',code)}catch(e){}applyLang();poll();}
|
|
644
|
+
document.getElementById('lang-ru').onclick=function(){setLang('ru');};
|
|
645
|
+
document.getElementById('lang-en').onclick=function(){setLang('en');};
|
|
627
646
|
document.getElementById('tpath').onclick=function(){document.getElementById('tt').click();};
|
|
628
647
|
document.getElementById('tt').onchange=function(){if(this.files&&this.files[0])uploadFile(this.files[0]);this.value='';};
|
|
629
|
-
function uploadFile(f){var nb=document.getElementById('notes');nb.innerHTML='<div class=note>⏳
|
|
630
|
-
fetch(BASE+'/api/add-file',{method:'POST',headers:{'content-type':'application/octet-stream'},body:f}).then(function(r){return r.json()}).then(function(d){nb.innerHTML=d.error?'<div class=note>⚠ '+esc(d.error)+'</div>':'';poll();toTorrents();}).catch(function(){nb.innerHTML='<div class=note>⚠
|
|
648
|
+
function uploadFile(f){var nb=document.getElementById('notes');nb.innerHTML='<div class=note>⏳ '+esc(tr('uploading'))+' '+esc(f.name)+'…</div>';
|
|
649
|
+
fetch(BASE+'/api/add-file',{method:'POST',headers:{'content-type':'application/octet-stream'},body:f}).then(function(r){return r.json()}).then(function(d){nb.innerHTML=d.error?'<div class=note>⚠ '+esc(errText(d.error))+'</div>':'';poll();toTorrents();}).catch(function(){nb.innerHTML='<div class=note>⚠ '+esc(tr('uploadErr'))+'</div>';});}
|
|
631
650
|
document.addEventListener('dragover',function(e){e.preventDefault();document.body.classList.add('drop');});
|
|
632
651
|
document.addEventListener('dragleave',function(e){if(e.target===document.body||e.relatedTarget===null)document.body.classList.remove('drop');});
|
|
633
652
|
document.addEventListener('drop',function(e){e.preventDefault();document.body.classList.remove('drop');
|
|
@@ -637,27 +656,30 @@ var t=(e.dataTransfer.getData('text/uri-list')||'')+(e.dataTransfer.getData('tex
|
|
|
637
656
|
var mm=t.match(/magnet:\\?[^\\s]+/);if(mm){addTorrent(mm[0]);}
|
|
638
657
|
});
|
|
639
658
|
function render(list){var box=document.getElementById('torrents');
|
|
640
|
-
if(!list.length){box.innerHTML='<div class=note
|
|
659
|
+
if(!list.length){box.innerHTML='<div class=note>'+esc(tr('empty'))+'</div>';return;}
|
|
641
660
|
box.innerHTML=list.map(function(t){
|
|
642
|
-
var files=t.files.map(function(f){return '<div class=fin><label><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> <button type=button class=play data-act=vlc data-id='+t.id+' data-i='+f.i+'>VLC</button> <button type=button class=play data-act=web data-id='+t.id+' data-i='+f.i+'
|
|
661
|
+
var files=t.files.map(function(f){return '<div class=fin><label><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> <button type=button class=play data-act=vlc data-id='+t.id+' data-i='+f.i+'>VLC</button> <button type=button class=play data-act=web data-id='+t.id+' data-i='+f.i+'>'+esc(tr('browser'))+'</button></div>';}).join('');
|
|
643
662
|
var first=t.files.filter(function(f){return f.selected;})[0];
|
|
644
663
|
var wi=first?first.i:0;
|
|
645
664
|
var nSel=t.files.filter(function(f){return f.selected;}).length;
|
|
646
665
|
var bar=t.ready?t.progress:0;
|
|
647
|
-
var hint=(t.ready&&nSel===0)?'<div class=note
|
|
648
|
-
return '<div class=card><div class=tname>'+esc(t.name)+'</div><div class=tmeta>'+(t.ready?'⏬ '+t.progress+'% · '+esc(t.speed)+' ·
|
|
666
|
+
var hint=(t.ready&&nSel===0)?'<div class=note>'+esc(tr('hint'))+'</div>':'';
|
|
667
|
+
return '<div class=card><div class=tname>'+esc(t.name)+'</div><div class=tmeta>'+(t.ready?'⏬ '+t.progress+'% · '+esc(t.speed)+' · '+esc(tr('peers'))+' '+t.peers:'⏳ '+esc(tr('waitMeta')))+'</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+'>'+esc(tr('all'))+'</button><button data-act=none data-id='+t.id+'>'+esc(tr('none'))+'</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+'>▶ '+esc(tr('browser'))+'</button><button class=del data-act=del data-id='+t.id+'>'+esc(tr('del'))+'</button></div></div>';
|
|
649
668
|
}).join('');
|
|
650
669
|
[].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});};});
|
|
651
670
|
[].forEach.call(box.querySelectorAll('button[data-act]'),function(b){b.onclick=function(ev){ev.preventDefault();ev.stopPropagation();var id=+b.getAttribute('data-id');var act=b.getAttribute('data-act');
|
|
652
|
-
if(act==='vlc'){var one=b.getAttribute('data-i');var label=b.textContent;b.textContent='…';var body={id:id};if(one!=null&&one!=='')body.index=+one;api('/api/vlc',body).then(function(d){b.textContent=d.error?('⚠ '+
|
|
671
|
+
if(act==='vlc'){var one=b.getAttribute('data-i');var label=b.textContent;b.textContent='…';var body={id:id};if(one!=null&&one!=='')body.index=+one;api('/api/vlc',body).then(function(d){b.textContent=d.error?('⚠ '+errText(d.error).slice(0,80)):label;});}
|
|
653
672
|
if(act==='web'){window.open(BASE+'/view/'+id+'/'+b.getAttribute('data-i'));}
|
|
654
673
|
if(act==='del'){api('/api/remove',{id:id}).then(poll);}
|
|
655
674
|
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);}
|
|
656
675
|
};});
|
|
657
676
|
}
|
|
677
|
+
var pollTimer=null;
|
|
658
678
|
function poll(){api('/api/status').then(render).catch(function(){});}
|
|
659
679
|
function toTorrents(){var el=document.getElementById('torrents');el.scrollIntoView({behavior:'smooth',block:'start'});}
|
|
660
|
-
|
|
680
|
+
applyLang();
|
|
681
|
+
poll();
|
|
682
|
+
pollTimer=setInterval(poll,1500);
|
|
661
683
|
</script></body></html>`;
|
|
662
684
|
|
|
663
685
|
function readBody(req) {
|
|
@@ -875,6 +897,31 @@ function makeWebServer(client, st, opts, cacheDir, base) {
|
|
|
875
897
|
});
|
|
876
898
|
}
|
|
877
899
|
|
|
900
|
+
const PLAYER_I18N = {
|
|
901
|
+
ru: {
|
|
902
|
+
sound: '▶ со звуком',
|
|
903
|
+
prep: 'готовлю файл к стриму…',
|
|
904
|
+
ffmpeg: 'нужен ffmpeg',
|
|
905
|
+
nodur: 'не вижу длительность файла',
|
|
906
|
+
noplayer: 'плеер браузера не загрузился',
|
|
907
|
+
track: 'дорожка',
|
|
908
|
+
nosub: 'без субтитров',
|
|
909
|
+
sub: 'субтитры',
|
|
910
|
+
peers: 'пиры',
|
|
911
|
+
},
|
|
912
|
+
en: {
|
|
913
|
+
sound: '▶ with sound',
|
|
914
|
+
prep: 'preparing the stream…',
|
|
915
|
+
ffmpeg: 'ffmpeg is required',
|
|
916
|
+
nodur: 'cannot read the duration',
|
|
917
|
+
noplayer: 'the browser player did not load',
|
|
918
|
+
track: 'track',
|
|
919
|
+
nosub: 'no subtitles',
|
|
920
|
+
sub: 'subtitles',
|
|
921
|
+
peers: 'peers',
|
|
922
|
+
},
|
|
923
|
+
};
|
|
924
|
+
|
|
878
925
|
async function webReq(req, res, client, st, opts, cacheDir, base) {
|
|
879
926
|
const u = new URL(req.url, 'http://127.0.0.1');
|
|
880
927
|
const ensureSel = (e, i) => { const f = e?.t.files[i]; if (f && !e.sel.has(i)) { e.sel.add(i); try { f.select(); } catch {} } };
|
|
@@ -888,7 +935,8 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
|
|
|
888
935
|
|
|
889
936
|
if (p === '/' || p === '') {
|
|
890
937
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
891
|
-
res.
|
|
938
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
939
|
+
res.end(WEB_HTML.replaceAll('__BASE__', base).replaceAll('__BOOT__', st.boot));
|
|
892
940
|
return;
|
|
893
941
|
}
|
|
894
942
|
if (p === '/hls.min.js') {
|
|
@@ -900,6 +948,21 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
|
|
|
900
948
|
} catch { res.statusCode = 404; res.end('no hls.js'); }
|
|
901
949
|
return;
|
|
902
950
|
}
|
|
951
|
+
if (p === '/api/health') return json(200, { ok: true, version: PKG_VERSION });
|
|
952
|
+
if (p === '/api/quit' && req.method === 'POST') {
|
|
953
|
+
const b = await readBody(req).catch(() => ({}));
|
|
954
|
+
// Старая вкладка после «Закрыть» не должна гасить уже новый процесс.
|
|
955
|
+
if (!b || b.boot !== st.boot) return json(409, { error: 'stale' });
|
|
956
|
+
res.statusCode = 200;
|
|
957
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
958
|
+
res.setHeader('Connection', 'close');
|
|
959
|
+
res.end(JSON.stringify({ ok: true }));
|
|
960
|
+
setTimeout(() => {
|
|
961
|
+
if (typeof st.quit === 'function') st.quit();
|
|
962
|
+
else process.exit(0);
|
|
963
|
+
}, 80);
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
903
966
|
if (p === '/api/search') {
|
|
904
967
|
const q = u.searchParams.get('q') || '';
|
|
905
968
|
const { items, notes } = await searchIndexers(q);
|
|
@@ -914,7 +977,7 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
|
|
|
914
977
|
req.on('error', reject);
|
|
915
978
|
});
|
|
916
979
|
const buf = Buffer.concat(chunks);
|
|
917
|
-
if (!buf.length) return json(400, { error: '
|
|
980
|
+
if (!buf.length) return json(400, { error: 'empty-file' });
|
|
918
981
|
const id = st.nextId++;
|
|
919
982
|
st.pending.push(id);
|
|
920
983
|
try { client.add(buf, { path: cacheDir }); }
|
|
@@ -924,7 +987,7 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
|
|
|
924
987
|
if (p === '/api/add' && req.method === 'POST') {
|
|
925
988
|
const b = await readBody(req);
|
|
926
989
|
const source = String(b.source || '');
|
|
927
|
-
if (!source) return json(400, { error: '
|
|
990
|
+
if (!source) return json(400, { error: 'need-source' });
|
|
928
991
|
const btih = (source.match(/btih:([0-9a-f]{40})/i) || [])[1];
|
|
929
992
|
if (btih) {
|
|
930
993
|
const ex = [...st.torrents.values()].find(e => e.t.infoHash === btih.toLowerCase());
|
|
@@ -953,7 +1016,7 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
|
|
|
953
1016
|
if (p === '/api/select' && req.method === 'POST') {
|
|
954
1017
|
const b = await readBody(req);
|
|
955
1018
|
const e = st.torrents.get(Number(b.id));
|
|
956
|
-
if (!e) return json(404, { error: '
|
|
1019
|
+
if (!e) return json(404, { error: 'no-torrent' });
|
|
957
1020
|
e.t.files.forEach(f => f.deselect());
|
|
958
1021
|
e.sel = new Set((b.indices || []).map(Number));
|
|
959
1022
|
e.sel.forEach(i => e.t.files[i]?.select());
|
|
@@ -962,18 +1025,18 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
|
|
|
962
1025
|
if (p === '/api/vlc' && req.method === 'POST') {
|
|
963
1026
|
const b = await readBody(req);
|
|
964
1027
|
const e = st.torrents.get(Number(b.id));
|
|
965
|
-
if (!e) return json(404, { error: '
|
|
1028
|
+
if (!e) return json(404, { error: 'no-torrent' });
|
|
966
1029
|
let idx;
|
|
967
1030
|
if (b.index != null && b.index !== '' && Number.isFinite(Number(b.index))) {
|
|
968
1031
|
const i = Number(b.index);
|
|
969
|
-
if (!e.t.files[i]) return json(400, { error: '
|
|
1032
|
+
if (!e.t.files[i]) return json(400, { error: 'no-file' });
|
|
970
1033
|
e.sel.add(i);
|
|
971
1034
|
try { e.t.files[i].select(); } catch {}
|
|
972
1035
|
idx = [i];
|
|
973
1036
|
} else idx = [...e.sel];
|
|
974
|
-
if (!idx.length) return json(400, { error: '
|
|
1037
|
+
if (!idx.length) return json(400, { error: 'pick-files' });
|
|
975
1038
|
const vlcBin = detectVLCPath();
|
|
976
|
-
if (!vlcBin) return json(500, { error: '
|
|
1039
|
+
if (!vlcBin) return json(500, { error: 'no-vlc' });
|
|
977
1040
|
const subs = await collectSubtitles(e.t, idx);
|
|
978
1041
|
const urls = idx.map(i => 'http://127.0.0.1:' + st.port + base + '/s/' + e.id + '/' + i);
|
|
979
1042
|
const args = buildVLCArgs({ urls, caching: opts.caching, subs, startAt: 0 });
|
|
@@ -1016,7 +1079,7 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
|
|
|
1016
1079
|
if (mT) {
|
|
1017
1080
|
const e = st.torrents.get(Number(mT[1]));
|
|
1018
1081
|
const f = e?.t.files[Number(mT[2])];
|
|
1019
|
-
if (!f) return json(404, { error: '
|
|
1082
|
+
if (!f) return json(404, { error: 'no-file' });
|
|
1020
1083
|
ensureSel(e, Number(mT[2]));
|
|
1021
1084
|
const ffprobe = detectFFBin('ffprobe');
|
|
1022
1085
|
if (!ffprobe) return json(200, { ffmpeg: false, tracks: [] });
|
|
@@ -1212,14 +1275,22 @@ select{background:#1b1b1f;color:#ddd;border:1px solid #333;border-radius:6px;pad
|
|
|
1212
1275
|
#tm{font-variant-numeric:tabular-nums;min-width:9em}
|
|
1213
1276
|
video{flex:1;width:100%;min-height:0;background:#000}
|
|
1214
1277
|
#n{color:#9ab}
|
|
1215
|
-
#ov{position:fixed;inset:0;display:none;align-items:center;justify-content:center;background:rgba(0,0,0,.45);cursor:pointer;font-size:28px;color:#fff}
|
|
1216
|
-
|
|
1278
|
+
#ov{position:fixed;inset:0;display:none;align-items:center;justify-content:center;background:rgba(0,0,0,.45);cursor:pointer;font-size:28px;color:#fff}
|
|
1279
|
+
#langs{display:flex;gap:4px}#langs button{background:#1b1b1f;color:#ddd;border:1px solid #333;border-radius:6px;padding:2px 8px;cursor:pointer}#langs button.on{border-color:#7fd4ff;color:#fff}</style>
|
|
1280
|
+
<div class=bar><b>${esc(f.name)}</b><span id=langs><button type=button id=lru>RU</button><button type=button id=len>EN</button></span><select id=a style=display:none></select><select id=s style=display:none></select><span id=tm>00:00 / --:--</span><span id=n></span></div>
|
|
1217
1281
|
<video id=v controls autoplay playsinline></video>
|
|
1218
1282
|
<div id=ov>▶ со звуком</div>
|
|
1219
1283
|
<script src="${base}/hls.min.js"></script>
|
|
1220
1284
|
<script>
|
|
1221
1285
|
var B=${JSON.stringify(base)},ID=${mV[1]},I=${mV[2]};
|
|
1286
|
+
var PK=${JSON.stringify(PLAYER_I18N)};
|
|
1222
1287
|
var v=document.getElementById("v"),aS=document.getElementById("a"),sS=document.getElementById("s"),n=document.getElementById("n"),tm=document.getElementById("tm"),ov=document.getElementById("ov");
|
|
1288
|
+
function LG(){try{return localStorage.getItem("to-lang")==="en"?"en":"ru"}catch(e){return "ru"}}
|
|
1289
|
+
function pt(k){var d=PK[LG()]||PK.ru;return (d&&d[k])||(PK.ru&&PK.ru[k])||k}
|
|
1290
|
+
function paintLang(){var L=LG();document.documentElement.lang=L;var a=document.getElementById("lru"),b=document.getElementById("len");if(a)a.className=L==="ru"?"on":"";if(b)b.className=L==="en"?"on":"";ov.textContent=pt("sound")}
|
|
1291
|
+
document.getElementById("lru").onclick=function(){try{localStorage.setItem("to-lang","ru")}catch(e){}paintLang();fillTracks()};
|
|
1292
|
+
document.getElementById("len").onclick=function(){try{localStorage.setItem("to-lang","en")}catch(e){}paintLang();fillTracks()};
|
|
1293
|
+
paintLang();
|
|
1223
1294
|
v.muted=false;v.volume=1;
|
|
1224
1295
|
ov.onclick=function(){v.muted=false;v.volume=1;v.play().catch(function(){});ov.style.display="none"};
|
|
1225
1296
|
var tries=0,player=null,cues=[],subUrl="",armed=false;
|
|
@@ -1240,27 +1311,29 @@ player.loadSource(u);player.attachMedia(v);
|
|
|
1240
1311
|
player.on(Hls.Events.MANIFEST_PARSED,start);
|
|
1241
1312
|
player.on(Hls.Events.ERROR,function(ev,d){if(!d.fatal)return;if(d.type===Hls.ErrorTypes.NETWORK_ERROR){player.startLoad(v.currentTime||want);return}n.textContent="HLS: "+(d.details||d.type)})}
|
|
1242
1313
|
else{var once=function(){v.removeEventListener("loadedmetadata",once);start()};v.addEventListener("loadedmetadata",once);v.src=u}};
|
|
1314
|
+
var lastD=null,filling=false;
|
|
1315
|
+
function fillTracks(){if(!lastD)return;filling=true;var d=lastD;var prevA=aS.value,prevS=sS.value;var A=d.tracks||[],S=d.subs||[];aS.innerHTML="";sS.innerHTML="";
|
|
1316
|
+
var SI=S.filter(function(t){return ["subrip","ass","ssa","srt","mov_text","webvtt","text"].indexOf(t.codec)>=0}).map(function(t){return t.s});
|
|
1317
|
+
A.forEach(function(t){var o=document.createElement("option");o.value=t.a;o.textContent=(t.lang&&t.lang!=="und"?t.lang:pt("track")+" "+(t.a+1))+(t.title?" — "+t.title:"")+" ["+t.codec+"]";aS.appendChild(o)});
|
|
1318
|
+
if(A.length){aS.value=(prevA!==""&&A.some(function(t){return String(t.a)===prevA}))?prevA:String(A[0].a);aS.style.display="";if(A.length<2)aS.disabled=true}
|
|
1319
|
+
if(SI.length){var o0=document.createElement("option");o0.value="";o0.textContent=pt("nosub");sS.appendChild(o0);
|
|
1320
|
+
S.filter(function(t){return SI.indexOf(t.s)>=0}).forEach(function(t){var o=document.createElement("option");o.value=t.s;o.textContent=(t.lang&&t.lang!=="und"?t.lang:pt("sub")+" "+(t.s+1))+(t.title?" — "+t.title:"");sS.appendChild(o)});sS.value=prevS||"";sS.style.display=""}
|
|
1321
|
+
filling=false}
|
|
1243
1322
|
var load=function(){fetch(B+"/api/tracks/"+ID+"/"+I).then(function(r){return r.json()}).then(function(d){
|
|
1244
|
-
if((d.retry||!(d.duration>0))&&tries++<20){n.textContent="
|
|
1245
|
-
if(!d.ffmpeg){n.textContent="
|
|
1246
|
-
if(!(d.duration>0)){n.textContent="
|
|
1323
|
+
if((d.retry||!(d.duration>0))&&tries++<20){n.textContent=pt("prep");setTimeout(load,2000);return;}
|
|
1324
|
+
if(!d.ffmpeg){n.textContent=pt("ffmpeg");return;}
|
|
1325
|
+
if(!(d.duration>0)){n.textContent=pt("nodur");return;}
|
|
1247
1326
|
var nativeHls=!!v.canPlayType("application/vnd.apple.mpegurl");
|
|
1248
|
-
if(!(window.Hls&&Hls.isSupported())&&!nativeHls){n.textContent="
|
|
1327
|
+
if(!(window.Hls&&Hls.isSupported())&&!nativeHls){n.textContent=pt("noplayer");return;}
|
|
1249
1328
|
tm.textContent="00:00 / "+fmt(d.duration);
|
|
1250
|
-
|
|
1251
|
-
aS.innerHTML="";sS.innerHTML="";
|
|
1252
|
-
var SI=S.filter(function(t){return ["subrip","ass","ssa","srt","mov_text","webvtt","text"].indexOf(t.codec)>=0}).map(function(t){return t.s});
|
|
1253
|
-
A.forEach(function(t){var o=document.createElement("option");o.value=t.a;o.textContent=(t.lang&&t.lang!=="und"?t.lang:"дорожка "+(t.a+1))+(t.title?" — "+t.title:"")+" ["+t.codec+"]";aS.appendChild(o)});
|
|
1254
|
-
if(A.length){aS.value=String(A[0].a);aS.style.display="";if(A.length<2)aS.disabled=true}
|
|
1255
|
-
if(SI.length){var o0=document.createElement("option");o0.value="";o0.textContent="без субтитров";sS.appendChild(o0);
|
|
1256
|
-
S.filter(function(t){return SI.indexOf(t.s)>=0}).forEach(function(t){var o=document.createElement("option");o.value=t.s;o.textContent=(t.lang&&t.lang!=="und"?t.lang:"субтитры "+(t.s+1))+(t.title?" — "+t.title:"");sS.appendChild(o)});sS.value="";sS.style.display=""}
|
|
1329
|
+
lastD=d;fillTracks();
|
|
1257
1330
|
var go=function(keep){var a=aS.options.length?aS.value:0;var t=0;if(keep){t=v.currentTime;if(!isFinite(t)||t<0)t=0}play(B+"/hls/"+ID+"/"+I+"/index.m3u8?a="+encodeURIComponent(a),t)};
|
|
1258
|
-
aS.onchange=function(){go(true)};
|
|
1259
|
-
sS.onchange=function(){var s=sS.value;cues=[];cueDiv.innerHTML="";subUrl=s!==""?B+"/hls/"+ID+"/"+I+"/sub.vtt?si="+encodeURIComponent(s):"";if(subUrl)pollSub()};
|
|
1331
|
+
aS.onchange=function(){if(filling)return;go(true)};
|
|
1332
|
+
sS.onchange=function(){if(filling)return;var s=sS.value;cues=[];cueDiv.innerHTML="";subUrl=s!==""?B+"/hls/"+ID+"/"+I+"/sub.vtt?si="+encodeURIComponent(s):"";if(subUrl)pollSub()};
|
|
1260
1333
|
if(!armed){armed=true;go(false)}
|
|
1261
1334
|
}).catch(function(){if(tries++<20)setTimeout(load,2000)})};
|
|
1262
1335
|
v.addEventListener("playing",function(){n.textContent="";ov.style.display="none";tick()});
|
|
1263
|
-
setInterval(function(){fetch(B+"/api/status").then(function(r){return r.json()}).then(function(list){var t=list.find(function(x){return x.id===ID});if(t&&v.readyState<2)n.textContent=t.speed+" ·
|
|
1336
|
+
setInterval(function(){fetch(B+"/api/status").then(function(r){return r.json()}).then(function(list){var t=list.find(function(x){return x.id===ID});if(t&&v.readyState<2)n.textContent=t.speed+" · "+pt("peers")+" "+t.peers+(t.progress>0?" · "+t.progress+"%":"")}).catch(function(){})},2000);
|
|
1264
1337
|
load();
|
|
1265
1338
|
</script>`);
|
|
1266
1339
|
return;
|
|
@@ -1274,7 +1347,7 @@ async function startWeb(opts, srcArg) {
|
|
|
1274
1347
|
const client = new WebTorrent({ destroyStoreOnDestroy: false });
|
|
1275
1348
|
const token = opts.lan ? crypto.randomBytes(8).toString('hex') : '';
|
|
1276
1349
|
const base = token ? '/t/' + token : '';
|
|
1277
|
-
const st = { torrents: new Map(), nextId: 1, vlc: [], port: 0, pending: [], autoSelect: !!srcArg };
|
|
1350
|
+
const st = { torrents: new Map(), nextId: 1, vlc: [], port: 0, pending: [], autoSelect: !!srcArg, boot: crypto.randomBytes(8).toString('hex') };
|
|
1278
1351
|
client.on('torrent', t => {
|
|
1279
1352
|
t.on('error', () => {});
|
|
1280
1353
|
const id = st.pending.shift();
|
|
@@ -1297,6 +1370,9 @@ async function startWeb(opts, srcArg) {
|
|
|
1297
1370
|
const server = makeWebServer(client, st, opts, cacheDir, base);
|
|
1298
1371
|
const host = opts.lan ? '0.0.0.0' : '127.0.0.1';
|
|
1299
1372
|
st.port = await listenOnFreePort(server, host, opts.port);
|
|
1373
|
+
if (process.env.WTUI_PIDFILE) {
|
|
1374
|
+
try { fs.writeFileSync(process.env.WTUI_PIDFILE, String(process.pid)); } catch {}
|
|
1375
|
+
}
|
|
1300
1376
|
const url = 'http://127.0.0.1:' + st.port + base + '/';
|
|
1301
1377
|
console.log('🌐 GUI: ' + url);
|
|
1302
1378
|
if (opts.lan) console.log('📱 В сети: http://' + localIP() + ':' + st.port + base + '/ (токен в URL)');
|
|
@@ -1312,15 +1388,18 @@ async function startWeb(opts, srcArg) {
|
|
|
1312
1388
|
try { server.close(); } catch {}
|
|
1313
1389
|
if (st.hls) for (const h of st.hls.procs.values()) { try { h.proc.kill('SIGKILL'); } catch {} }
|
|
1314
1390
|
try { fs.rmSync(path.join(os.tmpdir(), 'wtui-hls-' + process.pid), { recursive: true, force: true }); } catch {}
|
|
1315
|
-
|
|
1316
|
-
process.exit(code);
|
|
1391
|
+
// Порт надо отдать сразу, иначе следующий запуск .app не встаёт. Кэш не стираем.
|
|
1392
|
+
const bye = setTimeout(() => process.exit(code), 200);
|
|
1393
|
+
bye.unref();
|
|
1394
|
+
try { client.destroy(() => process.exit(code)); } catch { process.exit(code); }
|
|
1317
1395
|
}
|
|
1396
|
+
st.quit = () => { shutdown(0); };
|
|
1318
1397
|
process.on('SIGINT', () => shutdown(0));
|
|
1319
1398
|
process.on('SIGTERM', () => shutdown(0));
|
|
1320
1399
|
const NET_NOISE = /premature|aborted|EPIPE|ECONNRESET|ETIMEDOUT|socket|stream|duplicate torrent/i;
|
|
1321
1400
|
process.on('unhandledRejection', e => { const m = (e && (e.message || e)) + ''; if (!NET_NOISE.test(m)) console.error('unhandledRejection:', m); });
|
|
1322
1401
|
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')); });
|
|
1323
|
-
console.log('Ctrl+C
|
|
1402
|
+
console.log('Выход: кнопка «Закрыть» на странице или Ctrl+C. Скачанное остаётся в ' + cacheDir + '. Стирает его только кнопка «Очистить кэш».');
|
|
1324
1403
|
}
|
|
1325
1404
|
|
|
1326
1405
|
// ---- Main ----
|