Files
manga/ARCHITECTURE.md
2026-04-29 16:50:04 +03:00

659 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Manga Downloader — Архитектура и устройство проекта
## Содержание
1. [Общее описание](#1-общее-описание)
2. [Структура файлов](#2-структура-файлов)
3. [Стек технологий](#3-стек-технологий)
4. [Схема архитектуры](#4-схема-архитектуры)
5. [Модули бэкенда](#5-модули-бэкенда)
- [browser.py](#browserpy)
- [scraper.py](#scraperpy)
- [exporter.py](#exporterpy)
- [state.py](#statepy)
- [worker.py](#workerpy)
- [api.py](#apipy)
- [cli.py](#clipy)
6. [База данных](#6-база-данных)
7. [REST API](#7-rest-api)
8. [WebSocket протокол](#8-websocket-протокол)
9. [Фронтенд](#9-фронтенд)
10. [Жизненный цикл загрузки манги](#10-жизненный-цикл-загрузки-манги)
11. [Параллельная загрузка](#11-параллельная-загрузка)
12. [Конфигурация](#12-конфигурация)
13. [Docker-инфраструктура](#13-docker-инфраструктура)
---
## 1. Общее описание
Приложение скачивает мангу с сайтов типа readmanga.ru, обходя JS-защиту (DDoS-Guard, антибот) с помощью управляемого браузера Chromium. Поддерживает два режима работы:
- **Веб-интерфейс** — FastAPI-сервер + SPA на чистом JS (порт 8000). Позволяет управлять загрузками через браузер с realtime-прогрессом через WebSocket.
- **CLI** — консольные команды `download` и `analyze` для запуска через `docker compose run`.
---
## 2. Структура файлов
```
manga/
├── src/ # Весь бэкенд-код (Python-пакет)
│ ├── __init__.py
│ ├── api.py # FastAPI-приложение, REST + WebSocket
│ ├── browser.py # Обёртка над Playwright/Chromium
│ ├── cli.py # CLI-команды (click)
│ ├── downloader.py # (legacy, не используется в web-режиме)
│ ├── exporter.py # Экспорт CBZ / PDF / EPUB
│ ├── scraper.py # Парсер страниц readmanga.ru
│ ├── state.py # SQLite ORM (StateDB)
│ └── worker.py # Асинхронный воркер загрузки
├── frontend/
│ └── index.html # SPA — весь фронтенд в одном файле
├── output/ # Смонтированная папка с CBZ/PDF/EPUB
│ └── <Название манги>/
│ ├── v01_ch0001.0.cbz
│ └── ...
├── state/ # Смонтированная папка с состоянием
│ ├── progress.db # SQLite база данных
│ └── manga.log # Логи (ротация по 10 МБ)
├── Dockerfile # Сборка образа
├── docker-compose.yml # Конфигурация сервисов
├── requirements.txt # Python-зависимости
├── debug_site.py # Утилита отладки (скриншот страницы)
├── debug_cdn.py # Утилита отладки CDN-запросов
├── analyze_speed.py # Анализ скорости загрузки
├── README.md # Краткое руководство пользователя
└── ARCHITECTURE.md # Этот файл
```
---
## 3. Стек технологий
| Компонент | Библиотека | Назначение |
|-----------|------------|------------|
| Браузер | `playwright==1.44.0` (Chromium) | Открытие защищённых JS-страниц |
| Web-фреймворк | `fastapi==0.111.0` + `uvicorn` | REST API и WebSocket |
| БД | SQLite (stdlib `sqlite3`) | Хранение состояния |
| CLI | `click==8.1.7` | Консольные команды |
| Изображения | `Pillow==10.3.0` | Открытие/конвертация для PDF |
| PDF | `img2pdf==0.5.1` (fallback: Pillow) | Склейка изображений в PDF |
| EPUB | `ebooklib==0.18` | Сборка EPUB3-файла |
| Логирование | `loguru==0.7.2` | Удобные форматированные логи |
| Фронтенд | Tailwind CSS (CDN) + Vanilla JS | SPA без сборщика |
---
## 4. Схема архитектуры
```
Браузер пользователя
│ HTTP / WebSocket (:8000)
┌─────────────────────────────────┐
│ FastAPI (api.py) │
│ │
│ REST endpoints WebSocket /ws │
│ │ │ │
│ asyncio.Queue ws_manager │
│ │ (broadcast) │
│ ▼ ▲ │
│ queue_worker() │ │
│ │ events │
│ ▼ │ │
│ download_manga() ────┘ │ ◄── worker.py
│ │ │
└───────┼─────────────────────────┘
├─► BrowserManager (browser.py)
│ └─► Playwright Chromium
│ │
│ ┌───────────┴──────────┐
│ ▼ ▼
│ get_manga_info() get_chapter_images_and_download()
│ (scraper.py) (scraper.py)
├─► export() (exporter.py) → CBZ / PDF / EPUB
└─► StateDB (state.py) → progress.db
```
---
## 5. Модули бэкенда
### browser.py
**Отвечает за:** запуск и управление Playwright Chromium.
**Ключевые детали:**
- `BrowserManager` — контекстный менеджер (`async with BrowserManager() as bm`), запускает/останавливает браузер.
- `new_page()` — создаёт новый `BrowserContext` + `Page`. Каждый контекст независим (отдельные куки, заголовки).
- Браузер запускается с антидетект-настройками:
- `--disable-blink-features=AutomationControlled`
- JavaScript-патч `STEALTH_JS`: скрывает `navigator.webdriver`, подставляет плагины и языки.
- Реалистичный `User-Agent` (Chrome 124 Linux).
- Заголовки `Accept-Language: ru-RU`, `Referer: https://3.readmanga.ru/` — сервер возвращает 404 без Referer.
- `shm_size: 2gb` в Docker — Chromium требует shared memory для рендеринга.
---
### scraper.py
**Отвечает за:** парсинг страниц манги и скачивание изображений глав.
#### Модели данных
```python
@dataclass
class Chapter:
title: str # "Том 1, Глава 5"
url: str # полный URL главы
number: float # 5.0 (из data-num / 10)
volume: int # 1
@dataclass
class MangaInfo:
title: str # русский тайтл (для имени папки)
url: str
chapters: list[Chapter]
pub_status: str # "completed" / "ongoing" / "unknown"
title_ru: str # чистый русский тайтл
title_full: str # полный тайтл со страницы
```
#### `get_manga_info(page, url)`
1. Открывает страницу манги через `_navigate()` (3 попытки, задержка 3×N сек).
2. Извлекает тайтл: сначала из DOM (`.names .name`), fallback — парсинг `<title>` (убирает «Манга», «онлайн», английские названия в скобках).
3. Извлекает `pub_status` по тексту `.elem_status .value` (ищет «завершён» / «продолжается»).
4. Раскрывает полный список глав кнопкой «Все главы» (`_expand_chapters`).
5. Парсит главы из `#chapters-list tr.item-row``td[data-num]` (num/10 = номер главы, `data-vol` = том). Fallback: `<a href*="/vol">`.
#### `get_chapter_images_and_download(page, chapter_url, dest_dir, ...)`
Самая сложная функция — обходит CDN-защиту изображений:
1. Открывает главу с параметром `?mtr=1` (флаг согласия 18+).
2. Устанавливает `page.route("**/*", route_handler)` — перехватывает все сетевые запросы.
3. Ждёт `readerInit` в JS-коде страницы — там содержится массив URL всех изображений.
4. Извлекает URL из `readerInit` через JS-eval (`_extract_images_from_js`). Fallback: ищет `img.manga-page` в DOM.
5. Перехватывает img-запросы через `route.fetch()` — запрос идёт браузерным стеком (правильные `Sec-Fetch-*`, cookies DDoS-Guard). Сохраняет байты в памяти.
6. Листает читалку клавишей `ArrowRight` — читалка подгружает страницы лениво.
7. Матчит перехваченные URL с индексами из `readerInit` (основной путь + fallback по имени файла).
8. Сохраняет в `dest_dir/0000.jpg`, `0001.png`, ...
**CDN-фильтр:** принимает только запросы к `one-way.work`, `staticfa.`, `cdnmanga`, `reimg`. Отклоняет статику сайта (`resrmr.`, `/static/`).
---
### exporter.py
**Отвечает за:** сборку файлов из набора изображений с встраиванием метаданных.
#### `MangaMeta` — датакласс метаданных
Передаётся в `export()` из воркера. Поля:
| Поле | Описание |
|------|---------|
| `series` | Название серии (title_ru) |
| `series_full` | Полное название со страницы (title_full) |
| `chapter_title` | Название главы |
| `number` | Номер главы (float) |
| `volume` | Том |
| `chapters_total` | Всего глав (для completed — записывается в Count) |
| `pub_status` | `completed` / `ongoing` / `unknown` |
| `source_url` | URL источника |
| `language` | `ru` |
#### Форматы и метаданные
| Формат | Реализация | Метаданные |
|--------|------------|-----------|
| **CBZ** | `zipfile` + `ComicInfo.xml` первым файлом | Anansi v2 schema: `Series`, `Number`, `Volume`, `Count` (если completed), `LanguageISO`, `Manga=YesAndRightToLeft`, `Web` |
| **PDF** | `img2pdf` (fallback: Pillow) + `pypdf` post-processing | PDF `/Info` (`/Title`, `/Subject`) + XMP Dublin Core (`dc:title`, `dc:description`, `dc:language`, `dc:source`) |
| **EPUB** | `ebooklib` | Dublin Core, `calibre:series` + `calibre:series_index`, EPUB3 `belongs-to-collection` + `group-position` |
`_patch_pdf_meta()` открывает готовый PDF через `pypdf`, добавляет метаданные и перезаписывает файл. Если `pypdf` не установлен — молча пропускает (graceful degradation).
---
### state.py
**Отвечает за:** всё взаимодействие с SQLite БД.
#### Класс `StateDB`
Подключение: `sqlite3.connect(path, check_same_thread=False)`.
#### Таблицы
**`mangas`** — одна строка на мангу:
| Колонка | Тип | Описание |
|---------|-----|---------|
| `url` | TEXT UNIQUE | URL страницы манги |
| `title` / `title_ru` / `title_full` | TEXT | Варианты названия |
| `pub_status` | TEXT | `completed` / `ongoing` / `unknown` |
| `auto_update` | INTEGER | 0/1 — включено ли авто-обновление |
| `status` | TEXT | `queued` / `downloading` / `done` / `failed` / `stopped` |
| `format` | TEXT | `cbz` / `pdf` / `epub` / `all` |
| `chapters_total` | INTEGER | Кол-во глав (из scraper) |
| `chapters_done` | INTEGER | **Стальной счётчик** — не используется в API напрямую¹ |
| `last_checked_at` | TEXT | Время последней проверки новых глав |
¹ API всегда пересчитывает `chapters_done` запросом `SELECT COUNT(*) FROM chapters WHERE status='done'`.
**`chapters`** — одна строка на главу:
| Колонка | Тип | Описание |
|---------|-----|---------|
| `chapter_url` | TEXT UNIQUE | URL главы |
| `manga_url` | TEXT | FK → mangas.url |
| `number` | REAL | Номер главы (5.0) |
| `volume` | INTEGER | Том |
| `status` | TEXT | `pending` / `done` / `failed` |
| `pages_total` / `pages_done` | INTEGER | Прогресс страниц |
| `output_cbz` / `output_pdf` / `output_epub` | TEXT | Путь к файлу |
**`history`** — лог событий:
| `event_type` | Описание |
|---|---|
| `downloaded` | Глава успешно скачана |
| `auto_downloaded` | Скачана в режиме авто-обновления |
| `new_chapter_found` | Найдена новая глава при проверке |
| `check_started` / `check_done` | Начало/конец проверки обновлений |
#### Ключевые методы
- `add_manga(url, fmt)``bool` — добавляет, возвращает `False` если уже есть.
- `upsert_chapter(...)` — INSERT OR UPDATE (по `chapter_url`).
- `chapter_status(chapter_url)``str | None`.
- `sync_chapters_done(url)` — пересчитывает и сохраняет `chapters_done` из таблицы chapters, возвращает число.
- `get_autos()` — манги с `auto_update=1` не в статусе `downloading`.
---
### worker.py
**Отвечает за:** асинхронное скачивание манги с параллельными главами.
#### `download_manga(url, fmt, resume, is_update, on_event, chapter_concurrency)`
Полный цикл скачивания одной манги:
```
1. update_manga_status → "downloading"
2. BrowserManager.__aenter__
3. get_manga_info() → MangaInfo
4. update_manga_info() в БД
5. upsert_chapter() для каждой главы
6. Делим главы:
├── to_skip (status == "done" и resume=True)
└── to_download (всё остальное)
7. Отправляем chapter_skipped события для to_skip
8. asyncio.Semaphore(chapter_concurrency)
9. asyncio.gather(*[process_chapter(ch) for ch in to_download])
10. sync_chapters_done() → update_manga_status → "done"
```
#### `process_chapter(ch)` (внутренняя корутина)
```
async with sem: # ограничение параллельности
1. Повторная проверка chapter_status (race condition guard)
2. ctx.new_page() — новая вкладка в общем контексте
3. get_chapter_images_and_download() → list[Path]
4. export() для каждого формата
5. mark_done() / mark_failed()
6. add_history()
7. chapter_done event
finally: ch_page.close()
```
#### Потокобезопасность
`db_lock = asyncio.Lock()` — все обращения к SQLite через `await db_call(fn, *args)`. Это необходимо, так как несколько корутин работают одновременно, а `sqlite3` не является asyncio-safe.
`counter_lock = asyncio.Lock()` — атомарный инкремент счётчика `chapters_done` для корректных данных в событиях прогресса.
#### `check_for_updates(url, on_event)`
Проверяет новые главы: сравнивает список глав из scraper с известными в БД. Возвращает список новых `chapter_url`.
---
### api.py
**Отвечает за:** FastAPI-приложение — HTTP-сервер, очередь загрузок, планировщик обновлений.
#### Глобальное состояние
```python
download_queue: asyncio.Queue # очередь job'ов {url, fmt, is_update}
active_tasks: dict[str, asyncio.Task] # url → текущая Task загрузки
ws_manager: ConnectionManager # set активных WebSocket-соединений
```
#### Жизненный цикл при старте (`startup_event`)
1. Запускает `queue_worker()` как фоновую Task.
2. Запускает `update_scheduler()` как фоновую Task.
3. Восстанавливает из БД незавершённые задачи (status `queued`/`downloading` → снова в очередь).
#### `queue_worker()`
Последовательно извлекает задачи из `download_queue`. На каждую создаёт `asyncio.Task` через `download_manga()`, сохраняет в `active_tasks`. Обрабатывает `CancelledError` (stop/prioritize).
#### `update_scheduler()`
Через 5 минут после старта, затем каждые `UPDATE_INTERVAL_HOURS` (по умолчанию 6 ч):
- Вызывает `check_for_updates()` для каждой манги с `auto_update=1`.
- При нахождении новых глав — добавляет задачу в очередь с флагом `is_update=True`.
#### `_enrich_manga(m, db)`
Вспомогательная функция: обогащает строку из `mangas` реальными данными:
- `chapters_done``COUNT(*)` из таблицы `chapters` (не стальная колонка).
- `size_bytes` / `size_human` — размер папки на диске.
- `is_active` — есть ли Task в `active_tasks`.
- `errors_count` — сумма failed и partial глав.
Используется в `/api/mangas` и в WebSocket snapshot — гарантирует консистентность данных.
---
### cli.py
**Отвечает за:** консольные команды для запуска без веб-интерфейса.
#### Команды
**`download <URL>`**
```
--format / -f cbz|pdf|epub|all (default: cbz)
--chapters / -c диапазон: "1-10", "5", "1,3,7"
--output / -o папка вывода
--resume пропускать скачанные (default: True)
--force / -F игнорировать БД, скачать заново
--concurrency параллельных загрузок (default: 4)
--verbose / -v DEBUG-вывод
```
**`analyze <URL>`**
Открывает страницу, выводит список всех глав и метаданные без скачивания.
CLI использует те же `BrowserManager`, `scraper`, `exporter`, `StateDB`, что и web-режим, но запускает их напрямую через `asyncio.run()`.
---
## 6. База данных
**Файл:** `/app/state/progress.db` (монтируется из `./state/progress.db` на хосте).
**Открытие:** `sqlite3.connect(path, check_same_thread=False)` — один объект `StateDB` на запрос/worker, не shared между потоками.
**Миграции:** при каждом запуске `StateDB._init()` выполняет `ALTER TABLE ... ADD COLUMN` в блоке `try/except` — безопасно добавляет новые колонки в старые БД.
**Важно:** колонка `mangas.chapters_done` является устаревшим денормализованным счётчиком. В API и WebSocket snapshot всегда используется динамический подсчёт из таблицы `chapters`. Это важно, чтобы не показывать некорректные числа (> chapters_total) после перезапусков.
---
## 7. REST API
Базовый URL: `http://localhost:8000`
| Метод | Путь | Описание |
|-------|------|---------|
| `GET` | `/api/mangas` | Список всех манг с реальными счётчиками |
| `GET` | `/api/mangas/detail?url=` | Детали: главы, файлы, статистика ошибок |
| `POST` | `/api/queue` | Добавить мангу(и) в очередь `{urls: [...], format: "cbz"}` |
| `POST` | `/api/mangas/stop?url=` | Остановить загрузку |
| `POST` | `/api/mangas/resume?url=` | Возобновить |
| `POST` | `/api/mangas/prioritize?url=` | Переместить в начало очереди (вытесняет текущую) |
| `POST` | `/api/mangas/retry_errors?url=` | Сбросить failed/partial главы → pending |
| `POST` | `/api/mangas/auto_update?url=&enabled=` | Вкл/выкл авто-обновление |
| `POST` | `/api/mangas/check_now?url=` | Немедленно проверить новые главы |
| `DELETE` | `/api/mangas?url=` | Удалить мангу из БД |
| `GET` | `/api/stats` | Глобальная статистика (кол-во по статусам, размер) |
| `GET` | `/api/history?limit=&manga_url=` | История событий |
| `GET` | `/api/news?limit=` | Только события `downloaded`/`auto_downloaded` |
| `WS` | `/ws` | WebSocket для realtime-обновлений |
---
## 8. WebSocket протокол
### Сервер → Клиент (события)
| `type` | Данные | Когда |
|--------|--------|-------|
| `snapshot` | `{mangas: [...]}` | При подключении — полный список с реальными счётчиками |
| `manga_queued` | `{url, format}` | Добавлена в очередь |
| `manga_start` | `{url}` | Начало загрузки |
| `manga_info` | `{url, title, chapters_total, pub_status, ...}` | Получены метаданные |
| `manga_done` | `{url, chapters_done, chapters_total}` | Загрузка завершена |
| `manga_failed` | `{url, error}` | Ошибка |
| `manga_stopped` | `{url}` | Остановлена |
| `manga_prioritized` | `{url, preempted_url}` | Приоритет изменён |
| `manga_preview` | `{url, title, chapters_total, ...}` | Быстрый предпросмотр после добавления |
| `chapter_start` | `{url, chapter_url, chapter_number, chapters_done, chapters_total}` | Начало главы |
| `chapter_done` | `{url, chapter_url, chapters_done, chapters_total}` | Глава готова |
| `chapter_skipped` | `{url, chapter_url, chapters_done}` | Глава пропущена (уже скачана) |
| `chapter_failed` | `{url, chapter_url, error?}` | Ошибка главы |
| `page_done` | `{url, chapter_url, pages_done, pages_total}` | Страница скачана |
| `check_started` / `check_done` | `{url, new_chapters?}` | Проверка обновлений |
| `new_chapter_found` | `{url, chapter_url, chapter_number}` | Найдена новая глава |
| `auto_update_changed` | `{url, auto_update}` | Изменён флаг авто-обновления |
### Клиент → Сервер
| Сообщение | Действие |
|-----------|---------|
| `"ping"` | Сервер отвечает `{"type": "pong"}` — keepalive |
---
## 9. Фронтенд
**Файл:** `frontend/index.html` — весь фронтенд в одном HTML-файле (~1000 строк).
**Стек:** Tailwind CSS (CDN), Vanilla JS (без фреймворков и сборщика).
### Архитектура состояния
```javascript
const state = {
mangas: {}, // url → объект манги (из snapshot/API + WS-обновления)
chapters: {}, // url → массив глав (загружается по запросу в модалке)
};
```
### Поток данных
```
DOMContentLoaded
├─ loadStats() ──────────────────────► GET /api/stats
├─ connectWS() ──────────────────────► WS /ws
│ │
│ └─ snapshot event ──────────► state.mangas = enriched list
│ + live events ──────────► state.mangas[url].* обновляется
└─ fetch('/api/mangas') ─────────────► state.mangas = полный список
(перезаписывает snapshot если пришёл раньше)
```
**Важный нюанс порядка:** `connectWS()` не awaited, поэтому snapshot может прийти после `fetch('/api/mangas')` и перезаписать правильные значения устаревшими из БД. Именно поэтому WS snapshot теперь тоже использует `_enrich_manga()` с пересчётом из таблицы `chapters`.
### Вкладки
- **Манга** — список всех манг, добавление, управление.
- **Новости** — события `downloaded`/`auto_downloaded` (что скачалось).
- **История** — все события из таблицы `history`.
### Модальное окно детали
Открывается кликом на строку манги. Загружает `GET /api/mangas/detail?url=` с полным списком глав, файлами на диске, статистикой ошибок.
---
## 10. Жизненный цикл загрузки манги
```
POST /api/queue {urls: ["https://..."]}
├── db.add_manga(url) → status="queued"
├── download_queue.put({url, fmt})
├── ws_manager.broadcast(manga_queued)
└── asyncio.create_task(_fetch_preview(url)) ← быстрый предпросмотр
└── get_manga_info() → ws manga_preview (название, кол-во глав)
queue_worker() (фоновая Task)
└── job = await download_queue.get()
└── asyncio.create_task(download_manga(url, fmt, ...))
├── status → "downloading"
├── get_manga_info() → ws manga_info
├── upsert_chapter() × N
├── to_skip → ws chapter_skipped × M
└── asyncio.gather(
process_chapter(ch1), ─┐
process_chapter(ch2), ├── параллельно
... ─┘
limit: Semaphore(CHAPTER_CONCURRENCY)
)
├── ctx.new_page()
├── get_chapter_images_and_download()
│ ├── page.route() перехват img
│ ├── ArrowRight листание
│ └── сохранение байт
├── export() → .cbz/.pdf/.epub
├── db.mark_done()
└── ws chapter_done / chapter_failed
└── status → "done" / "failed"
└── ws manga_done / manga_failed
```
---
## 11. Параллельная загрузка
### Параллельность глав
`CHAPTER_CONCURRENCY` (env, default `3`) — сколько глав загружается одновременно.
```
asyncio.Semaphore(CHAPTER_CONCURRENCY)
Все N глав запускаются сразу через asyncio.gather(),
но одновременно в браузере открыто не более CHAPTER_CONCURRENCY вкладок.
```
Все вкладки работают в **одном** `BrowserContext` — это важно: cookies DDoS-Guard получены при открытии страницы манги и автоматически применяются ко всем вкладкам контекста.
### Защита от race condition
1. **Повторная проверка статуса** внутри семафора: если пока ждали семафор другая горутина уже скачала эту главу — пропустить.
2. **`db_lock`** — все SQLite-операции сериализованы через `asyncio.Lock()`. `sqlite3` не поддерживает concurrent writes.
3. **`counter_lock`** — атомарный инкремент счётчика `chapters_done` для правильных данных в WS-событиях.
### Параллельность манг
Манги в очереди обрабатываются **последовательно** (один воркер). Параллельная загрузка нескольких манг одновременно не реализована, чтобы не перегружать сайт и не создавать проблем с памятью Chromium.
---
## 12. Конфигурация
### Переменные окружения
| Переменная | Default | Описание |
|------------|---------|---------|
| `CHAPTER_CONCURRENCY` | `3` | Кол-во глав, загружаемых параллельно |
| `UPDATE_INTERVAL_HOURS` | `6` | Интервал авто-проверки новых глав (часы) |
| `PYTHONUNBUFFERED` | `1` | Немедленный вывод логов (Docker) |
### Пути (hardcoded в коде)
| Константа | Путь |
|-----------|------|
| `OUTPUT_DIR` | `/app/output` |
| `FRONTEND_DIR` | `/app/frontend` |
| `DB_PATH` | `/app/state/progress.db` |
| Лог | `/app/state/manga.log` (ротация 10 МБ) |
---
## 13. Docker-инфраструктура
### Dockerfile
```
FROM mcr.microsoft.com/playwright/python:v1.44.0-jammy
└── Ubuntu 22.04 + Python + все системные зависимости для Chromium
RUN pip install -r requirements.txt
RUN playwright install chromium --with-deps
CMD uvicorn src.api:app --host 0.0.0.0 --port 8000
```
### docker-compose.yml
```yaml
volumes:
- ./output:/app/output # CBZ/PDF/EPUB файлы
- ./state:/app/state # БД и логи
ports:
- "8000:8000" # Веб-интерфейс
shm_size: "2gb" # Chromium требует shared memory
environment:
- UPDATE_INTERVAL_HOURS=6
restart: unless-stopped # Автоперезапуск при падении
```
### CLI-режим (через compose run)
```bash
# Скачать мангу без веб-интерфейса
docker compose run --rm --entrypoint "" manga \
python -m src.cli download https://3.readmanga.ru/magicheskaia_bitva --format cbz
# Анализ
docker compose run --rm --entrypoint "" manga \
python -m src.cli analyze https://3.readmanga.ru/magicheskaia_bitva
```
### Хранение данных
После остановки контейнера все данные сохраняются на хосте:
- `./output/` — скачанные файлы.
- `./state/progress.db` — состояние БД (что скачано, что в очереди).
- `./state/manga.log` — логи.
При следующем запуске `startup_event` восстанавливает незавершённые задачи из БД в очередь.