Files
manga/ARCHITECTURE.md
2026-04-30 17:45:16 +03:00

631 lines
35 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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-файла |
| Расписание | `croniter==3.0.3` | Парсинг cron-выражений для планировщика |
| Логирование | `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 | Время последней проверки новых глав |
| `folder_name` | TEXT | Кастомное имя папки на диске (NULL → вычисляется из `title_ru`) |
¹ 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`.
- `update_manga_meta_fields(url, title_ru, title_full)` — обновляет пользовательские метаданные (название), не меняет папку.
- `set_folder_name(url, folder_name)` — устанавливает кастомное имя папки.
---
### 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. Определение папки:
├── если db.get_manga(url)["folder_name"] задан → использует его
└── иначе → _safe_name(title_ru or title)
7. Делим главы:
├── to_skip (status == "done" и resume=True)
└── to_download (всё остальное)
8. Отправляем chapter_skipped события для to_skip
9. asyncio.Semaphore(chapter_concurrency)
10. asyncio.gather(*[process_chapter(ch) for ch in to_download])
11. 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-соединений
```
#### Вспомогательные функции
- `_safe_name(s)` — транслитерирует строку в безопасное имя папки (strip спецсимволов, пробелы → `_`, max 80 символов).
- `_manga_folder(m)` — возвращает `Path` к папке манги: если `m["folder_name"]` задан — использует его, иначе вычисляет из `title` через `_safe_name()`. Используется везде: `_enrich_manga`, `_manga_detail`, `delete_manga`, `rename_folder`.
- `_broadcast_queue_positions()` — отправляет всем WS-клиентам событие `queue_positions` с актуальным словарём `{url: позиция}`. Вызывается после любого изменения очереди: старт/конец задачи в воркере, `prioritize`, `stop`, `resume`, `add_to_queue`, `force_redownload`.
#### Жизненный цикл при старте (`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_SCHEDULE` (cron-синтаксис):
- Читает расписание через `_parse_schedule()`: приоритет `UPDATE_SCHEDULE` (cron-строка) → `UPDATE_INTERVAL_HOURS` (legacy, число часов → конвертируется в cron автоматически). Если обе переменные пусты — планировщик не запускается.
- Вычисляет время до следующего слота через `croniter.get_next()` и спит ровно до него.
- Запускает `_run_auto_updates_with_retry()` — обёртка с **тремя попытками**: при ошибке ждёт 5 минут и повторяет; после трёх неудач логирует и ждёт следующего слота. Цикл **никогда не прерывается**.
- `_run_auto_updates()` — сама логика: вызывает `check_for_updates()` для каждой манги с `auto_update=1`, ставит новые главы в очередь.
#### `_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=` | Немедленно проверить новые главы |
| `POST` | `/api/mangas/update_meta` | Изменить `title_ru`/`title_full` и применить к метаданным файлов `{url, title_ru, title_full}` |
| `POST` | `/api/mangas/rename_folder` | Переименовать папку на диске и обновить пути в БД `{url, folder_name}` |
| `POST` | `/api/mangas/refresh_meta?url=` | Обновить метаданные в уже скачанных файлах |
| `POST` | `/api/mangas/force_redownload?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, ...}` | Быстрый предпросмотр после добавления |
| `manga_meta_updated` | `{url, title, title_ru, title_full}` | Метаданные отредактированы пользователем |
| `manga_folder_renamed` | `{url, folder_name}` | Папка переименована |
| `queue_positions` | `{positions: {url: номер}}` | Актуальные позиции в очереди — отправляется при любом изменении очереди |
| `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}` | Изменён флаг авто-обновления |
| `meta_refreshed` | `{url, updated, failed}` | Метаданные файлов обновлены |
### Клиент → Сервер
| Сообщение | Действие |
|-----------|---------|
| `"ping"` | Сервер отвечает `{"type": "pong"}` — keepalive |
---
## 9. Фронтенд
**Файл:** `frontend/index.html` — весь фронтенд в одном HTML-файле (~1500 строк).
**Стек:** 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=` с полным списком глав, файлами на диске, статистикой ошибок. Содержит кнопки:
- **✏️ Редактировать название** — открывает модалку изменения `title_ru` / `title_full`. Папка не переименовывается, но метаданные всех файлов обновляются автоматически через `_do_refresh_meta`.
- **📁 Переименовать папку** — открывает модалку смены имени папки на диске (доступна при статусе не `downloading`). Физически переименовывает папку, обновляет пути в `chapters.output_*`, сохраняет `folder_name` в `mangas`.
- **🏷 Обновить метатеги** — принудительно обновляет метаданные в уже скачанных файлах (для манги в статусе `done`).
- **↺ Скачать заново** — сбрасывает все главы и ставит в очередь повторно.
### Карточки манги (кнопки)
| Кнопка | Условие отображения | Действие |
|--------|---------------------|---------|
| | всегда | Открыть детальное модальное окно |
| ⚠️ N | `errors_count > 0` | Открыть вкладку ошибок в модалке |
| ⏸ | `status` = `downloading` или `queued` | Остановить загрузку |
| ▶ | `status` = `stopped` или `failed` | Возобновить |
| 🚀 | `status` = `queued` (только в очереди, не активная) | Переместить в начало очереди |
| ✕ | всегда | Удалить |
### Позиции в очереди
Отображаются на карточке как «Позиция в очереди: N». Обновляются в реальном времени через событие `queue_positions` (не перерендер всего списка, а точечное обновление через `updateMangaRow`). Событие рассылается сервером при каждом изменении состояния очереди.
---
## 12. Конфигурация
### Переменные окружения
| Переменная | Default | Описание |
|------------|---------|---------|
| `CHAPTER_CONCURRENCY` | `3` | Кол-во глав, загружаемых параллельно |
| `UPDATE_SCHEDULE` | — | Расписание авто-проверки (cron-синтаксис). Пример: `0 */6 * * *`. Если пусто — планировщик отключён |
| `UPDATE_INTERVAL_HOURS` | — | Устаревший аналог `UPDATE_SCHEDULE`: число часов → конвертируется в cron автоматически |
| `AUTH_LOGIN` / `AUTH_PASSWORD` | — | Логин и пароль для веб-интерфейса. Если оба заданы — включается авторизация |
| `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_SCHEDULE=0 */6 * * * # каждые 6 часов (cron)
- AUTH_LOGIN=...
- AUTH_PASSWORD=...
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` восстанавливает незавершённые задачи из БД в очередь.