35 KiB
Manga Downloader — Архитектура и устройство проекта
Содержание
- Общее описание
- Структура файлов
- Стек технологий
- Схема архитектуры
- Модули бэкенда
- База данных
- REST API
- WebSocket протокол
- Фронтенд
- Жизненный цикл загрузки манги
- Параллельная загрузка
- Конфигурация
- 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 |
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
Отвечает за: парсинг страниц манги и скачивание изображений глав.
Модели данных
@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)
- Открывает страницу манги через
_navigate()(3 попытки, задержка 3×N сек). - Извлекает тайтл: сначала из DOM (
.names .name), fallback — парсинг<title>(убирает «Манга», «онлайн», английские названия в скобках). - Извлекает
pub_statusпо тексту.elem_status .value(ищет «завершён» / «продолжается»). - Раскрывает полный список глав кнопкой «Все главы» (
_expand_chapters). - Парсит главы из
#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-защиту изображений:
- Открывает главу с параметром
?mtr=1(флаг согласия 18+). - Устанавливает
page.route("**/*", route_handler)— перехватывает все сетевые запросы. - Ждёт
readerInitв JS-коде страницы — там содержится массив URL всех изображений. - Извлекает URL из
readerInitчерез JS-eval (_extract_images_from_js). Fallback: ищетimg.manga-pageв DOM. - Перехватывает img-запросы через
route.fetch()— запрос идёт браузерным стеком (правильныеSec-Fetch-*, cookies DDoS-Guard). Сохраняет байты в памяти. - Листает читалку клавишей
ArrowRight— читалка подгружает страницы лениво. - Матчит перехваченные URL с индексами из
readerInit(основной путь + fallback по имени файла). - Сохраняет в
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 |
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-сервер, очередь загрузок, планировщик обновлений.
Глобальное состояние
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)
- Запускает
queue_worker()как фоновую Task. - Запускает
update_scheduler()как фоновую Task. - Восстанавливает из БД незавершённые задачи (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 (без фреймворков и сборщика).
Архитектура состояния
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
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 восстанавливает незавершённые задачи из БД в очередь.