Compare commits
4 Commits
86ce4913b8
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e52708cda9 | ||
|
|
3a35b2d4d9 | ||
|
|
1f3b6d5fe2 | ||
|
|
bff8164c5d |
152
API.md
152
API.md
@@ -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
|
||||||
|
|||||||
@@ -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,8 +193,35 @@ 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]:
|
||||||
"""İşlenmiş videoları getir (status=1)"""
|
"""İşlenmiş videoları getir (status=1)"""
|
||||||
@@ -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)"""
|
||||||
|
|||||||
@@ -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', ''))
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -178,19 +191,67 @@ 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(
|
||||||
|
f"[TRANSCRIPT] {video_id} için {len(transcript_list)} transcript adayı bulundu"
|
||||||
|
)
|
||||||
|
|
||||||
# Eski formatı döndürmek için to_raw_data() kullan
|
selected_transcript = None
|
||||||
# Format: [{'text': '...', 'start': 1.36, 'duration': 1.68}, ...]
|
|
||||||
|
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__
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
112
test_curl.sh
Executable 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 "=========================================="
|
||||||
|
|
||||||
Reference in New Issue
Block a user