# 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 — парсинг `` (убирает «Манга», «онлайн», английские названия в скобках). 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` восстанавливает незавершённые задачи из БД в очередь.