Rthur2003 commited on
Commit
047eaf1
·
1 Parent(s): 4ea2acb

Add YouTube API service for video details, comments, and posting functionality

Browse files
app/routes/commend/gemini_service.py ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Gemini AI Service for Crown Commend
3
+ Generates contextual YouTube comments using Google's Gemini API.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ from typing import Optional, Tuple, List, Dict, Any
10
+
11
+ import google.generativeai as genai
12
+
13
+ from ...services.logging_config import get_logger
14
+
15
+ logger = get_logger(__name__)
16
+
17
+ # Gemini model configuration
18
+ _model = None
19
+
20
+
21
+ def _get_model():
22
+ """Lazy-load Gemini model."""
23
+ global _model
24
+
25
+ if _model is not None:
26
+ return _model
27
+
28
+ api_key = os.getenv('COMMEND_GEMINI_API_KEY')
29
+ if not api_key:
30
+ raise ValueError("COMMEND_GEMINI_API_KEY environment variable is required")
31
+
32
+ try:
33
+ genai.configure(api_key=api_key)
34
+ _model = genai.GenerativeModel('gemini-1.5-flash')
35
+ logger.info("Gemini model initialized successfully")
36
+ return _model
37
+ except Exception as e:
38
+ logger.error(f"Failed to initialize Gemini: {e}")
39
+ raise ValueError(f"Gemini initialization failed: {e}")
40
+
41
+
42
+ def summarize_transcript(transcript_text: str, language: str) -> Tuple[Optional[str], Optional[str]]:
43
+ """Video transcript'ini özetler."""
44
+ try:
45
+ model = _get_model()
46
+
47
+ if not transcript_text or len(transcript_text.strip()) < 50:
48
+ return None, "Insufficient transcript data"
49
+
50
+ prompt = f"""
51
+ Bu video transcript'ini {language} dilinde 2-3 cümlelik kısa bir özet haline getir:
52
+
53
+ {transcript_text}
54
+
55
+ ONEMLI: Sadece özetin kendisini yaz, başka açıklama yapma.
56
+ """
57
+
58
+ response = model.generate_content(prompt)
59
+ return response.text.strip(), None
60
+
61
+ except Exception as e:
62
+ logger.error(f"Transcript summarization error: {e}")
63
+ return None, "Failed to summarize transcript"
64
+
65
+
66
+ def generate_comment(
67
+ video_details: Dict[str, Any],
68
+ comment_style: str,
69
+ language: str,
70
+ existing_comments: Optional[List[Dict[str, str]]] = None,
71
+ transcript_summary: Optional[str] = None
72
+ ) -> Tuple[Optional[str], Optional[str]]:
73
+ """
74
+ AI-powered comment generation using Gemini.
75
+
76
+ Args:
77
+ video_details: Video metadata (title, channel, stats, etc.)
78
+ comment_style: Style preference (supportive, analytical, humorous, etc.)
79
+ language: Target language for the comment
80
+ existing_comments: Sample comments from the video
81
+ transcript_summary: Optional transcript summary
82
+
83
+ Returns:
84
+ (generated_comment, error_message)
85
+ """
86
+ try:
87
+ model = _get_model()
88
+
89
+ # Format existing comments
90
+ comment_section = "No comments available to analyze."
91
+ if existing_comments:
92
+ comment_section = "\n".join(
93
+ [f"- '{c['text'][:150]}' (by {c['author']})" for c in existing_comments[:5]]
94
+ )
95
+
96
+ # Build the prompt
97
+ prompt = f"""
98
+ ### GOREV ###
99
+ Sen Crown Commend AI'sin - YouTube videolarına özgün, yaratıcı ve değerli yorumlar üreten bir yapay zeka.
100
+ Yorum dili: **{language}**
101
+
102
+ ### VIDEO ANALIZI ###
103
+
104
+ **Mevcut Yorumlar (ONCELIKLI):**
105
+ {comment_section}
106
+
107
+ **Video Icerigi:**
108
+ {f"- Ozet: {transcript_summary}" if transcript_summary else "- Icerik ozeti mevcut degil"}
109
+
110
+ **Video Bilgileri:**
111
+ - Baslik: {video_details.get('title', 'Bilinmiyor')}
112
+ - Kanal: {video_details.get('channelName', 'Bilinmiyor')}
113
+ - Aciklama: {video_details.get('description', '')[:300]}
114
+
115
+ **Istatistikler:**
116
+ - Izlenme: {video_details.get('viewCount', 0):,}
117
+ - Begeni: {video_details.get('likeCount', 0):,}
118
+ - Sure: {video_details.get('duration', 0) // 60} dakika
119
+
120
+ ### YORUM STRATEJISI ###
121
+ **{comment_style}** stilinde yorum yaz:
122
+
123
+ 1. **Gercekcilik:** Normal bir izleyici gibi yaz, abartma.
124
+ 2. **Emoji Siniri:** Hic emoji kullanma veya en fazla 1 adet kullan.
125
+ 3. **Dogallik:** Samimi ama sade bir dil kullan.
126
+ 4. **Video Odak:** Videonun icerigini ve mevcut yorumlari on planda tut.
127
+ 5. **Kisalik:** Uzun yorumlardan kacin, net ve oz ol.
128
+
129
+ ### YORUM KURALLARI ###
130
+ - **Dil:** Tamamen **{language}** dilinde yaz (disclaimer dahil)
131
+ - **Uzunluk:** 1-3 cumle arasi, kisa ve etkili
132
+ - **Ton:** Video konusuna uygun (ciddi/eglenceli/destekleyici)
133
+ - **Ozgunluk:** Klise ifadelerden kacin
134
+ - **Disclaimer:** Yorumun sonuna yeni satirda disclaimer ekle:
135
+ - Turkish: "Not: Bu yorum AI tarafindan uretilmistir."
136
+ - English: "Note: This comment was generated by AI."
137
+ - Russian: "Primechanie: Etot kommentariy sozdan II."
138
+ - Chinese: "Zhu: Ci pinglun you AI shengcheng."
139
+ - Japanese: "Chu: Kono komento wa AI ni yotte seisei saremashita."
140
+
141
+ ### ORNEKLER ###
142
+ Kotu: "Harika video! Cok begendim!"
143
+ Iyi: "3:45'teki o detay gercekten onemli bir noktaydi, bu konuya hic bu acidan bakmamistim."
144
+
145
+ **Yorumun:**
146
+ """
147
+
148
+ response = model.generate_content(prompt)
149
+ generated_text = response.text.strip()
150
+
151
+ # Clean up the response if needed
152
+ if generated_text.startswith('"') and generated_text.endswith('"'):
153
+ generated_text = generated_text[1:-1]
154
+
155
+ return generated_text, None
156
+
157
+ except Exception as e:
158
+ logger.error(f"Comment generation error: {e}")
159
+ error_str = str(e).lower()
160
+
161
+ if "api key" in error_str:
162
+ return None, "Invalid API key configuration"
163
+ elif "quota" in error_str or "limit" in error_str:
164
+ return None, "API quota exceeded, please try again later"
165
+ else:
166
+ return None, f"Failed to generate comment: {str(e)}"
app/routes/commend/youtube_service.py ADDED
@@ -0,0 +1,247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ YouTube API Service for Crown Commend
3
+ Handles video details, comments, and posting functionality.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
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
15
+ from google.auth.transport.requests import Request
16
+ from googleapiclient.discovery import build
17
+ import isodate
18
+
19
+ 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]:
51
+ """YouTube URL'sinden video ID'sini çıkarır."""
52
+ patterns = [
53
+ r"(?<=v=)[^&#]+",
54
+ r"(?<=be/)[^&#]+",
55
+ r"(?<=embed/)[^&#]+",
56
+ r"(?<=shorts/)[^&#]+"
57
+ ]
58
+
59
+ for pattern in patterns:
60
+ match = re.search(pattern, video_url)
61
+ if match:
62
+ return match.group(0)
63
+
64
+ return None
65
+
66
+
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:
73
+ video_id = extract_video_id(video_url)
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",
81
+ id=video_id
82
+ )
83
+ response = request.execute()
84
+
85
+ if not response.get('items'):
86
+ return None, "Video not found"
87
+
88
+ item = response['items'][0]
89
+ snippet = item['snippet']
90
+ stats = item.get('statistics', {})
91
+ content = item.get('contentDetails', {})
92
+
93
+ # Duration parsing
94
+ duration_iso = content.get('duration', 'PT0S')
95
+ duration_seconds = isodate.parse_duration(duration_iso).total_seconds()
96
+
97
+ details = {
98
+ 'videoId': video_id,
99
+ 'title': snippet.get('title'),
100
+ 'channelName': snippet.get('channelTitle'),
101
+ 'channelId': snippet.get('channelId'),
102
+ 'description': snippet.get('description', '')[:500],
103
+ 'thumbnailUrl': snippet.get('thumbnails', {}).get('high', {}).get('url'),
104
+ 'viewCount': int(stats.get('viewCount', 0)),
105
+ 'likeCount': int(stats.get('likeCount', 0)),
106
+ 'commentCount': int(stats.get('commentCount', 0)),
107
+ 'duration': int(duration_seconds),
108
+ 'publishedAt': snippet.get('publishedAt')
109
+ }
110
+
111
+ return details, None
112
+
113
+ except ValueError as e:
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",
127
+ id=channel_id
128
+ )
129
+ response = request.execute()
130
+
131
+ if not response.get('items'):
132
+ return None, "Channel not found"
133
+
134
+ item = response['items'][0]
135
+ stats = item.get('statistics', {})
136
+ snippet = item.get('snippet', {})
137
+
138
+ return {
139
+ 'subscriberCount': int(stats.get('subscriberCount', 0)),
140
+ 'videoCount': int(stats.get('videoCount', 0)),
141
+ 'channelDescription': snippet.get('description', '')[:200]
142
+ }, None
143
+
144
+ except Exception as e:
145
+ logger.error(f"Error fetching channel details: {e}")
146
+ return None, "Failed to fetch channel details"
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",
156
+ videoId=video_id,
157
+ maxResults=max_results,
158
+ order="relevance"
159
+ )
160
+ response = request.execute()
161
+
162
+ comments = []
163
+ for item in response.get('items', []):
164
+ top_comment = item['snippet']['topLevelComment']['snippet']
165
+ comments.append({
166
+ 'author': top_comment.get('authorDisplayName'),
167
+ 'text': top_comment.get('textDisplay'),
168
+ 'likeCount': top_comment.get('likeCount', 0)
169
+ })
170
+
171
+ return comments, None
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]]:
179
+ """Video transcript/altyazısını çeker."""
180
+ try:
181
+ from youtube_transcript_api import YouTubeTranscriptApi
182
+
183
+ transcript_list = YouTubeTranscriptApi.list_transcripts(video_id)
184
+
185
+ # Dil önceliği: tr -> en -> auto
186
+ for lang in ['tr', 'en']:
187
+ try:
188
+ transcript = transcript_list.find_transcript([lang])
189
+ break
190
+ except:
191
+ continue
192
+ else:
193
+ try:
194
+ transcript = transcript_list.find_generated_transcript(['tr', 'en'])
195
+ except:
196
+ return None, "No transcript available"
197
+
198
+ transcript_data = transcript.fetch()
199
+ full_text = ' '.join([entry['text'] for entry in transcript_data])
200
+
201
+ # Token limiti için kısalt
202
+ if len(full_text) > 8000:
203
+ full_text = full_text[:8000] + "..."
204
+
205
+ return full_text, None
206
+
207
+ except Exception as e:
208
+ logger.warning(f"Transcript fetch failed: {e}")
209
+ return None, "Transcript not available"
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": {
219
+ "videoId": video_id,
220
+ "topLevelComment": {
221
+ "snippet": {
222
+ "textOriginal": comment_text
223
+ }
224
+ }
225
+ }
226
+ }
227
+
228
+ response = youtube.commentThreads().insert(
229
+ part="snippet",
230
+ body=request_body
231
+ ).execute()
232
+
233
+ return {
234
+ 'commentId': response.get('id'),
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()
241
+
242
+ if "forbidden" in error_str or "permission" in error_str:
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"