Spaces:
Sleeping
Sleeping
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"
|