diff --git a/family/plans/watchlist-disk-overflow-fix.md b/family/plans/watchlist-disk-overflow-fix.md index f1fc1fd3..6239c40e 100644 --- a/family/plans/watchlist-disk-overflow-fix.md +++ b/family/plans/watchlist-disk-overflow-fix.md @@ -11,6 +11,8 @@ tags: - disk - radarr - incident + - mark-queued + - horror-filter related: - '[[family/projects/watchlist-automation]]' - '[[family/plans/watchlist-cleanup]]' @@ -23,6 +25,11 @@ related: > Radarr лежит с `disk I/O error`. `watchlist-nightly` и `watchlist-discover` остановлены. > Причина: `sync-down --batch 5` качал 5 фильмов ежедневно **без проверки свободного места**. +> **Три независимых дефекта (все найдены 2026-09-21 по коду):** +> 1. **Нет проверки места** → диск забит под ноль (`sync_down.py`) +> 2. **`mark_queued` молча проваливается** → 3 метки `⬇️` потеряны (`parser.py:77`) +> 3. **Правила «ужасы не качаются» НЕТ в коде вообще** → `🧟‍♀️` только рисуется, фильтр отсутствует; `⭐` приоритетнее `🧟` тоже не реализовано + --- ## Статус: Фаза 0 ожидает апрува Alex @@ -30,11 +37,14 @@ related: | Фаза | Что | Статус | |---|---|---| | 0 | Освободить место (удалить 5 remux = 403.4 ГБ) | 🔴 **ЖДЁТ АПРУВА** | -| 0b | Снять `⬇️` у Неистового в watchlist | ⏸ после Фазы 0 | -| 1 | `MIN_FREE_GB` в скрипт (стоп-кран) | ⏸ | -| 2 | Лимит размера файла | ❌ **НЕ ДЕЛАЕМ** — Alex: «лимит качества и так настроен» | -| 3 | Починить `cleanup` (видит только Radarr) | ⏸ отдельно | -| 4 | Вернуть крон с меньшим `--batch` | ⏸ после Фазы 1 | +| 0b | Снять `⬇️` у Шиндлера (396) + Неистового (107) | ⏸ после Фазы 0 | +| 1 | **Фикс `mark_queued`** — эмодзи-префиксы + возврат `bool` + лог провала | ⏸ **НОВОЕ** | +| 2 | `MIN_FREE_GB` в скрипт (стоп-кран) | ⏸ | +| 2b | Лимит размера файла | ❌ **НЕ ДЕЛАЕМ** — Alex: «лимит качества и так настроен» | +| 3 | **Фильтр ужасов + приоритет `⭐ > 🧟`** (правило новое, в коде его нет) | ⏸ **НОВОЕ** | +| 3b | Починить `cleanup` (видит только Radarr) | ⏸ отдельно | +| 4 | Восстановить 3 потерянные `⬇️` (Babylon, Midsommar, Triangle) | ⏸ после Фазы 1 | +| 5 | Вернуть крон с `--batch 2` | ⏸ после Фаз 1–2 | **Сделано 2026-09-21:** крон `watchlist-nightly` + `watchlist-discover` остановлен. Бэкап crontab: `~/crontab-backup-20260921.txt`. @@ -68,21 +78,32 @@ related: **Признак remux — имя файла:** `Remux` / `BDREMUX` / `BDRemux` / `BD-Remux`. -### 🔴 Метки `⬇️` в watchlist — есть только у одного из пяти +### 🔴 Метки `⬇️` — факт: у 3 из 5 метки ПОТЕРЯНЫ (баг `mark_queued`) -`family/documents/movies-watchlist.md`: +`family/documents/movies-watchlist.md` — **все 5 фильмов есть в списке** (проверено `grep` по ru+en): -| Фильм | Строка | Метка | +| Фильм | Строка | Метка `⬇️` | |---|---|---| -| Неистовый (Unhinged) | 107: `- [x] ⬇️ Неистовый` | ✅ | -| Babylon (2022) | 83: `- [ ] Babylon (2022)` | ❌ | -| Midsommar | 81: `- [ ] 🧟‍♀️ midsommar 2019` | ❌ | -| Triangle of Sadness | 82: `- [ ] Triangle of Sadness 2022` | ❌ | -| Schindler's List | — | **отсутствует в файле** | +| Неистовый (Unhinged) | 107: `- [x] ⬇️ Неистовый` | ✅ есть | +| Список Шиндлера | 396: `- [ ] ⬇️ Список Шиндлера` | ✅ есть | +| Babylon (2022) | 83: `- [ ] Babylon (2022)` | ❌ **потеряна** | +| Midsommar | 81: `- [ ] 🧟‍♀️ midsommar 2019` | ❌ **потеряна** | +| Triangle of Sadness | 82: `- [ ] Triangle of Sadness 2022` | ❌ **потеряна** | -`sync-down` ставит `⬇️` только на то, что добавил сам. Эти четыре попали в Radarr иначе. +⚠️ **Питфолл поиска:** Шиндлер записан **по-русски** («Список Шиндлера»). `grep -i schindler` его **не находит** → ошибочный вывод «фильма нет в списке». Искать и ru, и en. -**Решение Alex:** снять `⬇️` (там, где есть). `[x]` у Неистового **не трогать** — это отметка просмотра. +**Корень потери меток — баг `mark_queued` (`parser.py:77`):** +```python +pattern = re.compile(r'(^\s*-\s*\[ \]\s*)(' + re.escape(title) + r')', re.MULTILINE) +new_text = pattern.sub(r'\1⬇️ \2', text, count=1) +if new_text != text: + path.write_text(new_text) # ← молча ничего, если не совпало +``` +1. Между `]` и названием бывает **эмодзи-маркер** (`🧟‍♀️`, `✭`) → regex не матчит. Строка `- [ ] 🧟‍♀️ midsommar 2019` не подпадает. +2. `title` может не совпадать буквально (год/скобки) → не матчит. +3. **Тихий провал:** нет ошибки, нет лога. `sync_down.py` вызывает `mark_queued` (строки 98, 104, 113, 122) и **не проверяет результат** → считает, что всё ок. + +**Решение Alex:** снять `⬇️` там, где есть (Шиндлер, Неистовый). `[x]` у Неистового **не трогать** — это отметка просмотра. ### Почему удаляем файлами, а не через Radarr API @@ -95,7 +116,128 @@ Radarr **не отвечает** — диск в нуле, SQLite не може --- -## Фаза 1 — Стоп-кран `MIN_FREE_GB` (главный фикс) +## Фаза 1 — Фикс `mark_queued` (корень потери меток) 🆕 + +**Файл:** `watchlist_sync/parser.py:77` + +**Дефект:** regex ожидает название **сразу** после `- [ ]`, но в файле бывает эмодзи-маркер. Плюс тихий провал: +```python +pattern = re.compile(r'(^\s*-\s*\[ \]\s*)(' + re.escape(title) + r')', re.MULTILINE) +new_text = pattern.sub(r'\1⬇️ \2', text, count=1) +if new_text != text: + path.write_text(new_text) # ← молча ничего, если не совпало +``` + +**Что чинить:** +```python +# 1. Разрешить опциональные маркеры перед названием (в т.ч. ⭐ — он может стоять где угодно) +pattern = re.compile( + r'(^\s*-\s*\[ \]\s*)' + r'([🧟‍♀️✭🗓️❓⭐]\s*)?' + r'(' + escaped + r')', + re.MULTILINE +) +# 2. Возвращать bool — вставилась ли метка +# 3. При провале — log warning / raise, а не тишина +``` + +**Плюс:** в `sync_down.py` (строки **98, 104, 113, 122** — четыре места) **проверять результат**: +```python +if not mark_queued(watchlist, item.title): + print(f" [!] failed to mark ⬇️ for {item.title!r}") +``` + +**Проверка:** прогнать на копии файла → убедиться, что 3 проблемные строки (81, 82, 83) получают метку. + +⚠️ Наличие `⭐` в коде уже известно (`parser.py:_clean`): *«U+2B50 may sit ANYWHERE in the title ("Простите за беспокойство ⭐ 2018")»* → проверять **всю строку**, не только конец. + +--- + +## Фаза 3 — Фильтр ужасов + приоритет `⭐ > 🧟` (ПРАВИЛО НОВОЕ) 🆕 + +**🔴 Установлено фактами: правила «ужасы не качаются» в коде НЕТ вообще.** + +| Компонент | Есть? | Где | +|---|---|---| +| `is_horror()` — определяет жанр | ✅ | `resolver.py:43` (по жанрам TMDB, `27` = Horror) | +| `mark_horror()` — ставит `🧟‍♀️` | ✅ | `parser.py:156`, вызов `cli.py:127` | +| **Фильтр в `sync_down`** | ❌ **ОТСУТСТВУЕТ** | `sync_down.py` не вызывает `is_horror()` — проверено по всему файлу | + +`🧟‍♀️` — **только визуальная метка**. Единственный фильтр в `sync_down` — `MIN_RATING` (строка 63). +Поэтому **Midsommar скачался**, будучи помеченным `🧟‍♀️`. + +**Правило, которое надо реализовать (сформулировано Alex 2026-09-21):** + +| Случай | Действие | +|---|---| +| Ужас + `⭐` | **качаем** — `⭐` приоритетнее `🧟` | +| Ужас без `⭐` | **пропускаем** | +| Не ужас | качаем как обычно | + +```python +def should_skip_horror(candidate, raw_line: str) -> bool: + if not candidate.is_horror(): + return False + if '\u2b50' in raw_line: # ⭐ — явный приоритет пользователя + return False + return True # ужасы без ⭐ — пропускаем +``` + +**Что нужно:** +1. `parser.py` — читать наличие `⭐` **в любой позиции** строки → новое поле `priority: bool` в `WatchItem` +2. `sync_down.py` — перед добавлением в Radarr: `if should_skip_horror(c, item.raw): continue` +3. `cli.py` — при маркировке: ужас + `⭐` → `🧟‍♀️` **не навешивать** + +--- + +## Фаза 4 — Восстановить потерянные `⬇️` + +Три фильма без метки (после фикса `mark_queued` — правильным инструментом, не руками): +- строка **83**: `Babylon (2022)` +- строка **81**: `🧟‍♀️ midsommar 2019` +- строка **82**: `Triangle of Sadness 2022` + +**Проверка:** `grep` по каждой строке → метка на месте. + +--- + +## Фаза 5 — Вернуть крон + +После того как Фазы 1–2 проверены и стоп-кран работает: +- вернуть строки `watchlist-nightly` (01:00) и `watchlist-discover` (вс 09:00) в crontab; +- **снизить `--batch` с 5 до 2**. + +--- + +## Фаза 3b — Починить `cleanup` (отдельно, осторожно) + +**Дефект:** cleanup сопоставляет `[x]` из watchlist с **Radarr-библиотекой**, а просмотренное лежит в `/media/movies` (ручные торренты). Пересечение = 0 → удаляет 0 файлов. + +**Замер 2026-09-21:** `watchlist [x] items: 177` / `radarr movies with file: 28` / `nothing to delete`. + +**Это не причина инцидента** — cleanup не мог занять место, он только удаляет. Расширять его — отдельная задача, идёт после Фаз 0–2. + +--- + +## Порог `MIN_FREE_GB` — ⏸ ожидает решения Alex + +Предложение: **100 ГБ**. Alex ещё не подтвердил число. + +--- + +## Уроки + +1. **Любой скрипт, ставящий закачки в очередь, ОБЯЗАН проверять свободное место до добавления.** Лимита качества недостаточно — он про тип релиза, не про размер. +2. **«Radarr молчит / I/O error» на заполненном диске — сначала проверить место**, потом конфиг и сеть. +3. **Крон в 01:00 без присмотра = диск заполняется незаметно.** Тревога при >порога не настроена: cleanup писал «nothing to delete» и молчал. +4. **Не выдавать догадки за факт.** Первая версия разбора указывала на `cleanup` как виновника — Alex поправил: cleanup удаляет, а не занимает. Виновник — `sync-down`. +5. **Функция, которая «тихо ничего не делает» при провале — источник невидимых дефектов.** `mark_queued` молчал → 3 метки потеряны, никто не заметил. Возвращать `bool` и логировать. +6. **Метка ≠ правило.** `🧟‍♀️` рисовалась, но фильтра не было — Midsommar скачался. Если метка должна что-то запрещать — запрет надо писать отдельно. +7. **Искать и по-русски, и по-латинице.** `grep -i schindler` не нашёл «Список Шиндлера» → ложный вывод «фильма нет». Проверять оба написания перед любым утверждением. + +--- + +## Фаза 2 — Стоп-кран `MIN_FREE_GB` (главный фикс заполнения) **Цель:** перед добавлением фильма в Radarr проверить свободное место. Если мало — не добавлять. @@ -131,33 +273,6 @@ Alex 2026-09-21: **«лимит качества и так настроен»**. --- -## Фаза 3 — Починить `cleanup` (отдельно, осторожно) - -**Дефект:** cleanup сопоставляет `[x]` из watchlist с **Radarr-библиотекой**, а просмотренное лежит в `/media/movies` (ручные торренты). Пересечение = 0 → удаляет 0 файлов. - -**Замер 2026-09-21:** `watchlist [x] items: 177` / `radarr movies with file: 28` / `nothing to delete`. - -**Это не причина инцидента** — cleanup не мог занять место, он только удаляет. Расширять его — отдельная задача, идёт после Фаз 0–1. - ---- - -## Фаза 4 — Вернуть крон - -После того как Фаза 1 проверена и стоп-кран работает: -- вернуть строки `watchlist-nightly` (01:00) и `watchlist-discover` (вс 09:00) в crontab; -- **снизить `--batch`** (сейчас 5) — 5 фильмов/сутки без учёта размера и есть корень инцидента. - ---- - -## Уроки - -1. **Любой скрипт, ставящий закачки в очередь, ОБЯЗАН проверять свободное место до добавления.** Лимита качества недостаточно — он про тип релиза, не про размер. -2. **«Radarr молчит / I/O error» на заполненном диске — сначала проверить место**, потом конфиг и сеть. -3. **Крон в 01:00 без присмотра = диск заполняется незаметно.** Тревога при >порога не настроена: cleanup писал «nothing to delete» и молчал. -4. **Не выдавать догадки за факт.** Первая версия разбора указывала на `cleanup` как виновника — Alex поправил: cleanup удаляет, а не занимает. Виновник — `sync-down`. - ---- - ## Связанные заметки - [[family/projects/watchlist-automation]] — полный pipeline, статус крона diff --git a/personal/tech/inter-site-throughput-taiga-to-mac.md b/personal/tech/inter-site-throughput-taiga-to-mac.md index 3e2d8b4c..c43793e2 100644 --- a/personal/tech/inter-site-throughput-taiga-to-mac.md +++ b/personal/tech/inter-site-throughput-taiga-to-mac.md @@ -49,6 +49,19 @@ related: - "Один день в Стамбуле"/"Рождественские хроники" — ремукс `-codec:v:0 copy` в HLS (не перекодирование), стоп на ~2-3 сек. - В моменте (пока ТВ крутит 720p) — ffmpeg-дочерних процессов нет, папка transcodes пуста → **прямой stream/direct, транскодинг не идёт**. +## 🧭 2026-09-21: развилка решена — многопоточный HLS не существует в Jellyfin/webOS + +Перепроверено по первоисточникам (см. [[personal/tech/multistream-single-flow-limit]]): +- Jellyfin «multiple parallel connections» — **Planned** 5 лет, кода нет (230 голосов). +- hls.js multi-segment prefetch (#6753) — открыт, не реализован. +- webOS-приложения используют **нативный media pipeline** → подменить логику загрузки нельзя. +- **Несколько xray-туннелей не помогут** — один физический путь, одно congestion window. +- Speedtest 120 Мбит = 8 параллельных сессий. Одна сессия упирается в **~28–45 Мбит** (подтверждено замерами ниже). + +**Рабочие пути:** ограничить битрейт в клиенте / Moonfin с буфером / **Lampac на Кра́кене** +(буфер 50–100 МБ + GStreamer транскод) / prefetch-proxy. Полный разбор — в +[[personal/tech/multistream-single-flow-limit]]. + ## Решение / направление 1. **Правильное лекарство** (не мультипоток ради мультипотока, а либо снижение латентности, либо запас по битрейту под single-flow): лимит транскода ~12–18 Мбит, ЛИБО гнать трафик ТВ→TrueNAS через **xray tunnel** (`vpn.mallexxx.duckdns.org`, смотри `[[xray-reverse-tunnel-kraken-truenas]]`) вместо прямого межоператорского пути. 2. Jellyfin **не умеет** штатно разбивать один плейбек на параллельные TCP. Параллельность (28→51 Мбит) достижима только поднятием файла способом «качалка в N коннектов» + прокси, либо через туннель. diff --git a/personal/tech/multistream-single-flow-limit.md b/personal/tech/multistream-single-flow-limit.md new file mode 100644 index 00000000..393b8c73 --- /dev/null +++ b/personal/tech/multistream-single-flow-limit.md @@ -0,0 +1,196 @@ +--- +title: Многопоточный стриминг и обход single-flow лимита (Jellyfin / webOS / TorrServer) +created: '2026-09-21' +updated: '2026-09-21' +type: research +namespace: personal +status: research-complete +tags: + - jellyfin + - webos + - streaming + - tcp + - lampa + - torrserver + - bandwidth +related: + - '[[personal/tech/inter-site-throughput-taiga-to-mac]]' + - '[[personal/tech/xray-reverse-tunnel-kraken-truenas]]' + - '[[family/how-to/kraken-network]]' +--- + +# Многопоточный стриминг — что реально существует + +> **Контекст:** Jellyfin на TrueNAS (Новосибирск) тормозит при просмотре из Бишкека. +> Speedtest через reverse-туннель показывает **до 120 Мбит/с**, но видео дёргается. +> Разбор: почему, и какие пути обхода существуют. Дата: 2026-09-21. + +--- + +## 1. Почему speedtest 120 Мбит, а видео тормозит + +**Speedtest открывает 8+ параллельных TCP-сессий.** Каждая независимо разгоняется своим congestion window. +8 сессий × ~15 Мбит = 120 Мбит. Это доказывает, что **канал физически пропускает 120 Мбит**. + +**Но одна TCP-сессия не берёт 120 Мбит.** Скорость ≈ окно / RTT. При RTT ~104 мс для 120 Мбит нужно +окно ≈ 1.5 МБ «в полёте», не подтверждённых. Одна сессия разгоняется не сразу, при потерях окно режется +вдвое, на межоператорском стыке потери почти всегда есть. + +**Замеры межсайтовой пропускной способности (2026-09-02):** +| Режим | Пропускная способность | +|---|---| +| Mac → TrueNAS, 1 SCP-поток | ~28 Мбит/с | +| Mac → TrueNAS, 4 параллельных | ~51 Мбит/с (×1.8) | +| Speedtest (8 потоков) | до 120 Мбит/с | + +Рост ×1.8 при 4 потоках **доказывает**: ограничение — не полоса транзита, а **один TCP-поток при высоком RTT**. + +**Аналогия (уточнена Alex):** не «узкая труба», а **8 касс, где канал пропустит 120 чел/мин — +если открыты все 8. Один покупатель с одной корзиной в одной кассе уедет не быстрее этой кассы.** +Узкое — не канал, а **одно соединение на длинном пути**. + +--- + +## 2. Почему туннели не помогают + +**Замеры 2026-09-02 через `xray-test-client`:** +| Режим | Egress | Down | Стабильность | +|---|---|---|---| +| `kraken-user` (reverse) | `92.62.70.41` (Kraken) | ~45 Мбит | нестабилен, падал к 2 МБ/с | +| `user1` (direct/REALITY) | `90.189.160.148` (TrueNAS) | ~37 Мбит | 3.67–5.63 МБ/с | +| Контроль (Mac → Cloudflare напрямую) | — | ~55 МБ/с | — | + +**VLESS/REALITY идёт через тот же маршрут** ТТК → Ростелеком (хоп 8, +50 мс). Меняется инкапсуляция, +не физика. REALITY/Vision добавляет обёртки → обычно **чуть хуже** прямого, не лучше. + +### 🔴 Несколько xray-туннелей НЕ дадут скорости + +Это MPTCP/bonding — он агрегирует **разные физические пути**. Путь **один**. +N туннелей по одному пути разделят тот же congestion window — суммарно не больше одного, плюс оверхед. +Подтверждено замерами: reverse 45 и direct 37 — один коридор, не ×2. + +**Реальная агрегация требует второго независимого линка** (мобильный 4G/5G как backup-WAN). + +--- + +## 3. Многопоточный HLS — существует, но не реализован + +| Решение | Статус | +|---|---| +| **Jellyfin** feature request «multiple parallel connections» | ✅ **Planned**, 230 голосов (features.jellyfin.org/posts/2171). Issue #3206 (2020) закрыт как stale. **5 лет, кода нет** | +| **hls.js** issue #6753 (multi-segment prefetch) | Открыт Oct 2024, milestone 1.8.0, «Top priorities». **Кода нет, PR отсутствует** | +| **ExoPlayer** #5978 (parallelize DASH/HLS) | Android, не webOS | + +**Механизм, которого все ждут:** клиент тянет N сегментов одновременно → N независимых TCP-окон → 120 Мбит. +**Ровно то, что нужно. Но нигде не реализовано.** + +⚠️ **На webOS это безнадёжно:** приложения используют **нативный media pipeline** (Starfish / HTML5 `