Files
Youtube2Feed/src/web_server.py
salvacybersec bff8164c5d api issues
2025-11-13 08:51:55 +03:00

684 lines
28 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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
import time
import logging
import random
from pathlib import Path
# Logger oluştur
logger = logging.getLogger(__name__)
sys.path.insert(0, str(Path(__file__).parent.parent))
from src.database import Database
from src.video_fetcher import fetch_videos_from_rss_bridge, get_channel_id_from_handle, extract_video_id
from src.transcript_extractor import TranscriptExtractor
from src.transcript_cleaner import TranscriptCleaner
from src.rss_generator import RSSGenerator
from src.security import (
init_security, get_security_manager,
require_api_key, rate_limit, validate_input
)
app = Flask(__name__)
# Security config yükle
_security_config = None
def load_security_config():
"""Security config'i yükle"""
global _security_config
if _security_config is None:
config_path = Path(__file__).parent.parent / 'config' / 'security.yaml'
if config_path.exists():
with open(config_path, 'r', encoding='utf-8') as f:
_security_config = yaml.safe_load(f).get('security', {})
else:
_security_config = {}
return _security_config
# Security manager'ı initialize et
def init_app_security():
"""Security manager'ı uygulama başlangıcında initialize et"""
config = load_security_config()
api_keys = config.get('api_keys', {})
default_rate_limit = config.get('default_rate_limit', 60)
init_security(api_keys, default_rate_limit)
# Security headers ve CORS middleware
@app.after_request
def add_security_headers(response):
"""Security header'ları ekle"""
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():
# 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', {})
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'])
)
# Rate limit bilgisini header'a ekle
if hasattr(g, 'rate_limit_remaining'):
response.headers['X-RateLimit-Remaining'] = str(g.rate_limit_remaining)
return response
# OPTIONS handler for CORS
@app.route('/', methods=['OPTIONS'])
@app.route('/<path:path>', methods=['OPTIONS'])
def handle_options(path=None):
"""CORS preflight request handler"""
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()
# Global instances (lazy loading)
db = None
extractor = None
cleaner = None
def get_db():
"""Database instance'ı al (singleton)"""
global db
if db is None:
db = Database()
db.init_database()
return db
def get_extractor():
"""Transcript extractor instance'ı al"""
global extractor
if extractor is None:
extractor = TranscriptExtractor()
return extractor
def get_cleaner():
"""Transcript cleaner instance'ı al"""
global cleaner
if cleaner is None:
cleaner = TranscriptCleaner()
return cleaner
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 ve validate et
Args:
channel_id: Direkt Channel ID (UC...)
channel: Channel handle (@username) veya username
channel_url: Full YouTube channel URL
Returns:
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:
normalized_id = channel_id
# Eğer URL formatında ise parse et
elif 'youtube.com/channel/' in channel_id:
parts = channel_id.split('/channel/')
if len(parts) > 1:
normalized_id = parts[-1].split('?')[0].split('/')[0]
# Channel handle (@username)
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}"
logger.info(f"[NORMALIZE] Channel handle URL oluşturuldu: {handle_url}")
normalized_id = get_channel_id_from_handle(handle_url)
# Channel 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:
# 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:
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
def process_channel(channel_id: str, max_items: int = 50) -> dict:
"""
Kanal için transcript feed'i oluştur
Returns:
RSS feed string ve metadata
"""
db = get_db()
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
logger.info(f"[PROCESS] Channel {channel_id} için RSS-Bridge'den video listesi çekiliyor (limit: {rss_bridge_limit})")
try:
videos = fetch_videos_from_rss_bridge(
base_url="https://rss-bridge.org/bridge01",
channel_id=channel_id,
format="Atom",
max_items=rss_bridge_limit
)
logger.info(f"[PROCESS] RSS-Bridge'den {len(videos)} video alındı")
except Exception as e:
logger.error(f"[PROCESS] ❌ RSS-Bridge hatası: {type(e).__name__} - {str(e)}")
raise Exception(f"RSS-Bridge hatası: {e}")
# Yeni videoları veritabanına ekle
new_videos_count = 0
for video in videos:
video['channel_id'] = channel_id
if not db.is_video_processed(video['video_id']):
db.add_video(video)
new_videos_count += 1
if new_videos_count > 0:
logger.info(f"[PROCESS] {new_videos_count} yeni video veritabanına eklendi")
else:
logger.debug(f"[PROCESS] Tüm videolar zaten veritabanında")
# Bekleyen videoları işle (max_items kadar, küçük batch'ler halinde)
# YouTube IP blocking'i önlemek için her batch'te sadece 5 video işlenir
# max_items: Her istekte kaç video transcript işleneceği (maksimum 100)
batch_size = 5 # Her batch'te işlenecek video sayısı (küçük batch = daha az blocking riski)
processed_count = 0 # İşlenen transcript sayısı
# 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]
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 >= 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
batch_videos = all_pending_videos[batch_start:batch_start + batch_size]
logger.info(f"[BATCH] Batch {current_batch}/{total_batches} başlatılıyor ({len(batch_videos)} video, Toplam işlenen: {processed_count}/{max_items})")
batch_processed = 0
batch_cached = 0
batch_failed = 0
for video in batch_videos:
if processed_count >= max_items:
break
video_id = video['video_id']
video_title = video.get('video_title', 'N/A')[:50]
# Cache kontrolü: 3 gün içinde işlenmiş transcript varsa atla
if db.is_transcript_cached(video_id, cache_days=3):
logger.debug(f"[CACHE] Video {video_id} ({video_title}) transcript'i cache'de, atlanıyor")
batch_cached += 1
continue
try:
logger.info(f"[VIDEO] Video işleniyor: {video_id} - {video_title}")
# Transcript çıkar
transcript = extractor.fetch_transcript(
video_id,
languages=['tr', 'en']
)
if transcript:
# Transcript temizle
logger.debug(f"[VIDEO] Video {video_id} transcript'i temizleniyor...")
raw, clean = cleaner.clean_transcript(transcript, sentences_per_paragraph=3)
# Veritabanına kaydet (her batch hemen kaydedilir)
db.update_video_transcript(
video_id,
raw,
clean,
status=1,
language='tr'
)
processed_count += 1
batch_processed += 1
logger.info(f"[VIDEO] ✅ Video {video_id} başarıyla işlendi ve kaydedildi ({processed_count}/{max_items})")
else:
logger.warning(f"[VIDEO] ⚠️ Video {video_id} transcript'i alınamadı (None döndü)")
batch_failed += 1
db.mark_video_failed(video_id, "Transcript None döndü")
except Exception as e:
error_type = type(e).__name__
error_msg = str(e)[:200]
logger.error(f"[VIDEO] ❌ Video {video_id} işleme hatası: {error_type} - {error_msg}")
db.mark_video_failed(video_id, str(e))
batch_failed += 1
# Batch özeti
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)
# İ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 (yeni işlenenler)
newly_processed = db.get_processed_videos(
limit=max_items,
channel_id=channel_id
)
# 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': all_processed_videos[:max_items],
'channel_id': channel_id,
'count': len(all_processed_videos[:max_items])
}
@app.route('/', methods=['GET'])
@require_api_key # API key zorunlu
@validate_input # Input validation
def generate_feed():
"""
RSS-Bridge benzeri URL template:
Örnekler:
- /?channel_id=UC9h8BDcXwkhZtnqoQJ7PggA&format=Atom
- /?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)
# 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
# Maksimum 100 transcript (20'şer batch'ler halinde işlenir)
max_items = min(max_items, 100)
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
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:
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, 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)',
'max_items': 'Maksimum transcript sayısı (varsayılan: 10, maksimum: 100, 20\'şer batch\'ler halinde işlenir)'
}
}), 400
try:
# Kanalı işle
result = process_channel(normalized_channel_id, max_items=max_items)
if not result['videos']:
return jsonify({
'error': 'Henüz işlenmiş video yok',
'channel_id': normalized_channel_id,
'message': 'Transcript\'ler arka planda işleniyor. Lütfen birkaç dakika sonra tekrar deneyin.',
'note': 'YouTube IP blocking nedeniyle transcript çıkarımı yavaş olabilir. İlk istekte birkaç dakika bekleyin.'
}), 404
# RSS feed oluştur
channel_info = {
'id': normalized_channel_id,
'title': f"YouTube Transcript Feed - {normalized_channel_id}",
'link': f"https://www.youtube.com/channel/{normalized_channel_id}",
'description': f'Full-text transcript RSS feed for channel {normalized_channel_id}',
'language': 'en'
}
generator = RSSGenerator(channel_info)
for video in result['videos']:
generator.add_video_entry(video)
# Format'a göre döndür
response_headers = {}
if hasattr(g, 'rate_limit_remaining'):
response_headers['X-RateLimit-Remaining'] = str(g.rate_limit_remaining)
if format_type == 'rss':
rss_content = generator.generate_rss_string()
response_headers['Content-Type'] = 'application/rss+xml; charset=utf-8'
return Response(
rss_content,
mimetype='application/rss+xml',
headers=response_headers
)
else: # Atom
# Feedgen Atom desteği
atom_content = generator.generate_atom_string()
response_headers['Content-Type'] = 'application/atom+xml; charset=utf-8'
return Response(
atom_content,
mimetype='application/atom+xml',
headers=response_headers
)
except Exception as e:
return jsonify({
'error': str(e),
'channel_id': normalized_channel_id
}), 500
@app.route('/health', methods=['GET'])
@rate_limit(limit_per_minute=120) # Health check için daha yüksek limit
def health():
"""Health check endpoint"""
return jsonify({'status': 'ok', 'service': 'YouTube Transcript RSS Feed'})
@app.route('/info', methods=['GET'])
@require_api_key # API key zorunlu
def info():
"""API bilgileri"""
return jsonify({
'service': 'YouTube Transcript RSS Feed Generator',
'version': '1.0.0',
'endpoints': {
'/': 'RSS Feed Generator',
'/health': 'Health Check',
'/info': 'API Info'
},
'usage': {
'channel_id': 'UC... (YouTube Channel ID)',
'channel': '@username veya username',
'channel_url': 'Full YouTube channel URL',
'format': 'Atom veya Rss (varsayılan: Atom)',
'max_items': 'Her istekte işlenecek maksimum transcript sayısı (varsayılan: 10, maksimum: 100, 20\'şer batch\'ler halinde işlenir)'
},
'examples': [
'/?channel_id=UC9h8BDcXwkhZtnqoQJ7PggA&format=Atom',
'/?channel=@tavakfi&format=Rss',
'/?channel_url=https://www.youtube.com/@tavakfi&format=Atom&max_items=50'
]
})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)