upd
This commit is contained in:
194
ARCHITECTURE.md
194
ARCHITECTURE.md
@@ -249,6 +249,7 @@ class MangaInfo:
|
||||
| `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'`.
|
||||
|
||||
@@ -280,6 +281,8 @@ class MangaInfo:
|
||||
- `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)` — устанавливает кастомное имя папки.
|
||||
|
||||
---
|
||||
|
||||
@@ -297,13 +300,16 @@ class MangaInfo:
|
||||
3. get_manga_info() → MangaInfo
|
||||
4. update_manga_info() в БД
|
||||
5. upsert_chapter() для каждой главы
|
||||
6. Делим главы:
|
||||
6. Определение папки:
|
||||
├── если db.get_manga(url)["folder_name"] задан → использует его
|
||||
└── иначе → _safe_name(title_ru or title)
|
||||
7. Делим главы:
|
||||
├── to_skip (status == "done" и resume=True)
|
||||
└── to_download (всё остальное)
|
||||
7. Отправляем chapter_skipped события для to_skip
|
||||
8. asyncio.Semaphore(chapter_concurrency)
|
||||
9. asyncio.gather(*[process_chapter(ch) for ch in to_download])
|
||||
10. sync_chapters_done() → update_manga_status → "done"
|
||||
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)` (внутренняя корутина)
|
||||
@@ -344,6 +350,12 @@ 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.
|
||||
@@ -425,6 +437,10 @@ CLI использует те же `BrowserManager`, `scraper`, `exporter`, `Sta
|
||||
| `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=` | История событий |
|
||||
@@ -448,6 +464,9 @@ CLI использует те же `BrowserManager`, `scraper`, `exporter`, `Sta
|
||||
| `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}` | Глава пропущена (уже скачана) |
|
||||
@@ -456,6 +475,7 @@ CLI использует те же `BrowserManager`, `scraper`, `exporter`, `Sta
|
||||
| `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}` | Метаданные файлов обновлены |
|
||||
|
||||
### Клиент → Сервер
|
||||
|
||||
@@ -467,7 +487,7 @@ CLI использует те же `BrowserManager`, `scraper`, `exporter`, `Sta
|
||||
|
||||
## 9. Фронтенд
|
||||
|
||||
**Файл:** `frontend/index.html` — весь фронтенд в одном HTML-файле (~1000 строк).
|
||||
**Файл:** `frontend/index.html` — весь фронтенд в одном HTML-файле (~1500 строк).
|
||||
|
||||
**Стек:** Tailwind CSS (CDN), Vanilla JS (без фреймворков и сборщика).
|
||||
|
||||
@@ -506,153 +526,23 @@ DOMContentLoaded
|
||||
|
||||
### Модальное окно детали
|
||||
|
||||
Открывается кликом на строку манги. Загружает `GET /api/mangas/detail?url=` с полным списком глав, файлами на диске, статистикой ошибок.
|
||||
Открывается кликом на строку манги. Загружает `GET /api/mangas/detail?url=` с полным списком глав, файлами на диске, статистикой ошибок. Содержит кнопки:
|
||||
- **✏️ Редактировать название** — открывает модалку изменения `title_ru` / `title_full`. Папка не переименовывается, но метаданные всех файлов обновляются автоматически через `_do_refresh_meta`.
|
||||
- **📁 Переименовать папку** — открывает модалку смены имени папки на диске (доступна при статусе не `downloading`). Физически переименовывает папку, обновляет пути в `chapters.output_*`, сохраняет `folder_name` в `mangas`.
|
||||
- **🏷 Обновить метатеги** — принудительно обновляет метаданные в уже скачанных файлах (для манги в статусе `done`).
|
||||
- **↺ Скачать заново** — сбрасывает все главы и ставит в очередь повторно.
|
||||
|
||||
---
|
||||
### Карточки манги (кнопки)
|
||||
|
||||
## 10. Жизненный цикл загрузки манги
|
||||
| Кнопка | Условие отображения | Действие |
|
||||
|--------|---------------------|---------|
|
||||
| ℹ️ | всегда | Открыть детальное модальное окно |
|
||||
| ⚠️ N | `errors_count > 0` | Открыть вкладку ошибок в модалке |
|
||||
| ⏸ | `status` = `downloading` или `queued` | Остановить загрузку |
|
||||
| ▶ | `status` = `stopped` или `failed` | Возобновить |
|
||||
| 🚀 | `status` = `queued` (только в очереди, не активная) | Переместить в начало очереди |
|
||||
| ✕ | всегда | Удалить |
|
||||
|
||||
```
|
||||
POST /api/queue {urls: ["https://..."]}
|
||||
│
|
||||
├── db.add_manga(url) → status="queued"
|
||||
├── download_queue.put({url, fmt})
|
||||
├── ws_manager.broadcast(manga_queued)
|
||||
└── asyncio.create_task(_fetch_preview(url)) ← быстрый предпросмотр
|
||||
│
|
||||
└── get_manga_info() → ws manga_preview (название, кол-во глав)
|
||||
|
||||
queue_worker() (фоновая Task)
|
||||
│
|
||||
└── job = await download_queue.get()
|
||||
│
|
||||
└── asyncio.create_task(download_manga(url, fmt, ...))
|
||||
│
|
||||
├── status → "downloading"
|
||||
├── get_manga_info() → ws manga_info
|
||||
├── upsert_chapter() × N
|
||||
│
|
||||
├── to_skip → ws chapter_skipped × M
|
||||
│
|
||||
└── asyncio.gather(
|
||||
process_chapter(ch1), ─┐
|
||||
process_chapter(ch2), ├── параллельно
|
||||
... ─┘
|
||||
limit: Semaphore(CHAPTER_CONCURRENCY)
|
||||
)
|
||||
│
|
||||
├── ctx.new_page()
|
||||
├── get_chapter_images_and_download()
|
||||
│ ├── page.route() перехват img
|
||||
│ ├── ArrowRight листание
|
||||
│ └── сохранение байт
|
||||
├── export() → .cbz/.pdf/.epub
|
||||
├── db.mark_done()
|
||||
└── ws chapter_done / chapter_failed
|
||||
|
||||
└── status → "done" / "failed"
|
||||
└── ws manga_done / manga_failed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Параллельная загрузка
|
||||
|
||||
### Параллельность глав
|
||||
|
||||
`CHAPTER_CONCURRENCY` (env, default `3`) — сколько глав загружается одновременно.
|
||||
|
||||
```
|
||||
asyncio.Semaphore(CHAPTER_CONCURRENCY)
|
||||
|
||||
Все N глав запускаются сразу через asyncio.gather(),
|
||||
но одновременно в браузере открыто не более CHAPTER_CONCURRENCY вкладок.
|
||||
```
|
||||
|
||||
Все вкладки работают в **одном** `BrowserContext` — это важно: cookies DDoS-Guard получены при открытии страницы манги и автоматически применяются ко всем вкладкам контекста.
|
||||
|
||||
### Защита от race condition
|
||||
|
||||
1. **Повторная проверка статуса** внутри семафора: если пока ждали семафор другая горутина уже скачала эту главу — пропустить.
|
||||
2. **`db_lock`** — все SQLite-операции сериализованы через `asyncio.Lock()`. `sqlite3` не поддерживает concurrent writes.
|
||||
3. **`counter_lock`** — атомарный инкремент счётчика `chapters_done` для правильных данных в WS-событиях.
|
||||
|
||||
### Параллельность манг
|
||||
|
||||
Манги в очереди обрабатываются **последовательно** (один воркер). Параллельная загрузка нескольких манг одновременно не реализована, чтобы не перегружать сайт и не создавать проблем с памятью Chromium.
|
||||
|
||||
---
|
||||
|
||||
## 12. Конфигурация
|
||||
|
||||
### Переменные окружения
|
||||
|
||||
| Переменная | Default | Описание |
|
||||
|------------|---------|---------|
|
||||
| `CHAPTER_CONCURRENCY` | `3` | Кол-во глав, загружаемых параллельно |
|
||||
| `UPDATE_INTERVAL_HOURS` | `6` | Интервал авто-проверки новых глав (часы) |
|
||||
| `PYTHONUNBUFFERED` | `1` | Немедленный вывод логов (Docker) |
|
||||
|
||||
### Пути (hardcoded в коде)
|
||||
|
||||
| Константа | Путь |
|
||||
|-----------|------|
|
||||
| `OUTPUT_DIR` | `/app/output` |
|
||||
| `FRONTEND_DIR` | `/app/frontend` |
|
||||
| `DB_PATH` | `/app/state/progress.db` |
|
||||
| Лог | `/app/state/manga.log` (ротация 10 МБ) |
|
||||
|
||||
---
|
||||
|
||||
## 13. Docker-инфраструктура
|
||||
|
||||
### Dockerfile
|
||||
|
||||
```
|
||||
FROM mcr.microsoft.com/playwright/python:v1.44.0-jammy
|
||||
└── Ubuntu 22.04 + Python + все системные зависимости для Chromium
|
||||
|
||||
RUN pip install -r requirements.txt
|
||||
RUN playwright install chromium --with-deps
|
||||
|
||||
CMD uvicorn src.api:app --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
### docker-compose.yml
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- ./output:/app/output # CBZ/PDF/EPUB файлы
|
||||
- ./state:/app/state # БД и логи
|
||||
|
||||
ports:
|
||||
- "8000:8000" # Веб-интерфейс
|
||||
|
||||
shm_size: "2gb" # Chromium требует shared memory
|
||||
environment:
|
||||
- UPDATE_INTERVAL_HOURS=6
|
||||
|
||||
restart: unless-stopped # Автоперезапуск при падении
|
||||
```
|
||||
|
||||
### CLI-режим (через compose run)
|
||||
|
||||
```bash
|
||||
# Скачать мангу без веб-интерфейса
|
||||
docker compose run --rm --entrypoint "" manga \
|
||||
python -m src.cli download https://3.readmanga.ru/magicheskaia_bitva --format cbz
|
||||
|
||||
# Анализ
|
||||
docker compose run --rm --entrypoint "" manga \
|
||||
python -m src.cli analyze https://3.readmanga.ru/magicheskaia_bitva
|
||||
```
|
||||
|
||||
### Хранение данных
|
||||
|
||||
После остановки контейнера все данные сохраняются на хосте:
|
||||
- `./output/` — скачанные файлы.
|
||||
- `./state/progress.db` — состояние БД (что скачано, что в очереди).
|
||||
- `./state/manga.log` — логи.
|
||||
|
||||
При следующем запуске `startup_event` восстанавливает незавершённые задачи из БД в очередь.
|
||||
### Позиции в очереди
|
||||
|
||||
Отображаются на карточке как «Позиция в очереди: N». Обновляются в реальном времени через событие `queue_positions` (не перерендер всего списка, а точечное обновление через `updateMangaRow`). Событие рассылается сервером при каждом изменении состояния очереди.
|
||||
|
||||
Reference in New Issue
Block a user