"""Anissia API 클라이언트 — 애니 편성표 + 자막 정보 조회. API Base: https://api.anissia.net """ import httpx import logging from dataclasses import dataclass, field from typing import Optional logger = logging.getLogger("variet.tools.anissia") BASE_URL = "https://api.anissia.net" WEEK_NAMES = { 0: "일", 1: "월", 2: "화", 3: "수", 4: "목", 5: "금", 6: "토", 7: "기타", } @dataclass class CaptionInfo: """자막 제작 정보.""" episode: str name: str # 제작자 이름 website: str # 제작자 사이트 URL updated: str # 업데이트 시각 @dataclass class AnimeInfo: """애니메이션 정보.""" anime_no: int subject: str # 한글 제목 original_subject: str # 원어 제목 (일어) genres: str week: int time: str status: str # ON / OFF caption_count: int start_date: str end_date: str website: str twitter: str class AnissiaClient: """Anissia REST API 클라이언트.""" def __init__(self, timeout: float = 15.0): self._timeout = timeout async def get_schedule(self, week: int) -> list[AnimeInfo]: """요일별 편성표 조회 (week: 0=일 ~ 6=토, 7=기타).""" async with httpx.AsyncClient(timeout=self._timeout) as client: resp = await client.get(f"{BASE_URL}/anime/schedule/{week}") resp.raise_for_status() data = resp.json() if data.get("code") != "ok": raise RuntimeError(f"Anissia API 오류: {data}") return [ AnimeInfo( anime_no=item["animeNo"], subject=item["subject"], original_subject=item.get("originalSubject", ""), genres=item.get("genres", ""), week=item.get("week", week) if isinstance(item.get("week"), int) else int(item.get("week", week)), time=item.get("time", ""), status=item.get("status", ""), caption_count=item.get("captionCount", 0), start_date=item.get("startDate", ""), end_date=item.get("endDate", ""), website=item.get("website", ""), twitter=item.get("twitter", ""), ) for item in data["data"] ] async def get_all_schedule(self) -> list[AnimeInfo]: """전체 요일 편성표 조회 (0~7).""" all_anime = [] for week in range(8): try: schedule = await self.get_schedule(week) all_anime.extend(schedule) except Exception as e: logger.warning(f"편성표 조회 실패 (week={week}): {e}") return all_anime async def get_captions(self, anime_no: int) -> list[CaptionInfo]: """특정 애니 자막 목록 조회.""" async with httpx.AsyncClient(timeout=self._timeout) as client: resp = await client.get(f"{BASE_URL}/anime/caption/animeNo/{anime_no}") resp.raise_for_status() data = resp.json() if data.get("code") != "ok": raise RuntimeError(f"Anissia caption API 오류: {data}") return [ CaptionInfo( episode=item.get("episode", ""), name=item.get("name", ""), website=item.get("website", ""), updated=item.get("updDt", ""), ) for item in data["data"] ] async def search_anime(self, keyword: str) -> list[AnimeInfo]: """키워드로 전체 편성표에서 검색 (한글/일어 제목 매칭).""" all_anime = await self.get_all_schedule() keyword_lower = keyword.lower() return [ a for a in all_anime if keyword_lower in a.subject.lower() or keyword_lower in a.original_subject.lower() ]