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:
2026-03-08 16:07:16 +09:00
parent 49ee5f397c
commit c92433b0b1
36 changed files with 3663 additions and 128 deletions

175
tests/test_anime_tools.py Normal file
View File

@@ -0,0 +1,175 @@
"""애니메이션 도구 테스트 — API 파싱 + 제목 매칭 검증."""
import asyncio
import sys
import os
import io
if sys.stdout.encoding != "utf-8":
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
async def test_anissia():
"""Anissia API 테스트."""
print("=== Anissia API Test ===")
from tools.anissia_client import AnissiaClient
client = AnissiaClient()
# 편성표 조회 (일요일)
schedule = await client.get_schedule(0)
print(f" 일요일 편성: {len(schedule)}")
for a in schedule[:3]:
print(f" [{a.anime_no}] {a.subject} ({a.original_subject}) 자막:{a.caption_count}")
# 검색
results = await client.search_anime("프리렌")
print(f" '프리렌' 검색: {len(results)}")
for a in results:
print(f" {a.subject}{a.original_subject}")
# 자막 조회 (첫 번째 결과)
if schedule:
first = schedule[0]
captions = await client.get_captions(first.anime_no)
print(f" '{first.subject}' 자막: {len(captions)}")
for c in captions:
print(f" {c.name}{c.episode}화 — {c.website[:50] if c.website else '(없음)'}")
print(" ✅ Anissia OK\n")
async def test_nyaa():
"""Nyaa.si RSS 테스트."""
print("=== Nyaa RSS Test ===")
from tools.nyaa_client import NyaaClient
client = NyaaClient()
results = await client.search("Frieren")
print(f" 'Frieren ASW HEVC' 검색: {len(results)}")
for t in results[:5]:
ep_str = f"{t.episode}" if t.episode else "?"
print(f" [{t.group}] {ep_str} {t.size} seeders:{t.seeders}")
print(f" magnet: {t.magnet_link[:60]}...")
print(" ✅ Nyaa OK\n")
async def test_title_matcher():
"""제목 매칭 테스트."""
print("=== Title Matcher Test ===")
from tools.title_matcher import (
japanese_to_romaji, normalize_title, title_similarity,
make_nas_folder_name, get_quarter,
)
# 로마자 변환
tests = [
("葬送のフリーレン", "sousou no furiren"),
("鬼滅の刃", "kimetsu no yaiba"),
("ワンピース", "wanpisu"),
]
for jp, expected_approx in tests:
romaji = japanese_to_romaji(jp)
print(f" {jp}{romaji} (기대: ~{expected_approx})")
# 유사도 계산
pairs = [
("Sousou no Frieren", "sousou no furiren"),
("Kimetsu no Yaiba", "kimetsu no yaiba"),
("완전 다른 제목", "completely different"),
]
for a, b in pairs:
sim = title_similarity(a, b)
print(f" 유사도 '{a}' vs '{b}': {sim:.2f}")
# NAS 폴더명
folder = make_nas_folder_name("장송의프리렌 2기", "2026-01-11")
print(f" NAS 폴더: {folder}")
assert folder == "[26_1분기]장송의프리렌 2기", f"Expected [26_1분기]장송의프리렌 2기, got {folder}"
# 분기 계산
q_tests = [
("2026-01-11", (26, 1)),
("2026-04-01", (26, 2)),
("2026-07-15", (26, 3)),
("2026-10-05", (26, 4)),
]
for date, expected in q_tests:
result = get_quarter(date)
assert result == expected, f"get_quarter({date}) = {result}, expected {expected}"
print(f" {date}{result[0]}{result[1]}분기 ✓")
print(" ✅ Title Matcher OK\n")
async def test_subtitle_parser():
"""자막 파서 테스트 (HTML 파싱)."""
print("=== Subtitle Parser Test ===")
from tools.subtitle_downloader import (
parse_google_drive_links,
parse_tistory_links,
parse_naver_links,
)
# Google Drive 파싱
gdrive_html = '''
<a href="https://drive.google.com/file/d/abc123/view?usp=sharing">1화 자막</a>
<a href="https://drive.google.com/file/d/def456/view">2화 자막</a>
'''
gd_results = parse_google_drive_links(gdrive_html)
print(f" Google Drive 파싱: {len(gd_results)}")
for r in gd_results:
print(f" {r.filename}{r.download_url} (ep={r.episode})")
assert len(gd_results) >= 2, "Google Drive 파싱 실패"
# Tistory 파싱
tistory_html = '''
<a href="https://blog.kakaocdn.net/dna/test/file.zip?credential=abc">file.zip</a>
'''
ts_results = parse_tistory_links(tistory_html)
print(f" Tistory 파싱: {len(ts_results)}")
assert len(ts_results) >= 1, "Tistory 파싱 실패"
# Naver 파싱
naver_html = '''
<a class="se-file-save-button" href="https://download.blog.naver.com/path/test.zip">다운로드</a>
'''
nv_results = parse_naver_links(naver_html)
print(f" Naver 파싱: {len(nv_results)}")
assert len(nv_results) >= 1, "Naver 파싱 실패"
print(" ✅ Subtitle Parser OK\n")
async def test_qbit_connection():
"""qBittorrent 연결 테스트."""
print("=== qBittorrent Connection Test ===")
from tools.qbit_client import QBitClient
client = QBitClient()
result = await client.test_connection()
if result["connected"]:
print(f" ✅ 연결 성공: v{result['version']} (API {result['api_version']})")
else:
print(f" ⚠️ 연결 실패: {result.get('error', '?')}")
print(f" URL: {result['url']}")
print(" (qBittorrent Web UI가 꺼져있을 수 있음)")
print()
async def main():
await test_title_matcher()
await test_subtitle_parser()
await test_anissia()
await test_nyaa()
await test_qbit_connection()
print("🎉 All tests completed!")
if __name__ == "__main__":
asyncio.run(main())