- tools/anissia_client.py: Anissia API 클라이언트 (편성표/자막) - tools/nyaa_client.py: Nyaa.si RSS 토렌트 검색 - tools/qbit_client.py: qBittorrent Web API 클라이언트 - tools/subtitle_downloader.py: Google Drive/Tistory/Naver 자막 파서 - tools/title_matcher.py: 제목 매칭 + NAS 폴더명 생성 - tools/anime_pipeline.py: 전체 파이프라인 오케스트레이터 - tools/nas_scanner.py: NAS 폴더/파일 스캔 - prompts/unified.md: anime 모드 추가 (AI 평문 의도 분류) - api/discord_bot.py: AI 평문 anime 핸들러 + /anime 슬래시 커맨드 - config.py: qBittorrent/NAS 설정 추가 - .agents/: agent_guide 워크플로우 통합 - docs/devlog: 세션 기록
121 lines
3.8 KiB
Python
121 lines
3.8 KiB
Python
"""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()
|
|
]
|