diff --git a/requirements.txt b/requirements.txt index f918284b..97f66782 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,3 +8,4 @@ pypdf pymupdf google-genai requests +Pillow diff --git a/scripts/lib/cover.py b/scripts/lib/cover.py new file mode 100644 index 00000000..07256572 --- /dev/null +++ b/scripts/lib/cover.py @@ -0,0 +1,62 @@ +""" +Orijinal epub'dan kapak resmini çıkarıp eptran-web için küçültür. + +web_ingest.push_book() bunu, çağıran taraf açıkça bir kapak vermediyse +kendisi kullanır — böylece convert.py ve backfill_ingest.py'de hiçbir +değişiklik gerekmeden hem yeni kitaplar hem eski kitaplar kapağıyla +birlikte gönderilir. +""" +import io +import os + +import ebooklib +from ebooklib import epub + +MAX_WIDTH = 400 +FALLBACK_MAX_BYTES = 1_000_000 + + +def load_cover(book_slug: str, originals_dir: str = "input/.originals"): + """ + input/.originals/.epub içinden kapak resmini bulur ve + (bytes, mime) döner. Bulunamazsa ya da hata olursa None. + + Resim ~400px genişliğe küçültülüp JPEG'e çevrilir (site küçük bir grid + kartında gösteriyor, DB'ye gömülüyor: birkaç on KB yeter). Pillow yoksa + ham resim <=1MB ise olduğu gibi, değilse atlanır. + + Fail-soft: kapak çıkarılamaması çeviri/PR/ingest akışını asla etkilemez. + """ + path = os.path.join(originals_dir, f"{book_slug}.epub") + if not os.path.exists(path): + return None + + try: + book = epub.read_epub(path) + + item = next(iter(book.get_items_of_type(ebooklib.ITEM_COVER)), None) + if item is None: + images = list(book.get_items_of_type(ebooklib.ITEM_IMAGE)) + item = next((i for i in images if "cover" in i.get_name().lower()), None) + if item is None and images: + item = images[0] + if item is None: + return None + + raw = item.get_content() + try: + from PIL import Image + + img = Image.open(io.BytesIO(raw)).convert("RGB") + if img.width > MAX_WIDTH: + new_h = round(img.height * MAX_WIDTH / img.width) + img = img.resize((MAX_WIDTH, new_h), Image.LANCZOS) + buf = io.BytesIO() + img.save(buf, format="JPEG", quality=82, optimize=True) + return buf.getvalue(), "image/jpeg" + except ImportError: + mime = getattr(item, "media_type", None) or "image/jpeg" + return (raw, mime) if len(raw) <= FALLBACK_MAX_BYTES else None + except Exception as e: + print(f" Uyarı: kapak çıkarılamadı: {e}") + return None diff --git a/scripts/lib/web_ingest.py b/scripts/lib/web_ingest.py index f35a946e..5016de41 100644 --- a/scripts/lib/web_ingest.py +++ b/scripts/lib/web_ingest.py @@ -24,17 +24,22 @@ import requests +from lib.cover import load_cover + INGEST_TIMEOUT_SECONDS = 60 def push_book(book_slug: str, chapters: list, title: str, - author: str | None = None, description: str | None = None) -> None: + author: str | None = None, description: str | None = None, + cover: tuple | None = None) -> None: """ eptran-web'in /api/ingest'ine bir kitabın TÜM bölümlerini format=txt olarak gönderir. chapters: convert.py'nin load_txt_chapters()'ından dönen liste — her eleman {"title": ..., "body": ...} içerir. + cover : opsiyonel (bytes, mime). Verilmezse input/.originals/.epub'dan + otomatik çıkarılır (lib/cover.py). eptran-web'de kapak olarak saklanır. WEB_INGEST_URL : örn. https://eptran-web-virid.vercel.app/api/ingest WEB_INGEST_SECRET: eptran-web'deki INGEST_SECRET ile AYNI değer @@ -68,6 +73,16 @@ def push_book(book_slug: str, chapters: list, title: str, fname = f"{i + 1:03d}_{book_slug}.txt" files.append(("files[]", (fname, content.encode("utf-8"), "text/plain"))) + # Çağıran açıkça bir kapak vermediyse orijinal epub'dan kendimiz çıkarırız + # (bkz. lib/cover.py) — convert.py ve backfill'in değişmesine gerek kalmaz. + if cover is None: + cover = load_cover(book_slug) + + if cover: + cover_bytes, cover_mime = cover + ext = "png" if cover_mime == "image/png" else "jpg" + files.append(("cover", (f"cover.{ext}", cover_bytes, cover_mime))) + try: resp = requests.post( url, data=data, files=files, @@ -91,4 +106,5 @@ def push_book(book_slug: str, chapters: list, title: str, print(f" eptran-web'e gönderildi: novelId={result.get('novelId')}, " f"yeni bölüm={result.get('chaptersCreated')}, " - f"güncellenen bölüm={result.get('chaptersUpdated')}") + f"güncellenen bölüm={result.get('chaptersUpdated')}, " + f"kapak={'evet' if result.get('coverSaved') else 'yok'}")