fix(anime): 파이프라인 5건 수정 — 에피소드 정규식(v2/S01E), 릴리스 그룹 필터, 자막 보호, 배치 다운로드, 타임아웃
This commit is contained in:
119
main.py
119
main.py
@@ -1,24 +1,53 @@
|
||||
"""Variet Agent — 진입점.
|
||||
|
||||
FastAPI 서버 + Discord Bot을 동시 실행합니다.
|
||||
FastAPI 서버 + Discord Bot + APScheduler를 동시 실행합니다.
|
||||
상시 실행 안정화: 파일 로깅, graceful shutdown, 자가 헬스체크.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
import signal
|
||||
from logging.handlers import RotatingFileHandler
|
||||
from pathlib import Path
|
||||
|
||||
# config를 먼저 import → .env 로드
|
||||
import config
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# 로깅 설정 (파일 + 콘솔)
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
LOG_DIR = Path(__file__).parent / "logs"
|
||||
LOG_DIR.mkdir(exist_ok=True)
|
||||
|
||||
handlers = [
|
||||
logging.StreamHandler(sys.stdout),
|
||||
RotatingFileHandler(
|
||||
LOG_DIR / "variet.log",
|
||||
maxBytes=10 * 1024 * 1024, # 10MB
|
||||
backupCount=5,
|
||||
encoding="utf-8",
|
||||
),
|
||||
]
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
|
||||
datefmt="%H:%M:%S",
|
||||
handlers=[logging.StreamHandler(sys.stdout)],
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
handlers=handlers,
|
||||
)
|
||||
logger = logging.getLogger("variet")
|
||||
|
||||
# 외부 라이브러리 로그 레벨 조정
|
||||
logging.getLogger("discord").setLevel(logging.WARNING)
|
||||
logging.getLogger("uvicorn").setLevel(logging.WARNING)
|
||||
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# 서브시스템
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
async def run_api_server():
|
||||
"""FastAPI 서버를 uvicorn으로 실행."""
|
||||
@@ -28,7 +57,7 @@ async def run_api_server():
|
||||
"api.server:app",
|
||||
host=config.API_HOST,
|
||||
port=config.API_PORT,
|
||||
log_level="info",
|
||||
log_level="warning",
|
||||
reload=False,
|
||||
)
|
||||
server = uvicorn.Server(uvi_config)
|
||||
@@ -41,12 +70,49 @@ async def run_discord_bot():
|
||||
await start_bot()
|
||||
|
||||
|
||||
async def run_scheduler():
|
||||
"""APScheduler — 주기적 작업 실행."""
|
||||
try:
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
except ImportError:
|
||||
logger.warning("APScheduler 미설치 — 스케줄러 비활성화 (pip install apscheduler)")
|
||||
return
|
||||
|
||||
scheduler = AsyncIOScheduler()
|
||||
|
||||
# 헬스체크: 5분마다
|
||||
async def health_check():
|
||||
logger.debug("헬스체크 OK")
|
||||
|
||||
scheduler.add_job(health_check, "interval", minutes=5, id="health_check")
|
||||
|
||||
# TODO: 여기에 주기적 작업 추가
|
||||
# 예: 편성표 체크, 완료 토렌트 정리 등
|
||||
# scheduler.add_job(check_schedule, "cron", hour=18, id="anime_schedule")
|
||||
|
||||
scheduler.start()
|
||||
logger.info(f"스케줄러 시작 — {len(scheduler.get_jobs())}개 작업 등록")
|
||||
|
||||
# 스케줄러가 종료되지 않도록 대기
|
||||
try:
|
||||
while True:
|
||||
await asyncio.sleep(3600)
|
||||
except asyncio.CancelledError:
|
||||
scheduler.shutdown(wait=False)
|
||||
logger.info("스케줄러 종료")
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# 메인
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
async def main():
|
||||
"""API 서버 + Discord Bot 동시 실행."""
|
||||
"""API 서버 + Discord Bot + 스케줄러 동시 실행."""
|
||||
logger.info("=" * 50)
|
||||
logger.info("Variet Agent 시작")
|
||||
logger.info(f" API: http://{config.API_HOST}:{config.API_PORT}")
|
||||
logger.info(f" Discord Bot: {'토큰 설정됨' if config.DISCORD_BOT_TOKEN else '⚠ 토큰 없음'}")
|
||||
logger.info(f" 로그: {LOG_DIR / 'variet.log'}")
|
||||
logger.info("=" * 50)
|
||||
|
||||
tasks = []
|
||||
@@ -60,16 +126,47 @@ async def main():
|
||||
else:
|
||||
logger.warning("DISCORD_BOT_TOKEN이 없습니다. Bot 없이 API만 실행합니다.")
|
||||
|
||||
# 스케줄러
|
||||
tasks.append(asyncio.create_task(run_scheduler()))
|
||||
|
||||
# Graceful shutdown 핸들러
|
||||
shutdown_event = asyncio.Event()
|
||||
|
||||
def _signal_handler():
|
||||
logger.info("종료 신호 수신...")
|
||||
shutdown_event.set()
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
for sig in (signal.SIGINT, signal.SIGTERM):
|
||||
try:
|
||||
loop.add_signal_handler(sig, _signal_handler)
|
||||
except NotImplementedError:
|
||||
# Windows에서는 add_signal_handler 미지원
|
||||
pass
|
||||
|
||||
try:
|
||||
await asyncio.gather(*tasks)
|
||||
# 어느 하나가 종료되거나 shutdown 신호가 오면 종료
|
||||
done, pending = await asyncio.wait(
|
||||
[*tasks, asyncio.create_task(shutdown_event.wait())],
|
||||
return_when=asyncio.FIRST_COMPLETED,
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
logger.info("종료 요청...")
|
||||
logger.info("KeyboardInterrupt 수신...")
|
||||
except Exception as e:
|
||||
logger.error(f"실행 오류: {e}")
|
||||
logger.error(f"실행 오류: {e}", exc_info=True)
|
||||
finally:
|
||||
# 정리
|
||||
from api.discord_bot import stop_bot
|
||||
await stop_bot()
|
||||
# 모든 태스크 정리
|
||||
for t in tasks:
|
||||
if not t.done():
|
||||
t.cancel()
|
||||
|
||||
# Discord Bot 정리
|
||||
try:
|
||||
from api.discord_bot import stop_bot
|
||||
await stop_bot()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
logger.info("Variet Agent 종료")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user