This commit is contained in:
2026-04-30 17:14:21 +03:00
parent 0aa057c991
commit 7c5ce807b8
4 changed files with 345 additions and 11 deletions

View File

@@ -55,6 +55,13 @@ download_queue: asyncio.Queue = asyncio.Queue()
active_tasks: dict[str, asyncio.Task] = {}
async def _broadcast_queue_positions():
"""Отправляет всем клиентам актуальные позиции в очереди."""
queue_list = list(download_queue._queue) # type: ignore
positions = {job["url"]: i + 1 for i, job in enumerate(queue_list)}
await ws_manager.broadcast({"type": "queue_positions", "positions": positions})
async def queue_worker():
"""Последовательно обрабатывает очередь загрузок. Перезапускается при краше."""
while True:
@@ -84,9 +91,12 @@ async def _queue_worker_loop():
if skip:
download_queue.task_done()
await _broadcast_queue_positions()
continue
logger.info("Воркер: начинаю скачивать {}", url)
# Позиции изменились — уведомляем клиентов
await _broadcast_queue_positions()
dl_task = asyncio.create_task(download_manga(
url=url,
fmt=fmt,
@@ -116,6 +126,7 @@ async def _queue_worker_loop():
finally:
active_tasks.pop(url, None)
download_queue.task_done()
await _broadcast_queue_positions()
@app.on_event("startup")
@@ -186,6 +197,19 @@ async def _run_auto_updates():
# ── Вспомогательные функции ───────────────────
def _safe_name(s: str) -> str:
return re.sub(r'[^\w\s\-]', '', s).strip().replace(" ", "_")[:80]
def _manga_folder(m: dict) -> Path:
"""Возвращает папку манги с учётом кастомного имени."""
if m.get("folder_name"):
return OUTPUT_DIR / m["folder_name"]
title = m.get("title") or ""
safe_title = _safe_name(title)
return OUTPUT_DIR / safe_title
def _dir_size(path: Path) -> int:
"""Размер директории в байтах."""
if not path.exists():
@@ -203,9 +227,7 @@ def _format_size(bytes_val: int) -> str:
def _enrich_manga(m: dict, db: StateDB) -> dict:
"""Обогащает строку манги реальными счётчиками из таблицы chapters."""
title = m.get("title") or ""
safe_title = re.sub(r'[^\w\s\-]', '', title).strip().replace(" ", "_")[:80]
size_bytes = _dir_size(OUTPUT_DIR / safe_title)
size_bytes = _dir_size(_manga_folder(m))
ch_done_count = db.conn.execute(
"SELECT COUNT(*) FROM chapters WHERE manga_url=? AND status='done'",
(m["url"],)
@@ -238,9 +260,7 @@ def _manga_detail(manga: dict, db: StateDB) -> dict:
chapters = db.get_all_chapters(url)
# Определяем директорию манги
title = manga.get("title") or ""
safe_title = re.sub(r'[^\w\s\-]', '', title).strip().replace(" ", "_")[:80]
manga_dir = OUTPUT_DIR / safe_title
manga_dir = _manga_folder(manga)
size_bytes = _dir_size(manga_dir)
# Файлы
@@ -361,6 +381,7 @@ async def add_to_queue(body: AddMangaRequest):
"url": url,
"format": body.format,
})
await _broadcast_queue_positions()
# Запускаем фоновую задачу предпросмотра (без Chromium — быстро)
asyncio.create_task(_fetch_preview(url))
else:
@@ -525,6 +546,7 @@ async def prioritize_manga(url: str):
"url": url,
"preempted_url": current_url,
})
await _broadcast_queue_positions()
return {"ok": True}
finally:
db.close()
@@ -619,6 +641,96 @@ async def _do_refresh_meta(url: str):
db.close()
class UpdateMetaRequest(BaseModel):
url: str
title_ru: str
title_full: str = ""
@app.post("/api/mangas/update_meta")
async def update_meta(body: UpdateMetaRequest):
"""Обновить метаданные манги (название серии) и применить к файлам."""
db = StateDB()
try:
manga = db.get_manga(body.url)
if not manga:
raise HTTPException(status_code=404, detail="Манга не найдена")
db.update_manga_meta_fields(
body.url,
title_ru=body.title_ru or None,
title_full=body.title_full or None,
)
finally:
db.close()
# Обновляем метаданные в файлах фоново
asyncio.create_task(_do_refresh_meta(body.url))
await ws_manager.broadcast({
"type": "manga_meta_updated",
"url": body.url,
"title": body.title_ru,
"title_ru": body.title_ru,
"title_full": body.title_full,
})
return {"ok": True}
class RenameFolderRequest(BaseModel):
url: str
folder_name: str
@app.post("/api/mangas/rename_folder")
async def rename_folder(body: RenameFolderRequest):
"""Переименовать папку манги и обновить пути в БД."""
new_folder = _safe_name(body.folder_name)
if not new_folder:
raise HTTPException(status_code=400, detail="Некорректное имя папки")
db = StateDB()
try:
manga = db.get_manga(body.url)
if not manga:
raise HTTPException(status_code=404, detail="Манга не найдена")
if manga["status"] == "downloading" and body.url in active_tasks:
raise HTTPException(status_code=400, detail="Нельзя переименовать — манга загружается")
old_dir = _manga_folder(manga)
new_dir = OUTPUT_DIR / new_folder
if old_dir != new_dir:
if new_dir.exists():
raise HTTPException(status_code=400, detail=f"Папка '{new_folder}' уже существует")
if old_dir.exists():
import shutil
shutil.move(str(old_dir), str(new_dir))
logger.info("Папка переименована: {}{}", old_dir, new_dir)
# Обновляем пути в таблице chapters
chapters = db.get_all_chapters(body.url)
for ch in chapters:
updates = {}
for col in ("output_cbz", "output_pdf", "output_epub"):
p = ch.get(col)
if p and str(old_dir) in p:
updates[col] = p.replace(str(old_dir), str(new_dir))
if updates:
sets = ", ".join(f"{k}=?" for k in updates)
db.conn.execute(
f"UPDATE chapters SET {sets} WHERE chapter_url=?",
[*updates.values(), ch["chapter_url"]]
)
db.conn.commit()
db.set_folder_name(body.url, new_folder)
await ws_manager.broadcast({
"type": "manga_folder_renamed",
"url": body.url,
"folder_name": new_folder,
})
return {"ok": True, "folder_name": new_folder}
finally:
db.close()
@app.post("/api/mangas/force_redownload")
async def force_redownload(url: str):
"""Сбросить все главы на pending и поставить мангу заново в очередь."""
@@ -641,6 +753,7 @@ async def force_redownload(url: str):
db.update_manga_status(url, "queued")
await download_queue.put({"url": url, "fmt": manga["format"], "resume": False})
await ws_manager.broadcast({"type": "manga_queued", "url": url, "format": manga["format"]})
await _broadcast_queue_positions()
return {"ok": True}
finally:
db.close()
@@ -664,6 +777,7 @@ async def stop_manga(url: str):
# Манга в очереди (ещё не начата) — просто помечаем как stopped
db.update_manga_status(url, "stopped")
await ws_manager.broadcast({"type": "manga_stopped", "url": url})
await _broadcast_queue_positions()
return {"ok": True}
finally:
@@ -684,6 +798,7 @@ async def resume_manga(url: str):
db.update_manga_status(url, "queued")
await download_queue.put({"url": url, "fmt": manga["format"]})
await ws_manager.broadcast({"type": "manga_queued", "url": url, "format": manga["format"]})
await _broadcast_queue_positions()
return {"ok": True}
finally:
db.close()
@@ -701,9 +816,7 @@ async def delete_manga(url: str, delete_files: bool = False):
deleted_size = 0
if delete_files:
title = manga.get("title") or ""
safe_title = re.sub(r'[^\w\s\-]', '', title).strip().replace(" ", "_")[:80]
manga_dir = OUTPUT_DIR / safe_title
manga_dir = _manga_folder(manga)
if manga_dir.exists() and manga_dir.is_dir():
deleted_size = _dir_size(manga_dir)
import shutil