From e52708cda9c02e5c43135a048d92c530c1d6b343 Mon Sep 17 00:00:00 2001 From: salvacybersec Date: Thu, 13 Nov 2025 13:18:42 +0300 Subject: [PATCH] =?UTF-8?q?ilgin=C3=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/database.py | 51 +++++++++++++++++++- src/rss_generator.py | 11 ++++- src/transcript_extractor.py | 93 ++++++++++++++++++++++++++++++------- src/web_server.py | 87 +++++++++++++++++++++++----------- test_curl.sh | 16 +++++++ 5 files changed, 213 insertions(+), 45 deletions(-) diff --git a/src/database.py b/src/database.py index 22d57f2..2a7583b 100644 --- a/src/database.py +++ b/src/database.py @@ -177,6 +177,12 @@ class Database: conn = self.connect() cursor = conn.cursor() now_utc = datetime.now(timezone.utc).isoformat() + + # Debug: Transcript kaydı öncesi kontrol + import logging + logger = logging.getLogger(__name__) + logger.debug(f"[DATABASE] Video {video_id} transcript kaydediliyor - Raw uzunluk: {len(raw) if raw else 0}, Clean uzunluk: {len(clean) if clean else 0}, Status: {status}") + cursor.execute(""" UPDATE videos SET transcript_raw = ?, @@ -187,7 +193,34 @@ class Database: last_updated_utc = ? WHERE video_id = ? """, (raw, clean, status, language, now_utc, now_utc, video_id)) + + # Kayıt başarılı mı kontrol et + rows_affected = cursor.rowcount conn.commit() + + if rows_affected > 0: + # Kayıt sonrası doğrulama + cursor.execute("SELECT transcript_clean, transcript_status FROM videos WHERE video_id = ?", (video_id,)) + row = cursor.fetchone() + if row: + # sqlite3.Row objesi dict gibi davranır (row['column'] şeklinde erişilebilir) + # Ama isinstance(row, dict) False döner, bu yüzden try-except kullanıyoruz + try: + saved_clean = row['transcript_clean'] + saved_status = row['transcript_status'] + except (KeyError, TypeError, IndexError): + # Fallback: index ile erişim + saved_clean = row[0] if len(row) > 0 else None + saved_status = row[1] if len(row) > 1 else None + + if saved_clean and saved_status == status: + logger.info(f"[DATABASE] ✅ Video {video_id} transcript başarıyla kaydedildi - Clean uzunluk: {len(saved_clean)}, Status: {saved_status}") + else: + logger.warning(f"[DATABASE] ⚠️ Video {video_id} transcript kaydı şüpheli - Clean: {'var' if saved_clean else 'yok'}, Status: {saved_status}") + else: + logger.error(f"[DATABASE] ❌ Video {video_id} transcript kaydı sonrası doğrulama başarısız - Video bulunamadı") + else: + logger.warning(f"[DATABASE] ⚠️ Video {video_id} transcript kaydı yapılamadı - Hiçbir satır güncellenmedi (video_id mevcut mu?)") def get_processed_videos(self, limit: Optional[int] = None, channel_id: Optional[str] = None) -> List[Dict]: @@ -218,7 +251,23 @@ class Database: params.append(limit) cursor.execute(query, params) - return [dict(row) for row in cursor.fetchall()] + videos = [dict(row) for row in cursor.fetchall()] + + # Debug: transcript_clean alanının varlığını kontrol et ve logla + import logging + logger = logging.getLogger(__name__) + videos_with_transcript = [v for v in videos if v.get('transcript_clean')] + videos_without_transcript = [v for v in videos if not v.get('transcript_clean')] + + if videos_without_transcript: + video_ids_no_transcript = [v.get('video_id', 'N/A') for v in videos_without_transcript[:5]] + logger.warning(f"[DATABASE] ⚠️ {len(videos_without_transcript)} video transcript_clean alanı olmadan döndürüldü (ilk 5 ID: {', '.join(video_ids_no_transcript)})") + + if videos_with_transcript: + video_ids_with_transcript = [v.get('video_id', 'N/A') for v in videos_with_transcript[:5]] + logger.debug(f"[DATABASE] ✅ {len(videos_with_transcript)} video transcript_clean alanı ile döndürüldü (ilk 5 ID: {', '.join(video_ids_with_transcript)})") + + return videos def mark_video_failed(self, video_id: str, reason: Optional[str] = None): """Video'yu başarısız olarak işaretle (status=2)""" diff --git a/src/rss_generator.py b/src/rss_generator.py index 03ee045..4e45ee9 100644 --- a/src/rss_generator.py +++ b/src/rss_generator.py @@ -5,6 +5,9 @@ from feedgen.feed import FeedGenerator from datetime import datetime from typing import List, Dict import pytz +import logging + +logger = logging.getLogger(__name__) class RSSGenerator: @@ -35,10 +38,16 @@ class RSSGenerator: Args: video: Video metadata dict """ + video_id = video.get('video_id', 'N/A') + + # transcript_clean alanı kontrolü + if not video.get('transcript_clean'): + logger.warning(f"[RSS] ⚠️ Video {video_id} RSS feed'e ekleniyor ama transcript_clean alanı yok!") + fe = self.fg.add_entry() # GUID (video ID) - fe.id(video['video_id']) + fe.id(video_id) # Title fe.title(video.get('video_title', '')) diff --git a/src/transcript_extractor.py b/src/transcript_extractor.py index b6ab43b..96cc4b6 100644 --- a/src/transcript_extractor.py +++ b/src/transcript_extractor.py @@ -1,7 +1,20 @@ """ YouTube transcript çıkarımı modülü """ -from youtube_transcript_api import YouTubeTranscriptApi +from youtube_transcript_api import ( + YouTubeTranscriptApi, + TranscriptsDisabled, + NoTranscriptFound, +) + +try: + from youtube_transcript_api import NoTranscriptAvailable # type: ignore +except ImportError: + try: + from youtube_transcript_api._errors import NoTranscriptAvailable # type: ignore + except ImportError: # pragma: no cover - fallback for unexpected API changes + class NoTranscriptAvailable(Exception): # type: ignore + """Fallback exception when youtube_transcript_api does not expose NoTranscriptAvailable.""" from typing import List, Dict, Optional import time import logging @@ -177,41 +190,89 @@ class TranscriptExtractor: try: logger.debug(f"[TRANSCRIPT] YouTube Transcript API çağrısı yapılıyor: video_id={video_id}") - - # YouTube Transcript API kullanımı (yeni versiyon) - # API instance oluştur ve fetch() metodunu kullan + api = YouTubeTranscriptApi() - fetched_transcript = api.fetch(video_id, languages=languages) - - # Eski formatı döndürmek için to_raw_data() kullan - # Format: [{'text': '...', 'start': 1.36, 'duration': 1.68}, ...] + transcript_list = api.list(video_id) + logger.debug( + f"[TRANSCRIPT] {video_id} için {len(transcript_list)} transcript adayı bulundu" + ) + + selected_transcript = None + + if languages: + try: + selected_transcript = transcript_list.find_transcript(languages) + logger.debug( + f"[TRANSCRIPT] Öncelikli dillerden transcript bulundu: {selected_transcript.language_code}" + ) + except NoTranscriptFound: + logger.warning( + f"[TRANSCRIPT] İstenen dillerde transcript bulunamadı: {languages}" + ) + + if not selected_transcript: + for language in languages: + try: + selected_transcript = transcript_list.find_manually_created_transcript([language]) + logger.debug( + f"[TRANSCRIPT] {language} dili için manuel transcript bulundu" + ) + break + except NoTranscriptFound: + try: + selected_transcript = transcript_list.find_generated_transcript([language]) + logger.debug( + f"[TRANSCRIPT] {language} dili için otomatik transcript bulundu" + ) + break + except NoTranscriptFound: + continue + + if not selected_transcript and transcript_list: + selected_transcript = transcript_list[0] + logger.info( + f"[TRANSCRIPT] İstenen diller bulunamadı, ilk uygun transcript seçildi: {selected_transcript.language_code}" + ) + + if not selected_transcript: + logger.warning(f"[TRANSCRIPT] Video {video_id} için hiç transcript bulunamadı") + return None + + fetched_transcript = selected_transcript.fetch() transcript = fetched_transcript.to_raw_data() - + transcript_count = len(transcript) if transcript else 0 - logger.info(f"[TRANSCRIPT] ✅ Video {video_id} transcript'i başarıyla çıkarıldı ({transcript_count} segment)") - + logger.info( + f"[TRANSCRIPT] ✅ Video {video_id} transcript'i başarıyla çıkarıldı ({transcript_count} segment, dil: {selected_transcript.language_code})" + ) + return transcript + except (TranscriptsDisabled, NoTranscriptAvailable) as e: + logger.error( + f"[TRANSCRIPT] ❌ Video {video_id} için transcript devre dışı bırakılmış veya mevcut değil: {type(e).__name__} - {e}" + ) + return None except Exception as e: error_msg = str(e) error_type = type(e).__name__ - + logger.error(f"[TRANSCRIPT] ❌ Video {video_id} transcript çıkarımı başarısız: {error_type} - {error_msg[:200]}") - + # IP blocking hatası tespit edilirse işaretle if "blocking" in error_msg.lower() or "blocked" in error_msg.lower() or "IP" in error_msg or "IpBlocked" in error_type: self.last_blocked_time = time.time() self.block_count += 1 wait_time = 300 + (self.block_count * 60) # 5 dakika + (blocking sayısı * 1 dakika) wait_time = min(wait_time, 1800) # Maksimum 30 dakika - + logger.error(f"[TRANSCRIPT] 🚫 IP blocking tespit edildi! Video: {video_id}") logger.error(f"[TRANSCRIPT] Toplam blocking sayısı: {self.block_count}, Sonraki isteklerde {wait_time} saniye ({wait_time/60:.1f} dakika) bekleme yapılacak") logger.warning(f"[TRANSCRIPT] IP blocking detayları: {error_msg[:500]}") - + # Çok fazla blocking varsa uyar if self.block_count >= 3: logger.error(f"[TRANSCRIPT] ⚠️ UYARI: {self.block_count} kez IP blocking alındı! YouTube IP'nizi geçici olarak engellemiş olabilir.") logger.error(f"[TRANSCRIPT] Öneriler: 1) Daha uzun bekleme (30+ dakika), 2) Farklı IP kullan, 3) Proxy/VPN kullan") - + return None diff --git a/src/web_server.py b/src/web_server.py index a44e790..401a222 100644 --- a/src/web_server.py +++ b/src/web_server.py @@ -259,7 +259,6 @@ def process_channel(channel_id: str, max_items: int = 50) -> dict: # ÖNCE: Mevcut işlenmiş videoları kontrol et existing_processed = db.get_processed_videos(limit=max_items, channel_id=channel_id) - logger.info(f"[PROCESS] Channel {channel_id} için {len(existing_processed)} mevcut işlenmiş video bulundu (max_items: {max_items})") # Debug: Veritabanında kaç video var (tüm status'ler) try: @@ -280,29 +279,43 @@ def process_channel(channel_id: str, max_items: int = 50) -> dict: # sqlite3.Row formatı status_counts[row[0]] = row[1] logger.info(f"[PROCESS] 📊 Veritabanı durumu - Channel {channel_id}: Status 0 (bekleyen): {status_counts.get(0, 0)}, Status 1 (işlenmiş): {status_counts.get(1, 0)}, Status 2 (başarısız): {status_counts.get(2, 0)}") - - # İşlenmiş videoların video_id'lerini de logla (ilk 5) - if len(existing_processed) > 0: - video_ids = [v.get('video_id', 'N/A') for v in existing_processed[:5]] - logger.info(f"[PROCESS] 📋 İşlenmiş video ID'leri (ilk 5): {', '.join(video_ids)}") except Exception as e: logger.error(f"[PROCESS] ❌ Veritabanı durumu kontrol hatası: {type(e).__name__} - {str(e)}") - # Eğer yeterli sayıda işlenmiş video varsa, onları hemen döndür - if len(existing_processed) >= max_items: - logger.info(f"[PROCESS] ✅ Yeterli işlenmiş video var ({len(existing_processed)}), yeni işleme başlatılmıyor") + # Veritabanı sorgusu tamamlandıktan SONRA log mesajları yazılmalı + logger.info(f"[PROCESS] Channel {channel_id} için {len(existing_processed)} mevcut işlenmiş video bulundu (max_items: {max_items})") + + # transcript_clean alanının varlığını kontrol et ve logla + existing_with_transcript = [v for v in existing_processed if v.get('transcript_clean')] + existing_without_transcript = [v for v in existing_processed if not v.get('transcript_clean')] + + if existing_without_transcript: + video_ids_no_transcript = [v.get('video_id', 'N/A') for v in existing_without_transcript[:5]] + logger.warning(f"[PROCESS] ⚠️ {len(existing_without_transcript)} mevcut video transcript_clean alanı olmadan bulundu (ilk 5 ID: {', '.join(video_ids_no_transcript)})") + + if existing_with_transcript: + video_ids_with_transcript = [v.get('video_id', 'N/A') for v in existing_with_transcript[:5]] + logger.info(f"[PROCESS] 📋 İşlenmiş video ID'leri (transcript_clean ile, ilk 5): {', '.join(video_ids_with_transcript)}") + + # Sadece transcript_clean alanı olan videoları say + existing_with_transcript_count = len(existing_with_transcript) + + # Eğer yeterli sayıda işlenmiş video varsa (transcript_clean ile), onları hemen döndür + if existing_with_transcript_count >= max_items: + logger.info(f"[PROCESS] ✅ Yeterli işlenmiş video var (transcript_clean ile: {existing_with_transcript_count}), yeni işleme başlatılmıyor") return { - 'videos': existing_processed[:max_items], + 'videos': existing_with_transcript[:max_items], 'channel_id': channel_id, - 'count': len(existing_processed[:max_items]) + 'count': len(existing_with_transcript[:max_items]) } # Eğer mevcut işlenmiş videolar varsa ama yeterli değilse, onları döndür ve yeni işlemeleri başlat # Ancak sadece ilk batch'i işle (hızlı yanıt için) - if len(existing_processed) > 0: - logger.info(f"[PROCESS] ⚠️ Mevcut işlenmiş video var ama yeterli değil ({len(existing_processed)}/{max_items}), yeni işleme başlatılıyor") - # Mevcut videoları döndürmek için sakla - videos_to_return = existing_processed.copy() + # Sadece transcript_clean alanı olan videoları döndür + if existing_with_transcript_count > 0: + logger.info(f"[PROCESS] ⚠️ Mevcut işlenmiş video var ama yeterli değil (transcript_clean ile: {existing_with_transcript_count}/{max_items}), yeni işleme başlatılıyor") + # Mevcut videoları döndürmek için sakla (sadece transcript_clean olanlar) + videos_to_return = existing_with_transcript.copy() else: videos_to_return = [] @@ -447,12 +460,15 @@ def process_channel(channel_id: str, max_items: int = 50) -> dict: channel_id=channel_id ) + # Sadece transcript_clean alanı olan yeni videoları filtrele + newly_processed_with_transcript = [v for v in newly_processed if v.get('transcript_clean')] + # Mevcut videoları ve yeni işlenen videoları birleştir (duplicate kontrolü ile) - all_processed_videos = videos_to_return.copy() # Önce mevcut videoları ekle + all_processed_videos = videos_to_return.copy() # Önce mevcut videoları ekle (zaten transcript_clean ile filtrelenmiş) existing_ids = {v['video_id'] for v in all_processed_videos} - # Yeni işlenen videoları ekle - for video in newly_processed: + # Yeni işlenen videoları ekle (sadece transcript_clean olanlar) + for video in newly_processed_with_transcript: if video['video_id'] not in existing_ids and len(all_processed_videos) < max_items: all_processed_videos.append(video) @@ -463,9 +479,9 @@ def process_channel(channel_id: str, max_items: int = 50) -> dict: ) # Debug: Gerçek durumu logla - newly_processed_count = len([v for v in newly_processed if v['video_id'] not in {v['video_id'] for v in videos_to_return}]) - logger.info(f"[PROCESS] ✅ Channel {channel_id} işleme tamamlandı - {len(all_processed_videos)} işlenmiş video döndürülüyor") - logger.info(f"[PROCESS] 📊 Detay: Mevcut işlenmiş: {len(videos_to_return)}, Yeni işlenen: {newly_processed_count}, Toplam: {len(all_processed_videos)}") + newly_processed_count = len([v for v in newly_processed_with_transcript if v['video_id'] not in {v['video_id'] for v in videos_to_return}]) + logger.info(f"[PROCESS] ✅ Channel {channel_id} işleme tamamlandı - {len(all_processed_videos)} işlenmiş video döndürülüyor (transcript_clean ile)") + logger.info(f"[PROCESS] 📊 Detay: Mevcut işlenmiş (transcript_clean ile): {len(videos_to_return)}, Yeni işlenen (transcript_clean ile): {newly_processed_count}, Toplam: {len(all_processed_videos)}") return { 'videos': all_processed_videos[:max_items], @@ -636,10 +652,19 @@ def generate_feed(): # RSS feed oluştur logger.info(f"[FEED] RSS feed oluşturuluyor - Channel: {normalized_channel_id}, Video sayısı: {len(result['videos'])}") - # Debug: Video ID'lerini logla - if result['videos']: - video_ids = [v.get('video_id', 'N/A') for v in result['videos'][:5]] - logger.info(f"[FEED] 📋 Feed'e eklenecek video ID'leri (ilk 5): {', '.join(video_ids)}") + # transcript_clean alanının varlığını kontrol et ve logla + videos_with_transcript = [v for v in result['videos'] if v.get('transcript_clean')] + videos_without_transcript = [v for v in result['videos'] if not v.get('transcript_clean')] + + if videos_without_transcript: + video_ids_no_transcript = [v.get('video_id', 'N/A') for v in videos_without_transcript[:5]] + logger.warning(f"[FEED] ⚠️ {len(videos_without_transcript)} video transcript_clean alanı olmadan feed'e ekleniyor (ilk 5 ID: {', '.join(video_ids_no_transcript)})") + + if videos_with_transcript: + video_ids_with_transcript = [v.get('video_id', 'N/A') for v in videos_with_transcript[:5]] + logger.info(f"[FEED] 📋 Feed'e eklenecek video ID'leri (transcript_clean ile, ilk 5): {', '.join(video_ids_with_transcript)}") + else: + logger.warning(f"[FEED] ⚠️ Hiçbir videoda transcript_clean alanı yok!") channel_info = { 'id': normalized_channel_id, @@ -651,10 +676,18 @@ def generate_feed(): generator = RSSGenerator(channel_info) + # Sadece transcript_clean alanı olan videoları feed'e ekle + added_count = 0 + skipped_count = 0 for video in result['videos']: - generator.add_video_entry(video) + if video.get('transcript_clean'): + generator.add_video_entry(video) + added_count += 1 + else: + skipped_count += 1 + logger.debug(f"[FEED] ⏭️ Video {video.get('video_id', 'N/A')} transcript_clean olmadığı için feed'e eklenmedi") - logger.info(f"[FEED] ✅ RSS feed oluşturuldu - {len(result['videos'])} video eklendi") + logger.info(f"[FEED] ✅ RSS feed oluşturuldu - {added_count} video eklendi, {skipped_count} video atlandı (transcript_clean yok)") # Format'a göre döndür response_headers = {} diff --git a/test_curl.sh b/test_curl.sh index e678e5e..3138b0d 100755 --- a/test_curl.sh +++ b/test_curl.sh @@ -78,6 +78,22 @@ curl -X GET "${BASE_URL}/?channel_id=UCsXVk37bltHxD1rDPwtNM8Q&format=Atom&max_it -H "X-API-Key: ${API_KEY}" \ -v +echo -e "\n\n==========================================" +echo "11. Yeni Kanal Test (UCmGSJVG3mCRXVOP4yZrU1Dw)" +echo " Not: İlk istekte 404 alınabilir (transcript henüz işlenmemiş)" +echo "==========================================" +curl -X GET "${BASE_URL}/?channel_id=UCmGSJVG3mCRXVOP4yZrU1Dw&format=Atom&max_items=5" \ + -H "X-API-Key: ${API_KEY}" \ + -v + +echo -e "\n\n==========================================" +echo "12. Yeni Kanal URL ile Test" +echo " Not: İlk istekte 404 alınabilir (transcript henüz işlenmemiş)" +echo "==========================================" +curl -X GET "${BASE_URL}/?channel_url=https://youtube.com/channel/UCmGSJVG3mCRXVOP4yZrU1Dw&format=Atom&max_items=5" \ + -H "X-API-Key: ${API_KEY}" \ + -v + echo -e "\n\n==========================================" echo "Test tamamlandı!" echo ""