Spaces:
Sleeping
Sleeping
feat: Enhance YouTube API integration with improved configuration checks and documentation
Browse files- README.md +3 -2
- app/routes/commend/router.py +11 -5
- app/routes/commend/youtube_service.py +100 -29
README.md
CHANGED
|
@@ -136,8 +136,9 @@ Response:
|
|
| 136 |
| `CROWNCODE_API_TIMEOUT_SEC` | `30` | External service timeout |
|
| 137 |
| `SES_ANALIZI_THRESHOLD` | `0.5` | Authenticity score threshold |
|
| 138 |
| `LOG_LEVEL` | `INFO` | Logging level |
|
| 139 |
-
| `COMMEND_GEMINI_API_KEY` | - | Gemini API key for Crown Commend |
|
| 140 |
-
| `
|
|
|
|
| 141 |
|
| 142 |
---
|
| 143 |
|
|
|
|
| 136 |
| `CROWNCODE_API_TIMEOUT_SEC` | `30` | External service timeout |
|
| 137 |
| `SES_ANALIZI_THRESHOLD` | `0.5` | Authenticity score threshold |
|
| 138 |
| `LOG_LEVEL` | `INFO` | Logging level |
|
| 139 |
+
| `COMMEND_GEMINI_API_KEY` | - | Gemini API key for Crown Commend (also used as YouTube API Key fallback) |
|
| 140 |
+
| `COMMEND_YOUTUBE_API_KEY` | - | YouTube Data API key for read operations (optional if Gemini key is set) |
|
| 141 |
+
| `COMMEND_TOKEN_JSON` | - | YouTube OAuth token JSON for posting comments (optional) |
|
| 142 |
|
| 143 |
---
|
| 144 |
|
app/routes/commend/router.py
CHANGED
|
@@ -18,7 +18,9 @@ from .youtube_service import (
|
|
| 18 |
get_channel_details,
|
| 19 |
get_video_comments,
|
| 20 |
get_video_transcript,
|
| 21 |
-
post_youtube_comment
|
|
|
|
|
|
|
| 22 |
)
|
| 23 |
from .gemini_service import generate_comment, summarize_transcript
|
| 24 |
from ...services.logging_config import get_logger
|
|
@@ -109,13 +111,17 @@ class CommendHealthResponse(BaseModel):
|
|
| 109 |
async def commend_health_check():
|
| 110 |
"""Check Commend service health and configuration."""
|
| 111 |
gemini_key = os.getenv('COMMEND_GEMINI_API_KEY')
|
| 112 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
return CommendHealthResponse(
|
| 115 |
-
status="ok" if
|
| 116 |
geminiConfigured=bool(gemini_key),
|
| 117 |
-
youtubeConfigured=
|
| 118 |
-
message="Crown Commend service is running"
|
| 119 |
)
|
| 120 |
|
| 121 |
|
|
|
|
| 18 |
get_channel_details,
|
| 19 |
get_video_comments,
|
| 20 |
get_video_transcript,
|
| 21 |
+
post_youtube_comment,
|
| 22 |
+
is_youtube_configured,
|
| 23 |
+
is_oauth_configured
|
| 24 |
)
|
| 25 |
from .gemini_service import generate_comment, summarize_transcript
|
| 26 |
from ...services.logging_config import get_logger
|
|
|
|
| 111 |
async def commend_health_check():
|
| 112 |
"""Check Commend service health and configuration."""
|
| 113 |
gemini_key = os.getenv('COMMEND_GEMINI_API_KEY')
|
| 114 |
+
youtube_configured = is_youtube_configured()
|
| 115 |
+
oauth_configured = is_oauth_configured()
|
| 116 |
+
|
| 117 |
+
# Service is OK if we can at least generate comments (Gemini + YouTube read)
|
| 118 |
+
is_ok = bool(gemini_key) and youtube_configured
|
| 119 |
|
| 120 |
return CommendHealthResponse(
|
| 121 |
+
status="ok" if is_ok else "degraded",
|
| 122 |
geminiConfigured=bool(gemini_key),
|
| 123 |
+
youtubeConfigured=youtube_configured,
|
| 124 |
+
message=f"Crown Commend service is running. OAuth (posting): {'enabled' if oauth_configured else 'disabled'}"
|
| 125 |
)
|
| 126 |
|
| 127 |
|
app/routes/commend/youtube_service.py
CHANGED
|
@@ -1,6 +1,10 @@
|
|
| 1 |
"""
|
| 2 |
YouTube API Service for Crown Commend
|
| 3 |
Handles video details, comments, and posting functionality.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
"""
|
| 5 |
|
| 6 |
from __future__ import annotations
|
|
@@ -8,7 +12,6 @@ from __future__ import annotations
|
|
| 8 |
import os
|
| 9 |
import re
|
| 10 |
import json
|
| 11 |
-
import tempfile
|
| 12 |
from typing import Optional, Tuple, List, Dict, Any
|
| 13 |
|
| 14 |
from google.oauth2.credentials import Credentials
|
|
@@ -20,31 +23,73 @@ from ...services.logging_config import get_logger
|
|
| 20 |
|
| 21 |
logger = get_logger(__name__)
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
-
def
|
| 25 |
"""
|
| 26 |
-
|
| 27 |
-
|
| 28 |
"""
|
| 29 |
-
|
| 30 |
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
if token_json:
|
| 34 |
-
try:
|
| 35 |
-
token_data = json.loads(token_json)
|
| 36 |
-
creds = Credentials.from_authorized_user_info(token_data, SCOPES)
|
| 37 |
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
raise ValueError("YouTube API authentication failed")
|
| 46 |
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
|
| 50 |
def extract_video_id(video_url: str) -> Optional[str]:
|
|
@@ -67,6 +112,7 @@ def extract_video_id(video_url: str) -> Optional[str]:
|
|
| 67 |
def get_video_details(video_url: str) -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
|
| 68 |
"""
|
| 69 |
YouTube video detaylarını çeker.
|
|
|
|
| 70 |
Returns: (details_dict, error_message)
|
| 71 |
"""
|
| 72 |
try:
|
|
@@ -74,7 +120,7 @@ def get_video_details(video_url: str) -> Tuple[Optional[Dict[str, Any]], Optiona
|
|
| 74 |
if not video_id:
|
| 75 |
return None, "Invalid YouTube URL"
|
| 76 |
|
| 77 |
-
youtube =
|
| 78 |
|
| 79 |
request = youtube.videos().list(
|
| 80 |
part="snippet,statistics,contentDetails",
|
|
@@ -114,13 +160,16 @@ def get_video_details(video_url: str) -> Tuple[Optional[Dict[str, Any]], Optiona
|
|
| 114 |
return None, str(e)
|
| 115 |
except Exception as e:
|
| 116 |
logger.error(f"Error fetching video details: {e}")
|
| 117 |
-
return None, "Failed to fetch video details"
|
| 118 |
|
| 119 |
|
| 120 |
def get_channel_details(channel_id: str) -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
|
| 121 |
-
"""
|
|
|
|
|
|
|
|
|
|
| 122 |
try:
|
| 123 |
-
youtube =
|
| 124 |
|
| 125 |
request = youtube.channels().list(
|
| 126 |
part="statistics,snippet",
|
|
@@ -147,9 +196,12 @@ def get_channel_details(channel_id: str) -> Tuple[Optional[Dict[str, Any]], Opti
|
|
| 147 |
|
| 148 |
|
| 149 |
def get_video_comments(video_id: str, max_results: int = 10) -> Tuple[List[Dict[str, str]], Optional[str]]:
|
| 150 |
-
"""
|
|
|
|
|
|
|
|
|
|
| 151 |
try:
|
| 152 |
-
youtube =
|
| 153 |
|
| 154 |
request = youtube.commentThreads().list(
|
| 155 |
part="snippet",
|
|
@@ -172,7 +224,8 @@ def get_video_comments(video_id: str, max_results: int = 10) -> Tuple[List[Dict[
|
|
| 172 |
|
| 173 |
except Exception as e:
|
| 174 |
logger.error(f"Error fetching comments: {e}")
|
| 175 |
-
|
|
|
|
| 176 |
|
| 177 |
|
| 178 |
def get_video_transcript(video_id: str) -> Tuple[Optional[str], Optional[str]]:
|
|
@@ -210,9 +263,12 @@ def get_video_transcript(video_id: str) -> Tuple[Optional[str], Optional[str]]:
|
|
| 210 |
|
| 211 |
|
| 212 |
def post_youtube_comment(video_id: str, comment_text: str) -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
|
| 213 |
-
"""
|
|
|
|
|
|
|
|
|
|
| 214 |
try:
|
| 215 |
-
youtube =
|
| 216 |
|
| 217 |
request_body = {
|
| 218 |
"snippet": {
|
|
@@ -235,6 +291,9 @@ def post_youtube_comment(video_id: str, comment_text: str) -> Tuple[Optional[Dic
|
|
| 235 |
'postedAt': response.get('snippet', {}).get('topLevelComment', {}).get('snippet', {}).get('publishedAt')
|
| 236 |
}, None
|
| 237 |
|
|
|
|
|
|
|
|
|
|
| 238 |
except Exception as e:
|
| 239 |
logger.error(f"Error posting comment: {e}")
|
| 240 |
error_str = str(e).lower()
|
|
@@ -243,5 +302,17 @@ def post_youtube_comment(video_id: str, comment_text: str) -> Tuple[Optional[Dic
|
|
| 243 |
return None, "Comment posting is disabled for this video"
|
| 244 |
elif "quota" in error_str:
|
| 245 |
return None, "API quota exceeded, please try again later"
|
|
|
|
|
|
|
| 246 |
else:
|
| 247 |
-
return None, "Failed to post comment"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""
|
| 2 |
YouTube API Service for Crown Commend
|
| 3 |
Handles video details, comments, and posting functionality.
|
| 4 |
+
|
| 5 |
+
Authentication Strategy:
|
| 6 |
+
- Read operations (video details, comments): Uses API Key (COMMEND_YOUTUBE_API_KEY)
|
| 7 |
+
- Write operations (post comment): Uses OAuth 2.0 (COMMEND_TOKEN_JSON)
|
| 8 |
"""
|
| 9 |
|
| 10 |
from __future__ import annotations
|
|
|
|
| 12 |
import os
|
| 13 |
import re
|
| 14 |
import json
|
|
|
|
| 15 |
from typing import Optional, Tuple, List, Dict, Any
|
| 16 |
|
| 17 |
from google.oauth2.credentials import Credentials
|
|
|
|
| 23 |
|
| 24 |
logger = get_logger(__name__)
|
| 25 |
|
| 26 |
+
# Cached service instances
|
| 27 |
+
_api_key_service = None
|
| 28 |
+
_oauth_service = None
|
| 29 |
+
|
| 30 |
|
| 31 |
+
def _get_api_key_service():
|
| 32 |
"""
|
| 33 |
+
API Key ile YouTube API servisi oluşturur.
|
| 34 |
+
Read-only işlemler için kullanılır (video detayları, yorumları okuma).
|
| 35 |
"""
|
| 36 |
+
global _api_key_service
|
| 37 |
|
| 38 |
+
if _api_key_service is not None:
|
| 39 |
+
return _api_key_service
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
+
api_key = os.getenv('COMMEND_YOUTUBE_API_KEY')
|
| 42 |
+
if not api_key:
|
| 43 |
+
# Fallback: Gemini API key'i YouTube için de kullanılabilir
|
| 44 |
+
api_key = os.getenv('COMMEND_GEMINI_API_KEY')
|
| 45 |
+
|
| 46 |
+
if not api_key:
|
| 47 |
+
raise ValueError("COMMEND_YOUTUBE_API_KEY environment variable is required")
|
| 48 |
+
|
| 49 |
+
try:
|
| 50 |
+
_api_key_service = build('youtube', 'v3', developerKey=api_key)
|
| 51 |
+
logger.info("YouTube API Key service initialized")
|
| 52 |
+
return _api_key_service
|
| 53 |
+
except Exception as e:
|
| 54 |
+
logger.error(f"YouTube API Key service initialization failed: {e}")
|
| 55 |
+
raise ValueError(f"YouTube API initialization failed: {e}")
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def _get_oauth_service():
|
| 59 |
+
"""
|
| 60 |
+
OAuth 2.0 ile YouTube API servisi oluşturur.
|
| 61 |
+
Write işlemleri için kullanılır (yorum gönderme).
|
| 62 |
+
"""
|
| 63 |
+
global _oauth_service
|
| 64 |
+
|
| 65 |
+
if _oauth_service is not None:
|
| 66 |
+
# Check if credentials are still valid
|
| 67 |
+
return _oauth_service
|
| 68 |
+
|
| 69 |
+
SCOPES = ['https://www.googleapis.com/auth/youtube.force-ssl']
|
| 70 |
|
| 71 |
+
token_json = os.getenv('COMMEND_TOKEN_JSON')
|
| 72 |
+
if not token_json:
|
| 73 |
+
raise ValueError("COMMEND_TOKEN_JSON environment variable is required for posting comments")
|
|
|
|
| 74 |
|
| 75 |
+
try:
|
| 76 |
+
token_data = json.loads(token_json)
|
| 77 |
+
creds = Credentials.from_authorized_user_info(token_data, SCOPES)
|
| 78 |
+
|
| 79 |
+
# Token expire olmuşsa refresh et
|
| 80 |
+
if creds.expired and creds.refresh_token:
|
| 81 |
+
logger.info("Refreshing expired OAuth token")
|
| 82 |
+
creds.refresh(Request())
|
| 83 |
+
|
| 84 |
+
_oauth_service = build('youtube', 'v3', credentials=creds)
|
| 85 |
+
logger.info("YouTube OAuth service initialized")
|
| 86 |
+
return _oauth_service
|
| 87 |
+
except json.JSONDecodeError as e:
|
| 88 |
+
logger.error(f"TOKEN_JSON is not valid JSON: {e}")
|
| 89 |
+
raise ValueError("YouTube OAuth token is malformed")
|
| 90 |
+
except Exception as e:
|
| 91 |
+
logger.error(f"OAuth service initialization failed: {e}")
|
| 92 |
+
raise ValueError(f"YouTube OAuth authentication failed: {e}")
|
| 93 |
|
| 94 |
|
| 95 |
def extract_video_id(video_url: str) -> Optional[str]:
|
|
|
|
| 112 |
def get_video_details(video_url: str) -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
|
| 113 |
"""
|
| 114 |
YouTube video detaylarını çeker.
|
| 115 |
+
API Key kullanır (OAuth gerektirmez).
|
| 116 |
Returns: (details_dict, error_message)
|
| 117 |
"""
|
| 118 |
try:
|
|
|
|
| 120 |
if not video_id:
|
| 121 |
return None, "Invalid YouTube URL"
|
| 122 |
|
| 123 |
+
youtube = _get_api_key_service()
|
| 124 |
|
| 125 |
request = youtube.videos().list(
|
| 126 |
part="snippet,statistics,contentDetails",
|
|
|
|
| 160 |
return None, str(e)
|
| 161 |
except Exception as e:
|
| 162 |
logger.error(f"Error fetching video details: {e}")
|
| 163 |
+
return None, f"Failed to fetch video details: {str(e)}"
|
| 164 |
|
| 165 |
|
| 166 |
def get_channel_details(channel_id: str) -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
|
| 167 |
+
"""
|
| 168 |
+
Kanal istatistiklerini çeker.
|
| 169 |
+
API Key kullanır.
|
| 170 |
+
"""
|
| 171 |
try:
|
| 172 |
+
youtube = _get_api_key_service()
|
| 173 |
|
| 174 |
request = youtube.channels().list(
|
| 175 |
part="statistics,snippet",
|
|
|
|
| 196 |
|
| 197 |
|
| 198 |
def get_video_comments(video_id: str, max_results: int = 10) -> Tuple[List[Dict[str, str]], Optional[str]]:
|
| 199 |
+
"""
|
| 200 |
+
Video yorumlarını çeker.
|
| 201 |
+
API Key kullanır.
|
| 202 |
+
"""
|
| 203 |
try:
|
| 204 |
+
youtube = _get_api_key_service()
|
| 205 |
|
| 206 |
request = youtube.commentThreads().list(
|
| 207 |
part="snippet",
|
|
|
|
| 224 |
|
| 225 |
except Exception as e:
|
| 226 |
logger.error(f"Error fetching comments: {e}")
|
| 227 |
+
# Yorumlar kapalı olabilir, boş liste dön
|
| 228 |
+
return [], None
|
| 229 |
|
| 230 |
|
| 231 |
def get_video_transcript(video_id: str) -> Tuple[Optional[str], Optional[str]]:
|
|
|
|
| 263 |
|
| 264 |
|
| 265 |
def post_youtube_comment(video_id: str, comment_text: str) -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
|
| 266 |
+
"""
|
| 267 |
+
YouTube'a yorum gönderir.
|
| 268 |
+
OAuth 2.0 gerektirir.
|
| 269 |
+
"""
|
| 270 |
try:
|
| 271 |
+
youtube = _get_oauth_service()
|
| 272 |
|
| 273 |
request_body = {
|
| 274 |
"snippet": {
|
|
|
|
| 291 |
'postedAt': response.get('snippet', {}).get('topLevelComment', {}).get('snippet', {}).get('publishedAt')
|
| 292 |
}, None
|
| 293 |
|
| 294 |
+
except ValueError as e:
|
| 295 |
+
# OAuth not configured
|
| 296 |
+
return None, str(e)
|
| 297 |
except Exception as e:
|
| 298 |
logger.error(f"Error posting comment: {e}")
|
| 299 |
error_str = str(e).lower()
|
|
|
|
| 302 |
return None, "Comment posting is disabled for this video"
|
| 303 |
elif "quota" in error_str:
|
| 304 |
return None, "API quota exceeded, please try again later"
|
| 305 |
+
elif "invalid" in error_str and "token" in error_str:
|
| 306 |
+
return None, "OAuth token expired or invalid"
|
| 307 |
else:
|
| 308 |
+
return None, f"Failed to post comment: {str(e)}"
|
| 309 |
+
|
| 310 |
+
|
| 311 |
+
def is_youtube_configured() -> bool:
|
| 312 |
+
"""YouTube API Key yapılandırılmış mı kontrol eder."""
|
| 313 |
+
return bool(os.getenv('COMMEND_YOUTUBE_API_KEY') or os.getenv('COMMEND_GEMINI_API_KEY'))
|
| 314 |
+
|
| 315 |
+
|
| 316 |
+
def is_oauth_configured() -> bool:
|
| 317 |
+
"""OAuth (yorum gönderme) yapılandırılmış mı kontrol eder."""
|
| 318 |
+
return bool(os.getenv('COMMEND_TOKEN_JSON'))
|