From 77592c9a5579c1ac110fb9532a3c3818078d845e Mon Sep 17 00:00:00 2001 From: StenFredd Date: Thu, 30 Apr 2026 17:18:03 +0300 Subject: [PATCH] upd --- ARCHITECTURE.md | 194 +++++++++++------------------------------------- README.md | 29 ++++++++ 2 files changed, 71 insertions(+), 152 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index a7cd719..67cd76a 100644 --- a/ARCHITECTURE.md +++ b/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`). Событие рассылается сервером при каждом изменении состояния очереди. diff --git a/README.md b/README.md index a325acf..1690ed2 100644 --- a/README.md +++ b/README.md @@ -130,4 +130,33 @@ output/ Для завершённых серий (`pub_status = completed`) в `ComicInfo.xml` записывается поле `` — Komga отображает прогресс чтения серии. +--- + +## Редактирование метаданных + +Через веб-интерфейс можно изменить название серии, не перекачивая файлы: + +1. Кликните на строку манги → откроется окно деталей. +2. Нажмите **✏️ Редактировать название**. +3. Измените «Название (ru)» и/или «Полное название». +4. Нажмите **Сохранить** — метаданные обновятся автоматически во всех скачанных файлах. + +> **Важно:** папка на диске при этом **не переименовывается**. Чтобы переименовать папку — используйте отдельную функцию ниже. + +--- + +## Переименование папки + +Через веб-интерфейс можно изменить имя папки, в которую сохраняются файлы манги: + +1. Кликните на строку манги → откроется окно деталей. +2. Нажмите **📁 Переименовать папку**. +3. Введите новое имя (спецсимволы удалятся автоматически, пробелы заменятся на `_`). +4. Нажмите **Переименовать**. + +После переименования: +- Физическая папка на диске будет переименована. +- Пути ко всем уже скачанным файлам обновятся в БД. +- Дозагрузка новых глав продолжится в переименованную папку. +