Rthur2003 commited on
Commit
eb80da4
·
1 Parent(s): c6a816d

feat: Enhance YouTube API integration with improved configuration checks and documentation

Browse files
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
- | `COMMEND_TOKEN_JSON` | - | YouTube OAuth token JSON for posting |
 
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
- youtube_token = os.getenv('COMMEND_TOKEN_JSON')
 
 
 
 
113
 
114
  return CommendHealthResponse(
115
- status="ok" if gemini_key and youtube_token else "degraded",
116
  geminiConfigured=bool(gemini_key),
117
- youtubeConfigured=bool(youtube_token),
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 _get_authenticated_service():
25
  """
26
- OAuth 2.0 ile YouTube API kimlik doğrulaması yapar.
27
- Environment variables üzerinden credentials okur.
28
  """
29
- SCOPES = ['https://www.googleapis.com/auth/youtube.force-ssl']
30
 
31
- # Production: TOKEN_JSON environment variable
32
- token_json = os.getenv('COMMEND_TOKEN_JSON')
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
- # Token expire olmuşsa refresh et
39
- if creds.expired and creds.refresh_token:
40
- creds.refresh(Request())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
- return build('youtube', 'v3', credentials=creds)
43
- except Exception as e:
44
- logger.error(f"TOKEN_JSON parse error: {e}")
45
- raise ValueError("YouTube API authentication failed")
46
 
47
- raise ValueError("COMMEND_TOKEN_JSON environment variable is required")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 = _get_authenticated_service()
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
- """Kanal istatistiklerini çeker."""
 
 
 
122
  try:
123
- youtube = _get_authenticated_service()
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
- """Video yorumlarını çeker."""
 
 
 
151
  try:
152
- youtube = _get_authenticated_service()
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
- return [], "Failed to fetch comments"
 
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
- """YouTube'a yorum gönderir."""
 
 
 
214
  try:
215
- youtube = _get_authenticated_service()
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'))