Update metadata
This commit is contained in:
@@ -222,6 +222,19 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="users-list" class="flex flex-col gap-2"></div>
|
<div id="users-list" class="flex flex-col gap-2"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Обновить все метаданные (только admin) -->
|
||||||
|
<div id="refresh-all-section" class="hidden px-5 py-4 border-t border-gray-800">
|
||||||
|
<h3 class="text-sm font-semibold text-gray-300 uppercase tracking-wider mb-1">Обновить все метаданные</h3>
|
||||||
|
<p class="text-xs text-gray-500 mb-3">Запускает браузер для каждой скачанной манги: обновляет обложку, синопсис, жанры и метаданные в файлах CBZ/PDF/EPUB.</p>
|
||||||
|
<div class="flex items-center gap-3 flex-wrap">
|
||||||
|
<button id="refresh-all-btn" onclick="refreshAllMeta()"
|
||||||
|
class="text-xs px-4 py-2 rounded-lg font-semibold text-white"
|
||||||
|
style="background:#312e81;border:1px solid #4338ca;color:#a78bfa">
|
||||||
|
🔄 Обновить все метаданные
|
||||||
|
</button>
|
||||||
|
<div id="refresh-all-status" class="text-xs text-gray-400 hidden"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Смена своего пароля -->
|
<!-- Смена своего пароля -->
|
||||||
<div id="chpwd-section" class="px-5 py-4 border-t border-gray-800">
|
<div id="chpwd-section" class="px-5 py-4 border-t border-gray-800">
|
||||||
<h3 class="text-sm font-semibold text-gray-300 uppercase tracking-wider mb-3">Сменить пароль</h3>
|
<h3 class="text-sm font-semibold text-gray-300 uppercase tracking-wider mb-3">Сменить пароль</h3>
|
||||||
@@ -703,6 +716,18 @@ function handleEvent(msg) {
|
|||||||
_updateMetaBtn(msg.url, msg.failed === -1 ? 'error' : 'done');
|
_updateMetaBtn(msg.url, msg.failed === -1 ? 'error' : 'done');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'refresh_all_started':
|
||||||
|
_handleRefreshAllStarted(msg);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'refresh_all_progress':
|
||||||
|
_handleRefreshAllProgress(msg);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'refresh_all_done':
|
||||||
|
_handleRefreshAllDone(msg);
|
||||||
|
break;
|
||||||
|
|
||||||
case 'manga_meta_updated':
|
case 'manga_meta_updated':
|
||||||
if(state.mangas[msg.url]) {
|
if(state.mangas[msg.url]) {
|
||||||
state.mangas[msg.url].title = msg.title;
|
state.mangas[msg.url].title = msg.title;
|
||||||
@@ -812,7 +837,7 @@ function switchTab(tab) {
|
|||||||
document.getElementById('manga-filters').classList.toggle('hidden', tab !== 'mangas');
|
document.getElementById('manga-filters').classList.toggle('hidden', tab !== 'mangas');
|
||||||
if(tab === 'history') loadHistory();
|
if(tab === 'history') loadHistory();
|
||||||
if(tab === 'news') { newsUnreadCount = 0; updateNewsBadge(); loadNews(); }
|
if(tab === 'news') { newsUnreadCount = 0; updateNewsBadge(); loadNews(); }
|
||||||
if(tab === 'settings') { loadSources(); showUsersSection(); }
|
if(tab === 'settings') { loadSources(); showUsersSection(); showRefreshAllSection(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateNewsBadge() {
|
function updateNewsBadge() {
|
||||||
@@ -1132,6 +1157,11 @@ function showUsersSection() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showRefreshAllSection() {
|
||||||
|
const el = document.getElementById('refresh-all-section');
|
||||||
|
if(el) el.classList.toggle('hidden', !isAdmin());
|
||||||
|
}
|
||||||
|
|
||||||
async function loadUsers() {
|
async function loadUsers() {
|
||||||
if(!isAdmin()) return;
|
if(!isAdmin()) return;
|
||||||
try {
|
try {
|
||||||
@@ -1654,7 +1684,7 @@ function _updateMetaBtn(url, result) {
|
|||||||
btn.style.color = '#4ade80';
|
btn.style.color = '#4ade80';
|
||||||
btn.style.borderColor = '#166534';
|
btn.style.borderColor = '#166534';
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
btn.innerHTML = '🏷 Обновить метатеги';
|
btn.innerHTML = '🏷 Обновить метаданные';
|
||||||
btn.style.color = '#a78bfa';
|
btn.style.color = '#a78bfa';
|
||||||
btn.style.borderColor = '#312e81';
|
btn.style.borderColor = '#312e81';
|
||||||
}, 2500);
|
}, 2500);
|
||||||
@@ -1664,12 +1694,12 @@ function _updateMetaBtn(url, result) {
|
|||||||
btn.style.color = '#f87171';
|
btn.style.color = '#f87171';
|
||||||
btn.style.borderColor = '#7f1d1d';
|
btn.style.borderColor = '#7f1d1d';
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
btn.innerHTML = '🏷 Обновить метатеги';
|
btn.innerHTML = '🏷 Обновить метаданные';
|
||||||
btn.style.color = '#a78bfa';
|
btn.style.color = '#a78bfa';
|
||||||
btn.style.borderColor = '#312e81';
|
btn.style.borderColor = '#312e81';
|
||||||
}, 3000);
|
}, 3000);
|
||||||
} else {
|
} else {
|
||||||
btn.innerHTML = '🏷 Обновить метатеги';
|
btn.innerHTML = '🏷 Обновить метаданные';
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.style.color = '#a78bfa';
|
btn.style.color = '#a78bfa';
|
||||||
btn.style.borderColor = '#312e81';
|
btn.style.borderColor = '#312e81';
|
||||||
@@ -1691,6 +1721,46 @@ async function refreshMetaModal(url) {
|
|||||||
// Спиннер появится через WS meta_refresh_started, исчезнет через meta_refreshed
|
// Спиннер появится через WS meta_refresh_started, исчезнет через meta_refreshed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function refreshAllMeta() {
|
||||||
|
const btn = document.getElementById('refresh-all-btn');
|
||||||
|
const status = document.getElementById('refresh-all-status');
|
||||||
|
const r = await fetch('/api/mangas/refresh_all_meta', {method:'POST'});
|
||||||
|
if(!r.ok) {
|
||||||
|
const err = await r.json().catch(() => ({}));
|
||||||
|
if(status) { status.textContent = err.detail || 'Ошибка запуска'; status.classList.remove('hidden'); status.style.color = '#f87171'; }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(btn) { btn.disabled = true; btn.textContent = '⏳ Запускаем...'; }
|
||||||
|
if(status) { status.textContent = 'Инициализация...'; status.classList.remove('hidden'); status.style.color = '#94a3b8'; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function _handleRefreshAllStarted(data) {
|
||||||
|
const btn = document.getElementById('refresh-all-btn');
|
||||||
|
const status = document.getElementById('refresh-all-status');
|
||||||
|
if(btn) { btn.disabled = true; btn.textContent = '⏳ Обновляем...'; }
|
||||||
|
if(status) { status.textContent = `0 / ${data.total}`; status.classList.remove('hidden'); status.style.color = '#94a3b8'; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function _handleRefreshAllProgress(data) {
|
||||||
|
const status = document.getElementById('refresh-all-status');
|
||||||
|
if(status) {
|
||||||
|
const title = data.title ? ` — ${data.title}` : '';
|
||||||
|
status.textContent = `${data.done + 1} / ${data.total}${title}`;
|
||||||
|
status.style.color = '#94a3b8';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _handleRefreshAllDone(data) {
|
||||||
|
const btn = document.getElementById('refresh-all-btn');
|
||||||
|
const status = document.getElementById('refresh-all-status');
|
||||||
|
if(btn) { btn.disabled = false; btn.textContent = '🔄 Обновить все метаданные'; }
|
||||||
|
if(status) {
|
||||||
|
status.textContent = `Готово: ${data.total} манг, обновлено файлов: ${data.total_updated}`;
|
||||||
|
status.style.color = '#4ade80';
|
||||||
|
status.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function forceRedownload(url, closeModalAfter = false) {
|
async function forceRedownload(url, closeModalAfter = false) {
|
||||||
if(!confirm('Скачать заново ВСЕ главы? Уже скачанные файлы будут перезаписаны.')) return;
|
if(!confirm('Скачать заново ВСЕ главы? Уже скачанные файлы будут перезаписаны.')) return;
|
||||||
const r = await fetch('/api/mangas/force_redownload?url='+encodeURIComponent(url), {method:'POST'});
|
const r = await fetch('/api/mangas/force_redownload?url='+encodeURIComponent(url), {method:'POST'});
|
||||||
@@ -2236,7 +2306,7 @@ function renderModalBody(data) {
|
|||||||
<button id="modal-refresh-meta-btn" onclick="refreshMetaModal('${escHtml(data.url)}')"
|
<button id="modal-refresh-meta-btn" onclick="refreshMetaModal('${escHtml(data.url)}')"
|
||||||
class="flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-semibold transition-colors"
|
class="flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-semibold transition-colors"
|
||||||
style="background:#1e1b4b;color:#a78bfa;border:1px solid #312e81">
|
style="background:#1e1b4b;color:#a78bfa;border:1px solid #312e81">
|
||||||
🏷 Обновить метатеги
|
🏷 Обновить метаданные
|
||||||
</button>` : ''}
|
</button>` : ''}
|
||||||
${data.status !== 'downloading' && data.status !== 'queued' && isAdmin() ? `
|
${data.status !== 'downloading' && data.status !== 'queued' && isAdmin() ? `
|
||||||
<button onclick="forceRedownloadModal('${escHtml(data.url)}')"
|
<button onclick="forceRedownloadModal('${escHtml(data.url)}')"
|
||||||
|
|||||||
156
src/api.py
156
src/api.py
@@ -18,7 +18,7 @@ from fastapi.staticfiles import StaticFiles
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from .state import StateDB
|
from .state import StateDB
|
||||||
from .worker import download_manga, check_for_updates
|
from .worker import download_manga, check_for_updates, refresh_manga_metadata
|
||||||
from .browser import BrowserManager
|
from .browser import BrowserManager
|
||||||
from .exporter import patch_meta, MangaMeta
|
from .exporter import patch_meta, MangaMeta
|
||||||
from .sources import registry, get_source_for_url, extract_domain
|
from .sources import registry, get_source_for_url, extract_domain
|
||||||
@@ -73,6 +73,7 @@ ws_manager = ConnectionManager()
|
|||||||
# ── Очередь загрузки ─────────────────────────
|
# ── Очередь загрузки ─────────────────────────
|
||||||
download_queue: asyncio.Queue = asyncio.Queue()
|
download_queue: asyncio.Queue = asyncio.Queue()
|
||||||
active_tasks: dict = {}
|
active_tasks: dict = {}
|
||||||
|
_refresh_all_running: bool = False
|
||||||
async def _broadcast_queue_positions():
|
async def _broadcast_queue_positions():
|
||||||
queue_list = list(download_queue._queue)
|
queue_list = list(download_queue._queue)
|
||||||
positions = {job["url"]: i + 1 for i, job in enumerate(queue_list)}
|
positions = {job["url"]: i + 1 for i, job in enumerate(queue_list)}
|
||||||
@@ -693,106 +694,75 @@ async def refresh_meta(url: str, current_user: dict = Depends(get_current_user))
|
|||||||
db.close()
|
db.close()
|
||||||
asyncio.create_task(_do_refresh_meta(url))
|
asyncio.create_task(_do_refresh_meta(url))
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
def _patch_meta_sync(manga: dict, chapters: list, chapters_total: int, pub_status: str) -> tuple[int, int]:
|
|
||||||
updated = failed = 0
|
|
||||||
url = manga["url"]
|
|
||||||
summary = manga.get("description") or ""
|
|
||||||
tags_raw = manga.get("tags") or ""
|
|
||||||
try:
|
|
||||||
tags_str = ", ".join(json.loads(tags_raw)) if tags_raw else ""
|
|
||||||
except Exception:
|
|
||||||
tags_str = ""
|
|
||||||
for ch in chapters:
|
|
||||||
for fmt_col in ("output_cbz", "output_pdf", "output_epub"):
|
|
||||||
fpath = ch.get(fmt_col)
|
|
||||||
if not fpath:
|
|
||||||
continue
|
|
||||||
p = Path(fpath)
|
|
||||||
if not p.exists():
|
|
||||||
continue
|
|
||||||
meta = MangaMeta(
|
|
||||||
series=manga.get("title_ru") or manga.get("title") or "",
|
|
||||||
series_full=manga.get("title_full") or "",
|
|
||||||
chapter_title=ch.get("title") or "",
|
|
||||||
number=float(ch.get("number") or 0),
|
|
||||||
volume=int(ch.get("volume") or 0),
|
|
||||||
chapters_total=chapters_total,
|
|
||||||
pub_status=pub_status,
|
|
||||||
source_url=url,
|
|
||||||
summary=summary,
|
|
||||||
tags=tags_str,
|
|
||||||
)
|
|
||||||
if patch_meta(p, meta):
|
|
||||||
updated += 1
|
|
||||||
else:
|
|
||||||
failed += 1
|
|
||||||
return updated, failed
|
|
||||||
|
|
||||||
def _refresh_cover_sync(manga: dict, manga_dir: Path) -> None:
|
|
||||||
"""Скачивает или обновляет обложку через urllib (синхронно, для asyncio.to_thread)."""
|
|
||||||
import urllib.request as _urllib_req
|
|
||||||
import re as _re
|
|
||||||
|
|
||||||
cover_url = manga.get("cover_url") or ""
|
|
||||||
if not cover_url:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Определяем Referer по URL обложки (MangaLib CDN — cdnlibs / mangalib)
|
|
||||||
if any(pat in cover_url for pat in ("mangalib", "cdnlibs", "imglib")):
|
|
||||||
referer = "https://mangalib.me/"
|
|
||||||
else:
|
|
||||||
from urllib.parse import urlparse as _up
|
|
||||||
parsed = _up(manga.get("url") or "")
|
|
||||||
referer = f"{parsed.scheme}://{parsed.netloc}/" if parsed.netloc else "https://readmanga.ru/"
|
|
||||||
|
|
||||||
try:
|
|
||||||
req = _urllib_req.Request(cover_url, headers={
|
|
||||||
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/124.0.0.0",
|
|
||||||
"Referer": referer,
|
|
||||||
"Accept": "image/png,image/jpeg,image/webp,image/*,*/*",
|
|
||||||
})
|
|
||||||
with _urllib_req.urlopen(req, timeout=30) as resp:
|
|
||||||
body = resp.read()
|
|
||||||
if len(body) < 500:
|
|
||||||
logger.warning("refresh_cover: слишком малый ответ ({} байт)", len(body))
|
|
||||||
return
|
|
||||||
m = _re.search(r"\.(jpg|jpeg|png|webp)(\?|$)", cover_url, _re.IGNORECASE)
|
|
||||||
ext = ("." + (m.group(1).lower() if m else "jpg")).replace(".jpeg", ".jpg")
|
|
||||||
cover_path = manga_dir / f"cover{ext}"
|
|
||||||
cover_path.write_bytes(body)
|
|
||||||
logger.info("Обложка обновлена: {} ({} байт)", cover_path.name, len(body))
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning("refresh_cover error {}: {}", cover_url, e)
|
|
||||||
|
|
||||||
|
|
||||||
async def _do_refresh_meta(url: str):
|
async def _do_refresh_meta(url: str):
|
||||||
db = StateDB()
|
|
||||||
try:
|
|
||||||
manga = db.get_manga(url)
|
|
||||||
if not manga:
|
|
||||||
return
|
|
||||||
chapters = db.get_all_chapters(url)
|
|
||||||
chapters_total = len(chapters)
|
|
||||||
pub_status = manga.get("pub_status", "unknown") or "unknown"
|
|
||||||
finally:
|
|
||||||
db.close()
|
|
||||||
try:
|
try:
|
||||||
await ws_manager.broadcast({"type": "meta_refresh_started", "url": url})
|
await ws_manager.broadcast({"type": "meta_refresh_started", "url": url})
|
||||||
updated, failed = await asyncio.to_thread(_patch_meta_sync, manga, chapters, chapters_total, pub_status)
|
updated, failed = await refresh_manga_metadata(url)
|
||||||
logger.info("refresh_meta {}: обновлено {}, ошибок {}", url, updated, failed)
|
|
||||||
|
|
||||||
# Обновляем обложку если у манги формат cbz
|
|
||||||
manga_fmt = manga.get("format", "cbz") or "cbz"
|
|
||||||
if manga_fmt in ("cbz", "all") and manga.get("cover_url"):
|
|
||||||
manga_dir = _manga_folder(manga)
|
|
||||||
if manga_dir.exists():
|
|
||||||
await asyncio.to_thread(_refresh_cover_sync, manga, manga_dir)
|
|
||||||
|
|
||||||
await ws_manager.broadcast({"type": "meta_refreshed", "url": url,
|
await ws_manager.broadcast({"type": "meta_refreshed", "url": url,
|
||||||
"updated": updated, "failed": failed})
|
"updated": updated, "failed": failed})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("_do_refresh_meta {}: {}", url, e)
|
logger.error("_do_refresh_meta {}: {}", url, e)
|
||||||
await ws_manager.broadcast({"type": "meta_refreshed", "url": url, "updated": 0, "failed": -1})
|
await ws_manager.broadcast({"type": "meta_refreshed", "url": url, "updated": 0, "failed": -1})
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/mangas/refresh_all_meta")
|
||||||
|
async def refresh_all_meta_endpoint(current_user: dict = Depends(require_admin)):
|
||||||
|
global _refresh_all_running
|
||||||
|
if _refresh_all_running:
|
||||||
|
raise HTTPException(status_code=409, detail="Обновление метаданных уже выполняется")
|
||||||
|
asyncio.create_task(_do_refresh_all_meta())
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/mangas/refresh_all_meta/status")
|
||||||
|
async def refresh_all_meta_status(current_user: dict = Depends(require_admin)):
|
||||||
|
return {"running": _refresh_all_running}
|
||||||
|
|
||||||
|
|
||||||
|
async def _do_refresh_all_meta():
|
||||||
|
global _refresh_all_running
|
||||||
|
_refresh_all_running = True
|
||||||
|
db = StateDB()
|
||||||
|
try:
|
||||||
|
mangas = db.get_all_mangas()
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
done_mangas = [m for m in mangas if m["status"] == "done"]
|
||||||
|
total = len(done_mangas)
|
||||||
|
await ws_manager.broadcast({"type": "refresh_all_started", "total": total})
|
||||||
|
logger.info("refresh_all_meta: начало, всего манг: {}", total)
|
||||||
|
|
||||||
|
total_updated = total_failed = 0
|
||||||
|
try:
|
||||||
|
for i, manga in enumerate(done_mangas):
|
||||||
|
url = manga["url"]
|
||||||
|
await ws_manager.broadcast({
|
||||||
|
"type": "refresh_all_progress",
|
||||||
|
"done": i,
|
||||||
|
"total": total,
|
||||||
|
"url": url,
|
||||||
|
"title": manga.get("title_ru") or manga.get("title") or url,
|
||||||
|
})
|
||||||
|
try:
|
||||||
|
updated, failed = await refresh_manga_metadata(url)
|
||||||
|
total_updated += updated
|
||||||
|
total_failed += failed
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("refresh_all_meta {}: {}", url, e)
|
||||||
|
|
||||||
|
await ws_manager.broadcast({
|
||||||
|
"type": "refresh_all_done",
|
||||||
|
"total": total,
|
||||||
|
"total_updated": total_updated,
|
||||||
|
"total_failed": total_failed,
|
||||||
|
})
|
||||||
|
logger.info("refresh_all_meta: завершено, обновлено файлов: {}, ошибок: {}",
|
||||||
|
total_updated, total_failed)
|
||||||
|
finally:
|
||||||
|
_refresh_all_running = False
|
||||||
|
|
||||||
|
|
||||||
class UpdateMetaRequest(BaseModel):
|
class UpdateMetaRequest(BaseModel):
|
||||||
url: str
|
url: str
|
||||||
title_ru: str
|
title_ru: str
|
||||||
|
|||||||
128
src/worker.py
128
src/worker.py
@@ -13,7 +13,7 @@ from .browser import BrowserManager
|
|||||||
from .sources import registry, get_source_for_url, extract_domain
|
from .sources import registry, get_source_for_url, extract_domain
|
||||||
import json as _json
|
import json as _json
|
||||||
from .sources.base import Chapter, MangaInfo, AuthRequiredError
|
from .sources.base import Chapter, MangaInfo, AuthRequiredError
|
||||||
from .exporter import export, MangaMeta
|
from .exporter import export, patch_meta, MangaMeta
|
||||||
from .state import StateDB
|
from .state import StateDB
|
||||||
from .utils import safe_name, safe_chapter_name
|
from .utils import safe_name, safe_chapter_name
|
||||||
|
|
||||||
@@ -555,3 +555,129 @@ async def check_for_updates(
|
|||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
async def refresh_manga_metadata(
|
||||||
|
url: str,
|
||||||
|
output_dir: Path = OUTPUT_DIR,
|
||||||
|
on_event: Optional[Callable] = None,
|
||||||
|
) -> tuple[int, int]:
|
||||||
|
"""
|
||||||
|
Обновляет метаданные манги через браузер: скачивает обложку через Playwright,
|
||||||
|
обновляет ComicInfo.xml/PDF/EPUB с актуальными данными (включая жанры и синопсис).
|
||||||
|
Возвращает (updated, failed).
|
||||||
|
"""
|
||||||
|
async def emit(event: dict):
|
||||||
|
if on_event:
|
||||||
|
try:
|
||||||
|
await on_event(event)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
db = StateDB()
|
||||||
|
db_lock = asyncio.Lock()
|
||||||
|
|
||||||
|
async def db_call(fn, *args, **kwargs):
|
||||||
|
async with db_lock:
|
||||||
|
return fn(*args, **kwargs)
|
||||||
|
|
||||||
|
try:
|
||||||
|
source = get_source_for_url(url, db)
|
||||||
|
if source is None:
|
||||||
|
manga_row = await db_call(db.get_manga, url)
|
||||||
|
if manga_row and manga_row.get("source_id"):
|
||||||
|
source = registry.get_by_db_id(manga_row["source_id"], db)
|
||||||
|
if source is None:
|
||||||
|
logger.warning("refresh_manga_metadata: источник не найден для {}", url)
|
||||||
|
return 0, 0
|
||||||
|
|
||||||
|
# Inject auth token for sources that need it
|
||||||
|
if hasattr(source, "auth_token"):
|
||||||
|
_src_row = await db_call(db.get_source_by_slug, source.slug)
|
||||||
|
if _src_row:
|
||||||
|
_settings_raw = _src_row.get("settings") or "{}"
|
||||||
|
try:
|
||||||
|
_settings = _json.loads(_settings_raw) if isinstance(_settings_raw, str) else (_settings_raw or {})
|
||||||
|
except Exception:
|
||||||
|
_settings = {}
|
||||||
|
source.auth_token = _settings.get("auth_token") or None
|
||||||
|
|
||||||
|
async with BrowserManager(headless=True) as bm:
|
||||||
|
_, page = await bm.new_page()
|
||||||
|
try:
|
||||||
|
manga = await source.get_manga_info(page, url)
|
||||||
|
if not manga:
|
||||||
|
logger.warning("refresh_manga_metadata: get_manga_info вернул None для {}", url)
|
||||||
|
return 0, 0
|
||||||
|
|
||||||
|
# Сохраняем свежие данные в БД
|
||||||
|
await db_call(
|
||||||
|
db.update_manga_info, url,
|
||||||
|
title=manga.title_ru or manga.title,
|
||||||
|
chapters_total=len(manga.chapters),
|
||||||
|
title_ru=manga.title_ru,
|
||||||
|
title_full=manga.title_full,
|
||||||
|
pub_status=manga.pub_status,
|
||||||
|
description=manga.description,
|
||||||
|
tags=_json.dumps(manga.tags, ensure_ascii=False) if manga.tags else "",
|
||||||
|
cover_url=manga.cover_url,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Скачиваем обложку через Playwright (правильные куки/заголовки)
|
||||||
|
manga_row = await db_call(db.get_manga, url)
|
||||||
|
manga_fmt = (manga_row or {}).get("format", "cbz") or "cbz"
|
||||||
|
folder_name = (
|
||||||
|
(manga_row.get("folder_name") if manga_row else None)
|
||||||
|
or safe_name(manga.title_ru or manga.title)
|
||||||
|
)
|
||||||
|
manga_dir = output_dir / folder_name
|
||||||
|
|
||||||
|
if manga.cover_url and manga_fmt in ("cbz", "all") and manga_dir.exists():
|
||||||
|
await _download_cover(manga.cover_url, manga_dir, url, page)
|
||||||
|
|
||||||
|
# Обновляем метаданные в файлах с актуальными данными из источника
|
||||||
|
chapters = await db_call(db.get_all_chapters, url)
|
||||||
|
chapters_total = len(chapters)
|
||||||
|
series = manga.title_ru or manga.title
|
||||||
|
series_full = manga.title_full or ""
|
||||||
|
pub_status = manga.pub_status or "unknown"
|
||||||
|
summary = manga.description or ""
|
||||||
|
tags_str = ", ".join(manga.tags) if manga.tags else ""
|
||||||
|
genre_str = ", ".join(manga.genres) if manga.genres else ""
|
||||||
|
|
||||||
|
def do_patch():
|
||||||
|
updated = failed = 0
|
||||||
|
for ch in chapters:
|
||||||
|
for fmt_col in ("output_cbz", "output_pdf", "output_epub"):
|
||||||
|
fpath = ch.get(fmt_col)
|
||||||
|
if not fpath:
|
||||||
|
continue
|
||||||
|
p = Path(fpath)
|
||||||
|
if not p.exists():
|
||||||
|
continue
|
||||||
|
meta = MangaMeta(
|
||||||
|
series=series,
|
||||||
|
series_full=series_full,
|
||||||
|
chapter_title=ch.get("title") or "",
|
||||||
|
number=float(ch.get("number") or 0),
|
||||||
|
volume=int(ch.get("volume") or 0),
|
||||||
|
chapters_total=chapters_total,
|
||||||
|
pub_status=pub_status,
|
||||||
|
source_url=url,
|
||||||
|
summary=summary,
|
||||||
|
tags=tags_str,
|
||||||
|
genre=genre_str,
|
||||||
|
)
|
||||||
|
if patch_meta(p, meta):
|
||||||
|
updated += 1
|
||||||
|
else:
|
||||||
|
failed += 1
|
||||||
|
return updated, failed
|
||||||
|
|
||||||
|
updated, failed = await asyncio.to_thread(do_patch)
|
||||||
|
logger.info("refresh_manga_metadata {}: обновлено {}, ошибок {}", url, updated, failed)
|
||||||
|
return updated, failed
|
||||||
|
finally:
|
||||||
|
await page.close()
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user