Переделал структуру медиа директорий
This commit is contained in:
+72
-34
@@ -114,8 +114,17 @@ SQLite. Схема покрывает приём, цикл ревью и отк
|
||||
- `file_link` — `download_id`, `apply_batch_id`, исходный → целевой путь,
|
||||
вид (видео/субтитры/…), статус, время. Батч нужен для точечного undo.
|
||||
|
||||
Дубликат `infohash` при приёме — повторно не добавляем, ведём к
|
||||
существующей загрузке (идемпотентность).
|
||||
### Идентификация торрента и повторное добавление
|
||||
|
||||
Идентификатор торрента — **infohash** (v1 SHA-1 / v2 SHA-256): берём из
|
||||
magnet (`xt=urn:btih:`) или считаем из `.torrent`; этим же оперирует сам
|
||||
qBittorrent. Идемпотентность — **только для активных задач**: повторное
|
||||
добавление, пока задача в работе, присоединяется к ней. Если прежняя
|
||||
задача для этого infohash уже терминальна (`done`/`cancelled`/`failed`/
|
||||
`reverted`), новое добавление заводит **новую** задачу — перекачать тот же
|
||||
торрент спустя месяцы можно без проблем (покажем, что infohash уже
|
||||
обрабатывался, и прежний результат). Разные раздачи одного фильма (репаки)
|
||||
имеют разные infohash → разные задачи.
|
||||
|
||||
## Конфигурация
|
||||
|
||||
@@ -126,36 +135,43 @@ TOML. В репозитории — `config.example.toml` с placeholder'ами;
|
||||
|
||||
```toml
|
||||
[qbittorrent]
|
||||
url = "http://127.0.0.1:8989" # работает при network_mode: host
|
||||
url = "http://qbit:8989" # по имени сервиса в общей docker-сети
|
||||
username = "admin"
|
||||
password = ""
|
||||
category = "jellybit"
|
||||
# qBit отдаёт пути своего контейнера ("/downloads/…"); транслируем в хост-путь
|
||||
path_map = { "/downloads" = "/srv/downloads" }
|
||||
savepath = "/srv/media/downloads" # куда qBit кладёт загрузки (задаём при добавлении)
|
||||
# Обычно пусто: все медиа-приложения монтируют /srv/media:/srv/media идентично,
|
||||
# поэтому путь из API уже = хост-путь. path_map — фолбэк, если пути разойдутся.
|
||||
path_map = {}
|
||||
|
||||
[paths]
|
||||
# хост-пути (видны внутри контейнера jellybit через mount /srv)
|
||||
downloads = "/srv/downloads"
|
||||
# хост-пути (видны внутри контейнера через mount /srv/media)
|
||||
downloads = "/srv/media/downloads"
|
||||
movies = "/srv/media/movies"
|
||||
series = "/srv/media/series"
|
||||
|
||||
[llm]
|
||||
# type — дискриминатор реализации; пока поддерживается "openai-compat"
|
||||
type = "openai-compat"
|
||||
base_url = "http://127.0.0.1:1234/v1"
|
||||
# LLM на хосте (LM Studio) из bridged-контейнера — через host.docker.internal,
|
||||
# не 127.0.0.1; либо вынести LLM в контейнер общей сети.
|
||||
base_url = "http://host.docker.internal:1234/v1"
|
||||
api_key = ""
|
||||
model = "qwen2.5-32b-instruct"
|
||||
proxy = "" # опц. HTTP-прокси (для удалённых эндпоинтов)
|
||||
timeout = "120s"
|
||||
max_retries = 3 # непарсящийся ответ после ретраев → review
|
||||
|
||||
[metadata.tmdb]
|
||||
enabled = false # включается ключом; без матча авто не делаем
|
||||
api_key = ""
|
||||
proxy = "" # опц. HTTP-прокси для доступа к базе
|
||||
timeout = "10s"
|
||||
|
||||
[metadata.tvdb]
|
||||
enabled = false
|
||||
api_key = ""
|
||||
proxy = ""
|
||||
timeout = "10s"
|
||||
|
||||
[worker]
|
||||
@@ -199,9 +215,11 @@ format = "json"
|
||||
- **ошибка:** `error`/`missingFiles` → `failed`.
|
||||
|
||||
Пути файлов берём из API (`save_path`/`content_path` + относительные
|
||||
имена), не из константы, и транслируем `path_map`. Предполагаем, что
|
||||
отдельный «incomplete»-каталог в qBittorrent выключен (иначе путь до
|
||||
завершения иной).
|
||||
имена), не из константы (обычно это уже хост-путь). «Incomplete»-каталог в
|
||||
qBittorrent **включён** (`/srv/media/incomplete`): пока качается — файлы
|
||||
там, по завершении qBit переносит их в `/srv/media/downloads` (состояние
|
||||
`moving` — дожидаемся окончания переноса и только потом берём финальный
|
||||
путь).
|
||||
|
||||
## Раскладка файлов
|
||||
|
||||
@@ -223,21 +241,30 @@ Jellyfin ([jellyfin-layout.md](jellyfin-layout.md)). Правила:
|
||||
- **Одна ФС обязательна.** `link(2)` через границы ФС даёт `EXDEV` —
|
||||
падаем с понятной ошибкой; по построению этого не должно случаться.
|
||||
|
||||
### Пути и контейнеры (три namespace)
|
||||
### Пути и контейнеры — единая песочница `/srv/media`
|
||||
|
||||
`/srv/downloads` и `/srv/media` — одна ФС на хосте (подтверждено), но
|
||||
путь существует в трёх пространствах имён:
|
||||
Весь медиа-стек лежит под одним каталогом и монтируется **идентично**
|
||||
(`/srv/media:/srv/media`) во все медиа-приложения:
|
||||
|
||||
- **контейнер qBittorrent** видит только `/downloads` (= хост
|
||||
`/srv/downloads`); `/srv/media` он не монтирует — и не должен.
|
||||
- **контейнер jellybit** монтирует **общего родителя `/srv`** одним
|
||||
bind-mount'ом — так `downloads` и `media` гарантированно на одной ФС
|
||||
внутри него, и хардлинк проходит.
|
||||
- **хост** — где обе ветки реально на одном томе.
|
||||
```
|
||||
/srv/media/
|
||||
incomplete/ ← qBit качает сюда
|
||||
downloads/ ← готовые раздачи (источник хардлинка)
|
||||
movies/ series/ ← библиотека Jellyfin (цель хардлинка)
|
||||
```
|
||||
|
||||
Поэтому путь из qBittorrent (`/downloads/…`) транслируется в хост-путь
|
||||
(`/srv/downloads/…`) по `qbittorrent.path_map`, и уже он используется как
|
||||
источник хардлинка.
|
||||
Так как всё под одним mount'ом, и **хардлинк** (downloads → movies/series),
|
||||
и **мгновенный move** qBit (incomplete → downloads) работают — нет границ
|
||||
между точками монтирования (`EXDEV`). Путь из qBittorrent
|
||||
(`save_path`/`content_path`) уже равен хост-пути, трансляция не нужна
|
||||
(`path_map` — фолбэк, обычно пуст). Секреты и чужие приложения
|
||||
(`/srv/applications`) в эту песочницу не попадают.
|
||||
|
||||
- **qBit** — `savepath=/srv/media/downloads`, temp `/srv/media/incomplete`.
|
||||
- **jellybit** — читает `downloads`, пишет в `movies`/`series`; свой
|
||||
SQLite/конфиг — отдельным mount'ом `/srv/applications/jellybit/data`.
|
||||
- **Jellyfin** — библиотеки указывают на `movies`/`series` (не на корень
|
||||
`/srv/media`, иначе в индекс попадут downloads/incomplete).
|
||||
|
||||
## Деплой
|
||||
|
||||
@@ -250,13 +277,16 @@ Jellybit работает в **docker** — в одной среде с qBittorr
|
||||
|
||||
Параметры запуска (в umbar-compose):
|
||||
|
||||
- **`network_mode: host`** — чтобы `127.0.0.1:8989` достучался до
|
||||
qBittorrent (он на bridge с проброшенным портом; из отдельного
|
||||
bridge-контейнера `127.0.0.1` — это он сам). Заодно `:8080` слушает на
|
||||
хосте. Прецедент в umbar — glances.
|
||||
- **Общая docker-сеть** (external, напр. `media-net`) — jellybit, qBit и
|
||||
(позже) Jellyfin в ней; адресуемся по именам (`http://qbit:8989`,
|
||||
`http://jellyfin:8096`). Веб-UI jellybit публикуем на хост (`8080:8080`)
|
||||
для LAN. Учесть: qBit валидирует Host-заголовок — выставить
|
||||
`WebUI\ServerDomains=*` (umbar); LLM на хосте достаётся через
|
||||
`host.docker.internal` (`extra_hosts: host-gateway`).
|
||||
- **`user: "1000:1000"`**, UMASK 022 — единый системный пользователь
|
||||
umbar; созданные каталоги 0755, файлы-ссылки наследуют inode источника.
|
||||
- **mount `/srv`** (общий родитель) — для хардлинков (см. выше).
|
||||
- **mount `/srv/media`** (единая песочница) — для хардлинков и move
|
||||
(см. «Пути и контейнеры»); `/srv/applications/jellybit/data` — отдельно.
|
||||
- **mount данных** `/srv/applications/jellybit/data` → `/data`: SQLite
|
||||
(`/data/jellybit.db`) и `config.toml`. Без него редеплой стёр бы всё
|
||||
in-flight состояние.
|
||||
@@ -283,20 +313,28 @@ Dockerfile .dockerignore config.example.toml
|
||||
|
||||
## Решённые вопросы
|
||||
|
||||
- Пути/контейнеры — три namespace сведены: qBit отдаёт `/downloads`,
|
||||
транслируем в хост через `path_map`; jellybit монтирует `/srv`.
|
||||
- Сеть jellybit↔qBittorrent — `network_mode: host`.
|
||||
- Пути/контейнеры — единая песочница `/srv/media:/srv/media` (подпапки
|
||||
incomplete/downloads/movies/series) монтируется идентично во все
|
||||
медиа-приложения; путь из API = хост-путь; хардлинк и move в пределах
|
||||
одного mount'а. `/srv/applications` в песочницу не попадает.
|
||||
- Сеть — общая docker-сеть, адресация по именам (`qbit:8989`); host-режим
|
||||
не используем. qBit: `WebUI\ServerDomains=*`; LLM на хосте — через
|
||||
`host.docker.internal`.
|
||||
- qBit: «incomplete» включён (`/srv/media/incomplete`), завершение
|
||||
проходит через `moving`; jellybit авторизуется логином/паролем
|
||||
(docker-подсеть не входит в LAN-whitelist qBit).
|
||||
- Внешние базы — HTTP-прокси на клиента (`proxy` в `[metadata.*]`/`[llm]`).
|
||||
- Идентификатор торрента — infohash; идемпотентность только для активных
|
||||
задач (повторная закачка спустя время → новая задача).
|
||||
- Состояние — на persistent-томе `/srv/applications/jellybit/data`.
|
||||
- Детект завершения — поллинг; webhook — на будущее (drafts/ideas).
|
||||
- Источник (magnet/URL/.torrent) отдаём в qBittorrent — без SSRF.
|
||||
- Авто-раскладка требует подтверждённого матча в базе; иначе review.
|
||||
- Веб-UI в v1 без авторизации (доверенная LAN, опц. allowlist подсетей).
|
||||
- Форма запуска — docker, образ собирается на сервере; контейнер под
|
||||
`1000:1000`, `network_mode: host`, mount `/srv` + data-том.
|
||||
`1000:1000`, в общей docker-сети, mount `/srv/media` + data-том.
|
||||
|
||||
## Открытые вопросы
|
||||
|
||||
- Конфигурация Jellyfin (URL + API-ключ) и триггер скана — когда Jellyfin
|
||||
будет развёрнут в umbar (сейчас его там нет).
|
||||
- Подтвердить, что «incomplete»-каталог qBittorrent выключен (иначе путь
|
||||
файлов до завершения иной).
|
||||
|
||||
@@ -38,7 +38,7 @@ series/
|
||||
## Сопоставление источник → цель
|
||||
|
||||
Источник берём по пути из qBittorrent (`save_path`/`content_path` +
|
||||
относительное имя, после трансляции `path_map` в хост-путь). Для каждого
|
||||
относительное имя; это уже хост-путь, `path_map` — фолбэк). Для каждого
|
||||
распознанного **файла** (не каталога) создаётся **хардлинк** в
|
||||
`paths.movies`/`paths.series`; целевые каталоги — `mkdir` (0755,
|
||||
`1000:1000`). Исходный файл остаётся на месте (раздача продолжается),
|
||||
@@ -50,8 +50,8 @@ inode общий — диск не дублируется.
|
||||
же inode → готово; другой файл → коллизия → review). Инварианты и undo —
|
||||
в [architecture.md](architecture.md) → «Раскладка файлов».
|
||||
|
||||
Требование: целевой и исходный каталоги — на одной ФС (внутри контейнера
|
||||
jellybit это обеспечивает mount `/srv`).
|
||||
Требование: целевой и исходный каталоги — на одной ФС/одном mount'е
|
||||
(внутри контейнера это обеспечивает единая песочница `/srv/media`).
|
||||
|
||||
## Крайние случаи
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
|
||||
- Имя торрента и структура каталогов.
|
||||
- Список файлов с размерами и расширениями. Абсолютный путь источника
|
||||
восстанавливаем как `save_path`/`content_path` из qBit (после трансляции
|
||||
`path_map`) + относительное имя файла; учитываем одно- и многофайловые
|
||||
торренты.
|
||||
восстанавливаем как `save_path`/`content_path` из qBit (= хост-путь;
|
||||
`path_map` обычно тождественен) + относительное имя файла; учитываем
|
||||
одно- и многофайловые торренты.
|
||||
- Текстовый контекст человека (+ накопленные подсказки из review).
|
||||
- Распарсенное сообщение торрент-бота (если через Telegram): название с
|
||||
годом, качество, переводы — см. пример в [BRIEF.md](../../BRIEF.md).
|
||||
|
||||
Reference in New Issue
Block a user