feat: import F95 covers using personal encrypted sessions
This commit is contained in:
1 parent
3f51f06fec
commit
219005d16a
16 files changed
+763
-26
No files matched your search
@@ -1,9 +1,12 @@
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from pydantic import BaseModel
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
from pydantic import BaseModel, SecretStr
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
|
||||
from app import f95
|
||||
from app.deps import CurrentUser, has_perm
|
||||
from app.deps import DB, CurrentUser, has_perm
|
||||
from app.f95_session import open_session, seal
|
||||
from app.routers.games import make_cover
|
||||
|
||||
router = APIRouter(prefix="/api/metadata", tags=["metadata"])
|
||||
|
||||
@@ -12,18 +15,81 @@ class F95In(BaseModel):
|
||||
url: str
|
||||
|
||||
|
||||
class F95SessionIn(BaseModel):
|
||||
cookie: SecretStr
|
||||
user_agent: str = ""
|
||||
|
||||
|
||||
class MetaOut(BaseModel):
|
||||
url: str
|
||||
title: str
|
||||
description: str
|
||||
tags: list[str]
|
||||
title: str = ""
|
||||
description: str = ""
|
||||
tags: list[str] = []
|
||||
cover_hash: str | None = None
|
||||
cover_warning: str | None = None
|
||||
auth_required: bool = False
|
||||
metadata_available: bool = True
|
||||
|
||||
|
||||
def load_metadata(url: str, cookie: str = "", user_agent: str = "") -> dict:
|
||||
url = f95.validate_url(url)
|
||||
try:
|
||||
metadata = f95.load(url, cookie, user_agent)
|
||||
except f95.F95AuthError as e:
|
||||
return {"url": url, "auth_required": True, "metadata_available": False, "cover_warning": str(e)}
|
||||
logged_in = metadata.pop("logged_in", None)
|
||||
metadata["cover_hash"] = None
|
||||
if image := metadata.pop("image_url", None):
|
||||
try:
|
||||
metadata["cover_hash"] = make_cover(f95.fetch_image(image, metadata["url"], cookie, user_agent))
|
||||
except f95.F95AuthError as e:
|
||||
metadata["auth_required"] = True
|
||||
metadata["cover_warning"] = f"{e}. Остальные данные загружены."
|
||||
except f95.F95Error as e:
|
||||
metadata["cover_warning"] = f"{e}. Остальные данные загружены."
|
||||
except (UnidentifiedImageError, OSError, ValueError, Image.DecompressionBombError):
|
||||
metadata["cover_warning"] = "Не удалось загрузить превью F95zone. Остальные данные загружены."
|
||||
else:
|
||||
metadata["auth_required"] = not cookie or logged_in in ("false", "0")
|
||||
metadata["cover_warning"] = "В теме не найдено доступное превью. Остальные данные загружены."
|
||||
return metadata
|
||||
|
||||
|
||||
def require_import(user: CurrentUser) -> None:
|
||||
if not (has_perm(user, "games.upload") or has_perm(user, "games.edit")):
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN, "Нет прав")
|
||||
|
||||
|
||||
@router.put("/f95/session", status_code=204)
|
||||
async def save_f95_session(body: F95SessionIn, user: CurrentUser, db: DB) -> None:
|
||||
require_import(user)
|
||||
cookie = body.cookie.get_secret_value().strip()
|
||||
if cookie.lower().startswith("cookie:"):
|
||||
cookie = cookie[7:].strip()
|
||||
agent = body.user_agent.strip()
|
||||
if (not cookie or len(cookie) > 16384 or len(agent) > 2048
|
||||
or any(ord(c) < 32 or ord(c) > 126 for c in cookie + agent)
|
||||
or not all("=" in part and part.strip().split("=", 1)[0] for part in cookie.split(";"))):
|
||||
raise HTTPException(422, "Вставьте значение заголовка Cookie из запроса F95zone одной строкой")
|
||||
user.f95_session = seal(user.id, cookie, agent)
|
||||
await db.commit()
|
||||
|
||||
|
||||
@router.delete("/f95/session", status_code=204)
|
||||
async def delete_f95_session(user: CurrentUser, db: DB) -> None:
|
||||
user.f95_session = None
|
||||
await db.commit()
|
||||
|
||||
|
||||
@router.post("/f95", response_model=MetaOut)
|
||||
async def from_f95(body: F95In, user: CurrentUser) -> dict:
|
||||
if not (has_perm(user, "games.upload") or has_perm(user, "games.edit")):
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN, "Нет прав")
|
||||
async def from_f95(body: F95In, user: CurrentUser, db: DB) -> dict:
|
||||
require_import(user)
|
||||
cookie, agent = open_session(user.id, user.f95_session)
|
||||
try:
|
||||
return await run_in_threadpool(f95.load, body.url)
|
||||
result = await run_in_threadpool(load_metadata, body.url, cookie, agent)
|
||||
if result.get("auth_required") and user.f95_session:
|
||||
user.f95_session = None
|
||||
await db.commit()
|
||||
return result
|
||||
except f95.F95Error as e:
|
||||
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, str(e))
|
||||
Reference in new issue
Block a user