fix(anime): 파이프라인 5건 수정 — 에피소드 정규식(v2/S01E), 릴리스 그룹 필터, 자막 보호, 배치 다운로드, 타임아웃

This commit is contained in:
2026-03-15 08:27:08 +09:00
parent 63818999d9
commit 9f74812710
40 changed files with 2759 additions and 815 deletions

229
handlers/anime_handler.py Normal file
View File

@@ -0,0 +1,229 @@
"""애니메이션 핸들러 — Discord Bot에서 분리된 애니 관련 처리.
AnimePipeline을 직접 호출하여 결과를 Discord Embed로 렌더링합니다.
NLU에서 mode="anime"로 분류된 요청도 처리합니다.
"""
import logging
import discord
from discord import app_commands
from handlers.renderer import safe_send_embed
logger = logging.getLogger("variet.handlers.anime")
async def handle_anime_message(
message: discord.Message,
parsed: dict,
):
"""NLU에서 anime으로 분류된 메시지 처리.
Args:
message: Discord 메시지
parsed: NLU 분류 결과 dict {mode, action, title, episode?, ...}
"""
from tools.anime_pipeline import AnimePipeline
pipeline = AnimePipeline()
action = parsed.get("action", "search")
title = parsed.get("title", parsed.get("query", ""))
async with message.channel.typing():
try:
if action in ("list", "scan"):
# NAS 현황 조회
from tools.nas_scanner import NasScanner
scanner = NasScanner()
if not scanner.is_accessible():
await message.channel.send(embed=discord.Embed(
title="❌ NAS 접근 불가",
description=f"경로: `{scanner.base_path}`",
color=0xE74C3C,
))
return
# title이 있으면 키워드 검색, 없으면 이번 분기
if title:
folders = scanner.search(title)
label = f"'{title}' 검색 결과"
# 키워드 검색 0건이면 이번 분기로 fallback
if not folders:
folders = scanner.get_current_quarter_anime()
label = "이번 분기 애니"
else:
folders = scanner.get_current_quarter_anime()
label = "이번 분기 애니"
if not folders:
await message.channel.send(embed=discord.Embed(
title=f"📁 {label}",
description="해당하는 폴더가 없습니다.",
color=0xF39C12,
))
return
desc_lines = []
for f in folders[:15]:
sub_info = f"자막 {f.subtitle_count}" if f.subtitle_count else "자막 없음"
desc_lines.append(
f"• `{f.folder_name}`\n 영상 {f.video_count}개 | {sub_info} | {f.total_size_gb:.1f}GB"
)
embed = discord.Embed(
title=f"📁 {label} ({len(folders)}개)",
description="\n".join(desc_lines)[:2000],
color=0x3498DB,
)
await safe_send_embed(message.channel, embed)
return
elif action == "search" and title:
result = await pipeline.search(title)
elif action == "search" and not title:
await message.channel.send(embed=discord.Embed(
title="🔍 애니 검색",
description="검색할 제목을 입력해주세요.\n예: `프리렌 검색해줘`",
color=0xF39C12,
))
return
elif action == "download" and title:
mode = parsed.get("download_mode", "auto")
episode = parsed.get("episode")
result = await pipeline.download(title, mode=mode, episode=episode)
elif action == "status":
status = await pipeline.get_status()
if not status:
await message.channel.send(embed=discord.Embed(
title="🎬 다운로드 현황",
description="다운로드 중인 항목 없음",
color=0x3498DB,
))
return
desc = "\n".join(
f"{s['progress']} `{s['name'][:40]}` {s['speed']}"
for s in status
)
await message.channel.send(embed=discord.Embed(
title=f"🎬 다운로드 현황 ({len(status)}건)",
description=desc[:2000],
color=0x3498DB,
))
return
else:
# 알 수 없는 action → search로 fallback
if title:
result = await pipeline.search(title)
else:
await message.channel.send(embed=discord.Embed(
title="❓ 애니 명령",
description="무엇을 도와드릴까요?\n• 목록 조회: `NAS에 뭐있어?`\n• 검색: `프리렌 검색`\n• 다운로드: `프리렌 10화 받아줘`\n• 상태: `다운로드 현황`",
color=0xF39C12,
))
return
# 결과 임베드
embed = discord.Embed(
title=f"🎬 {result.message[:100]}" if result.message else "🎬 결과",
description=result.message[:2000] if result.message else "완료",
color=0x2ECC71 if result.success else 0xE74C3C,
)
if result.errors:
embed.add_field(
name="⚠️ 오류",
value="\n".join(f"{e}" for e in result.errors[:5])[:1000],
inline=False,
)
await safe_send_embed(message.channel, embed)
except Exception as e:
logger.error(f"애니 핸들러 오류: {e}", exc_info=True)
await message.channel.send(embed=discord.Embed(
title="❌ 애니 처리 오류",
description=f"```{str(e)[:300]}```",
color=0xE74C3C,
))
def register_anime_commands(bot, ws_manager):
"""애니메이션 슬래시 커맨드 등록."""
anime_group = app_commands.Group(name="anime", description="애니메이션 자막/영상 자동화")
@anime_group.command(name="search", description="애니 검색 (편성표 + 자막 + 토렌트)")
@app_commands.describe(title="검색할 애니 제목 (한글)")
async def anime_search(interaction: discord.Interaction, title: str):
await interaction.response.defer()
from tools.anime_pipeline import AnimePipeline
pipeline = AnimePipeline()
result = await pipeline.search(title)
embed = discord.Embed(
title=f"🔍 {result.anime.subject}" if result.anime else f"🔍 {title}",
description=result.message[:2000],
color=0x2ECC71 if result.success else 0xE74C3C,
)
await interaction.followup.send(embed=embed)
@anime_group.command(name="download", description="자막+영상 자동 다운로드")
@app_commands.describe(title="애니 제목 (한글)", episode="특정 화수 (없으면 최신)")
async def anime_download(interaction: discord.Interaction, title: str, episode: int = None):
await interaction.response.defer()
from tools.anime_pipeline import AnimePipeline
pipeline = AnimePipeline()
result = await pipeline.download(title, mode="auto", episode=episode)
embed = discord.Embed(
title=f"📥 {title}",
description=result.message[:2000],
color=0x2ECC71 if result.success else 0xE74C3C,
)
await interaction.followup.send(embed=embed)
@anime_group.command(name="sub", description="자막만 다운로드")
@app_commands.describe(title="애니 제목 (한글)", episode="특정 화수")
async def anime_sub(interaction: discord.Interaction, title: str, episode: int = None):
await interaction.response.defer()
from tools.anime_pipeline import AnimePipeline
pipeline = AnimePipeline()
result = await pipeline.download(title, mode="sub_only", episode=episode)
embed = discord.Embed(
title=f"📝 자막: {title}",
description=result.message[:2000],
color=0x2ECC71 if result.success else 0xE74C3C,
)
await interaction.followup.send(embed=embed)
@anime_group.command(name="video", description="영상만 다운로드 (자막 없어도 강제)")
@app_commands.describe(title="애니 제목 (한글)", episode="특정 화수")
async def anime_video(interaction: discord.Interaction, title: str, episode: int = None):
await interaction.response.defer()
from tools.anime_pipeline import AnimePipeline
pipeline = AnimePipeline()
result = await pipeline.download(title, mode="video_only", episode=episode)
embed = discord.Embed(
title=f"🎬 영상: {title}",
description=result.message[:2000],
color=0x2ECC71 if result.success else 0xE74C3C,
)
await interaction.followup.send(embed=embed)
@anime_group.command(name="status", description="현재 다운로드 큐 상태")
async def anime_status(interaction: discord.Interaction):
await interaction.response.defer()
from tools.anime_pipeline import AnimePipeline
pipeline = AnimePipeline()
status = await pipeline.get_status()
if not status:
desc = "다운로드 중인 항목 없음"
else:
desc = "\n".join(
f"{s['progress']} `{s['name'][:40]}` {s['speed']}"
for s in status
)
embed = discord.Embed(
title=f"🎬 다운로드 현황 ({len(status)}건)",
description=desc[:2000],
color=0x3498DB,
)
await interaction.followup.send(embed=embed)
bot.tree.add_command(anime_group)
logger.info("애니메이션 슬래시 커맨드 등록 완료")