Compare commits

..

4 Commits

Author SHA1 Message Date
salvacybersec
e52708cda9 ilginç 2025-11-13 13:18:42 +03:00
salvacybersec
3a35b2d4d9 oha 2025-11-13 09:04:37 +03:00
salvacybersec
1f3b6d5fe2 api issues 2025-11-13 08:52:33 +03:00
salvacybersec
bff8164c5d api issues 2025-11-13 08:51:55 +03:00
8 changed files with 789 additions and 77 deletions

152
API.md
View File

@@ -16,18 +16,28 @@ Production için base URL değişebilir.
API key'i iki şekilde gönderebilirsiniz: API key'i iki şekilde gönderebilirsiniz:
### 1. HTTP Header (Önerilen) ### 1. HTTP Header (Programatik kullanım için önerilen)
```http ```http
X-API-Key: your_api_key_here X-API-Key: your_api_key_here
``` ```
### 2. Query Parameter **Not:** RSS okuyucular HTTP header gönderemediği için bu yöntem sadece programatik kullanım için uygundur.
### 2. Query Parameter (RSS okuyucular ve tarayıcılar için zorunlu)
``` ```
?api_key=your_api_key_here ?api_key=your_api_key_here
``` ```
**Önemli:** RSS okuyucular, tarayıcılar ve feed aggregator'lar için **mutlaka query parameter** kullanılmalıdır çünkü bu uygulamalar HTTP header gönderemez.
**Güvenlik Notu:** API key'i URL'de kullanmak güvenlik açısından ideal değildir çünkü:
- URL'ler log dosyalarında, tarayıcı geçmişinde ve referrer header'larında görünebilir
- Ancak RSS okuyucular için bu tek seçenektir
- Production'da farklı API key'ler kullanarak riski azaltabilirsiniz
- API key'lerinizi düzenli olarak rotate edin
### API Key Alma ### API Key Alma
API key'ler `config/security.yaml` dosyasından yönetilir. Yeni bir API key eklemek için: API key'ler `config/security.yaml` dosyasından yönetilir. Yeni bir API key eklemek için:
@@ -93,21 +103,46 @@ YouTube kanalı için transcript feed'i oluşturur.
**Örnek İstekler:** **Örnek İstekler:**
**URL-Based Sorgular (RSS Okuyucular ve Tarayıcılar için):**
```bash ```bash
# Channel ID ile # Channel ID ile (API key URL'de)
http://localhost:5000/?channel_id=UC9h8BDcXwkhZtnqoQJ7PggA&api_key=demo_key_12345&format=Atom
# Channel handle ile (API key URL'de)
http://localhost:5000/?channel=@tavakfi&api_key=demo_key_12345&format=Atom&max_items=10
# Channel URL ile (API key URL'de) - ÖNERİLEN
http://localhost:5000/?channel_url=https://www.youtube.com/@tavakfi&api_key=demo_key_12345&format=Atom&max_items=50
# RSS format ile
http://localhost:5000/?channel_url=https://www.youtube.com/@politicalfronts&api_key=demo_key_12345&format=Rss&max_items=20
```
**Programatik Kullanım (HTTP Header ile):**
```bash
# Channel ID ile (Header'da API key)
curl -H "X-API-Key: demo_key_12345" \ curl -H "X-API-Key: demo_key_12345" \
"http://localhost:5000/?channel_id=UC9h8BDcXwkhZtnqoQJ7PggA&format=Atom" "http://localhost:5000/?channel_id=UC9h8BDcXwkhZtnqoQJ7PggA&format=Atom"
# Channel handle ile # Channel handle ile (Header'da API key)
curl -H "X-API-Key: demo_key_12345" \ curl -H "X-API-Key: demo_key_12345" \
"http://localhost:5000/?channel=@tavakfi&format=Atom" "http://localhost:5000/?channel=@tavakfi&format=Atom"
# Channel URL ile # Channel URL ile (Header'da API key)
curl -H "X-API-Key: demo_key_12345" \ curl -H "X-API-Key: demo_key_12345" \
"http://localhost:5000/?channel_url=https://www.youtube.com/@tavakfi&format=Atom&max_items=50" "http://localhost:5000/?channel_url=https://www.youtube.com/@tavakfi&format=Atom&max_items=50"
```
# Query parametresi ile API key **Production URL Örnekleri:**
curl "http://localhost:5000/?channel_id=UC9h8BDcXwkhZtnqoQJ7PggA&api_key=demo_key_12345&format=Rss"
```
# Production URL ile Channel URL kullanımı
https://yt2feed.aligundogar.com.tr/?channel_url=https://www.youtube.com/@politicalfronts&api_key=your_api_key&format=Atom
# Production URL ile Channel ID kullanımı
https://yt2feed.aligundogar.com.tr/?channel_id=UC9h8BDcXwkhZtnqoQJ7PggA&api_key=your_api_key&format=Rss&max_items=50
``` ```
**Başarılı Yanıt:** **Başarılı Yanıt:**
@@ -332,10 +367,60 @@ API CORS desteği sağlar. Preflight request'ler için `OPTIONS` metodu kullanı
RSS reader uygulamanızda feed URL'si olarak kullanın: RSS reader uygulamanızda feed URL'si olarak kullanın:
**Önemli:** RSS okuyucular HTTP header gönderemediği için API key'i **mutlaka query parametresi** olarak eklemeniz gerekir.
**URL Formatı:**
``` ```
http://your-api.com/?channel_id=UC9h8BDcXwkhZtnqoQJ7PggA&format=Rss&api_key=your_api_key BASE_URL/?channel_url=YOUTUBE_CHANNEL_URL&api_key=YOUR_API_KEY&format=Atom&max_items=10
``` ```
**Örnek URL'ler (Production):**
```
# Channel URL ile (ÖNERİLEN - En kolay ve güvenilir)
https://yt2feed.aligundogar.com.tr/?channel_url=https://www.youtube.com/@politicalfronts&api_key=your_api_key&format=Atom
# Channel URL ile RSS format
https://yt2feed.aligundogar.com.tr/?channel_url=https://www.youtube.com/@politicalfronts&api_key=your_api_key&format=Rss&max_items=50
# Channel ID ile
https://yt2feed.aligundogar.com.tr/?channel_id=UC9h8BDcXwkhZtnqoQJ7PggA&api_key=your_api_key&format=Atom&max_items=20
# Channel Handle ile
https://yt2feed.aligundogar.com.tr/?channel=@tavakfi&api_key=your_api_key&format=Atom&max_items=50
# Channel URL ile (Channel ID formatında)
https://yt2feed.aligundogar.com.tr/?channel_url=https://www.youtube.com/channel/UC9h8BDcXwkhZtnqoQJ7PggA&api_key=your_api_key&format=Rss
```
**Örnek URL'ler (Localhost - Test için):**
```
# Localhost test URL'i
http://localhost:5000/?channel_url=https://www.youtube.com/@politicalfronts&api_key=demo_key_12345&format=Atom
# Localhost ile Channel ID
http://localhost:5000/?channel_id=UC9h8BDcXwkhZtnqoQJ7PggA&api_key=demo_key_12345&format=Rss&max_items=10
```
**RSS Okuyucu Adımları:**
1. RSS okuyucunuzda "Feed Ekle", "Subscribe" veya "Add Feed" seçeneğini açın
2. Feed URL alanına yukarıdaki formatlardan birini yapıştırın
3. `your_api_key` kısmını kendi API key'inizle değiştirin
4. Feed'i ekleyin ve kaydedin
**Popüler RSS Okuyucular:**
- **Feedly**: Feed URL'sini direkt yapıştırın
- **Inoreader**: "Add New" > "Feed" > URL yapıştırın
- **NewsBlur**: "Add Site" > URL yapıştırın
- **The Old Reader**: "Add Subscription" > URL yapıştırın
- **NetNewsWire**: "File" > "Add Feed" > URL yapıştırın
**Not:**
- İlk istekte transcript'ler henüz işlenmemiş olabilir. Birkaç dakika bekleyip tekrar deneyin.
- API key'inizi URL'de paylaşmayın, sadece kendi RSS okuyucunuzda kullanın.
### 2. Programatik Kullanım (Python) ### 2. Programatik Kullanım (Python)
```python ```python
@@ -363,6 +448,31 @@ else:
### 3. Programatik Kullanım (JavaScript) ### 3. Programatik Kullanım (JavaScript)
**URL-Based (Tarayıcı için):**
```javascript
// Tarayıcıdan kullanım - API key URL'de
const apiKey = "your_api_key_here";
const channelUrl = "https://www.youtube.com/@politicalfronts";
const feedUrl = `https://yt2feed.aligundogar.com.tr/?channel_url=${encodeURIComponent(channelUrl)}&api_key=${apiKey}&format=Atom`;
fetch(feedUrl)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.text();
})
.then(feedContent => {
console.log(feedContent);
})
.catch(error => {
console.error("Error:", error);
});
```
**Header-Based (Node.js/Backend için):**
```javascript ```javascript
const apiKey = "your_api_key_here"; const apiKey = "your_api_key_here";
const channelId = "UC9h8BDcXwkhZtnqoQJ7PggA"; const channelId = "UC9h8BDcXwkhZtnqoQJ7PggA";
@@ -388,13 +498,37 @@ fetch(`http://localhost:5000/?channel_id=${channelId}&format=Atom`, {
### 4. cURL ile Test ### 4. cURL ile Test
**URL-Based Sorgular (RSS Okuyucular ve Tarayıcılar için):**
```bash ```bash
# API key ile test # API key URL'de - Channel URL ile (ÖNERİLEN)
API_KEY="demo_key_12345"
CHANNEL_URL="https://www.youtube.com/@politicalfronts"
curl "http://localhost:5000/?channel_url=${CHANNEL_URL}&api_key=${API_KEY}&format=Atom&max_items=50"
# API key URL'de - Channel ID ile
CHANNEL_ID="UC9h8BDcXwkhZtnqoQJ7PggA"
curl "http://localhost:5000/?channel_id=${CHANNEL_ID}&api_key=${API_KEY}&format=Rss&max_items=20"
# Production URL ile
curl "https://yt2feed.aligundogar.com.tr/?channel_url=https://www.youtube.com/@politicalfronts&api_key=${API_KEY}&format=Atom"
```
**Header-Based Sorgular (Programatik Kullanım için):**
```bash
# API key header'da - Channel ID ile
API_KEY="demo_key_12345" API_KEY="demo_key_12345"
CHANNEL_ID="UC9h8BDcXwkhZtnqoQJ7PggA" CHANNEL_ID="UC9h8BDcXwkhZtnqoQJ7PggA"
curl -H "X-API-Key: $API_KEY" \ curl -H "X-API-Key: $API_KEY" \
"http://localhost:5000/?channel_id=$CHANNEL_ID&format=Atom&max_items=50" "http://localhost:5000/?channel_id=$CHANNEL_ID&format=Atom&max_items=50"
# API key header'da - Channel URL ile
CHANNEL_URL="https://www.youtube.com/@tavakfi"
curl -H "X-API-Key: $API_KEY" \
"http://localhost:5000/?channel_url=${CHANNEL_URL}&format=Atom&max_items=50"
``` ```
## Notlar ## Notlar

View File

@@ -177,6 +177,12 @@ class Database:
conn = self.connect() conn = self.connect()
cursor = conn.cursor() cursor = conn.cursor()
now_utc = datetime.now(timezone.utc).isoformat() 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(""" cursor.execute("""
UPDATE videos UPDATE videos
SET transcript_raw = ?, SET transcript_raw = ?,
@@ -187,7 +193,34 @@ class Database:
last_updated_utc = ? last_updated_utc = ?
WHERE video_id = ? WHERE video_id = ?
""", (raw, clean, status, language, now_utc, now_utc, 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() 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, def get_processed_videos(self, limit: Optional[int] = None,
channel_id: Optional[str] = None) -> List[Dict]: channel_id: Optional[str] = None) -> List[Dict]:
@@ -218,7 +251,23 @@ class Database:
params.append(limit) params.append(limit)
cursor.execute(query, params) 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): def mark_video_failed(self, video_id: str, reason: Optional[str] = None):
"""Video'yu başarısız olarak işaretle (status=2)""" """Video'yu başarısız olarak işaretle (status=2)"""

View File

@@ -5,6 +5,9 @@ from feedgen.feed import FeedGenerator
from datetime import datetime from datetime import datetime
from typing import List, Dict from typing import List, Dict
import pytz import pytz
import logging
logger = logging.getLogger(__name__)
class RSSGenerator: class RSSGenerator:
@@ -35,10 +38,16 @@ class RSSGenerator:
Args: Args:
video: Video metadata dict 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() fe = self.fg.add_entry()
# GUID (video ID) # GUID (video ID)
fe.id(video['video_id']) fe.id(video_id)
# Title # Title
fe.title(video.get('video_title', '')) fe.title(video.get('video_title', ''))

View File

@@ -116,13 +116,21 @@ class SecurityManager:
Returns: Returns:
Geçerli mi? Geçerli mi?
YouTube Channel ID formatı:
- UC ile başlamalı
- Toplam 24 karakter (UC + 22 karakter)
- Sadece alfanumerik karakterler ve alt çizgi (_) içermeli
- Tire (-) karakteri içermemeli
""" """
if not channel_id: if not channel_id:
return False return False
# UC ile başlayan 24 karakter # UC ile başlayan, 24 karakter, sadece alfanumerik ve alt çizgi (tire YOK)
if re.match(r'^UC[a-zA-Z0-9_-]{22}$', channel_id): if re.match(r'^UC[a-zA-Z0-9_]{22}$', channel_id):
return True # Double check: Tire karakteri içermemeli
if '-' not in channel_id:
return True
return False return False
@@ -212,7 +220,15 @@ def require_api_key(f):
security = get_security_manager() security = get_security_manager()
# API key'i header'dan veya query'den al # API key'i header'dan veya query'den al
api_key = request.headers.get('X-API-Key') or request.args.get('api_key') api_key_header = request.headers.get('X-API-Key')
api_key_query = request.args.get('api_key')
api_key = api_key_header or api_key_query
# Debug logging (sadece query parametresi varsa)
if api_key_query and not api_key_header:
import logging
logger = logging.getLogger(__name__)
logger.debug(f"[API_KEY] Query parametresinden API key alındı: {api_key_query[:10]}... (uzunluk: {len(api_key_query) if api_key_query else 0})")
is_valid, key_info = security.validate_api_key(api_key) is_valid, key_info = security.validate_api_key(api_key)

View File

@@ -1,7 +1,20 @@
""" """
YouTube transcript çıkarımı modülü 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 from typing import List, Dict, Optional
import time import time
import logging import logging
@@ -177,41 +190,89 @@ class TranscriptExtractor:
try: try:
logger.debug(f"[TRANSCRIPT] YouTube Transcript API çağrısı yapılıyor: video_id={video_id}") 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() api = YouTubeTranscriptApi()
fetched_transcript = api.fetch(video_id, languages=languages) transcript_list = api.list(video_id)
logger.debug(
# Eski formatı döndürmek için to_raw_data() kullan f"[TRANSCRIPT] {video_id} için {len(transcript_list)} transcript adayı bulundu"
# Format: [{'text': '...', 'start': 1.36, 'duration': 1.68}, ...] )
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 = fetched_transcript.to_raw_data()
transcript_count = len(transcript) if transcript else 0 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 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: except Exception as e:
error_msg = str(e) error_msg = str(e)
error_type = type(e).__name__ error_type = type(e).__name__
logger.error(f"[TRANSCRIPT] ❌ Video {video_id} transcript çıkarımı başarısız: {error_type} - {error_msg[:200]}") 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 # 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: 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.last_blocked_time = time.time()
self.block_count += 1 self.block_count += 1
wait_time = 300 + (self.block_count * 60) # 5 dakika + (blocking sayısı * 1 dakika) wait_time = 300 + (self.block_count * 60) # 5 dakika + (blocking sayısı * 1 dakika)
wait_time = min(wait_time, 1800) # Maksimum 30 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] 🚫 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.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]}") logger.warning(f"[TRANSCRIPT] IP blocking detayları: {error_msg[:500]}")
# Çok fazla blocking varsa uyar # Çok fazla blocking varsa uyar
if self.block_count >= 3: 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] ⚠️ 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") logger.error(f"[TRANSCRIPT] Öneriler: 1) Daha uzun bekleme (30+ dakika), 2) Farklı IP kullan, 3) Proxy/VPN kullan")
return None return None

View File

@@ -4,35 +4,59 @@ RSS-Bridge kullanarak video metadata çıkarımı
import feedparser import feedparser
import re import re
import requests import requests
import logging
from urllib.parse import urlencode from urllib.parse import urlencode
from typing import List, Dict, Optional from typing import List, Dict, Optional
from datetime import datetime from datetime import datetime
logger = logging.getLogger(__name__)
def get_channel_id_from_handle(handle_url: str) -> Optional[str]: def get_channel_id_from_handle(handle_url: str) -> Optional[str]:
""" """
Channel handle URL'inden Channel ID'yi web scraping ile bulur. Channel handle URL'inden Channel ID'yi web scraping ile bulur.
Örnek: https://www.youtube.com/@tavakfi -> UC... Örnek: https://www.youtube.com/@tavakfi -> UC...
YouTube Channel ID formatı: UC ile başlayan, 24 karakter, sadece alfanumerik ve alt çizgi
""" """
try: try:
response = requests.get(handle_url) logger.info(f"[CHANNEL_ID] Channel ID çıkarılıyor: {handle_url}")
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
response = requests.get(handle_url, headers=headers, timeout=10)
response.raise_for_status() response.raise_for_status()
html_content = response.text html_content = response.text
# İlk pattern: "externalId":"UC..." # YouTube Channel ID pattern: UC ile başlayan, 24 karakter, sadece alfanumerik ve alt çizgi (tire YOK)
match = re.search(r'"externalId":"(UC[a-zA-Z0-9_-]{22})"', html_content) # Pattern'ler: UC + 22 karakter = toplam 24 karakter
if match: patterns = [
return match.group(1) (r'"externalId":"(UC[a-zA-Z0-9_]{22})"', 'externalId'),
(r'"channelId":"(UC[a-zA-Z0-9_]{22})"', 'channelId'),
(r'"browseEndpoint"\s*:\s*\{[^}]*"browseId"\s*:\s*"(UC[a-zA-Z0-9_]{22})"', 'browseEndpoint'),
(r'channelId["\']?\s*:\s*["\']?(UC[a-zA-Z0-9_]{22})', 'genel channelId'),
(r'/channel/(UC[a-zA-Z0-9_]{22})', 'URL pattern'),
]
# Alternatif pattern: "channelId":"UC..." for pattern, pattern_name in patterns:
match_alt = re.search(r'"channelId":"(UC[a-zA-Z0-9_-]{22})"', html_content) match = re.search(pattern, html_content)
if match_alt: if match:
return match_alt.group(1) channel_id = match.group(1)
# Double check: UC ile başlamalı ve 24 karakter olmalı
if channel_id.startswith('UC') and len(channel_id) == 24:
# Tire karakteri içermemeli
if '-' not in channel_id:
logger.info(f"[CHANNEL_ID] ✅ Channel ID bulundu: {channel_id} (pattern: {pattern_name})")
return channel_id
else:
logger.warning(f"[CHANNEL_ID] ⚠️ Geçersiz channel ID (tire içeriyor): {channel_id}")
logger.warning(f"[CHANNEL_ID] ❌ Channel ID bulunamadı: {handle_url}")
return None return None
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
logger.error(f"[CHANNEL_ID] ❌ Hata: {type(e).__name__} - {str(e)}")
raise Exception(f"Error fetching channel page: {e}") raise Exception(f"Error fetching channel page: {e}")

View File

@@ -3,6 +3,7 @@ Flask web server - RSS-Bridge benzeri URL template sistemi
""" """
from flask import Flask, request, Response, jsonify, g from flask import Flask, request, Response, jsonify, g
from typing import Optional from typing import Optional
from urllib.parse import unquote, urlparse
import sys import sys
import os import os
import yaml import yaml
@@ -58,8 +59,22 @@ def add_security_headers(response):
config = load_security_config() config = load_security_config()
headers = config.get('security_headers', {}) headers = config.get('security_headers', {})
# RSS feed'ler için Content-Security-Policy'yi daha esnek yap
# RSS okuyucular ve tarayıcılar için sorun çıkarmasın
is_feed_response = (
'application/atom+xml' in response.content_type or
'application/rss+xml' in response.content_type or
'application/xml' in response.content_type or
'text/xml' in response.content_type
)
for header, value in headers.items(): for header, value in headers.items():
response.headers[header] = value # RSS feed'ler için CSP'yi atla veya daha esnek yap
if header == 'Content-Security-Policy' and is_feed_response:
# RSS feed'ler için CSP'yi daha esnek yap
response.headers[header] = "default-src 'self' 'unsafe-inline' data: blob: *"
else:
response.headers[header] = value
# CORS headers # CORS headers
cors_config = config.get('cors', {}) cors_config = config.get('cors', {})
@@ -90,7 +105,29 @@ def add_security_headers(response):
@app.route('/<path:path>', methods=['OPTIONS']) @app.route('/<path:path>', methods=['OPTIONS'])
def handle_options(path=None): def handle_options(path=None):
"""CORS preflight request handler""" """CORS preflight request handler"""
return Response(status=200) config = load_security_config()
cors_config = config.get('cors', {})
response = Response(status=200)
if cors_config.get('enabled', True):
origins = cors_config.get('allowed_origins', ['*'])
if '*' in origins:
response.headers['Access-Control-Allow-Origin'] = '*'
else:
origin = request.headers.get('Origin')
if origin in origins:
response.headers['Access-Control-Allow-Origin'] = origin
response.headers['Access-Control-Allow-Methods'] = ', '.join(
cors_config.get('allowed_methods', ['GET', 'OPTIONS'])
)
response.headers['Access-Control-Allow-Headers'] = ', '.join(
cors_config.get('allowed_headers', ['Content-Type', 'X-API-Key'])
)
response.headers['Access-Control-Max-Age'] = '3600'
return response
# Uygulama başlangıcında security'yi initialize et # Uygulama başlangıcında security'yi initialize et
init_app_security() init_app_security()
@@ -130,7 +167,7 @@ def normalize_channel_id(channel_id: Optional[str] = None,
channel: Optional[str] = None, channel: Optional[str] = None,
channel_url: Optional[str] = None) -> Optional[str]: channel_url: Optional[str] = None) -> Optional[str]:
""" """
Farklı formatlardan channel ID'yi normalize et Farklı formatlardan channel ID'yi normalize et ve validate et
Args: Args:
channel_id: Direkt Channel ID (UC...) channel_id: Direkt Channel ID (UC...)
@@ -138,36 +175,74 @@ def normalize_channel_id(channel_id: Optional[str] = None,
channel_url: Full YouTube channel URL channel_url: Full YouTube channel URL
Returns: Returns:
Normalize edilmiş Channel ID veya None Normalize edilmiş ve validate edilmiş Channel ID veya None
""" """
security = get_security_manager()
normalized_id = None
# Direkt Channel ID varsa # Direkt Channel ID varsa
if channel_id: if channel_id:
if channel_id.startswith('UC') and len(channel_id) == 24: if channel_id.startswith('UC') and len(channel_id) == 24:
return channel_id normalized_id = channel_id
# Eğer URL formatında ise parse et # Eğer URL formatında ise parse et
if 'youtube.com/channel/' in channel_id: elif 'youtube.com/channel/' in channel_id:
parts = channel_id.split('/channel/') parts = channel_id.split('/channel/')
if len(parts) > 1: if len(parts) > 1:
return parts[-1].split('?')[0].split('/')[0] normalized_id = parts[-1].split('?')[0].split('/')[0]
# Channel handle (@username) # Channel handle (@username)
if channel: if not normalized_id and channel:
# Channel parametresini normalize et (@ işareti olabilir veya olmayabilir)
channel = channel.strip()
if not channel.startswith('@'): if not channel.startswith('@'):
channel = f"@{channel}" channel = f"@{channel}"
handle_url = f"https://www.youtube.com/{channel}" handle_url = f"https://www.youtube.com/{channel}"
return get_channel_id_from_handle(handle_url) logger.info(f"[NORMALIZE] Channel handle URL oluşturuldu: {handle_url}")
normalized_id = get_channel_id_from_handle(handle_url)
# Channel URL # Channel URL
if channel_url: if not normalized_id and channel_url:
# Handle URL # URL'yi temizle ve normalize et
channel_url = channel_url.strip()
# Handle URL (@username formatı)
if '/@' in channel_url: if '/@' in channel_url:
return get_channel_id_from_handle(channel_url) # URL'den handle'ı çıkar
if '/@' in channel_url:
# https://www.youtube.com/@username formatı
normalized_id = get_channel_id_from_handle(channel_url)
else:
# Sadece @username formatı
handle = channel_url.replace('@', '').strip()
if handle:
handle_url = f"https://www.youtube.com/@{handle}"
normalized_id = get_channel_id_from_handle(handle_url)
# Channel ID URL # Channel ID URL
elif '/channel/' in channel_url: elif '/channel/' in channel_url:
parts = channel_url.split('/channel/') parts = channel_url.split('/channel/')
if len(parts) > 1: if len(parts) > 1:
return parts[-1].split('?')[0].split('/')[0] channel_id_part = parts[-1].split('?')[0].split('/')[0].split('&')[0]
# Eğer UC ile başlıyorsa ve 24 karakter ise, direkt kullan
if channel_id_part.startswith('UC') and len(channel_id_part) == 24:
normalized_id = channel_id_part
else:
# Parse etmeye çalış
normalized_id = channel_id_part
# Sadece handle (@username) formatı
elif channel_url.startswith('@'):
handle = channel_url.replace('@', '').strip()
if handle:
handle_url = f"https://www.youtube.com/@{handle}"
normalized_id = get_channel_id_from_handle(handle_url)
# Direkt channel ID formatı (UC...)
elif channel_url.startswith('UC') and len(channel_url) == 24:
normalized_id = channel_url
# Validate: Channel ID formatını kontrol et
if normalized_id and security.validate_channel_id(normalized_id):
return normalized_id
# Geçersiz format
return None return None
@@ -182,6 +257,68 @@ def process_channel(channel_id: str, max_items: int = 50) -> dict:
extractor = get_extractor() extractor = get_extractor()
cleaner = get_cleaner() cleaner = get_cleaner()
# ÖNCE: Mevcut işlenmiş videoları kontrol et
existing_processed = db.get_processed_videos(limit=max_items, channel_id=channel_id)
# Debug: Veritabanında kaç video var (tüm status'ler)
try:
conn = db.connect()
cursor = conn.cursor()
cursor.execute("""
SELECT transcript_status, COUNT(*) as count
FROM videos
WHERE channel_id = ?
GROUP BY transcript_status
""", (channel_id,))
rows = cursor.fetchall()
status_counts = {}
for row in rows:
if isinstance(row, dict):
status_counts[row['transcript_status']] = row['count']
else:
# 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)}")
except Exception as e:
logger.error(f"[PROCESS] ❌ Veritabanı durumu kontrol hatası: {type(e).__name__} - {str(e)}")
# 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_with_transcript[:max_items],
'channel_id': channel_id,
'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)
# 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 = []
# RSS-Bridge'den videoları çek (max_items'ın 2 katı kadar çek, böylece yeterli video olur) # RSS-Bridge'den videoları çek (max_items'ın 2 katı kadar çek, böylece yeterli video olur)
# RSS-Bridge'den daha fazla video çekiyoruz çünkü bazıları transcript'siz olabilir # RSS-Bridge'den daha fazla video çekiyoruz çünkü bazıları transcript'siz olabilir
rss_bridge_limit = max(max_items * 2, 50) # En az 50 video çek rss_bridge_limit = max(max_items * 2, 50) # En az 50 video çek
@@ -220,15 +357,29 @@ def process_channel(channel_id: str, max_items: int = 50) -> dict:
# Tüm bekleyen videoları al (channel_id'ye göre filtrele) # Tüm bekleyen videoları al (channel_id'ye göre filtrele)
all_pending_videos = [v for v in db.get_pending_videos() if v['channel_id'] == channel_id] all_pending_videos = [v for v in db.get_pending_videos() if v['channel_id'] == channel_id]
logger.info(f"[PROCESS] Channel {channel_id} için {len(all_pending_videos)} bekleyen video bulundu (max_items: {max_items})")
# Debug: Tüm videoları kontrol et (bekleyen + işlenmiş)
all_videos_count = len([v for v in db.get_processed_videos(limit=1000, channel_id=channel_id)])
logger.info(f"[PROCESS] Channel {channel_id} için {len(all_pending_videos)} bekleyen video, {all_videos_count} işlenmiş video bulundu (max_items: {max_items})")
# Eğer mevcut işlenmiş videolar varsa, sadece eksik kadar işle
remaining_needed = max_items - len(videos_to_return)
# max_items kadar transcript işlenene kadar batch'ler halinde işle # max_items kadar transcript işlenene kadar batch'ler halinde işle
total_batches = (len(all_pending_videos) + batch_size - 1) // batch_size total_batches = (len(all_pending_videos) + batch_size - 1) // batch_size
current_batch = 0 current_batch = 0
# İlk istek için sadece ilk batch'i işle (hızlı yanıt için)
# Sonraki isteklerde daha fazla işlenmiş video olacak
max_batches_to_process = 1 if len(videos_to_return) == 0 else min(3, total_batches) # İlk istekte 1 batch, sonra 3 batch
for batch_start in range(0, len(all_pending_videos), batch_size): for batch_start in range(0, len(all_pending_videos), batch_size):
if processed_count >= max_items: if processed_count >= remaining_needed:
logger.info(f"[PROCESS] Maksimum transcript sayısına ulaşıldı ({processed_count}/{max_items})") logger.info(f"[PROCESS] Yeterli transcript işlendi ({processed_count}/{remaining_needed})")
break
if current_batch >= max_batches_to_process:
logger.info(f"[PROCESS] İlk batch'ler işlendi ({current_batch}/{max_batches_to_process}), kalan işlemeler sonraki isteklerde yapılacak")
break break
current_batch += 1 current_batch += 1
@@ -292,24 +443,50 @@ def process_channel(channel_id: str, max_items: int = 50) -> dict:
logger.info(f"[BATCH] Batch {current_batch}/{total_batches} tamamlandı - İşlenen: {batch_processed}, Cache: {batch_cached}, Başarısız: {batch_failed}") logger.info(f"[BATCH] Batch {current_batch}/{total_batches} tamamlandı - İşlenen: {batch_processed}, Cache: {batch_cached}, Başarısız: {batch_failed}")
# Batch tamamlandı, uzun bekleme (YouTube IP blocking önleme için) # Batch tamamlandı, uzun bekleme (YouTube IP blocking önleme için)
if processed_count < max_items and batch_start + batch_size < len(all_pending_videos): # İlk batch'ler için daha kısa bekleme (hızlı yanıt için), sonraki batch'ler için uzun bekleme
# Blocking varsa daha uzun bekle if processed_count < remaining_needed and batch_start + batch_size < len(all_pending_videos):
wait_time = 60 + random.uniform(0, 30) # 60-90 saniye random (human-like) # İlk batch'ler için kısa bekleme (2-5 saniye), sonraki batch'ler için uzun bekleme (60-90 saniye)
logger.info(f"[BATCH] Batch'ler arası bekleme: {wait_time:.1f} saniye ({wait_time/60:.1f} dakika) - YouTube IP blocking önleme") if current_batch <= max_batches_to_process:
wait_time = 2 + random.uniform(0, 3) # 2-5 saniye (hızlı yanıt için)
logger.info(f"[BATCH] Batch'ler arası kısa bekleme: {wait_time:.1f} saniye (hızlı yanıt için)")
else:
wait_time = 60 + random.uniform(0, 30) # 60-90 saniye random (human-like)
logger.info(f"[BATCH] Batch'ler arası uzun bekleme: {wait_time:.1f} saniye ({wait_time/60:.1f} dakika) - YouTube IP blocking önleme")
time.sleep(wait_time) time.sleep(wait_time)
# İşlenmiş videoları getir # İşlenmiş videoları getir (yeni işlenenler)
processed_videos = db.get_processed_videos( newly_processed = db.get_processed_videos(
limit=max_items, limit=max_items,
channel_id=channel_id channel_id=channel_id
) )
logger.info(f"[PROCESS] ✅ Channel {channel_id} işleme tamamlandı - {len(processed_videos)} işlenmiş video döndürülüyor") # 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 (zaten transcript_clean ile filtrelenmiş)
existing_ids = {v['video_id'] for v in all_processed_videos}
# 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)
# Tarihe göre sırala (en yeni önce)
all_processed_videos.sort(
key=lambda x: x.get('published_at_utc', '') or '',
reverse=True
)
# Debug: Gerçek durumu logla
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 { return {
'videos': processed_videos, 'videos': all_processed_videos[:max_items],
'channel_id': channel_id, 'channel_id': channel_id,
'count': len(processed_videos) 'count': len(all_processed_videos[:max_items])
} }
@@ -325,10 +502,58 @@ def generate_feed():
- /?channel=@tavakfi&format=Atom - /?channel=@tavakfi&format=Atom
- /?channel_url=https://www.youtube.com/@tavakfi&format=Atom - /?channel_url=https://www.youtube.com/@tavakfi&format=Atom
""" """
# User-Agent kontrolü (RSS okuyucu tespiti için)
user_agent = request.headers.get('User-Agent', '')
is_rss_reader = any(keyword in user_agent.lower() for keyword in [
'rss', 'feed', 'reader', 'aggregator', 'feedly', 'newsblur',
'inoreader', 'theoldreader', 'netnewswire', 'reeder'
])
# Query parametrelerini al (validate_input decorator zaten sanitize etti) # Query parametrelerini al (validate_input decorator zaten sanitize etti)
channel_id = request.args.get('channel_id') # URL decode işlemi (tarayıcılar URL'leri encode edebilir, özellikle channel_url içinde başka URL varsa)
channel = request.args.get('channel') # @username veya username channel_id_raw = request.args.get('channel_id')
channel_url = request.args.get('channel_url') channel_raw = request.args.get('channel') # @username veya username
channel_url_raw = request.args.get('channel_url')
# Channel ID'yi decode et
channel_id = None
if channel_id_raw:
channel_id = unquote(channel_id_raw) if '%' in channel_id_raw else channel_id_raw
# Channel handle'ı decode et
channel = None
if channel_raw:
channel = unquote(channel_raw) if '%' in channel_raw else channel_raw
# @ işaretini temizle ve normalize et
channel = channel.strip().lstrip('@')
# Channel URL'yi decode et (eğer encode edilmişse)
# Flask request.args zaten decode eder ama channel_url içinde başka URL olduğu için double encoding olabilir
channel_url = None
if channel_url_raw:
# Önce raw değeri al (Flask'ın decode ettiği değer)
channel_url = channel_url_raw
# Eğer hala encode edilmiş görünüyorsa (%, + gibi karakterler varsa), decode et
if '%' in channel_url or '+' in channel_url:
# Birden fazla kez encode edilmiş olabilir, güvenli decode
max_decode_attempts = 3
for _ in range(max_decode_attempts):
decoded = unquote(channel_url)
if decoded == channel_url: # Artık decode edilecek bir şey yok
break
channel_url = decoded
if '%' not in channel_url: # Tamamen decode edildi
break
# URL formatını kontrol et ve düzelt
if channel_url and not channel_url.startswith(('http://', 'https://')):
# Eğer protocol yoksa, https ekle
if channel_url.startswith('www.youtube.com') or channel_url.startswith('youtube.com'):
channel_url = 'https://' + channel_url
elif channel_url.startswith('@'):
channel_url = 'https://www.youtube.com/' + channel_url
format_type = request.args.get('format', 'Atom').lower() # Atom veya Rss format_type = request.args.get('format', 'Atom').lower() # Atom veya Rss
try: try:
max_items = int(request.args.get('max_items', 10)) # Default: 10 transcript max_items = int(request.args.get('max_items', 10)) # Default: 10 transcript
@@ -337,18 +562,74 @@ def generate_feed():
except (ValueError, TypeError): except (ValueError, TypeError):
max_items = 10 max_items = 10
# Debug logging (tarayıcı istekleri için)
logger.info(f"[REQUEST] Tarayıcı isteği - Raw params: channel_id={channel_id_raw}, channel={channel_raw}, channel_url={channel_url_raw[:100] if channel_url_raw else None}")
logger.info(f"[REQUEST] Processed params: channel_id={channel_id}, channel={channel}, channel_url={channel_url[:100] if channel_url else None}")
logger.info(f"[REQUEST] Full URL: {request.url}")
logger.info(f"[REQUEST] Query string: {request.query_string.decode('utf-8') if request.query_string else None}")
# RSS okuyucu tespiti için log
if is_rss_reader:
logger.info(f"[RSS_READER] RSS okuyucu tespit edildi: {user_agent[:100]}")
# Channel ID'yi normalize et # Channel ID'yi normalize et
normalized_channel_id = normalize_channel_id( try:
channel_id=channel_id, normalized_channel_id = normalize_channel_id(
channel=channel, channel_id=channel_id,
channel_url=channel_url channel=channel,
) channel_url=channel_url
)
logger.info(f"[REQUEST] Normalized channel_id: {normalized_channel_id}")
except Exception as e:
logger.error(f"[REQUEST] ❌ Channel ID normalize hatası: {type(e).__name__} - {str(e)}")
normalized_channel_id = None
if not normalized_channel_id: if not normalized_channel_id:
return jsonify({ error_msg = 'Channel ID bulunamadı veya geçersiz format'
'error': 'Channel ID bulunamadı', if channel_url:
error_msg += f'. URL: {channel_url}'
elif channel:
error_msg += f'. Handle: {channel}'
elif channel_id:
error_msg += f'. Channel ID: {channel_id}'
logger.warning(f"[REQUEST] ❌ Channel ID bulunamadı - Raw: channel_id={channel_id_raw}, channel={channel_raw}, channel_url={channel_url_raw}")
logger.warning(f"[REQUEST] ❌ Processed: channel_id={channel_id}, channel={channel}, channel_url={channel_url}")
# RSS okuyucular için daha açıklayıcı hata mesajı
if is_rss_reader:
logger.warning(f"[RSS_READER] Channel ID bulunamadı - URL: {channel_url or channel or channel_id}")
return jsonify({
'error': error_msg,
'message': 'RSS okuyucunuzdan feed eklerken lütfen geçerli bir YouTube kanal URL\'si kullanın',
'received_params': {
'channel_id': channel_id_raw,
'channel': channel_raw,
'channel_url': channel_url_raw,
'decoded_channel_url': channel_url
},
'example_url': f'{request.url_root}?channel_url=https://www.youtube.com/@username&api_key=YOUR_API_KEY&format=Atom',
'usage': { 'usage': {
'channel_id': 'UC... (YouTube Channel ID)', 'channel_id': 'UC... (YouTube Channel ID, 24 karakter)',
'channel': '@username veya username',
'channel_url': 'https://www.youtube.com/@username veya https://www.youtube.com/channel/UC...',
'format': 'Atom veya Rss (varsayılan: Atom)',
'max_items': 'Maksimum transcript sayısı (varsayılan: 10, maksimum: 100)',
'api_key': 'API key query parametresi olarak eklenmelidir (RSS okuyucular header gönderemez)'
}
}), 400
return jsonify({
'error': error_msg,
'received_params': {
'channel_id': channel_id_raw,
'channel': channel_raw,
'channel_url': channel_url_raw,
'decoded_channel_url': channel_url
},
'message': 'YouTube Channel ID UC ile başlayan 24 karakter olmalı (sadece alfanumerik ve alt çizgi)',
'usage': {
'channel_id': 'UC... (YouTube Channel ID, 24 karakter)',
'channel': '@username veya username', 'channel': '@username veya username',
'channel_url': 'https://www.youtube.com/@username veya https://www.youtube.com/channel/UC...', 'channel_url': 'https://www.youtube.com/@username veya https://www.youtube.com/channel/UC...',
'format': 'Atom veya Rss (varsayılan: Atom)', 'format': 'Atom veya Rss (varsayılan: Atom)',
@@ -369,6 +650,22 @@ def generate_feed():
}), 404 }), 404
# RSS feed oluştur # RSS feed oluştur
logger.info(f"[FEED] RSS feed oluşturuluyor - Channel: {normalized_channel_id}, Video sayısı: {len(result['videos'])}")
# 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 = { channel_info = {
'id': normalized_channel_id, 'id': normalized_channel_id,
'title': f"YouTube Transcript Feed - {normalized_channel_id}", 'title': f"YouTube Transcript Feed - {normalized_channel_id}",
@@ -379,8 +676,18 @@ def generate_feed():
generator = RSSGenerator(channel_info) generator = RSSGenerator(channel_info)
# Sadece transcript_clean alanı olan videoları feed'e ekle
added_count = 0
skipped_count = 0
for video in result['videos']: 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 - {added_count} video eklendi, {skipped_count} video atlandı (transcript_clean yok)")
# Format'a göre döndür # Format'a göre döndür
response_headers = {} response_headers = {}

112
test_curl.sh Executable file
View File

@@ -0,0 +1,112 @@
#!/bin/bash
# Test curl komutları - POST response debug için
# API key: demo_key_12345 (config/security.yaml'dan)
BASE_URL="http://localhost:5000"
API_KEY="demo_key_12345"
echo "=========================================="
echo "1. Health Check (API key gerekmez)"
echo "=========================================="
curl -X GET "${BASE_URL}/health" -v
echo -e "\n\n=========================================="
echo "2. Info Endpoint (API key gerekir)"
echo "=========================================="
curl -X GET "${BASE_URL}/info" \
-H "X-API-Key: ${API_KEY}" \
-v
echo -e "\n\n=========================================="
echo "3. Channel ID ile Feed (Atom format)"
echo "=========================================="
curl -X GET "${BASE_URL}/?channel_id=UC9h8BDcXwkhZtnqoQJ7PggA&format=Atom&max_items=5" \
-H "X-API-Key: ${API_KEY}" \
-v
echo -e "\n\n=========================================="
echo "4. Channel Handle ile Feed (@kurzgesagt)"
echo " Not: İlk istekte 404 alınabilir (transcript henüz işlenmemiş)"
echo "=========================================="
curl -X GET "${BASE_URL}/?channel=@kurzgesagt&format=Atom&max_items=5" \
-H "X-API-Key: ${API_KEY}" \
-v
echo -e "\n\n=========================================="
echo "5. Channel URL ile Feed"
echo " Not: İlk istekte 404 alınabilir (transcript henüz işlenmemiş)"
echo "=========================================="
curl -X GET "${BASE_URL}/?channel_url=https://www.youtube.com/@kurzgesagt&format=Atom&max_items=5" \
-H "X-API-Key: ${API_KEY}" \
-v
echo -e "\n\n=========================================="
echo "6. RSS Format ile Feed"
echo "=========================================="
curl -X GET "${BASE_URL}/?channel_id=UC9h8BDcXwkhZtnqoQJ7PggA&format=Rss&max_items=5" \
-H "X-API-Key: ${API_KEY}" \
-v
echo -e "\n\n=========================================="
echo "7. API Key olmadan (401 hatası beklenir)"
echo "=========================================="
curl -X GET "${BASE_URL}/?channel_id=UC9h8BDcXwkhZtnqoQJ7PggA&format=Atom" \
-v
echo -e "\n\n=========================================="
echo "8. Geçersiz API Key ile (401 hatası beklenir)"
echo "=========================================="
curl -X GET "${BASE_URL}/?channel_id=UC9h8BDcXwkhZtnqoQJ7PggA&format=Atom" \
-H "X-API-Key: invalid_key" \
-v
echo -e "\n\n=========================================="
echo "9. POST Response Debug Test (max_items=1, hızlı test)"
echo " Not: İlk istekte 404 alınabilir (transcript henüz işlenmemiş)"
echo "=========================================="
curl -X GET "${BASE_URL}/?channel_id=UCsXVk37bltHxD1rDPwtNM8Q&format=Atom&max_items=1" \
-H "X-API-Key: ${API_KEY}" \
-v \
-o /tmp/test_feed.xml
echo -e "\n\n=========================================="
echo "10. Brotli Decode Test (yeni video ile)"
echo " Not: İlk istekte 404 alınabilir (transcript henüz işlenmemiş)"
echo "=========================================="
curl -X GET "${BASE_URL}/?channel_id=UCsXVk37bltHxD1rDPwtNM8Q&format=Atom&max_items=1" \
-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 ""
echo "✅ Brotli desteği eklendi (requirements.txt)"
echo " YouTube response'ları artık otomatik decode edilecek"
echo ""
echo "POST response debug dosyaları: output/flaresolverr_debug/"
echo " - post_response_*.txt (her POST response)"
echo " - post_response_error_*.txt (JSONDecodeError durumunda)"
echo ""
echo "⚠️ Docker container'ı yeniden build etmeniz gerekiyor:"
echo " sudo docker compose down"
echo " sudo docker compose build --no-cache"
echo " sudo docker compose up -d"
echo "=========================================="