feat(tools): 애니메이션 자동화 파이프라인 구현
- 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: 세션 기록
This commit is contained in:
120
tools/anissia_client.py
Normal file
120
tools/anissia_client.py
Normal file
@@ -0,0 +1,120 @@
|
||||
"""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()
|
||||
]
|
||||
Reference in New Issue
Block a user