Compare commits

..

2 Commits

Author SHA1 Message Date
salvacybersec
1f3b6d5fe2 api issues 2025-11-13 08:52:33 +03:00
salvacybersec
bff8164c5d api issues 2025-11-13 08:51:55 +03:00
5 changed files with 559 additions and 57 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:
### 1. HTTP Header (Önerilen)
### 1. HTTP Header (Programatik kullanım için önerilen)
```http
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
```
**Ö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'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:**
**URL-Based Sorgular (RSS Okuyucular ve Tarayıcılar için):**
```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" \
"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" \
"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" \
"http://localhost:5000/?channel_url=https://www.youtube.com/@tavakfi&format=Atom&max_items=50"
```
# Query parametresi ile API key
curl "http://localhost:5000/?channel_id=UC9h8BDcXwkhZtnqoQJ7PggA&api_key=demo_key_12345&format=Rss"
**Production URL Örnekleri:**
```
# 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:**
@@ -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:
**Ö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)
```python
@@ -363,6 +448,31 @@ else:
### 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
const apiKey = "your_api_key_here";
const channelId = "UC9h8BDcXwkhZtnqoQJ7PggA";
@@ -388,13 +498,37 @@ fetch(`http://localhost:5000/?channel_id=${channelId}&format=Atom`, {
### 4. cURL ile Test
**URL-Based Sorgular (RSS Okuyucular ve Tarayıcılar için):**
```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"
CHANNEL_ID="UC9h8BDcXwkhZtnqoQJ7PggA"
curl -H "X-API-Key: $API_KEY" \
"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

View File

@@ -116,13 +116,21 @@ class SecurityManager:
Returns:
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:
return False
# UC ile başlayan 24 karakter
if re.match(r'^UC[a-zA-Z0-9_-]{22}$', channel_id):
return True
# 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):
# Double check: Tire karakteri içermemeli
if '-' not in channel_id:
return True
return False
@@ -212,7 +220,15 @@ def require_api_key(f):
security = get_security_manager()
# 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)

View File

@@ -4,35 +4,59 @@ RSS-Bridge kullanarak video metadata çıkarımı
import feedparser
import re
import requests
import logging
from urllib.parse import urlencode
from typing import List, Dict, Optional
from datetime import datetime
logger = logging.getLogger(__name__)
def get_channel_id_from_handle(handle_url: str) -> Optional[str]:
"""
Channel handle URL'inden Channel ID'yi web scraping ile bulur.
Örnek: https://www.youtube.com/@tavakfi -> UC...
YouTube Channel ID formatı: UC ile başlayan, 24 karakter, sadece alfanumerik ve alt çizgi
"""
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()
html_content = response.text
# İlk pattern: "externalId":"UC..."
match = re.search(r'"externalId":"(UC[a-zA-Z0-9_-]{22})"', html_content)
if match:
return match.group(1)
# YouTube Channel ID pattern: UC ile başlayan, 24 karakter, sadece alfanumerik ve alt çizgi (tire YOK)
# Pattern'ler: UC + 22 karakter = toplam 24 karakter
patterns = [
(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..."
match_alt = re.search(r'"channelId":"(UC[a-zA-Z0-9_-]{22})"', html_content)
if match_alt:
return match_alt.group(1)
for pattern, pattern_name in patterns:
match = re.search(pattern, html_content)
if match:
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
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}")

View File

@@ -3,6 +3,7 @@ Flask web server - RSS-Bridge benzeri URL template sistemi
"""
from flask import Flask, request, Response, jsonify, g
from typing import Optional
from urllib.parse import unquote, urlparse
import sys
import os
import yaml
@@ -58,8 +59,22 @@ def add_security_headers(response):
config = load_security_config()
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():
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_config = config.get('cors', {})
@@ -90,7 +105,29 @@ def add_security_headers(response):
@app.route('/<path:path>', methods=['OPTIONS'])
def handle_options(path=None):
"""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
init_app_security()
@@ -130,7 +167,7 @@ def normalize_channel_id(channel_id: Optional[str] = None,
channel: Optional[str] = None,
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:
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
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
if channel_id:
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
if 'youtube.com/channel/' in channel_id:
elif 'youtube.com/channel/' in channel_id:
parts = channel_id.split('/channel/')
if len(parts) > 1:
return parts[-1].split('?')[0].split('/')[0]
normalized_id = parts[-1].split('?')[0].split('/')[0]
# 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('@'):
channel = f"@{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
if channel_url:
# Handle URL
if not normalized_id and channel_url:
# URL'yi temizle ve normalize et
channel_url = channel_url.strip()
# Handle URL (@username formatı)
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
elif '/channel/' in channel_url:
parts = channel_url.split('/channel/')
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
@@ -182,6 +257,28 @@ def process_channel(channel_id: str, max_items: int = 50) -> dict:
extractor = get_extractor()
cleaner = get_cleaner()
# ÖNCE: Mevcut işlenmiş videoları kontrol et
existing_processed = db.get_processed_videos(limit=max_items, channel_id=channel_id)
logger.info(f"[PROCESS] Channel {channel_id} için {len(existing_processed)} mevcut işlenmiş video bulundu")
# Eğer yeterli sayıda işlenmiş video varsa, onları hemen döndür
if len(existing_processed) >= max_items:
logger.info(f"[PROCESS] ✅ Yeterli işlenmiş video var ({len(existing_processed)}), yeni işleme başlatılmıyor")
return {
'videos': existing_processed[:max_items],
'channel_id': channel_id,
'count': len(existing_processed[:max_items])
}
# Eğer mevcut işlenmiş videolar varsa ama yeterli değilse, onları döndür ve yeni işlemeleri başlat
# Ancak sadece ilk batch'i işle (hızlı yanıt için)
if len(existing_processed) > 0:
logger.info(f"[PROCESS] ⚠️ Mevcut işlenmiş video var ama yeterli değil ({len(existing_processed)}/{max_items}), yeni işleme başlatılıyor")
# Mevcut videoları döndürmek için sakla
videos_to_return = existing_processed.copy()
else:
videos_to_return = []
# 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_limit = max(max_items * 2, 50) # En az 50 video çek
@@ -222,13 +319,24 @@ def process_channel(channel_id: str, max_items: int = 50) -> dict:
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})")
# 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
total_batches = (len(all_pending_videos) + batch_size - 1) // batch_size
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):
if processed_count >= max_items:
logger.info(f"[PROCESS] Maksimum transcript sayısına ulaşıldı ({processed_count}/{max_items})")
if processed_count >= remaining_needed:
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
current_batch += 1
@@ -292,24 +400,44 @@ 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}")
# Batch tamamlandı, uzun bekleme (YouTube IP blocking önleme için)
if processed_count < max_items and batch_start + batch_size < len(all_pending_videos):
# Blocking varsa daha uzun bekle
wait_time = 60 + random.uniform(0, 30) # 60-90 saniye random (human-like)
logger.info(f"[BATCH] Batch'ler arası bekleme: {wait_time:.1f} saniye ({wait_time/60:.1f} dakika) - YouTube IP blocking önleme")
# İlk batch'ler için daha kısa bekleme (hızlı yanıt için), sonraki batch'ler için uzun bekleme
if processed_count < remaining_needed and batch_start + batch_size < len(all_pending_videos):
# İlk batch'ler için kısa bekleme (2-5 saniye), sonraki batch'ler için uzun bekleme (60-90 saniye)
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)
# İşlenmiş videoları getir
processed_videos = db.get_processed_videos(
# İşlenmiş videoları getir (yeni işlenenler)
newly_processed = db.get_processed_videos(
limit=max_items,
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")
# Mevcut videoları ve yeni işlenen videoları birleştir (duplicate kontrolü ile)
all_processed_videos = videos_to_return.copy() # Önce mevcut videoları ekle
existing_ids = {v['video_id'] for v in all_processed_videos}
# Yeni işlenen videoları ekle
for video in newly_processed:
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
)
logger.info(f"[PROCESS] ✅ Channel {channel_id} işleme tamamlandı - {len(all_processed_videos)} işlenmiş video döndürülüyor (Mevcut: {len(videos_to_return)}, Yeni işlenen: {len(newly_processed)})")
return {
'videos': processed_videos,
'videos': all_processed_videos[:max_items],
'channel_id': channel_id,
'count': len(processed_videos)
'count': len(all_processed_videos[:max_items])
}
@@ -325,10 +453,58 @@ def generate_feed():
- /?channel=@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)
channel_id = request.args.get('channel_id')
channel = request.args.get('channel') # @username veya username
channel_url = request.args.get('channel_url')
# URL decode işlemi (tarayıcılar URL'leri encode edebilir, özellikle channel_url içinde başka URL varsa)
channel_id_raw = request.args.get('channel_id')
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
try:
max_items = int(request.args.get('max_items', 10)) # Default: 10 transcript
@@ -337,18 +513,74 @@ def generate_feed():
except (ValueError, TypeError):
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
normalized_channel_id = normalize_channel_id(
channel_id=channel_id,
channel=channel,
channel_url=channel_url
)
try:
normalized_channel_id = normalize_channel_id(
channel_id=channel_id,
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:
return jsonify({
'error': 'Channel ID bulunamadı',
error_msg = 'Channel ID bulunamadı veya geçersiz format'
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': {
'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_url': 'https://www.youtube.com/@username veya https://www.youtube.com/channel/UC...',
'format': 'Atom veya Rss (varsayılan: Atom)',

96
test_curl.sh Executable file
View File

@@ -0,0 +1,96 @@
#!/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 "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 "=========================================="