Files
manga/ARCHITECTURE.md
2026-05-01 03:27:33 +03:00

43 KiB
Raw Blame History

Manga Downloader — Архитектура и устройство проекта

Содержание

  1. Общее описание
  2. Структура файлов
  3. Стек технологий
  4. Схема архитектуры
  5. Модули бэкенда
  6. База данных
  7. REST API
  8. WebSocket протокол
  9. Фронтенд
  10. Жизненный цикл загрузки манги
  11. Параллельная загрузка
  12. Конфигурация
  13. Docker-инфраструктура

1. Общее описание

Приложение скачивает мангу с сайтов типа readmanga.ru, обходя JS-защиту (DDoS-Guard, антибот) с помощью управляемого браузера Chromium. Поддерживает два режима работы:

  • Веб-интерфейс — FastAPI-сервер + SPA на чистом JS (порт 8000). Позволяет управлять загрузками через браузер с realtime-прогрессом через WebSocket. Требует входа — поддерживает многопользовательский режим с ролями admin / user.
  • CLI — консольные команды download и analyze для запуска через docker compose run.

2. Структура файлов

manga/
├── src/                        # Весь бэкенд-код (Python-пакет)
│   ├── __init__.py
│   ├── api.py                  # FastAPI-приложение, REST + WebSocket
│   ├── auth.py                 # Хеширование паролей, генерация токенов сессий
│   ├── 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)        │
│                                 │
│  Auth middleware (cookie/session)│
│  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. Модули бэкенда

auth.py

Отвечает за: хеширование паролей и генерацию токенов сессий.

Константы:

COOKIE_NAME = "manga_session"
COOKIE_MAX_AGE = 30 * 24 * 3600  # 30 дней

Функции:

  • hash_password(password)str — хеширует пароль методом PBKDF2-SHA256, 260 000 итераций, случайная соль. Формат результата: pbkdf2:260000:<salt_hex>:<key_hex>.
  • verify_password(password, hashed)bool — проверяет пароль по хешу через hmac.compare_digest (защита от timing-атак).
  • generate_session_token()str — генерирует 48-байтный URL-safe токен (secrets.token_urlsafe).

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

Отвечает за: парсинг страниц манги и скачивание изображений глав.

Модели данных

@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-rowtd[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 Денормализованный счётчик¹
last_checked_at TEXT Время последней проверки новых глав
folder_name TEXT Кастомное имя папки на диске (NULL → вычисляется из title_ru)
source_id INTEGER FK → sources.id — источник манги
added_by INTEGER FK → users.id — кто добавил мангу

¹ 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 Начало/конец проверки обновлений

sources — источники (определяются в коде приложения):

Колонка Тип Описание
slug TEXT UNIQUE Идентификатор источника (readmanga)
display_name TEXT Отображаемое название
settings TEXT JSON-настройки источника

source_domains — домены, привязанные к источникам:

Колонка Тип Описание
source_id INTEGER FK → sources.id
domain TEXT UNIQUE Домен без схемы и www (readmanga.ru)

При первом запуске домены ReadManga автоматически засеиваются из списка _DEFAULT_READMANGA_DOMAINS в коде.

users — пользователи:

Колонка Тип Описание
id INTEGER PK
username TEXT UNIQUE Логин
password TEXT Хеш пароля (pbkdf2:iterations:salt:key)
role TEXT admin / user
is_env_admin INTEGER 1 — системный администратор из AUTH_LOGIN/AUTH_PASSWORD; его пароль нельзя изменить через интерфейс
created_at / updated_at TEXT ISO-8601 timestamp

sessions — активные сессии:

Колонка Тип Описание
token TEXT PK URL-safe токен (48 байт)
user_id INTEGER FK → users.id ON DELETE CASCADE
created_at / expires_at TEXT Сессия действительна 30 дней

Ключевые методы

  • add_manga(url, fmt, source_id, added_by)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) — устанавливает кастомное имя папки.
  • create_user(username, hashed_password, role, is_env_admin)dict — создаёт пользователя.
  • get_user_by_id(id) / get_user_by_username(username)dict | None.
  • get_all_users()list[dict] — все пользователи без поля password.
  • update_user(user_id, **kwargs) — обновляет разрешённые поля (username, password, role).
  • delete_user(user_id) — удаляет пользователя и все его сессии.
  • create_session(token, user_id, expires_at) — создаёт сессию.
  • get_session(token)dict | None — возвращает только не истёкшие сессии.
  • cleanup_expired_sessions()int — удаляет истёкшие сессии.

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-сервер, аутентификация, очередь загрузок, планировщик обновлений.

Auth-зависимости (Depends)

get_current_user(request)  # читает токен из cookie, валидирует сессию → dict пользователя
require_admin(user)         # get_current_user + проверка role == "admin"
_check_manga_access(manga, user)  # admin: полный доступ; user: только свои манги

Все endpoint'ы (кроме /api/login и /api/auth/check) требуют валидной сессии.

Глобальное состояние

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().
  • _broadcast_queue_positions() — отправляет всем WS-клиентам событие queue_positions. Вызывается при любом изменении очереди.

Жизненный цикл при старте (startup_event)

  1. Синхронизирует sources из кода реестра с БД (sync_sources).
  2. Авто-мигрирует source_id для манг без него (migrate_manga_sources).
  3. Удаляет истёкшие сессии (cleanup_expired_sessions).
  4. Bootstrap-admin: если таблица users пуста — создаёт пользователя из AUTH_LOGIN/AUTH_PASSWORD с ролью admin и флагом is_env_admin=True.
  5. Запускает queue_worker() и update_scheduler() как фоновые Task.
  6. Восстанавливает незавершённые задачи из БД (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 минут и повторяет; после трёх неудач логирует и ждёт следующего слота. Цикл никогда не прерывается.

_enrich_manga(m, db)

Вспомогательная функция: обогащает строку из mangas реальными данными:

  • chapters_doneCOUNT(*) из таблицы chapters (не денормализованная колонка).
  • size_bytes / size_human — размер папки на диске.
  • is_active — есть ли Task в active_tasks.
  • errors_count — сумма failed и partial глав.

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

Все endpoint'ы, кроме /api/login и /api/auth/check, требуют валидной сессии (cookie manga_session). Endpoint'ы с пометкой [admin] доступны только пользователям с ролью admin.

Аутентификация

Метод Путь Описание
GET /api/auth/check Проверить текущую сессию. Возвращает {authenticated, user: {id, username, role, is_env_admin}}
POST /api/login Войти {login, password}. Устанавливает cookie сессии на 30 дней
POST /api/logout Выйти. Удаляет сессию из БД и cookie

Управление пользователями [admin]

Метод Путь Описание
GET /api/users Список всех пользователей (без паролей)
POST /api/users Создать пользователя {username, password, role}
PATCH /api/users/{user_id} Изменить {username?, password?, role?}. Для is_env_admin=1 смена пароля заблокирована. Обычный пользователь может менять только свой пароль
DELETE /api/users/{user_id} Удалить пользователя. Системного администратора и последнего admin удалить нельзя

Источники

Метод Путь Описание
GET /api/sources Список источников с доменами и настройками
GET /api/resolve-source?url= Определить источник по URL манги
POST /api/sources/{id}/domains [admin] Добавить домен к источнику {domain}
DELETE /api/sources/{id}/domains/{domain} [admin] Удалить домен у источника

Манги

Метод Путь Описание
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= [admin] Переместить в начало очереди (вытесняет текущую)
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= [admin] Сбросить все главы и поставить в очередь заново
POST /api/mangas/switch-source [admin] Сменить источник манги {manga_url, source_id}
DELETE /api/mangas?url= [admin] Удалить мангу из БД

Прочее

Метод Путь Описание
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} Метаданные файлов обновлены
source_domain_added {source_id, domain} Добавлен домен к источнику
source_domain_removed {source_id, domain} Домен удалён у источника

Клиент → Сервер

Сообщение Действие
"ping" Сервер отвечает {"type": "pong"} — keepalive

9. Фронтенд

Файл: frontend/index.html — весь фронтенд в одном HTML-файле (~2350 строк).

Стек: Tailwind CSS (CDN), Vanilla JS (без фреймворков и сборщика).

Экран входа

При загрузке приложение вызывает GET /api/auth/check. Если сессия невалидна — показывается экран входа (#login-screen). После успешного POST /api/login сессия сохраняется в cookie, экран входа скрывается.

state.currentUser содержит {id, username, role, is_env_admin} — используется для скрытия/показа элементов интерфейса в зависимости от роли.

Архитектура состояния

const state = {
  mangas: {},       // url → объект манги (из snapshot/API + WS-обновления)
  chapters: {},     // url → массив глав (загружается по запросу в модалке)
  currentUser: null, // {id, username, role, is_env_admin}
  sources: [],      // список источников
};

Поток данных

DOMContentLoaded
    │
    ├─ checkAuth() ──────────────────────► GET /api/auth/check
    │       │                               (если ok → initApp())
    │       └─ showLoginScreen()
    │
    └─ initApp()
        ├─ loadStats() ──────────────────► GET /api/stats
        ├─ connectWS() ──────────────────► WS /ws
        │       │
        │       └─ snapshot event ───────► state.mangas = enriched list
        │           + live events ───────► state.mangas[url].* обновляется
        │
        └─ fetch('/api/mangas') ──────────► state.mangas = полный список

Важный нюанс порядка: connectWS() не awaited, поэтому snapshot может прийти после fetch('/api/mangas') и перезаписать правильные значения устаревшими из БД. Именно поэтому WS snapshot тоже использует _enrich_manga() с пересчётом из таблицы chapters.

Вкладки

  • Манга — список всех манг, добавление, управление.
  • Новости — события downloaded/auto_downloaded (что скачалось).
  • История — все события из таблицы history.
  • Настройки — управление источниками, пользователями (только admin), смена своего пароля.

Вкладка «Настройки»

При открытии загружает:

  • Список источников (GET /api/sources) с управлением доменами (только admin).
  • Список пользователей (GET /api/users, только admin) — в разделе Пользователи.
  • Раздел Сменить пароль — скрыт для системного администратора (is_env_admin=true).

Управление пользователями (только admin):

  • Создание: кнопка «+ Добавить» → модалка с логином, паролем, ролью.
  • Редактирование: кнопка ✏️ → модалка. Для системного администратора (is_env_admin) поле пароля скрыто.
  • Удаление: кнопка ✕ (недоступна для системного администратора и для самого себя).
  • Системный администратор помечен иконкой 🔒 с тултипом.

Модальное окно детали

Открывается кликом на строку манги. Загружает GET /api/mangas/detail?url= с полным списком глав, файлами на диске, статистикой ошибок. Содержит кнопки:

  • ✏️ Редактировать название — открывает модалку изменения title_ru / title_full. Папка не переименовывается, но метаданные всех файлов обновляются автоматически через _do_refresh_meta.
  • 📁 Переименовать папку — открывает модалку смены имени папки на диске (доступна при статусе не downloading). Физически переименовывает папку, обновляет пути в chapters.output_*, сохраняет folder_name в mangas.
  • 🏷 Обновить метатеги — принудительно обновляет метаданные в уже скачанных файлах (для манги в статусе done).
  • ↺ Скачать заново [admin] — сбрасывает все главы и ставит в очередь повторно.

Карточки манги (кнопки)

Кнопка Условие отображения Действие
всегда Открыть детальное модальное окно
⚠️ N errors_count > 0 Открыть вкладку ошибок в модалке
status = downloading или queued Остановить загрузку
status = stopped или failed Возобновить
🚀 status = queued (только в очереди, не активная) Переместить в начало очереди
[admin] всегда Удалить

Позиции в очереди

Отображаются на карточке как «Позиция в очереди: N». Обновляются в реальном времени через событие queue_positions (не перерендер всего списка, а точечное обновление через updateMangaRow). Событие рассылается сервером при каждом изменении состояния очереди.


12. Конфигурация

Переменные окружения

Переменная Default Описание
AUTH_LOGIN Логин системного администратора. Создаётся при первом старте, если таблица users пуста
AUTH_PASSWORD Пароль системного администратора. Для смены — изменить переменную и пересоздать контейнер
CHAPTER_CONCURRENCY 3 Кол-во глав, загружаемых параллельно
UPDATE_SCHEDULE Расписание авто-проверки (cron-синтаксис). Пример: 0 */6 * * *. Если пусто — планировщик отключён
UPDATE_INTERVAL_HOURS Устаревший аналог UPDATE_SCHEDULE: число часов → конвертируется в cron автоматически
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

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)

# Скачать мангу без веб-интерфейса
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 восстанавливает незавершённые задачи из БД в очередь.