146 lines
4.5 KiB
Python
146 lines
4.5 KiB
Python
|
|
import httpx
|
||
|
|
from typing import List, Optional
|
||
|
|
from sqlalchemy import select
|
||
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
|
from sqlalchemy.orm import selectinload
|
||
|
|
|
||
|
|
from ..db_models import Anime
|
||
|
|
from ..schemas import AnimeSearchResult
|
||
|
|
from ..config import downloader_settings
|
||
|
|
|
||
|
|
# Shared HTTP client for Shikimori API
|
||
|
|
_shikimori_client: Optional[httpx.AsyncClient] = None
|
||
|
|
|
||
|
|
|
||
|
|
def _get_shikimori_client() -> httpx.AsyncClient:
|
||
|
|
"""Get or create shared HTTP client for Shikimori."""
|
||
|
|
global _shikimori_client
|
||
|
|
if _shikimori_client is None or _shikimori_client.is_closed:
|
||
|
|
headers = {
|
||
|
|
"User-Agent": downloader_settings.shikimori_user_agent,
|
||
|
|
}
|
||
|
|
if downloader_settings.shikimori_token:
|
||
|
|
headers["Authorization"] = f"Bearer {downloader_settings.shikimori_token}"
|
||
|
|
_shikimori_client = httpx.AsyncClient(
|
||
|
|
headers=headers,
|
||
|
|
timeout=30.0,
|
||
|
|
)
|
||
|
|
return _shikimori_client
|
||
|
|
|
||
|
|
|
||
|
|
class ShikimoriService:
|
||
|
|
"""Service for Shikimori GraphQL API."""
|
||
|
|
|
||
|
|
GRAPHQL_URL = "https://shikimori.one/api/graphql"
|
||
|
|
|
||
|
|
def __init__(self):
|
||
|
|
self.client = _get_shikimori_client()
|
||
|
|
|
||
|
|
async def search(
|
||
|
|
self,
|
||
|
|
query: str,
|
||
|
|
year: Optional[int] = None,
|
||
|
|
status: Optional[str] = None,
|
||
|
|
limit: int = 20,
|
||
|
|
) -> List[AnimeSearchResult]:
|
||
|
|
"""Search anime by query using Shikimori GraphQL API."""
|
||
|
|
|
||
|
|
graphql_query = """
|
||
|
|
query($search: String, $limit: Int, $season: SeasonString, $status: AnimeStatusString) {
|
||
|
|
animes(search: $search, limit: $limit, season: $season, status: $status) {
|
||
|
|
id
|
||
|
|
russian
|
||
|
|
english
|
||
|
|
japanese
|
||
|
|
airedOn { year }
|
||
|
|
poster { originalUrl }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
"""
|
||
|
|
|
||
|
|
variables = {
|
||
|
|
"search": query,
|
||
|
|
"limit": limit,
|
||
|
|
}
|
||
|
|
if year:
|
||
|
|
variables["season"] = str(year)
|
||
|
|
if status:
|
||
|
|
variables["status"] = status
|
||
|
|
|
||
|
|
response = await self.client.post(
|
||
|
|
self.GRAPHQL_URL,
|
||
|
|
json={"query": graphql_query, "variables": variables},
|
||
|
|
)
|
||
|
|
response.raise_for_status()
|
||
|
|
data = response.json()
|
||
|
|
|
||
|
|
results = []
|
||
|
|
for anime in data.get("data", {}).get("animes", []):
|
||
|
|
results.append(AnimeSearchResult(
|
||
|
|
shikimori_id=int(anime["id"]),
|
||
|
|
title_russian=anime.get("russian"),
|
||
|
|
title_english=anime.get("english"),
|
||
|
|
title_japanese=anime.get("japanese"),
|
||
|
|
year=anime.get("airedOn", {}).get("year") if anime.get("airedOn") else None,
|
||
|
|
poster_url=anime.get("poster", {}).get("originalUrl") if anime.get("poster") else None,
|
||
|
|
))
|
||
|
|
|
||
|
|
return results
|
||
|
|
|
||
|
|
async def get_or_create_anime(self, db: AsyncSession, shikimori_id: int) -> Anime:
|
||
|
|
"""Get anime from DB or fetch from Shikimori and create."""
|
||
|
|
|
||
|
|
# Check if exists (with themes eagerly loaded)
|
||
|
|
query = (
|
||
|
|
select(Anime)
|
||
|
|
.where(Anime.shikimori_id == shikimori_id)
|
||
|
|
.options(selectinload(Anime.themes))
|
||
|
|
)
|
||
|
|
result = await db.execute(query)
|
||
|
|
anime = result.scalar_one_or_none()
|
||
|
|
|
||
|
|
if anime:
|
||
|
|
return anime
|
||
|
|
|
||
|
|
# Fetch from Shikimori
|
||
|
|
graphql_query = """
|
||
|
|
query($ids: String!) {
|
||
|
|
animes(ids: $ids, limit: 1) {
|
||
|
|
id
|
||
|
|
russian
|
||
|
|
english
|
||
|
|
japanese
|
||
|
|
airedOn { year }
|
||
|
|
poster { originalUrl }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
"""
|
||
|
|
|
||
|
|
response = await self.client.post(
|
||
|
|
self.GRAPHQL_URL,
|
||
|
|
json={"query": graphql_query, "variables": {"ids": str(shikimori_id)}},
|
||
|
|
)
|
||
|
|
response.raise_for_status()
|
||
|
|
data = response.json()
|
||
|
|
|
||
|
|
animes = data.get("data", {}).get("animes", [])
|
||
|
|
if not animes:
|
||
|
|
raise ValueError(f"Anime with ID {shikimori_id} not found on Shikimori")
|
||
|
|
|
||
|
|
anime_data = animes[0]
|
||
|
|
|
||
|
|
anime = Anime(
|
||
|
|
shikimori_id=shikimori_id,
|
||
|
|
title_russian=anime_data.get("russian"),
|
||
|
|
title_english=anime_data.get("english"),
|
||
|
|
title_japanese=anime_data.get("japanese"),
|
||
|
|
year=anime_data.get("airedOn", {}).get("year") if anime_data.get("airedOn") else None,
|
||
|
|
poster_url=anime_data.get("poster", {}).get("originalUrl") if anime_data.get("poster") else None,
|
||
|
|
)
|
||
|
|
|
||
|
|
db.add(anime)
|
||
|
|
await db.commit()
|
||
|
|
await db.refresh(anime)
|
||
|
|
|
||
|
|
return anime
|