optimization
This commit is contained in:
@@ -19,4 +19,4 @@ VOLUME ["/app/output", "/app/state"]
|
|||||||
|
|
||||||
# По умолчанию запускаем веб-сервер
|
# По умолчанию запускаем веб-сервер
|
||||||
ENTRYPOINT []
|
ENTRYPOINT []
|
||||||
CMD ["uvicorn", "src.api:app", "--host", "0.0.0.0", "--port", "8000"]
|
CMD ["uvicorn", "src.api:app", "--host", "0.0.0.0", "--port", "8000", "--no-access-log"]
|
||||||
|
|||||||
@@ -1,352 +0,0 @@
|
|||||||
# План реализации: Multi-Source архитектура
|
|
||||||
|
|
||||||
Рефакторинг под систему плагинов-адаптеров: каждый источник — отдельный класс с унифицированным `Protocol`-интерфейсом. Новые таблицы `sources` / `source_domains` в БД, автоопределение источника по домену URL, CRUD-API для доменов и UI-компоненты во фронтенде. Существующий `scraper.py` становится адаптером `ReadmangaSource`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Архитектура системы источников
|
|
||||||
|
|
||||||
**Организация**: `Protocol`-интерфейс + реестр (`SourceRegistry`) + slug-имена в коде.
|
|
||||||
|
|
||||||
Создать `src/sources/` — пакет с адаптерами:
|
|
||||||
|
|
||||||
```
|
|
||||||
src/sources/
|
|
||||||
__init__.py ← реестр + фабрика
|
|
||||||
base.py ← MangaSourceProtocol (Protocol-класс)
|
|
||||||
readmanga.py ← ReadmangaSource (перенесённый scraper.py)
|
|
||||||
```
|
|
||||||
|
|
||||||
### `base.py` — Protocol-интерфейс
|
|
||||||
|
|
||||||
```python
|
|
||||||
class MangaSourceProtocol(Protocol):
|
|
||||||
slug: str # "readmanga" — уникальный код в коде
|
|
||||||
display_name: str # "ReadManga" — для UI
|
|
||||||
|
|
||||||
async def get_manga_info(self, page, url) -> Optional[MangaInfo]: ...
|
|
||||||
async def get_chapter_images_and_download(
|
|
||||||
self, page, chapter_url, dest_dir, ...
|
|
||||||
) -> list[Path]: ...
|
|
||||||
```
|
|
||||||
|
|
||||||
### `__init__.py` — реестр и резолвинг
|
|
||||||
|
|
||||||
`SourceRegistry` — dict `slug → instance`. Список источников **определяется только в коде** — новый источник добавляется созданием нового класса и регистрацией в реестре. Через API управлять можно **только доменами**.
|
|
||||||
|
|
||||||
Экспортирует:
|
|
||||||
|
|
||||||
- `registry.get_by_slug(slug)` — по коду источника
|
|
||||||
- `registry.get_by_id(source_id, db)` — через БД: `sources.id → slug → экземпляр`
|
|
||||||
- `registry.all()` — полный список зарегистрированных источников (для синхронизации с БД и отображения в UI)
|
|
||||||
- `get_source_for_url(url, db)` — извлекает домен из URL, ищет в `source_domains`, возвращает адаптер или `None` (домен неизвестен)
|
|
||||||
|
|
||||||
### `readmanga.py` — `ReadmangaSource`
|
|
||||||
|
|
||||||
Класс с `slug = "readmanga"`. Весь текущий код `scraper.py` переезжает сюда без изменений. CDN-фильтр вынесен в атрибут `cdn_patterns: list[str]`, который можно переопределить настройками из `sources.settings` (JSON). Адаптер самодостаточен.
|
|
||||||
|
|
||||||
### Добавление нового источника
|
|
||||||
|
|
||||||
Создать файл `src/sources/mysource.py`, реализовать Protocol, зарегистрировать:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# src/sources/__init__.py
|
|
||||||
from .readmanga import ReadmangaSource
|
|
||||||
from .mysource import MySource
|
|
||||||
|
|
||||||
registry = SourceRegistry([
|
|
||||||
ReadmangaSource(),
|
|
||||||
MySource(),
|
|
||||||
])
|
|
||||||
```
|
|
||||||
|
|
||||||
При следующем старте приложения `StateDB._sync_sources()` автоматически добавит запись нового источника в таблицу `sources` (если её ещё нет). Удалять источники из кода не рекомендуется без предварительной миграции манг.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Изменения БД
|
|
||||||
|
|
||||||
### Новые таблицы
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE IF NOT EXISTS sources (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
slug TEXT UNIQUE NOT NULL, -- "readmanga" — совпадает с кодом
|
|
||||||
display_name TEXT NOT NULL,
|
|
||||||
settings TEXT DEFAULT '{}', -- JSON: cdn_patterns и др.
|
|
||||||
created_at TEXT
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS source_domains (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
source_id INTEGER NOT NULL REFERENCES sources(id),
|
|
||||||
domain TEXT UNIQUE NOT NULL -- "readmanga.ru", "readmanga.live"
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Изменение таблицы `mangas`
|
|
||||||
|
|
||||||
```sql
|
|
||||||
ALTER TABLE mangas ADD COLUMN source_id INTEGER REFERENCES sources(id);
|
|
||||||
```
|
|
||||||
|
|
||||||
Добавляется через существующий паттерн миграций в `StateDB._init()`.
|
|
||||||
|
|
||||||
### Синхронизация источников с кодом (`_sync_sources`)
|
|
||||||
|
|
||||||
При старте (в `_init()`) вызывается `_sync_sources(registry)`:
|
|
||||||
1. Для каждого источника из реестра — вставить запись в `sources` если ещё нет (по `slug`).
|
|
||||||
2. Обновить `display_name` если изменился.
|
|
||||||
3. **Не удалять** источники из БД даже если они убраны из реестра — только логировать предупреждение.
|
|
||||||
|
|
||||||
### Авто-миграция существующих манг
|
|
||||||
|
|
||||||
При старте пройтись по всем мангам с `source_id IS NULL`, определить домен из `url`, проставить `source_id` по совпадению в `source_domains`. Если домен не найден — оставить `NULL` (отобразится в UI как «источник не определён»).
|
|
||||||
|
|
||||||
### Сидинг доменов ReadManga
|
|
||||||
|
|
||||||
```python
|
|
||||||
DEFAULT_READMANGA_DOMAINS = [
|
|
||||||
"readmanga.ru", "readmanga.live", "readmanga.me", "readmanga.io",
|
|
||||||
"3.readmanga.ru",
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
Вставляется однократно при первом старте (если нет ни одного домена для `readmanga`).
|
|
||||||
|
|
||||||
### Новые методы `StateDB`
|
|
||||||
|
|
||||||
- `get_source_by_domain(domain)` → `dict | None`
|
|
||||||
- `get_all_sources()` → `list[dict]` (с вложенными доменами)
|
|
||||||
- `add_domain(source_id, domain)` → `bool`
|
|
||||||
- `remove_domain(source_id, domain)`
|
|
||||||
- `set_manga_source(manga_url, source_id)` — меняет источник + привязывает домен URL к новому источнику (см. §3)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Рефакторинг `scraper.py` и `worker.py`
|
|
||||||
|
|
||||||
### `src/scraper.py` — shim для обратной совместимости
|
|
||||||
|
|
||||||
После переноса кода в `ReadmangaSource`:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# src/scraper.py
|
|
||||||
from .sources.readmanga import ReadmangaSource as _src
|
|
||||||
from .sources.base import MangaInfo, Chapter
|
|
||||||
|
|
||||||
_instance = _src()
|
|
||||||
|
|
||||||
async def get_manga_info(page, url):
|
|
||||||
return await _instance.get_manga_info(page, url)
|
|
||||||
|
|
||||||
async def get_chapter_images_and_download(page, chapter_url, dest_dir, **kw):
|
|
||||||
return await _instance.get_chapter_images_and_download(page, chapter_url, dest_dir, **kw)
|
|
||||||
```
|
|
||||||
|
|
||||||
Это позволяет не ломать `worker.py` и `cli.py` на переходном этапе.
|
|
||||||
|
|
||||||
### `src/worker.py` — подключение реестра
|
|
||||||
|
|
||||||
В `download_manga(url, fmt, ...)`:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from .sources import get_source_for_url
|
|
||||||
|
|
||||||
source = get_source_for_url(url, db)
|
|
||||||
if source is None:
|
|
||||||
# Источник не определён — ошибка, уведомить через WS
|
|
||||||
await ws_broadcast({"type": "source_unknown", "url": url})
|
|
||||||
return
|
|
||||||
```
|
|
||||||
|
|
||||||
Передавать `source` в `process_chapter()` и далее в функции скачивания.
|
|
||||||
|
|
||||||
`check_for_updates()` — аналогично резолвит источник.
|
|
||||||
|
|
||||||
### Смена источника + перепривязка домена
|
|
||||||
|
|
||||||
```python
|
|
||||||
async def switch_source(manga_url: str, new_source_id: int, db: StateDB):
|
|
||||||
"""Меняет источник манги и привязывает домен URL к новому источнику."""
|
|
||||||
domain = extract_domain(manga_url) # извлечь домен из URL манги
|
|
||||||
old_domain_source = db.get_source_by_domain(domain)
|
|
||||||
|
|
||||||
# Перепривязать домен к новому источнику
|
|
||||||
if old_domain_source:
|
|
||||||
db.remove_domain(old_domain_source["id"], domain)
|
|
||||||
db.add_domain(new_source_id, domain)
|
|
||||||
|
|
||||||
# Сменить источник у манги
|
|
||||||
db.set_manga_source(manga_url, new_source_id)
|
|
||||||
|
|
||||||
# Сбросить failed/partial главы → pending
|
|
||||||
db.reset_failed_chapters(manga_url)
|
|
||||||
```
|
|
||||||
|
|
||||||
Таким образом, при следующем добавлении манги с того же домена источник будет определён автоматически правильно.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. API эндпоинты
|
|
||||||
|
|
||||||
**Создание и удаление источников через API недоступны** — источники определяются только в коде.
|
|
||||||
|
|
||||||
### Источники (только чтение + управление доменами)
|
|
||||||
|
|
||||||
| Метод | Путь | Описание |
|
|
||||||
|-------|------|----------|
|
|
||||||
| `GET` | `/api/sources` | Список всех источников с доменами |
|
|
||||||
| `POST` | `/api/sources/{id}/domains` | Добавить домен к источнику `{domain}` |
|
|
||||||
| `DELETE` | `/api/sources/{id}/domains/{domain}` | Удалить домен |
|
|
||||||
| `GET` | `/api/resolve-source?url=` | Определить источник по URL → `{source_id, slug, display_name} \| null` |
|
|
||||||
|
|
||||||
### Управление мангой
|
|
||||||
|
|
||||||
| Метод | Путь | Описание |
|
|
||||||
|-------|------|----------|
|
|
||||||
| `POST` | `/api/mangas/switch-source` | Сменить источник `{url, source_id}` (не во время загрузки) |
|
|
||||||
|
|
||||||
### Pydantic-модели
|
|
||||||
|
|
||||||
```python
|
|
||||||
class DomainAdd(BaseModel):
|
|
||||||
domain: str
|
|
||||||
|
|
||||||
class SourceOut(BaseModel):
|
|
||||||
id: int
|
|
||||||
slug: str
|
|
||||||
display_name: str
|
|
||||||
domains: list[str]
|
|
||||||
settings: dict
|
|
||||||
|
|
||||||
class SwitchSourceRequest(BaseModel):
|
|
||||||
url: str
|
|
||||||
source_id: int
|
|
||||||
# домен всегда перепривязывается автоматически
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Изменения фронтенда
|
|
||||||
|
|
||||||
### Диалог добавления манги
|
|
||||||
|
|
||||||
1. После ввода URL (debounce 400 мс) → GET `/api/resolve-source?url=...`
|
|
||||||
2. **Источник найден** → показать badge «Источник: ReadManga» под полем ввода
|
|
||||||
3. **Источник неизвестен** → показать предупреждение:
|
|
||||||
> ⚠ Домен не распознан. Выберите источник вручную:
|
|
||||||
|
|
||||||
Под предупреждением — `<select>` со списком всех доступных источников. Без выбора источника кнопка «Добавить» неактивна.
|
|
||||||
|
|
||||||
После добавления домен URL автоматически привязывается к выбранному источнику (бэкенд делает это в момент добавления манги).
|
|
||||||
|
|
||||||
### Карточка манги
|
|
||||||
|
|
||||||
- Badge с `source.display_name` рядом с названием (серый, если источник не определён → «Источник неизвестен»)
|
|
||||||
- Кнопка **«↔ Источник»** — видима всегда, кроме статуса `downloading`; открывает модал:
|
|
||||||
- Текущий источник (или «не определён»)
|
|
||||||
- `<select>` со всеми источниками
|
|
||||||
- Статичное предупреждение под select (всегда видимо): «⚠ Домен `xyz.com` будет перепривязан к выбранному источнику. Это затронет все манги с этого домена.»
|
|
||||||
- Кнопка «Применить» → POST `/api/mangas/switch-source`
|
|
||||||
|
|
||||||
### Новая вкладка «Настройки»
|
|
||||||
|
|
||||||
Добавить четвёртую вкладку в навигацию.
|
|
||||||
|
|
||||||
**Подраздел «Источники»** (единственный на данном этапе):
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─ Источники ──────────────────────────────────────────┐
|
|
||||||
│ Источники определяются в коде приложения. │
|
|
||||||
│ Здесь можно управлять доменами для каждого источника│
|
|
||||||
│ │
|
|
||||||
│ ┌────────────────────────────────────────────────────┐│
|
|
||||||
│ │ ReadManga slug: readmanga ││
|
|
||||||
│ │ Домены: ││
|
|
||||||
│ │ • readmanga.ru [✕] • readmanga.live [✕] ││
|
|
||||||
│ │ • 3.readmanga.ru [✕] [+ добавить домен] ││
|
|
||||||
│ └────────────────────────────────────────────────────┘│
|
|
||||||
│ ┌────────────────────────────────────────────────────┐│
|
|
||||||
│ │ Другой источник slug: other ││
|
|
||||||
│ │ ... ││
|
|
||||||
│ └────────────────────────────────────────────────────┘│
|
|
||||||
└──────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
Inline-редактирование:
|
|
||||||
- `[+ добавить домен]` → inline `<input>` + кнопка «✓» → POST `/api/sources/{id}/domains`
|
|
||||||
- `[✕]` рядом с доменом → DELETE `/api/sources/{id}/domains/{domain}`
|
|
||||||
|
|
||||||
Кнопок «Создать источник» или «Удалить источник» **нет**.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. WebSocket события
|
|
||||||
|
|
||||||
| `type` | Когда | Данные |
|
|
||||||
|--------|-------|--------|
|
|
||||||
| `source_domain_added` | POST /api/sources/{id}/domains | `{source_id, domain}` |
|
|
||||||
| `source_domain_removed` | DELETE /api/sources/{id}/domains/... | `{source_id, domain}` |
|
|
||||||
| `source_switched` | POST /api/mangas/switch-source | `{url, old_source_id, new_source_id, domain_rebound: true}` |
|
|
||||||
| `source_unknown` | Попытка загрузки манги без источника | `{url}` — фронт показывает уведомление |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Решённые вопросы
|
|
||||||
|
|
||||||
### 7.1 CDN-паттерны и настройки источника
|
|
||||||
|
|
||||||
Каждый источник хранит свои технические настройки (CDN-паттерны и т.п.) **только в коде** внутри класса-адаптера. Поле `settings` в таблице `sources` не используется для пользовательского редактирования — оно остаётся зарезервированным для внутренних нужд адаптера. Никакого UI для редактирования настроек нет.
|
|
||||||
|
|
||||||
```python
|
|
||||||
class ReadmangaSource:
|
|
||||||
slug = "readmanga"
|
|
||||||
display_name = "ReadManga"
|
|
||||||
cdn_patterns = ["one-way.work", "staticfa.", "cdnmanga", "reimg"]
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7.2 Домен, уже привязанный к другому источнику
|
|
||||||
|
|
||||||
При смене источника у манги перепривязка домена к новому источнику происходит **автоматически** без дополнительного подтверждения. Флаг `rebind_domain` не нужен.
|
|
||||||
|
|
||||||
В UI рядом с `<select>` источника отображается статичное предупреждение:
|
|
||||||
|
|
||||||
> ⚠ Домен `xyz.com` будет перепривязан к выбранному источнику. Это затронет все манги с этого домена.
|
|
||||||
|
|
||||||
Флаг `rebind_domain` в `SwitchSourceRequest` не нужен — бэкенд всегда перепривязывает домен.
|
|
||||||
|
|
||||||
### 7.3 Удалённые из кода источники
|
|
||||||
|
|
||||||
При старте логировать предупреждение для каждого источника в БД, которого нет в реестре. В UI такие манги показывают badge **«Источник недоступен»** красным цветом. Загрузка таких манг невозможна до смены источника.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Порядок реализации (этапы)
|
|
||||||
|
|
||||||
### Этап 1 — БД (без ломки текущей логики)
|
|
||||||
- Добавить таблицы `sources`, `source_domains` в `state.py`
|
|
||||||
- Добавить колонку `source_id` в `mangas`
|
|
||||||
- Реализовать `_sync_sources(registry)` + сидинг readmanga-доменов
|
|
||||||
- Авто-миграция существующих манг (проставить `source_id` по домену)
|
|
||||||
- Новые методы `StateDB`
|
|
||||||
|
|
||||||
### Этап 2 — Адаптер + Реестр
|
|
||||||
- Создать `src/sources/` пакет
|
|
||||||
- Перенести `scraper.py` → `src/sources/readmanga.py` (класс `ReadmangaSource`)
|
|
||||||
- Реализовать `SourceRegistry`, `get_source_for_url()`
|
|
||||||
- Написать shim `src/scraper.py` (обратная совместимость)
|
|
||||||
|
|
||||||
### Этап 3 — Worker + API
|
|
||||||
- Подключить реестр в `worker.py`
|
|
||||||
- Добавить `switch_source()` с перепривязкой домена
|
|
||||||
- Реализовать API эндпоинты (только домены + switch)
|
|
||||||
- WS-события
|
|
||||||
|
|
||||||
### Этап 4 — Фронтенд
|
|
||||||
- Badge источника на карточках манги
|
|
||||||
- Автоопределение при вводе URL + предупреждение + ручной выбор для неизвестных доменов
|
|
||||||
- Диалог смены источника с предупреждением о перепривязке домена
|
|
||||||
- Вкладка «Настройки → Источники»
|
|
||||||
|
|
||||||
|
|
||||||
@@ -8,6 +8,9 @@ services:
|
|||||||
- ./state:/app/state
|
- ./state:/app/state
|
||||||
environment:
|
environment:
|
||||||
- PYTHONUNBUFFERED=1
|
- PYTHONUNBUFFERED=1
|
||||||
|
# Заставляем glibc возвращать освобождённую память ОС (уменьшает RSS в простое)
|
||||||
|
- MALLOC_MMAP_THRESHOLD_=65536
|
||||||
|
- MALLOC_TRIM_THRESHOLD_=65536
|
||||||
# Расписание авто-проверки новых глав (cron-синтаксис).
|
# Расписание авто-проверки новых глав (cron-синтаксис).
|
||||||
# Примеры: "0 */6 * * *" — каждые 6 ч | "0 3 * * *" — каждый день в 03:00
|
# Примеры: "0 */6 * * *" — каждые 6 ч | "0 3 * * *" — каждый день в 03:00
|
||||||
# Оставьте пустым чтобы отключить планировщик.
|
# Оставьте пустым чтобы отключить планировщик.
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ ebooklib==0.18
|
|||||||
tqdm==4.66.4
|
tqdm==4.66.4
|
||||||
loguru==0.7.2
|
loguru==0.7.2
|
||||||
fastapi==0.111.0
|
fastapi==0.111.0
|
||||||
uvicorn[standard]==0.29.0
|
uvicorn==0.29.0
|
||||||
websockets==12.0
|
websockets==12.0
|
||||||
pypdf==4.2.0
|
pypdf==4.2.0
|
||||||
croniter==3.0.3
|
croniter==3.0.3
|
||||||
|
|||||||
14
src/api.py
14
src/api.py
@@ -3,6 +3,8 @@ FastAPI веб-сервер: REST API + WebSocket для мониторинга
|
|||||||
Многопользовательская система с ролями admin / user.
|
Многопользовательская система с ролями admin / user.
|
||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import ctypes
|
||||||
|
import gc
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
@@ -154,6 +156,7 @@ async def startup_event():
|
|||||||
_db.close()
|
_db.close()
|
||||||
asyncio.create_task(queue_worker())
|
asyncio.create_task(queue_worker())
|
||||||
asyncio.create_task(update_scheduler())
|
asyncio.create_task(update_scheduler())
|
||||||
|
asyncio.create_task(memory_trimmer())
|
||||||
db = StateDB()
|
db = StateDB()
|
||||||
try:
|
try:
|
||||||
for manga in db.get_all_mangas():
|
for manga in db.get_all_mangas():
|
||||||
@@ -188,6 +191,17 @@ def _parse_schedule() -> Optional[str]:
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
logger.error("UPDATE_INTERVAL_HOURS='{}' — не число", hours_raw)
|
logger.error("UPDATE_INTERVAL_HOURS='{}' — не число", hours_raw)
|
||||||
return None
|
return None
|
||||||
|
async def memory_trimmer():
|
||||||
|
"""Периодически принудительно возвращает неиспользуемую память ОС."""
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(600) # каждые 10 минут
|
||||||
|
gc.collect()
|
||||||
|
try:
|
||||||
|
ctypes.CDLL("libc.so.6").malloc_trim(0)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
async def update_scheduler():
|
async def update_scheduler():
|
||||||
cron_expr = _parse_schedule()
|
cron_expr = _parse_schedule()
|
||||||
if not cron_expr:
|
if not cron_expr:
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
"""
|
"""
|
||||||
Браузерный слой: запуск Playwright Chromium с антидетект-настройками.
|
Браузерный слой: запуск Playwright Chromium с антидетект-настройками.
|
||||||
"""
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import Optional
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from playwright.async_api import async_playwright, Browser, BrowserContext, Page
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from playwright.async_api import Browser, BrowserContext, Page
|
||||||
|
|
||||||
|
|
||||||
# Реалистичный User-Agent Chrome 124 Linux
|
# Реалистичный User-Agent Chrome 124 Linux
|
||||||
@@ -53,6 +57,7 @@ class BrowserManager:
|
|||||||
self._browser: Optional[Browser] = None
|
self._browser: Optional[Browser] = None
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
|
from playwright.async_api import async_playwright
|
||||||
self._playwright = await async_playwright().start()
|
self._playwright = await async_playwright().start()
|
||||||
self._browser = await self._playwright.chromium.launch(
|
self._browser = await self._playwright.chromium.launch(
|
||||||
headless=self.headless,
|
headless=self.headless,
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
"""
|
"""
|
||||||
Базовые модели данных и Protocol-интерфейс для источников манги.
|
Базовые модели данных и Protocol-интерфейс для источников манги.
|
||||||
"""
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, Protocol, runtime_checkable
|
from typing import TYPE_CHECKING, Optional, Protocol, runtime_checkable
|
||||||
|
|
||||||
from playwright.async_api import Page
|
if TYPE_CHECKING:
|
||||||
|
from playwright.async_api import Page
|
||||||
|
|
||||||
|
|
||||||
class AuthRequiredError(Exception):
|
class AuthRequiredError(Exception):
|
||||||
|
|||||||
@@ -9,16 +9,20 @@
|
|||||||
Получаем pages[] с полями: image (filename), url (relative path), slug (page index 1-based).
|
Получаем pages[] с полями: image (filename), url (relative path), slug (page index 1-based).
|
||||||
Изображения: {server}{page.url}, CDN = mixlib.me / imglib.info.
|
Изображения: {server}{page.url}, CDN = mixlib.me / imglib.info.
|
||||||
"""
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import json as _json
|
import json as _json
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import TYPE_CHECKING, Optional
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from playwright.async_api import Page
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from playwright.async_api import Page
|
||||||
|
|
||||||
from .base import Chapter, MangaInfo, AuthRequiredError
|
from .base import Chapter, MangaInfo, AuthRequiredError
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
"""
|
"""
|
||||||
Адаптер ReadManga: поддерживает readmanga.ru и все его клоны.
|
Адаптер ReadManga: поддерживает readmanga.ru и все его клоны.
|
||||||
"""
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import base64
|
import base64
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import TYPE_CHECKING, Optional
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from playwright.async_api import Page
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from playwright.async_api import Page
|
||||||
|
|
||||||
from .base import Chapter, MangaInfo
|
from .base import Chapter, MangaInfo
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user