diff --git a/src/transcript_extractor.py b/src/transcript_extractor.py index 4703d30..0976310 100644 --- a/src/transcript_extractor.py +++ b/src/transcript_extractor.py @@ -92,7 +92,13 @@ class TranscriptExtractor: def _make_flaresolverr_request(self, url: str, method: str = 'GET', **kwargs) -> Optional: """FlareSolverr üzerinden istek yap""" - if not self.use_flaresolverr: + # Availability kontrolü: Eğer FlareSolverr erişilemiyorsa hiç deneme + if not self.use_flaresolverr or not self.flaresolverr_url: + return None + + # Eğer daha önce erişilemediği tespit edildiyse, tekrar deneme + if hasattr(self, 'flaresolverr_available') and not self.flaresolverr_available: + logger.debug(f"[FLARESOLVERR] FlareSolverr erişilemiyor (available=False), istek atlanıyor") return None try: @@ -104,7 +110,7 @@ class TranscriptExtractor: flaresolverr_payload = { "cmd": "request.post", "url": url, - "maxTimeout": 60000, # 60 saniye timeout + "maxTimeout": 90000, # 90 saniye timeout (FlareSolverr'ın tarayıcı açması zaman alabilir) } # POST data'sını ekle @@ -116,7 +122,7 @@ class TranscriptExtractor: flaresolverr_payload = { "cmd": "request.get", "url": url, - "maxTimeout": 60000, # 60 saniye timeout + "maxTimeout": 90000, # 90 saniye timeout (FlareSolverr'ın tarayıcı açması zaman alabilir) } # Header'ları ekle @@ -126,11 +132,12 @@ class TranscriptExtractor: logger.debug(f"[FLARESOLVERR] İstek gönderiliyor: {url[:50]}...") - # FlareSolverr timeout'unu kısalt (erişilemezse hemen normal isteğe geç) + # FlareSolverr timeout'u (FlareSolverr'ın tarayıcı açması zaman alabilir) + # maxTimeout 90 saniye olduğu için, HTTP timeout'u da biraz daha uzun tutuyoruz response = requests.post( self.flaresolverr_url, json=flaresolverr_payload, - timeout=10 # 10 saniye timeout (erişilemezse hemen normal isteğe geç) + timeout=95 # 95 saniye timeout (maxTimeout'dan biraz fazla) ) if response.status_code == 200: @@ -147,11 +154,24 @@ class TranscriptExtractor: def __init__(self, status_code, text, headers, url, is_post=False): self.status_code = status_code self.text = text - self.content = text.encode('utf-8') if isinstance(text, str) else text - self.headers = headers if headers else {} + # Encoding'i doğru ayarla (UTF-8) + if isinstance(text, str): + self.content = text.encode('utf-8') + self.encoding = 'utf-8' + else: + self.content = text + self.encoding = 'utf-8' + # Header'ları dict olarak ayarla (requests.Response uyumluluğu için) + if isinstance(headers, dict): + self.headers = headers + else: + self.headers = dict(headers) if headers else {} self.url = url self.ok = 200 <= status_code < 300 self.is_post = is_post # POST isteği mi? + # requests.Response uyumluluğu için ek özellikler + self.reason = 'OK' if self.ok else 'Error' + self.apparent_encoding = 'utf-8' def json(self): import json @@ -173,22 +193,30 @@ class TranscriptExtractor: content_size = len(response_content) logger.info(f"[FLARESOLVERR] ✅ İstek başarılı: HTTP {status_code}, {content_size} byte içerik ({'POST' if is_post_request else 'GET'})") - # Debug: İçeriğin ilk 500 karakterini logla - logger.debug(f"[FLARESOLVERR] İçerik önizleme (ilk 500 karakter): {response_content[:500]}") + # Debug: İçeriğin ilk 2000 karakterini logla (daha detaylı analiz için) + logger.debug(f"[FLARESOLVERR] İçerik önizleme (ilk 2000 karakter): {response_content[:2000]}") # GET istekleri için HTML analizi (POST istekleri genellikle JSON) + transcript_urls = [] + yt_initial_player_response_found = False + if not is_post_request: import re - if 'ytInitialPlayerResponse' in response_content or 'ytInitialData' in response_content: - logger.debug(f"[FLARESOLVERR] ✅ YouTube player response bulundu HTML'de") + # YouTube player response kontrolü + if 'ytInitialPlayerResponse' in response_content: + logger.debug(f"[FLARESOLVERR] ✅ ytInitialPlayerResponse bulundu HTML'de") + yt_initial_player_response_found = True + elif 'ytInitialData' in response_content: + logger.debug(f"[FLARESOLVERR] ✅ ytInitialData bulundu HTML'de") + yt_initial_player_response_found = True else: logger.warning(f"[FLARESOLVERR] ⚠️ YouTube player response bulunamadı HTML'de") - # Debug: Transcript endpoint URL'lerini ara - transcript_urls = re.findall(r'https?://[^"\s]+timedtext[^"\s]*', response_content) + # Transcript endpoint URL'lerini ara (daha kapsamlı pattern) + transcript_urls = re.findall(r'https?://[^"\s<>]+timedtext[^"\s<>]*', response_content) if transcript_urls: logger.debug(f"[FLARESOLVERR] ✅ Transcript URL'leri bulundu: {len(transcript_urls)} adet") - logger.debug(f"[FLARESOLVERR] İlk transcript URL: {transcript_urls[0][:100]}...") + logger.debug(f"[FLARESOLVERR] İlk transcript URL: {transcript_urls[0][:150]}...") else: logger.warning(f"[FLARESOLVERR] ⚠️ Transcript URL'leri bulunamadı HTML'de") else: @@ -200,7 +228,36 @@ class TranscriptExtractor: except: logger.warning(f"[FLARESOLVERR] ⚠️ POST response JSON değil, HTML olabilir") - return FlareSolverrResponse(status_code, response_content, headers, url, is_post=is_post_request) + # Response objesine transcript URL'lerini ve analiz sonuçlarını ekle (alternatif parse için) + response_obj = FlareSolverrResponse(status_code, response_content, headers, url, is_post=is_post_request) + response_obj.transcript_urls = transcript_urls # Alternatif parse için sakla + response_obj.yt_initial_player_response_found = yt_initial_player_response_found + + # Debug: FlareSolverr response'unu geçici dosyaya kaydet (YouTubeDataUnparsable hatası için) + if not is_post_request and logger.isEnabledFor(logging.DEBUG): + try: + import tempfile + import os + from pathlib import Path + + # Video ID'yi URL'den çıkar + video_id_match = None + if 'watch?v=' in url: + video_id_match = url.split('watch?v=')[1].split('&')[0] + + if video_id_match: + debug_dir = Path('output') / 'flaresolverr_debug' + debug_dir.mkdir(parents=True, exist_ok=True) + debug_file = debug_dir / f"flaresolverr_response_{video_id_match}_{int(time.time())}.html" + + with open(debug_file, 'w', encoding='utf-8') as f: + f.write(response_content) + + logger.debug(f"[FLARESOLVERR] Debug: Response kaydedildi: {debug_file}") + except Exception as debug_err: + logger.debug(f"[FLARESOLVERR] Debug dosyası kaydedilemedi: {debug_err}") + + return response_obj else: error = result.get('message', 'Unknown error') logger.error(f"[FLARESOLVERR] ❌ FlareSolverr hatası: {error}") @@ -209,6 +266,16 @@ class TranscriptExtractor: logger.error(f"[FLARESOLVERR] ❌ FlareSolverr HTTP hatası: {response.status_code}") return None + except requests.exceptions.Timeout: + logger.warning(f"[FLARESOLVERR] ⚠️ FlareSolverr timeout (95 saniye), normal isteğe geçiliyor") + return None + except requests.exceptions.ReadTimeout: + logger.warning(f"[FLARESOLVERR] ⚠️ FlareSolverr read timeout, normal isteğe geçiliyor") + return None + except requests.exceptions.ConnectTimeout: + logger.warning(f"[FLARESOLVERR] ⚠️ FlareSolverr connect timeout (erişilemiyor), devre dışı bırakılıyor") + self.flaresolverr_available = False # Erişilemediği için devre dışı bırak + return None except Exception as e: logger.error(f"[FLARESOLVERR] ❌ FlareSolverr istek hatası: {type(e).__name__} - {str(e)[:200]}") return None @@ -233,11 +300,14 @@ class TranscriptExtractor: is_video_page = ('youtube.com/watch' in url or 'youtu.be/' in url) and '/api/' not in url # FlareSolverr kullanılıyorsa ve video sayfası ise - if extractor_instance.use_flaresolverr and is_video_page: + if extractor_instance.use_flaresolverr and is_video_page and extractor_instance.flaresolverr_available: logger.debug(f"[FLARESOLVERR] Video sayfası FlareSolverr üzerinden deneniyor: {url[:50]}...") flaresolverr_response = extractor_instance._make_flaresolverr_request(url, 'GET', **kwargs) if flaresolverr_response: logger.debug(f"[FLARESOLVERR] ✅ FlareSolverr başarılı, response döndürülüyor") + # FlareSolverr response'unu instance'a kaydet (YouTubeDataUnparsable hatası için analiz) + extractor_instance._last_flaresolverr_response = flaresolverr_response + # FlareSolverr response'unu requests.Response'a benzet class PatchedResponse: def __init__(self, flaresolverr_response): @@ -247,6 +317,9 @@ class TranscriptExtractor: self.headers = flaresolverr_response.headers self.url = flaresolverr_response.url self.ok = 200 <= self.status_code < 300 + # FlareSolverr response metadata'sını sakla + self.flaresolverr_transcript_urls = getattr(flaresolverr_response, 'transcript_urls', []) + self.flaresolverr_yt_response_found = getattr(flaresolverr_response, 'yt_initial_player_response_found', False) def json(self): import json @@ -278,9 +351,12 @@ class TranscriptExtractor: return PatchedResponse(flaresolverr_response) else: logger.debug(f"[FLARESOLVERR] FlareSolverr yanıt vermedi, normal istek deneniyor") - elif extractor_instance.use_flaresolverr and is_video_page and not extractor_instance.flaresolverr_available: - # FlareSolverr erişilemiyor, normal istek yap - logger.debug(f"[FLARESOLVERR] FlareSolverr erişilemiyor, normal istek yapılıyor: {url[:50]}...") + elif extractor_instance.use_flaresolverr and is_video_page: + # FlareSolverr erişilemiyor veya available=False, normal istek yap + if not extractor_instance.flaresolverr_available: + logger.debug(f"[FLARESOLVERR] FlareSolverr erişilemiyor (available=False), normal istek yapılıyor: {url[:50]}...") + else: + logger.debug(f"[FLARESOLVERR] FlareSolverr kullanılamıyor, normal istek yapılıyor: {url[:50]}...") elif extractor_instance.use_flaresolverr and ('youtube.com' in url or 'youtu.be' in url) and '/api/' in url: # Transcript API endpoint'leri için FlareSolverr kullanma, sadece header'ları ekle logger.debug(f"[FLARESOLVERR] Transcript API endpoint'i tespit edildi, FlareSolverr atlanıyor: {url[:50]}...") @@ -524,17 +600,65 @@ class TranscriptExtractor: logger.error(f"[TRANSCRIPT] ❌ Tüm denemeler başarısız (FlareSolverr ve normal istek)") return None - # YouTubeDataUnparsable hatası için retry yap + # YouTubeDataUnparsable hatası için retry yap ve alternatif yöntemler dene if "YouTubeDataUnparsable" in error_type or "Unparsable" in error_type: + logger.warning(f"[TRANSCRIPT] ⚠️ Video {video_id} parse hatası (Deneme {attempt + 1}/{max_retries + 1}): {error_type}") + logger.warning(f"[TRANSCRIPT] Hata mesajı: {error_msg[:300]}") + + # FlareSolverr kullanıldıysa, normal isteğe geç + if self.use_flaresolverr and attempt == 0: + logger.warning(f"[TRANSCRIPT] ⚠️ FlareSolverr ile parse edilemedi, normal isteğe geçiliyor...") + # FlareSolverr'ı geçici olarak devre dışı bırak + original_use_flaresolverr = self.use_flaresolverr + self.use_flaresolverr = False + try: + # Normal istek dene + api = YouTubeTranscriptApi() + fetched_transcript = api.fetch(video_id, languages=languages) + transcript = fetched_transcript.to_raw_data() + transcript_count = len(transcript) if transcript else 0 + logger.info(f"[TRANSCRIPT] ✅ Video {video_id} transcript'i normal istek ile başarıyla çıkarıldı ({transcript_count} segment)") + # FlareSolverr'ı tekrar etkinleştir + self.use_flaresolverr = original_use_flaresolverr + return transcript + except Exception as e2: + # FlareSolverr'ı tekrar etkinleştir + self.use_flaresolverr = original_use_flaresolverr + logger.warning(f"[TRANSCRIPT] ⚠️ Normal istek de başarısız: {type(e2).__name__} - {str(e2)[:200]}") + # Retry yap + if attempt < max_retries: + continue + + # Retry yap if attempt < max_retries: - logger.warning(f"[TRANSCRIPT] ⚠️ Video {video_id} parse hatası (Deneme {attempt + 1}/{max_retries + 1}): {error_type}") - logger.warning(f"[TRANSCRIPT] Hata mesajı: {error_msg[:300]}") logger.warning(f"[TRANSCRIPT] Retry yapılacak...") - continue # Retry yap + continue else: logger.error(f"[TRANSCRIPT] ❌ Video {video_id} parse hatası - Tüm denemeler başarısız: {error_type}") logger.error(f"[TRANSCRIPT] Hata detayları: {error_msg[:500]}") logger.error(f"[TRANSCRIPT] Bu video için transcript çıkarılamıyor (YouTube HTML yapısı değişmiş olabilir)") + + # Debug: FlareSolverr response'unu analiz et (eğer kullanıldıysa) + if self.use_flaresolverr and hasattr(self, '_last_flaresolverr_response'): + logger.debug(f"[TRANSCRIPT] FlareSolverr response analizi yapılıyor...") + last_response = self._last_flaresolverr_response + + # Transcript URL'leri var mı kontrol et + transcript_urls = getattr(last_response, 'transcript_urls', []) + if transcript_urls: + logger.warning(f"[TRANSCRIPT] ⚠️ FlareSolverr response'unda {len(transcript_urls)} transcript URL bulundu ama parse edilemedi") + logger.warning(f"[TRANSCRIPT] Bu URL'ler doğrudan kullanılabilir (fallback mekanizması henüz implement edilmedi)") + + # ytInitialPlayerResponse var mı kontrol et + yt_response_found = getattr(last_response, 'yt_initial_player_response_found', False) + if yt_response_found: + logger.warning(f"[TRANSCRIPT] ⚠️ FlareSolverr response'unda ytInitialPlayerResponse bulundu ama parse edilemedi") + logger.warning(f"[TRANSCRIPT] YouTube HTML yapısı değişmiş olabilir veya kütüphane FlareSolverr response'unu tanımıyor") + else: + logger.warning(f"[TRANSCRIPT] ⚠️ FlareSolverr response'unda ytInitialPlayerResponse bulunamadı") + + logger.debug(f"[TRANSCRIPT] FlareSolverr kullanıldı ama parse edilemedi - YouTube HTML yapısı değişmiş olabilir") + return None # Diğer hatalar için normal işlem