All checks were successful
news-summary-bot-cicd / build_push_deploy (push) Successful in 11m43s
110 lines
3.4 KiB
Python
110 lines
3.4 KiB
Python
import re
|
|
from datetime import datetime, timezone
|
|
|
|
import httpx
|
|
|
|
from app.config import settings
|
|
|
|
|
|
def _extract_video_id(video_url: str) -> str | None:
|
|
"""URL에서 YouTube 비디오 ID 추출."""
|
|
patterns = [
|
|
r"(?:youtu\.be/)([^?&]+)",
|
|
r"(?:v=)([^?&]+)",
|
|
]
|
|
for p in patterns:
|
|
m = re.search(p, video_url)
|
|
if m:
|
|
return m.group(1)
|
|
return None
|
|
|
|
|
|
def _parse_summary(summary: str) -> dict[str, str]:
|
|
"""요약 텍스트를 섹션별로 파싱."""
|
|
sections: dict[str, str] = {}
|
|
current_key = None
|
|
current_lines: list[str] = []
|
|
|
|
for line in summary.split("\n"):
|
|
# **한줄 요약**: ... 또는 ## 한줄 요약 형태 매칭
|
|
header_match = re.match(
|
|
r"^(?:##\s*|-\s*\*\*|\*\*)(한줄\s*요약|주요\s*내용|결론/?시사점)[:\*\s]*(.*)",
|
|
line,
|
|
)
|
|
if header_match:
|
|
if current_key:
|
|
sections[current_key] = "\n".join(current_lines).strip()
|
|
current_key = header_match.group(1).replace(" ", "")
|
|
rest = re.sub(r"^\*\*:?\s*", "", header_match.group(2)).strip()
|
|
current_lines = [rest] if rest else []
|
|
elif current_key is not None:
|
|
current_lines.append(line)
|
|
|
|
if current_key:
|
|
sections[current_key] = "\n".join(current_lines).strip()
|
|
|
|
return sections
|
|
|
|
|
|
async def send_to_discord(title: str, video_url: str, summary: str) -> None:
|
|
"""Discord 웹훅으로 요약 전송 (임베드 디자인)."""
|
|
video_id = _extract_video_id(video_url)
|
|
thumbnail_url = (
|
|
f"https://img.youtube.com/vi/{video_id}/hqdefault.jpg"
|
|
if video_id
|
|
else None
|
|
)
|
|
|
|
sections = _parse_summary(summary)
|
|
|
|
oneliner = sections.get("한줄요약", "")
|
|
main_points = sections.get("주요내용", "")
|
|
conclusion = sections.get("결론/시사점", sections.get("결론시사점", ""))
|
|
|
|
# 파싱 실패 시 전체 텍스트를 그대로 사용
|
|
if not oneliner and not main_points:
|
|
fields = [{"name": "🔗 원본 영상", "value": video_url, "inline": False}]
|
|
description = summary[:4096]
|
|
else:
|
|
description = f"### 💡 {oneliner}" if oneliner else ""
|
|
fields = []
|
|
if main_points:
|
|
fields.append({
|
|
"name": "📋 주요 내용",
|
|
"value": main_points[:1024],
|
|
"inline": False,
|
|
})
|
|
if conclusion:
|
|
fields.append({
|
|
"name": "🎯 결론 / 시사점",
|
|
"value": conclusion[:1024],
|
|
"inline": False,
|
|
})
|
|
fields.append({
|
|
"name": "🔗 원본 영상",
|
|
"value": video_url,
|
|
"inline": False,
|
|
})
|
|
|
|
embed = {
|
|
"title": f"📰 {title}",
|
|
"url": video_url,
|
|
"description": description,
|
|
"color": 0x2B2D31,
|
|
"fields": fields,
|
|
"footer": {
|
|
"text": "YouTube 뉴스 요약 봇",
|
|
"icon_url": "https://www.youtube.com/s/desktop/f5ced909/img/favicon_144x144.png",
|
|
},
|
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
}
|
|
|
|
if thumbnail_url:
|
|
embed["thumbnail"] = {"url": thumbnail_url}
|
|
|
|
payload = {"embeds": [embed]}
|
|
|
|
async with httpx.AsyncClient() as client:
|
|
resp = await client.post(settings.discord_webhook_url, json=payload)
|
|
resp.raise_for_status()
|