Refactor: [3.0.0] Discord 전송을 n8n으로 이관, YouTube RSS → Data API 전환
Some checks failed
news-summary-bot-cicd / build_push_deploy (push) Has been cancelled
Some checks failed
news-summary-bot-cicd / build_push_deploy (push) Has been cancelled
- FastAPI에서 Discord 직접 전송 제거, 요약 결과를 JSON으로 반환 - app/discord.py 삭제, DISCORD_WEBHOOK_URL 환경변수 제거 - 에러 시 500 대신 200 + status: error로 응답 (n8n에서 분기 처리) - CLAUDE.md에 n8n 워크플로우 노드별 상세 설정 문서화 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
184
CLAUDE.md
184
CLAUDE.md
@@ -4,9 +4,10 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
## 프로젝트 개요
|
## 프로젝트 개요
|
||||||
|
|
||||||
YouTube 뉴스/경제 채널의 새 영상을 감지하면 자막을 추출하고 Claude API로 요약한 뒤 Discord로 전송하는 봇.
|
YouTube 뉴스/경제 채널의 새 영상을 감지하면 자막을 추출하고 Claude API로 요약하는 봇.
|
||||||
- 영상 감지: n8n RSS 트리거 (외부)
|
- 영상 감지: n8n (YouTube Data API v3 `playlistItems.list`)
|
||||||
- 요약 처리: FastAPI 앱 (이 레포)
|
- 요약 처리: FastAPI 앱 (이 레포)
|
||||||
|
- Discord 전송: n8n (FastAPI 응답을 받아서 Discord 웹훅으로 전송)
|
||||||
- 배포: Docker Hub → OCI 서버에서 docker-compose pull
|
- 배포: Docker Hub → OCI 서버에서 docker-compose pull
|
||||||
|
|
||||||
## 빌드 & 실행
|
## 빌드 & 실행
|
||||||
@@ -23,39 +24,192 @@ docker compose up
|
|||||||
|
|
||||||
## 아키텍처
|
## 아키텍처
|
||||||
|
|
||||||
n8n(RSS 감지) → `POST /api/news/summarize` → 자막 추출 → Claude 요약 → Discord 웹훅
|
n8n(YouTube API로 새 영상 감지) → `POST /api/news/summarize` → 자막 추출 → Claude 요약 → JSON 응답 → n8n이 Discord 웹훅 전송
|
||||||
|
|
||||||
- `app/main.py` — FastAPI 엔드포인트 (`/summarize`, `/health`) — Nginx가 `/api/news/` prefix를 strip
|
- `app/main.py` — FastAPI 엔드포인트 (`/summarize`, `/health`) — Nginx가 `/api/news/` prefix를 strip
|
||||||
- `app/transcript.py` — YouTube 자막 추출 (`yt-dlp` + 쿠키 인증)
|
- `app/transcript.py` — YouTube 자막 추출 (`yt-dlp` + 쿠키 인증)
|
||||||
- `app/summarizer.py` — Claude Sonnet 4.6으로 요약 생성
|
- `app/summarizer.py` — Claude Sonnet 4.6으로 요약 생성
|
||||||
- `app/discord.py` — Discord 웹훅 전송
|
|
||||||
- `app/config.py` — 환경변수 설정 (pydantic-settings)
|
- `app/config.py` — 환경변수 설정 (pydantic-settings)
|
||||||
|
|
||||||
|
### API 응답 형식
|
||||||
|
|
||||||
|
```json
|
||||||
|
// 성공
|
||||||
|
{"status": "ok", "title": "...", "summary": "요약 텍스트"}
|
||||||
|
|
||||||
|
// 스킵 (라이브/쇼츠)
|
||||||
|
{"status": "skipped", "title": "...", "reason": "라이브/예정 영상은 요약 대상이 아닙니다"}
|
||||||
|
|
||||||
|
// 에러
|
||||||
|
{"status": "error", "title": "...", "error_type": "ValueError", "error_message": "..."}
|
||||||
|
```
|
||||||
|
|
||||||
## 환경변수
|
## 환경변수
|
||||||
|
|
||||||
`ANTHROPIC_API_KEY`, `DISCORD_WEBHOOK_URL` 필수. `API_SECRET`은 선택(n8n → FastAPI 인증용).
|
`ANTHROPIC_API_KEY` 필수. `API_SECRET`은 선택(n8n → FastAPI 인증용).
|
||||||
|
|
||||||
## 쿠키 인증 (YouTube 봇 감지 우회)
|
## 쿠키 인증 (YouTube 봇 감지 우회)
|
||||||
|
|
||||||
OCI 등 클라우드 서버에서 YouTube 자막 추출 시 봇 감지 차단을 우회하기 위해 쿠키 파일이 필요.
|
OCI 등 클라우드 서버에서 YouTube 자막 추출 시 봇 감지 차단을 우회하기 위해 쿠키 파일이 필요.
|
||||||
- 브라우저 확장(Get cookies.txt LOCALLY 등)으로 YouTube 쿠키를 `cookies.txt`로 export
|
- 브라우저 확장(Get cookies.txt LOCALLY 등)으로 YouTube 쿠키를 `cookies.txt`로 export
|
||||||
- 서버의 `compose.apps.yml`에서 `./news-summary-bot/cookies.txt:/app/cookies.txt:ro`로 마운트
|
- 서버의 `compose.apps.yml`에서 `./news-summary-bot/cookies.txt:/app/cookies.txt:ro`로 마운트
|
||||||
- 쿠키 만료 시(6개월~1년) 재export 필요 → 500 에러 발생 시 쿠키 갱신 확인
|
- 쿠키 만료 시(6개월~1년) 재export 필요 → 에러 발생 시 쿠키 갱신 확인
|
||||||
|
|
||||||
## n8n 워크플로우
|
## n8n 워크플로우
|
||||||
|
|
||||||
|
### 전체 흐름
|
||||||
|
|
||||||
```
|
```
|
||||||
RSS Feed Trigger (채널A) ──┐
|
Schedule Trigger (매 정시) ─→ 머니코믹스 playlistItems ─→ Edit Fields (channel_name: 머니코믹스) ──┐
|
||||||
├→ Merge → HTTP Request (POST /api/news/summarize)
|
└→ 슈카월드 playlistItems ─→ Edit Fields (channel_name: 슈카월드) ──┤
|
||||||
RSS Feed Trigger (채널B) ──┘
|
↓
|
||||||
|
Merge (Append)
|
||||||
|
↓
|
||||||
|
Code (새 영상만 필터링)
|
||||||
|
↓
|
||||||
|
HTTP Request (POST /api/news/summarize)
|
||||||
|
↓
|
||||||
|
Switch (status 분기)
|
||||||
|
├→ ok → Discord 요약 전송
|
||||||
|
├→ skipped → No Operation
|
||||||
|
└→ error → Discord 에러 알림
|
||||||
```
|
```
|
||||||
|
|
||||||
- **RSS Feed Trigger**: 채널별 RSS URL로 새 영상만 감지 (중복 방지 내장, Poll Time으로 주기 설정)
|
### 노드별 설정
|
||||||
- **Merge**: 두 채널의 새 영상을 하나의 리스트로 합침
|
|
||||||
- **HTTP Request**: 각 영상마다 `POST <서버IP>/api/news/summarize` 호출
|
|
||||||
|
|
||||||
요청 바디:
|
#### 1. Schedule Trigger
|
||||||
|
- **Type**: Schedule Trigger
|
||||||
|
- **Rule**: Every Hour, Minute=0
|
||||||
|
|
||||||
|
#### 2. YouTube API - playlistItems (채널별 각 1개)
|
||||||
|
- **Type**: HTTP Request
|
||||||
|
- **Method**: GET
|
||||||
|
- **URL**: `https://www.googleapis.com/youtube/v3/playlistItems`
|
||||||
|
- **Query Parameters**:
|
||||||
|
- `part`: `snippet`
|
||||||
|
- `playlistId`: 채널 업로드 목록 ID (채널 ID의 `UC` → `UU`로 변환)
|
||||||
|
- `maxResults`: `5`
|
||||||
|
- `key`: YouTube Data API v3 키
|
||||||
|
- **채널별 playlistId**:
|
||||||
|
- 머니코믹스: `UUJo6G1u0e_-wS-JQn3T-zEw` (채널 ID: `UCJo6G1u0e_-wS-JQn3T-zEw`)
|
||||||
|
- 슈카월드: `UUsJ6RuBiTVWRX156FVbeaGg` (채널 ID: `UCsJ6RuBiTVWRX156FVbeaGg`)
|
||||||
|
- **응답 예시**:
|
||||||
```json
|
```json
|
||||||
{"video_url": "https://youtu.be/xxx", "title": "영상 제목"}
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"snippet": {
|
||||||
|
"publishedAt": "2026-03-25T06:00:00Z",
|
||||||
|
"title": "영상 제목",
|
||||||
|
"resourceId": { "videoId": "abc123" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
```
|
```
|
||||||
`API_SECRET` 설정 시 헤더에 `X-Api-Secret` 포함.
|
|
||||||
|
#### 3. Edit Fields (채널별 각 1개)
|
||||||
|
- **Type**: Edit Fields (Set)
|
||||||
|
- **Mode**: Manual Mapping
|
||||||
|
- **Fields to Set**: `channel_name` (String) = `머니코믹스` 또는 `슈카월드`
|
||||||
|
- **Include Other Input Fields**: All (기존 데이터 유지)
|
||||||
|
|
||||||
|
#### 4. Merge
|
||||||
|
- **Type**: Merge
|
||||||
|
- **Mode**: Append
|
||||||
|
|
||||||
|
#### 5. Code (새 영상 필터링)
|
||||||
|
- **Type**: Code
|
||||||
|
- **Language**: JavaScript
|
||||||
|
- 최근 1시간 이내 발행된 영상만 필터링:
|
||||||
|
```javascript
|
||||||
|
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
|
||||||
|
|
||||||
|
return $input.all().filter(item => {
|
||||||
|
const items = item.json.items || [];
|
||||||
|
return items.some(i => new Date(i.snippet.publishedAt) > oneHourAgo);
|
||||||
|
}).flatMap(item => {
|
||||||
|
const channelName = item.json.channel_name || '';
|
||||||
|
return (item.json.items || [])
|
||||||
|
.filter(i => new Date(i.snippet.publishedAt) > oneHourAgo)
|
||||||
|
.map(i => ({
|
||||||
|
json: {
|
||||||
|
video_url: `https://www.youtube.com/watch?v=${i.snippet.resourceId.videoId}`,
|
||||||
|
title: i.snippet.title,
|
||||||
|
channel_name: channelName,
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6. HTTP Request (요약 API 호출)
|
||||||
|
- **Type**: HTTP Request
|
||||||
|
- **Method**: POST
|
||||||
|
- **URL**: `https://<서버주소>/api/news/summarize`
|
||||||
|
- **Body Content Type**: JSON
|
||||||
|
- **Body**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"video_url": "{{ $json.video_url }}",
|
||||||
|
"title": "{{ $json.title }}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Headers** (API_SECRET 사용 시): `X-Api-Secret: <시크릿값>`
|
||||||
|
- **Options**: "Always Output Data" 켜기 (에러 시에도 다음 노드로 전달)
|
||||||
|
|
||||||
|
#### 7. Switch (응답 분기)
|
||||||
|
- **Type**: Switch
|
||||||
|
- **Field**: `{{ $json.status }}`
|
||||||
|
- **Rules**:
|
||||||
|
- `ok` → Discord 요약 전송
|
||||||
|
- `skipped` → No Operation (무시)
|
||||||
|
- `error` → Discord 에러 알림
|
||||||
|
|
||||||
|
#### 8. Discord 요약 전송 (status=ok)
|
||||||
|
- **Type**: HTTP Request
|
||||||
|
- **Method**: POST
|
||||||
|
- **URL**: Discord 웹훅 URL
|
||||||
|
- **Body Content Type**: JSON
|
||||||
|
- **Body**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"embeds": [{
|
||||||
|
"title": "📰 [{{ $json.channel_name }}] {{ $json.title }}",
|
||||||
|
"url": "{{ $json.video_url }}",
|
||||||
|
"description": "{{ $json.summary }}",
|
||||||
|
"color": 2829105,
|
||||||
|
"thumbnail": {
|
||||||
|
"url": "https://img.youtube.com/vi/{{ $json.video_url.split('v=')[1] }}/hqdefault.jpg"
|
||||||
|
},
|
||||||
|
"footer": { "text": "YouTube 뉴스 요약 봇 • {{ $json.channel_name }}" }
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 9. Discord 에러 알림 (status=error)
|
||||||
|
- **Type**: HTTP Request
|
||||||
|
- **Method**: POST
|
||||||
|
- **URL**: Discord 웹훅 URL
|
||||||
|
- **Body Content Type**: JSON
|
||||||
|
- **Body**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"embeds": [{
|
||||||
|
"title": "❌ [{{ $json.channel_name }}] 뉴스 요약 실패",
|
||||||
|
"color": 15548997,
|
||||||
|
"fields": [
|
||||||
|
{ "name": "영상 제목", "value": "{{ $json.title }}", "inline": false },
|
||||||
|
{ "name": "에러 타입", "value": "`{{ $json.error_type }}`", "inline": true },
|
||||||
|
{ "name": "에러 내용", "value": "```\n{{ $json.error_message }}\n```", "inline": false }
|
||||||
|
],
|
||||||
|
"footer": { "text": "YouTube 뉴스 요약 봇 - 에러 알림" }
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### YouTube Data API 키 발급
|
||||||
|
1. [Google Cloud Console](https://console.cloud.google.com/) → 프로젝트 생성/선택
|
||||||
|
2. "YouTube Data API v3" 검색 → 사용 설정
|
||||||
|
3. 사용자 인증 정보 → API 키 생성
|
||||||
|
4. (선택) 키 제한: YouTube Data API v3만 허용, IP 제한 등
|
||||||
|
- 일일 쿼터: 10,000 units (`playlistItems.list`는 1 unit/요청 → 2채널 × 24회 = 48 units/일)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from pydantic_settings import BaseSettings
|
|||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
anthropic_api_key: str
|
anthropic_api_key: str
|
||||||
discord_webhook_url: str
|
|
||||||
api_secret: str = ""
|
api_secret: str = ""
|
||||||
|
|
||||||
model_config = {"env_file": ".env", "extra": "ignore"}
|
model_config = {"env_file": ".env", "extra": "ignore"}
|
||||||
|
|||||||
146
app/discord.py
146
app/discord.py
@@ -1,146 +0,0 @@
|
|||||||
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, channel_name: 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"📰 [{channel_name}] {title}" if channel_name else f"📰 {title}"
|
|
||||||
|
|
||||||
embed = {
|
|
||||||
"title": embed_title,
|
|
||||||
"url": video_url,
|
|
||||||
"description": description,
|
|
||||||
"color": 0x2B2D31,
|
|
||||||
"fields": fields,
|
|
||||||
"footer": {
|
|
||||||
"text": f"YouTube 뉴스 요약 봇 • {channel_name}" if channel_name else "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(timeout=30.0) as client:
|
|
||||||
resp = await client.post(settings.discord_webhook_url, json=payload)
|
|
||||||
resp.raise_for_status()
|
|
||||||
|
|
||||||
|
|
||||||
async def send_error_to_discord(
|
|
||||||
title: str, video_url: str, error: Exception, channel_name: str = ""
|
|
||||||
) -> None:
|
|
||||||
"""에러 발생 시 Discord 웹훅으로 에러 내용 전송."""
|
|
||||||
error_type = type(error).__name__
|
|
||||||
error_msg = str(error)[:1024]
|
|
||||||
|
|
||||||
error_title = f"❌ [{channel_name}] 뉴스 요약 실패" if channel_name else "❌ 뉴스 요약 실패"
|
|
||||||
|
|
||||||
fields = []
|
|
||||||
if channel_name:
|
|
||||||
fields.append({"name": "채널", "value": channel_name, "inline": True})
|
|
||||||
fields.extend([
|
|
||||||
{"name": "영상 제목", "value": title or "(제목 없음)", "inline": False},
|
|
||||||
{"name": "영상 URL", "value": video_url, "inline": False},
|
|
||||||
{"name": "에러 타입", "value": f"`{error_type}`", "inline": True},
|
|
||||||
{"name": "에러 내용", "value": f"```\n{error_msg}\n```", "inline": False},
|
|
||||||
])
|
|
||||||
|
|
||||||
embed = {
|
|
||||||
"title": error_title,
|
|
||||||
"color": 0xED4245,
|
|
||||||
"fields": fields,
|
|
||||||
"footer": {"text": f"YouTube 뉴스 요약 봇 - 에러 알림 • {channel_name}" if channel_name else "YouTube 뉴스 요약 봇 - 에러 알림"},
|
|
||||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
||||||
}
|
|
||||||
|
|
||||||
payload = {"embeds": [embed]}
|
|
||||||
|
|
||||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
||||||
await client.post(settings.discord_webhook_url, json=payload)
|
|
||||||
14
app/main.py
14
app/main.py
@@ -2,7 +2,6 @@ from fastapi import FastAPI, Header, HTTPException
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
from app.discord import send_error_to_discord, send_to_discord
|
|
||||||
from app.summarizer import summarize
|
from app.summarizer import summarize
|
||||||
from app.transcript import SkipVideo, extract_video_id, fetch_transcript
|
from app.transcript import SkipVideo, extract_video_id, fetch_transcript
|
||||||
|
|
||||||
@@ -12,7 +11,6 @@ app = FastAPI(title="News Summary Bot")
|
|||||||
class SummarizeRequest(BaseModel):
|
class SummarizeRequest(BaseModel):
|
||||||
video_url: str
|
video_url: str
|
||||||
title: str = ""
|
title: str = ""
|
||||||
channel_name: str = ""
|
|
||||||
|
|
||||||
|
|
||||||
@app.post("/summarize")
|
@app.post("/summarize")
|
||||||
@@ -24,20 +22,22 @@ async def summarize_video(
|
|||||||
raise HTTPException(status_code=401, detail="Unauthorized")
|
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||||
|
|
||||||
title = req.title or "제목 없음"
|
title = req.title or "제목 없음"
|
||||||
channel_name = req.channel_name or ""
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
video_id = extract_video_id(req.video_url)
|
video_id = extract_video_id(req.video_url)
|
||||||
transcript = fetch_transcript(video_id)
|
transcript = fetch_transcript(video_id)
|
||||||
summary = summarize(transcript, title)
|
summary = summarize(transcript, title)
|
||||||
await send_to_discord(title, req.video_url, summary, channel_name)
|
|
||||||
except SkipVideo as e:
|
except SkipVideo as e:
|
||||||
return {"status": "skipped", "title": title, "reason": str(e)}
|
return {"status": "skipped", "title": title, "reason": str(e)}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await send_error_to_discord(title, req.video_url, e, channel_name)
|
return {
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
"status": "error",
|
||||||
|
"title": title,
|
||||||
|
"error_type": type(e).__name__,
|
||||||
|
"error_message": str(e),
|
||||||
|
}
|
||||||
|
|
||||||
return {"status": "ok", "title": title, "summary_length": len(summary)}
|
return {"status": "ok", "title": title, "summary": summary}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
|
|||||||
Reference in New Issue
Block a user