api issues
This commit is contained in:
@@ -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)',
|
||||
|
||||
Reference in New Issue
Block a user