first commit

This commit is contained in:
salvacybersec
2025-11-13 03:25:21 +03:00
commit abe170a1f8
21 changed files with 2198 additions and 0 deletions

23
.dockerignore Normal file
View File

@@ -0,0 +1,23 @@
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
venv/
ENV/
.venv
*.db
*.sqlite
*.sqlite3
.git/
.gitignore
*.md
!README.md
.vscode/
.idea/
*.log
.DS_Store
data/*.db
output/*.xml

41
.gitignore vendored Normal file
View File

@@ -0,0 +1,41 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
*.egg-info/
dist/
build/
# Virtual environments
env/
venv/
ENV/
.venv
# IDE
.vscode/
.idea/
*.swp
*.swo
# Database
*.db
*.sqlite
*.sqlite3
data/*.db
# Output
output/*.xml
# Logs
*.log
# OS
.DS_Store
Thumbs.db
# Config (opsiyonel - hassas bilgiler varsa)
# config/config.yaml

26
Dockerfile Normal file
View File

@@ -0,0 +1,26 @@
FROM python:3.10-slim
# Çalışma dizinini ayarla
WORKDIR /app
# Sistem bağımlılıkları
RUN apt-get update && apt-get install -y \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Python bağımlılıklarını kopyala ve kur
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# SpaCy modelini indir
RUN python -m spacy download en_core_web_sm
# Uygulama kodunu kopyala
COPY . .
# Veri dizinlerini oluştur
RUN mkdir -p data output
# Varsayılan komut (web server)
CMD ["python", "app.py"]

123
README.md Normal file
View File

@@ -0,0 +1,123 @@
# YouTube Transcript RSS Feed Generator
YouTube video transkriptlerini otomatik olarak çıkarıp, tam metin içeren RSS feed'ine dönüştüren Docker tabanlı sistem.
## Özellikler
-**RSS-Bridge benzeri URL template** - Kanal adı/linki ile direkt feed
-**Web Server Modu** - Flask ile RESTful API
- ✅ RSS-Bridge entegrasyonu (100+ video desteği)
- ✅ Async rate limiting (AIOLimiter)
- ✅ SpaCy ile Sentence Boundary Detection
- ✅ SQLite veritabanı (durum yönetimi)
- ✅ Full-text RSS feed (`<content:encoded>`)
- ✅ Channel handle → Channel ID otomatik dönüştürme
- ✅ Atom ve RSS format desteği
## Hızlı Başlangıç
### Docker ile Web Server Modu (Önerilen)
```bash
# Build
docker-compose build
# Web server'ı başlat (port 5000)
docker-compose up -d
# Logları izle
docker-compose logs -f
```
### URL Template Kullanımı
RSS-Bridge benzeri URL template sistemi:
```
# Channel ID ile
http://localhost:5000/?channel_id=UC9h8BDcXwkhZtnqoQJ7PggA&format=Atom
# Channel Handle ile
http://localhost:5000/?channel=@tavakfi&format=Atom
# Channel URL ile
http://localhost:5000/?channel_url=https://www.youtube.com/@tavakfi&format=Atom
# RSS formatı
http://localhost:5000/?channel=@tavakfi&format=Rss
# Maksimum video sayısı
http://localhost:5000/?channel=@tavakfi&format=Atom&max_items=100
```
### Batch Mode (Manuel Çalıştırma)
```bash
# Tek seferlik çalıştırma
docker-compose run --rm yttranscriptrss python main.py
```
### Yerel Geliştirme
```bash
# Virtual environment oluştur
python -m venv venv
source venv/bin/activate # Linux/Mac
# veya
venv\Scripts\activate # Windows
# Bağımlılıkları kur
pip install -r requirements.txt
# SpaCy modelini indir
python -m spacy download en_core_web_sm
# Çalıştır
python main.py
```
## Yapılandırma
`config/config.yaml` dosyasını düzenleyin:
```yaml
channel:
id: "UC9h8BDcXwkhZtnqoQJ7PggA" # veya handle: "@username"
name: "Channel Name"
language: "tr"
rss_bridge:
base_url: "https://rss-bridge.org/bridge01"
format: "Atom"
max_items: 100
```
## Proje Yapısı
```
yttranscriptrss/
├── src/
│ ├── database.py # SQLite yönetimi
│ ├── video_fetcher.py # RSS-Bridge entegrasyonu
│ ├── transcript_extractor.py # Transcript çıkarımı
│ ├── transcript_cleaner.py # NLP ve temizleme
│ └── rss_generator.py # RSS feed oluşturma
├── config/
│ └── config.yaml # Yapılandırma
├── data/
│ └── videos.db # SQLite veritabanı
├── output/
│ └── transcript_feed.xml # RSS feed çıktısı
├── Dockerfile
├── docker-compose.yml
└── main.py
```
## Geliştirme Planı
Detaylı geliştirme planı için `development_plan.md` dosyasına bakın.
## Lisans
MIT

9
app.py Normal file
View File

@@ -0,0 +1,9 @@
#!/usr/bin/env python3
"""
Flask Web Server - RSS-Bridge benzeri URL template sistemi
"""
from src.web_server import app
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=False)

39
config/config.yaml Normal file
View File

@@ -0,0 +1,39 @@
channel:
# Channel ID veya Handle (otomatik dönüştürülür)
id: "UC9h8BDcXwkhZtnqoQJ7PggA" # Channel ID (UC ile başlar)
# veya handle kullanılabilir:
# handle: "@tavakfi" # Handle kullanılırsa otomatik olarak Channel ID'ye çevrilir
# veya full URL:
# handle_url: "https://www.youtube.com/@tavakfi"
name: "Channel Name"
url: "https://youtube.com/channel/..."
language: "tr"
rss_bridge:
# Public RSS-Bridge instance (varsayılan - önerilen)
base_url: "https://rss-bridge.org/bridge01"
# Self-hosted instance (opsiyonel - rate limiting sorunları için)
# base_url: "http://localhost:3000"
bridge_name: "YoutubeBridge" # Önemli: "YoutubeBridge" olmalı
context: "By channel id" # veya "By username"
format: "Atom" # veya "Rss" (feed için)
max_items: 100 # RSS-Bridge'den çekilecek maksimum video sayısı
use_fallback: true # RSS-Bridge erişilemezse native RSS kullan
# Opsiyonel filtreleme
duration_min: null # dakika cinsinden minimum süre
duration_max: null # dakika cinsinden maksimum süre
transcript:
languages: ["tr", "en"]
enable_sbd: true
paragraph_length: 3
rss:
title: "Channel Transcript Feed"
description: "Full-text transcript RSS feed"
output_file: "transcript_feed.xml"
automation:
check_interval_hours: 12
max_items: 100

739
development_plan.md Normal file
View File

@@ -0,0 +1,739 @@
# YouTube Transcript RSS Feed - Geliştirme Planı
## Proje Özeti
YouTube video transkriptlerini otomatik olarak çıkarıp, tam metin içeren RSS feed'ine dönüştüren otomatik bir pipeline geliştirilmesi. Sistem, Python tabanlı transcript çıkarımı ve RSS feed oluşturma ile RSS-Bridge entegrasyonu seçeneklerini içerir.
---
## Faz 1: Proje Altyapısı ve Ortam Kurulumu
### 1.1. Teknoloji Stack Seçimi
**Ana Yaklaşım: Python Tabanlı (Önerilen)**
- **Transcript Çıkarımı**: `youtube-transcript-api`
- **RSS Oluşturma**: `python-feedgen`
- **Video Listesi**: RSS-Bridge (RSS feed parser)
- **Dil**: Python 3.8+
### 1.2. Geliştirme Ortamı Kurulumu
**Görevler:**
- [ ] Python 3.8+ kurulumu doğrulama
- [ ] Virtual environment oluşturma (`python -m venv venv`)
- [ ] Gerekli paketlerin kurulumu:
```bash
pip install youtube-transcript-api
pip install feedgen
pip install python-dateutil
pip install feedparser # RSS-Bridge feed'lerini parse etmek için
pip install requests # HTTP istekleri için
pip install aiolimiter # Async rate limiting için
pip install httpx # Async HTTP client
pip install spacy # NLP ve Sentence Boundary Detection için
```
**Not**: SQLite Python'da built-in (`sqlite3` modülü), ekstra kurulum gerekmez.
**SpaCy Model**: `python -m spacy download en_core_web_sm` (veya `tr_core_news_sm` Türkçe için)
- [ ] Proje dizin yapısı oluşturma:
```
yttranscriptrss/
├── src/
│ ├── transcript_extractor.py
│ ├── transcript_cleaner.py
│ ├── rss_generator.py
│ ├── video_fetcher.py
│ └── database.py
├── config/
│ └── config.yaml
├── data/
│ └── videos.db
├── output/
│ └── transcript_feed.xml
├── tests/
├── requirements.txt
└── main.py
```
**Süre Tahmini**: 1-2 gün
---
## Faz 2: Transcript Çıkarımı ve Temizleme Modülü
### 2.1. Transcript Çıkarımı (`transcript_extractor.py`)
**Görevler:**
- [ ] `youtube-transcript-api` ile video ID'den transcript çıkarma
- [ ] Çoklu dil desteği (fallback mekanizması)
- Örnek: `languages=['tr', 'en']` - önce Türkçe, yoksa İngilizce
- [ ] Hata yönetimi:
- Transcript bulunamama durumları
- API rate limiting
- Geçersiz video ID'ler
- [ ] Raw transcript formatını anlama:
```python
# Format: [{"text": "...", "start": 0.0, "duration": 2.5}, ...]
```
**Kritik Gereksinimler:**
- Headless browser kullanmama (API tabanlı yaklaşım)
- Otomatik ve manuel transkriptleri destekleme
- **Async Rate Limiting**: AIOLimiter ile eş zamanlı istek yönetimi
- API limiti: 10 saniyede 5 istek
- Async batching ile paralel işleme
- **Retry-After Header**: 429 hatalarında `Retry-After` header'ını kullanma
- Dinamik bekleme süresi (statik delay yerine)
- Exponential backoff mekanizması
- Timeout ve retry mekanizmaları
**Süre Tahmini**: 3-4 gün
### 2.2. Transcript Temizleme ve Dönüştürme (`transcript_cleaner.py`)
**Görevler:**
- [ ] **Veri Temizleme Pipeline**:
- **Artifact Removal**:
- Zaman tabanlı işaretçileri kaldırma
- Konuşma dışı etiketler: `[Music]`, `[Applause]`, vb.
- Aşırı boşlukları temizleme
- **Normalizasyon**:
- Unicode karakter normalleştirme
- Metin standardizasyonu
- [ ] **Sentence Boundary Detection (SBD) - SpaCy Entegrasyonu**:
- **SpaCy Model Kullanımı**: `en_core_web_sm` veya `tr_core_news_sm`
- Noktalama işaretlerinin ötesine geçme:
- Doğal duraklama noktalarını tespit
- Anlamsal sınırları belirleme
- Konuşmacı değişikliklerini algılama
- Fragment'ları birleştirme ve cümle sınırlarını tespit etme
- Paragraf yapısı oluşturma (cümle sayısı veya anlamsal değişikliklere göre)
- **Özelleştirme**: Özel içerik için kural tabanlı SBD uzantıları
- [ ] **HTML Wrapping**:
- Temizlenmiş metni `<p>...</p>` tag'leri ile sarmalama
- Paragraf yapısını koruma
- Minimal HTML (sadece gerekli tag'ler)
- [ ] **XML Entity Escaping** (Kritik!):
- **Zorunlu karakter dönüşümleri**:
- `&` → `&amp;` (özellikle URL'lerde kritik!)
- `<` → `&lt;`
- `>` → `&gt;`
- `"` → `&quot;`
- `'` → `&apos;`
- **CDATA kullanımı**: İsteğe bağlı ama entity escaping hala gerekli
- URL'lerdeki ampersand'ların kaçışı özellikle önemli
**Algoritma Özeti:**
1. Artifact'ları kaldır ve normalize et
2. SpaCy ile fragment'ları birleştir ve cümle sınırlarını tespit et
3. Paragraflara böl (anlamsal veya cümle sayısına göre)
4. HTML tag'leri ekle (`<p>...</p>`)
5. XML entity escaping uygula (özellikle `&` karakterleri)
**Süre Tahmini**: 4-5 gün
---
## Faz 3: Video Metadata Çıkarımı ve Yönetimi
### 3.1. Video Metadata Fetcher (`video_fetcher.py`) - RSS-Bridge Entegrasyonu
**Görevler:**
- [ ] **RSS-Bridge Feed Parser**:
- **Public RSS-Bridge instance kullanımı (Önerilen)**:
- Base URL: `https://rss-bridge.org/bridge01/`
- Ücretsiz ve hazır kullanılabilir
- Rate limiting riski var ama başlangıç için yeterli
- RSS-Bridge YouTube Bridge endpoint'ini kullanma
- **Doğru URL formatı**:
- Public (Channel ID): `https://rss-bridge.org/bridge01/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=CHANNEL_ID&format=Atom`
- Public (Channel Handle): `https://rss-bridge.org/bridge01/?action=display&bridge=YoutubeBridge&context=By+username&u=USERNAME&format=Atom`
- Self-hosted (opsiyonel): `http://localhost:3000/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=CHANNEL_ID&format=Atom`
- **Önemli parametreler**:
- `bridge=YoutubeBridge` (Youtube değil!)
- `context=By+channel+id` veya `context=By+username`
- `c=CHANNEL_ID` (channel ID için) veya `u=USERNAME` (handle için)
- `format=Atom` veya `format=Rss` (feed için, Html değil)
- `duration_min` ve `duration_max` (opsiyonel filtreleme)
- [ ] **Feed Parsing** (`feedparser` kullanarak):
- RSS/Atom feed'ini parse etme
- Video entry'lerini çıkarma
- Metadata çıkarımı:
- Video ID (YouTube URL'den parse)
- Video başlığı (`entry.title`)
- Yayın tarihi (`entry.published` - timezone-aware)
- Video URL (`entry.link`)
- Video açıklaması (`entry.summary` - opsiyonel)
- Thumbnail URL (opsiyonel)
- [ ] **RSS-Bridge Avantajları**:
- Native feed'den daha fazla video (10-15 yerine 100+)
- Channel Handle (@username) desteği
- Filtreleme seçenekleri (duration, vb.)
- [ ] **Hata Yönetimi**:
- RSS-Bridge instance erişilemezse fallback (native RSS)
- Rate limiting handling
- Feed format validation
- [ ] **Channel ID Extraction (Handle'dan)**:
- Channel handle (@username) verildiğinde Channel ID'yi bulma
- Web scraping ile HTML source'dan Channel ID çıkarma
- Regex pattern: `"externalId":"(UC[a-zA-Z0-9_-]{22})"` veya `"channelId":"(UC[a-zA-Z0-9_-]{22})"`
- Channel ID formatı: `UC` ile başlar, 22 karakter
- Fallback mekanizması: İlk pattern bulunamazsa alternatif pattern dene
- Hata yönetimi: Request exception handling
- [ ] **Video ID Extraction**:
- YouTube URL'den video ID çıkarma
- Regex pattern: `youtube.com/watch?v=([a-zA-Z0-9_-]+)`
- Short URL desteği: `youtu.be/([a-zA-Z0-9_-]+)`
- [ ] `yt-dlp` entegrasyonu (opsiyonel, gelişmiş metadata için)
**RSS-Bridge Kullanım Örneği:**
```python
import feedparser
from urllib.parse import urlencode
# Public RSS-Bridge base URL
RSS_BRIDGE_BASE = "https://rss-bridge.org/bridge01"
# Channel ID ile feed URL'i oluştur
params = {
'action': 'display',
'bridge': 'YoutubeBridge',
'context': 'By channel id',
'c': 'UC9h8BDcXwkhZtnqoQJ7PggA', # Channel ID
'format': 'Atom' # veya 'Rss'
}
rss_bridge_url = f"{RSS_BRIDGE_BASE}/?{urlencode(params)}"
# Feed'i parse et
feed = feedparser.parse(rss_bridge_url)
# Video entry'lerini işle
for entry in feed.entries:
video_id = extract_video_id(entry.link)
video_title = entry.title
published_date = entry.published_parsed # timezone-aware
video_url = entry.link
```
**Gerçek Örnek URL:**
```
https://rss-bridge.org/bridge01/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UC9h8BDcXwkhZtnqoQJ7PggA&format=Atom
```
**Channel ID Bulma Fonksiyonu (Handle'dan):**
```python
import requests
import re
def get_channel_id_from_handle(handle_url):
"""
Channel handle URL'inden Channel ID'yi web scraping ile bulur.
Örnek: https://www.youtube.com/@tavakfi -> UC...
"""
try:
# HTML içeriğini çek
response = requests.get(handle_url)
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)
# 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)
return None # Channel ID bulunamadı
except requests.exceptions.RequestException as e:
raise Exception(f"Error fetching channel page: {e}")
# Kullanım örneği
handle_url = "https://www.youtube.com/@tavakfi"
channel_id = get_channel_id_from_handle(handle_url)
# Artık channel_id ile RSS-Bridge feed'ini çekebiliriz
```
**Süre Tahmini**: 3 gün
### 3.2. İşlenmiş Video Takibi (`database.py` - SQLite)
**Görevler:**
- [ ] SQLite veritabanı modülü oluşturma (`database.py`)
- [ ] Veritabanı şeması tasarımı:
```sql
-- Channels tablosu (kanal takibi için)
CREATE TABLE IF NOT EXISTS channels (
channel_id TEXT PRIMARY KEY, -- UC... formatında
channel_name TEXT,
channel_url TEXT,
last_checked_utc TEXT, -- ISO 8601 UTC format
created_at_utc TEXT DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_channels_last_checked ON channels(last_checked_utc);
-- Videos tablosu (detaylı şema)
CREATE TABLE IF NOT EXISTS videos (
video_id TEXT PRIMARY KEY, -- 11 karakterli YouTube video ID
channel_id TEXT, -- UC... formatında (FK)
video_title TEXT,
video_url TEXT,
published_at_utc TEXT, -- ISO 8601 UTC format (YYYY-MM-DDTHH:MM:SSZ)
processed_at_utc TEXT, -- ISO 8601 UTC format
transcript_status INTEGER DEFAULT 0, -- 0: Beklemede, 1: Çıkarıldı, 2: Başarısız
transcript_language TEXT,
transcript_raw TEXT, -- Ham, bölümlenmemiş transcript
transcript_clean TEXT, -- SBD ile işlenmiş, RSS için hazır HTML
last_updated_utc TEXT DEFAULT (datetime('now'))
);
-- Kritik index'ler (performans için)
CREATE INDEX IF NOT EXISTS idx_videos_channel_id ON videos(channel_id);
CREATE INDEX IF NOT EXISTS idx_videos_published_at_utc ON videos(published_at_utc);
CREATE INDEX IF NOT EXISTS idx_videos_transcript_status ON videos(transcript_status);
CREATE INDEX IF NOT EXISTS idx_videos_processed_at_utc ON videos(processed_at_utc);
```
**Önemli Notlar**:
- **Zaman Formatı**: UTC ISO 8601 (`YYYY-MM-DDTHH:MM:SSZ`) - SQLite'ın timezone desteği yok
- **transcript_status**: INTEGER (0, 1, 2) - String değil, performans için
- **Index Optimizasyonu**: `EXPLAIN QUERY PLAN` ile sorgu performansını doğrula
- **DATE() fonksiyonu kullanma**: Index kullanımını engeller, direkt timestamp karşılaştırması yap
- [ ] Database helper fonksiyonları:
- `init_database()` - Veritabanı ve tablo oluşturma
- `is_video_processed(video_id)` - Duplicate kontrolü
- `get_pending_videos()` - `transcript_status = 0` olan videoları getir
- `add_video(video_data)` - Yeni video kaydı (status=0 olarak)
- `update_video_transcript(video_id, raw, clean, status, language)` - Transcript güncelleme
- `get_processed_videos(limit=None, channel_id=None)` - İşlenmiş videoları getir
- `mark_video_failed(video_id, reason)` - Kalıcı hata işaretleme (status=2)
- **Query Performance**: `EXPLAIN QUERY PLAN` ile index kullanımını doğrula
- [ ] Yeni video tespiti algoritması:
1. RSS-Bridge feed'den son videoları çek
2. SQLite veritabanında `video_id` ile sorgula
3. Sadece yeni videoları (veritabanında olmayan) işle
- [ ] Transaction yönetimi (ACID compliance)
- [ ] Connection pooling ve error handling
**Avantajlar:**
- Daha hızlı sorgulama (index'li)
- Transaction desteği
- İlişkisel veri yapısı
- Daha iyi veri bütünlüğü
- İstatistik sorguları kolaylaşır
**Süre Tahmini**: 2 gün
---
## Faz 4: RSS Feed Oluşturma
### 4.1. RSS Generator (`rss_generator.py`)
**Görevler:**
- [ ] `python-feedgen` ile FeedGenerator oluşturma
- [ ] **Content Namespace Extension** ekleme:
```xml
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
```
- [ ] Channel metadata ayarlama:
- `fg.id()` - Channel ID
- `fg.title()` - Kanal başlığı
- `fg.link()` - Kanal URL'i
- `fg.language('tr')` - Dil
- `fg.lastBuildDate()` - Son güncelleme tarihi
- [ ] Item oluşturma:
- `fe.id(video_id)` - GUID (YouTube Video ID)
- `fe.title(video_title)` - Video başlığı
- `fe.link(href=video_url)` - Video linki
- `fe.published(datetime_with_timezone)` - Yayın tarihi (timezone-aware)
- `fe.description(summary)` - Kısa özet
- `fe.content(content=cleaned_transcript_html)` - Tam transcript (`<content:encoded>`)
**Kritik Gereksinimler:**
- Timezone-aware tarih formatı (RFC 822 veya ISO 8601)
- Video ID'nin GUID olarak kullanılması (immutable)
- Full-text için `<content:encoded>` tag'i
**Süre Tahmini**: 2-3 gün
### 4.2. RSS Output ve Serialization
**Görevler:**
- [ ] XML dosyası oluşturma:
```python
fg.rss_file('transcript_feed.xml', pretty=True, extensions=True)
```
- [ ] Pretty printing (okunabilirlik için)
- [ ] Extensions desteği (Content Namespace dahil)
**Süre Tahmini**: 1 gün
---
## Faz 5: Ana Pipeline ve Otomasyon
### 5.1. Main Pipeline (`main.py`)
**Görevler:**
- [ ] Tüm modüllerin entegrasyonu
- [ ] İş akışı:
1. SQLite veritabanını başlat (`init_database()`)
2. Configuration'dan channel bilgisini oku:
- Eğer handle (@username) verildiyse, `get_channel_id_from_handle()` ile Channel ID'ye çevir
- Channel ID zaten varsa direkt kullan
3. RSS-Bridge feed'den yeni videoları tespit et (`video_fetcher.py`)
4. SQLite veritabanında `video_id` ile duplicate kontrolü yap
5. Yeni videolar için:
a. Transcript çıkar
b. Transcript'i temizle
c. RSS feed'e ekle
d. SQLite'a kaydet (video metadata + işlenme durumu)
6. RSS feed'i güncelle (veritabanından tüm işlenmiş videoları çek)
7. XML dosyasını kaydet
- [ ] Hata yönetimi ve logging
- [ ] Configuration dosyası yükleme
- [ ] Database transaction yönetimi (rollback on error)
**Süre Tahmini**: 2-3 gün
### 5.2. Configuration Management (`config.yaml`)
**Görevler:**
- [ ] Yapılandırma dosyası oluşturma:
```yaml
channel:
# Channel ID veya Handle (otomatik dönüştürülür)
id: "UC9h8BDcXwkhZtnqoQJ7PggA" # Channel ID (UC ile başlar)
# veya handle kullanılabilir:
# handle: "@tavakfi" # Handle kullanılırsa otomatik olarak Channel ID'ye çevrilir
# veya full URL:
# handle_url: "https://www.youtube.com/@tavakfi"
name: "Channel Name"
url: "https://youtube.com/channel/..."
language: "tr"
rss_bridge:
# Public RSS-Bridge instance (varsayılan - önerilen)
base_url: "https://rss-bridge.org/bridge01"
# Self-hosted instance (opsiyonel - rate limiting sorunları için)
# base_url: "http://localhost:3000"
bridge_name: "YoutubeBridge" # Önemli: "YoutubeBridge" olmalı
context: "By channel id" # veya "By username"
format: "Atom" # veya "Rss" (feed için)
max_items: 100 # RSS-Bridge'den çekilecek maksimum video sayısı
use_fallback: true # RSS-Bridge erişilemezse native RSS kullan
# Opsiyonel filtreleme
duration_min: null # dakika cinsinden minimum süre
duration_max: null # dakika cinsinden maksimum süre
transcript:
languages: ["tr", "en"]
enable_sbd: true
paragraph_length: 3
rss:
title: "Channel Transcript Feed"
description: "Full-text transcript RSS feed"
output_file: "transcript_feed.xml"
automation:
check_interval_hours: 12
max_items: 100
```
**Süre Tahmini**: 1 gün
---
## Faz 6: Test ve Validasyon
### 6.1. Unit Testler
**Görevler:**
- [ ] Transcript extractor testleri
- [ ] Transcript cleaner testleri (SBD, XML escaping)
- [ ] RSS generator testleri
- [ ] Video fetcher testleri:
- RSS-Bridge feed parsing
- Channel ID extraction (handle'dan):
- Handle URL'den Channel ID çıkarma
- Regex pattern matching testleri
- Fallback pattern testleri
- Hata durumları (geçersiz handle, network error)
- Video ID extraction (URL'den)
- Fallback mekanizması (RSS-Bridge erişilemezse)
- Feed format validation (Atom/RSS)
- [ ] Database modülü testleri:
- Veritabanı oluşturma
- Video ekleme/sorgulama
- Duplicate kontrolü
- Transaction rollback testleri
- Test veritabanı kullanımı (in-memory SQLite)
**Süre Tahmini**: 2-3 gün
### 6.2. RSS Feed Validasyonu
**Görevler:**
- [ ] W3C Feed Validation Service ile doğrulama
- URL: https://validator.w3.org/feed/
- [ ] Validasyon checklist:
- [ ] XML entity escaping doğru mu?
- [ ] Zorunlu RSS 2.0 tag'leri mevcut mu? (`<title>`, `<link>`, `<description>`)
- [ ] Content Namespace doğru tanımlanmış mı?
- [ ] Timezone-aware tarih formatı doğru mu?
- [ ] GUID'ler unique ve immutable mi?
- [ ] Farklı RSS reader'larda test (Feedly, Tiny Tiny RSS, vb.)
**Süre Tahmini**: 1-2 gün
---
## Faz 7: Deployment ve Hosting
### 7.1. Static Hosting Seçimi
**Seçenekler:**
1. **GitHub Pages** (Önerilen)
- Ücretsiz
- CI/CD entegrasyonu
- Version control
2. **Static.app / StaticSave**
- Ücretsiz tier mevcut
- Dosya boyutu limitleri var
**Görevler:**
- [ ] GitHub repository oluşturma
- [ ] GitHub Actions workflow oluşturma (otomatik çalıştırma için)
- [ ] MIME type ayarları (`application/rss+xml`)
- [ ] Public URL oluşturma
**Süre Tahmini**: 1-2 gün
### 7.2. Otomasyon ve Scheduling
**Seçenekler:**
1. **GitHub Actions** (Önerilen)
- Cron job: Her 12-24 saatte bir
- Ücretsiz tier yeterli
2. **Cron Job** (VPS/Server)
- Tam kontrol
- Sunucu gereksinimi
3. **Cloud Functions** (AWS Lambda, Google Cloud Functions)
- Serverless
- Kullanım bazlı maliyet
**GitHub Actions Workflow Örneği (Optimize Edilmiş):**
```yaml
name: Update RSS Feed
on:
schedule:
- cron: '0 */12 * * *' # Her 12 saatte bir
workflow_dispatch: # Manuel tetikleme
jobs:
update-feed:
runs-on: ubuntu-latest
steps:
# 1. Checkout (commit SHA ile sabitlenmiş - güvenlik)
- uses: actions/checkout@8e5e7e5f366d5b8b75e3d67731b8b25a0a40a8a7 # v4 commit SHA
# 2. Python setup
- uses: actions/setup-python@0a5d62f8d0679a54b4c1a51b3c9c0e0e8e8e8e8e # v5 commit SHA
with:
python-version: '3.10'
# 3. Cache veritabanı (Git commit yerine - performans)
- name: Cache database
uses: actions/cache@v3
with:
path: data/videos.db
key: ${{ runner.os }}-videos-db-${{ hashFiles('data/videos.db') }}
restore-keys: |
${{ runner.os }}-videos-db-
# 4. Install dependencies
- run: pip install -r requirements.txt
- run: python -m spacy download en_core_web_sm
# 5. Run pipeline
- run: python main.py
# 6. Save database to cache
- name: Save database to cache
uses: actions/cache@v3
with:
path: data/videos.db
key: ${{ runner.os }}-videos-db-${{ hashFiles('data/videos.db') }}
# 7. Upload RSS feed as artifact
- name: Upload RSS feed
uses: actions/upload-artifact@v3
with:
name: transcript-feed
path: output/transcript_feed.xml
retention-days: 30
# 8. Deploy to GitHub Pages (opsiyonel)
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./output
```
**Önemli Notlar**:
- **Cache Kullanımı**: Veritabanı için `actions/cache` kullan (Git commit yerine) - daha hızlı
- **Action Pinning**: Tüm action'lar commit SHA ile sabitlenmiş (güvenlik)
- **Artifact**: RSS feed'i artifact olarak sakla (GitHub Pages'e deploy edilebilir)
- **SpaCy Model**: CI/CD'de model indirme adımı eklendi
**Süre Tahmini**: 1-2 gün
---
## Faz 8: İleri Özellikler ve Optimizasyon
### 8.1. Performans Optimizasyonu
**Görevler:**
- [ ] Paralel transcript çıkarımı (çoklu video için)
- [ ] Caching mekanizması
- [ ] Rate limiting yönetimi
- [ ] Batch processing optimizasyonu
**Süre Tahmini**: 2-3 gün
### 8.2. Self-Hosted RSS-Bridge Deployment (Opsiyonel - Rate Limiting Sorunları İçin)
**Görevler:**
- [ ] **Ne zaman self-hosting gerekir?**
- Public instance rate limiting'e maruz kalırsa
- Yüksek hacimli kullanım gerekiyorsa
- Özelleştirilmiş bridge'ler (YoutubeEmbedBridge) gerekiyorsa
- [ ] RSS-Bridge Docker deployment:
```bash
docker create --name=rss-bridge --publish 3000:80 \
--volume $(pwd)/rss-bridge-config:/config \
rssbridge/rss-bridge
docker start rss-bridge
```
- [ ] RSS-Bridge konfigürasyonu:
- `config.ini.php` ayarları
- `CACHE_TIMEOUT` ayarlama (TTL kontrolü)
- Custom bridge'ler ekleme (YoutubeEmbedBridge)
- [ ] Self-hosting avantajları:
- Rate limiting'den kaçınma (dedicated IP)
- Daha fazla video çekebilme (100+)
- Özelleştirilmiş bridge'ler
- Gizlilik ve kontrol
- [ ] Production deployment:
- Reverse proxy (Nginx) kurulumu
- SSL sertifikası (Let's Encrypt)
- Monitoring ve health checks
- [ ] YoutubeEmbedBridge entegrasyonu (ad-free playback):
- `YoutubeEmbedBridge.php` dosyasını `/config/bridges/` klasörüne ekle
- Container'ı restart et
- Embed bridge'i test et
**Not**: Public RSS-Bridge (`https://rss-bridge.org/bridge01/`) varsayılan olarak kullanılır ve çoğu durumda yeterlidir. Self-hosting sadece rate limiting sorunları yaşandığında veya özel gereksinimler olduğunda önerilir.
**Süre Tahmini**: 2-3 gün (opsiyonel)
### 8.3. Monitoring ve Logging
**Görevler:**
- [ ] Detaylı logging sistemi
- [ ] Hata bildirimleri (email, webhook)
- [ ] Feed health monitoring
- [ ] İstatistikler (SQLite sorguları ile):
- Toplam işlenen video sayısı
- Başarı/başarısızlık oranları
- Son işlenme tarihleri
- Dil dağılımı
- Günlük/haftalık istatistikler
**SQLite İstatistik Örnekleri:**
```sql
-- Toplam işlenen video sayısı
SELECT COUNT(*) FROM processed_videos;
-- Başarı oranı
SELECT
transcript_status,
COUNT(*) as count,
ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM processed_videos), 2) as percentage
FROM processed_videos
GROUP BY transcript_status;
-- Son 7 günde işlenen videolar
SELECT COUNT(*) FROM processed_videos
WHERE processed_at >= datetime('now', '-7 days');
```
**Süre Tahmini**: 2 gün
---
## Toplam Süre Tahmini
| Faz | Süre |
|-----|------|
| Faz 1: Altyapı | 1-2 gün |
| Faz 2: Transcript Modülü | 7-9 gün |
| Faz 3: Video Metadata | 6 gün |
| Faz 4: RSS Generation | 3-4 gün |
| Faz 5: Pipeline | 3-4 gün |
| Faz 6: Test & Validasyon | 3-5 gün |
| Faz 7: Deployment | 2-4 gün |
| Faz 8: İleri Özellikler | 4-9 gün (opsiyonel) |
| **TOPLAM (Temel)** | **25-35 gün** |
| **TOPLAM (Tam)** | **29-44 gün** |
---
## Kritik Başarı Faktörleri
1. **API Stabilitesi**: `youtube-transcript-api` kullanımı (scraping değil)
2. **XML Compliance**: Content Namespace Extension ve entity escaping
3. **Timezone Handling**: Tüm tarihler timezone-aware olmalı
4. **Duplicate Prevention**: Video ID GUID olarak kullanılmalı
5. **Efficient Processing**: Sadece yeni videolar işlenmeli
---
## Riskler ve Mitigasyon
| Risk | Etki | Mitigasyon |
|------|------|------------|
| YouTube API değişikliği | Yüksek | `youtube-transcript-api` güncellemelerini takip et |
| Transcript bulunamama | Orta | Fallback diller, hata yönetimi |
| Rate limiting | Orta | Exponential backoff, request throttling |
| XML validation hataları | Yüksek | Comprehensive testing, W3C validation |
| Hosting maliyeti | Düşük | GitHub Pages (ücretsiz) kullan |
---
## Sonraki Adımlar
1. **Hemen Başla**: Faz 1 - Proje altyapısı kurulumu
2. **MVP Hedefi**: Faz 1-6 tamamlanarak çalışan bir sistem
3. **Production Ready**: Faz 7 deployment ile canlıya alma
4. **Optimizasyon**: Faz 8 ile gelişmiş özellikler
---
## Referanslar ve Kaynaklar
- `youtube-transcript-api`: https://github.com/jdepoix/youtube-transcript-api
- `python-feedgen`: https://github.com/lkiesow/python-feedgen
- RSS 2.0 Specification: https://www.rssboard.org/rss-specification
- Content Namespace: http://purl.org/rss/1.0/modules/content/
- W3C Feed Validator: https://validator.w3.org/feed/
- RSS-Bridge: https://github.com/RSS-Bridge/rss-bridge

21
docker-compose.yml Normal file
View File

@@ -0,0 +1,21 @@
services:
yttranscriptrss:
build: .
container_name: yttranscriptrss
ports:
- "5000:5000" # Web server portu
volumes:
# Veritabanı ve çıktı dosyalarını kalıcı hale getir
- ./data:/app/data
- ./output:/app/output
# Config dosyasını mount et (değişiklikler için)
- ./config:/app/config
environment:
- PYTHONUNBUFFERED=1
- FLASK_ENV=production
restart: unless-stopped
# Web server modu (varsayılan)
command: python app.py
# Veya batch mode için:
# command: python main.py

67
id.md Normal file
View File

@@ -0,0 +1,67 @@
The most effective way to find a YouTube Channel ID from a channel handle (`@tavakfi`) using **Python without an API** is by web scraping the channel's HTML source code.
The Channel ID (which starts with `UC`) is embedded in the page source under a key like `"externalId"` or `"channelId"`.
Here is the Python script you can use:
### Python Channel ID Finder (No API)
This script uses the `requests` library to fetch the HTML content and the built-in `re` (regular expressions) library to search for the Channel ID.
```python
import requests
import re
def get_channel_id_from_handle(handle_url):
"""
Fetches the YouTube channel ID from a handle URL by scraping the source code.
"""
try:
# 1. Fetch the HTML content of the channel page
response = requests.get(handle_url)
response.raise_for_status() # Raise an exception for bad status codes
html_content = response.text
# 2. Search for the Channel ID using a Regular Expression
# The channel ID is often stored in the page data as "externalId" or "channelId"
# The regex looks for "externalId" or "channelId" followed by the UC... string
match = re.search(r'"externalId":"(UC[a-zA-Z0-9_-]{22})"', html_content)
if match:
# The Channel ID is captured in the first group of the regex match
return match.group(1)
else:
# Try alternative location for Channel ID (less common but worth checking)
match_alt = re.search(r'"channelId":"(UC[a-zA-Z0-9_-]{22})"', html_content)
if match_alt:
return match_alt.group(1)
else:
return "Channel ID not found in page source."
except requests.exceptions.RequestException as e:
return f"Error fetching URL: {e}"
# The handle URL you provided
channel_url = "https://www.youtube.com/@tavakfi"
# Run the function
channel_id = get_channel_id_from_handle(channel_url)
print(f"Channel Handle: {channel_url}")
print(f"Channel ID: {channel_id}")
```
-----
### How the Code Works
1. **`import requests` and `import re`**: These lines import the necessary libraries. `requests` handles fetching the webpage content, and `re` handles finding the specific text pattern (the Channel ID) within that content.
2. **`requests.get(handle_url)`**: This line sends an HTTP GET request to the YouTube handle URL (`@tavakfi`) to retrieve the raw HTML source code.
3. **`re.search(r'"externalId":"(UC[a-zA-Z0-9_-]{22})"', html_content)`**: This is the core scraping logic. It searches the entire HTML text for the pattern that YouTube uses to store the Channel ID.
* The Channel ID always starts with **`UC`** and is followed by exactly 22 alphanumeric characters, which is captured by the pattern `(UC[a-zA-Z0-9_-]{22})`.
* The ID is then extracted using `match.group(1)`.
This scraping technique allows you to reliably retrieve the unique `UC` Channel ID for any channel that uses the modern `@handle` URL format.
You can learn more about the channel and its content by watching this video: [Türkiye Research Foundation - Videos](https://www.youtube.com/@tavakfi/videos).

163
main.py Normal file
View File

@@ -0,0 +1,163 @@
#!/usr/bin/env python3
"""
YouTube Transcript RSS Feed Generator - Ana Pipeline
"""
import yaml
import os
import sys
from pathlib import Path
# Proje root'unu path'e ekle
sys.path.insert(0, str(Path(__file__).parent))
from src.database import Database
from src.video_fetcher import fetch_videos_from_rss_bridge, get_channel_id_from_handle
from src.transcript_extractor import TranscriptExtractor
from src.transcript_cleaner import TranscriptCleaner
from src.rss_generator import RSSGenerator
def load_config(config_path: str = "config/config.yaml") -> dict:
"""Config dosyasını yükle"""
with open(config_path, 'r', encoding='utf-8') as f:
return yaml.safe_load(f)
def get_channel_id(config: dict) -> str:
"""Config'den channel ID'yi al (handle varsa dönüştür)"""
channel_config = config.get('channel', {})
# Channel ID direkt varsa
if channel_config.get('id'):
return channel_config['id']
# Handle URL varsa
if channel_config.get('handle_url'):
channel_id = get_channel_id_from_handle(channel_config['handle_url'])
if channel_id:
return channel_id
# Handle varsa
if channel_config.get('handle'):
handle_url = f"https://www.youtube.com/{channel_config['handle']}"
channel_id = get_channel_id_from_handle(handle_url)
if channel_id:
return channel_id
raise ValueError("Channel ID bulunamadı! Config'de id, handle veya handle_url belirtin.")
def main():
"""Ana pipeline"""
print("YouTube Transcript RSS Feed Generator başlatılıyor...")
# Config yükle
config = load_config()
# Channel ID al
channel_id = get_channel_id(config)
print(f"Channel ID: {channel_id}")
# Database başlat
db = Database()
db.init_database()
# RSS-Bridge'den videoları çek
rss_bridge_config = config.get('rss_bridge', {})
print(f"RSS-Bridge'den videolar çekiliyor...")
try:
videos = fetch_videos_from_rss_bridge(
base_url=rss_bridge_config.get('base_url', 'https://rss-bridge.org/bridge01'),
channel_id=channel_id,
format=rss_bridge_config.get('format', 'Atom'),
max_items=rss_bridge_config.get('max_items', 100)
)
print(f"{len(videos)} video bulundu")
except Exception as e:
print(f"Hata: {e}")
return
# Yeni videoları veritabanına ekle
new_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_count += 1
print(f"{new_count} yeni video eklendi")
# Bekleyen videoları işle
pending_videos = db.get_pending_videos()
print(f"{len(pending_videos)} video işlenmeyi bekliyor")
if pending_videos:
extractor = TranscriptExtractor()
cleaner = TranscriptCleaner()
transcript_config = config.get('transcript', {})
for video in pending_videos[:10]: # İlk 10 video (test için)
print(f"İşleniyor: {video['video_title']}")
# Transcript çıkar
transcript = extractor.fetch_transcript(
video['video_id'],
languages=transcript_config.get('languages', ['en'])
)
if transcript:
# Transcript temizle
raw, clean = cleaner.clean_transcript(
transcript,
sentences_per_paragraph=transcript_config.get('paragraph_length', 3)
)
# Veritabanına kaydet
db.update_video_transcript(
video['video_id'],
raw,
clean,
status=1, # Başarılı
language=transcript_config.get('languages', ['en'])[0]
)
print(f"✓ Tamamlandı: {video['video_title']}")
else:
# Başarısız olarak işaretle
db.mark_video_failed(video['video_id'], "Transcript bulunamadı")
print(f"✗ Başarısız: {video['video_title']}")
# RSS feed oluştur
processed_videos = db.get_processed_videos(
limit=config.get('automation', {}).get('max_items', 100),
channel_id=channel_id
)
if processed_videos:
channel_info = {
'id': channel_id,
'title': config.get('rss', {}).get('title', 'Transcript Feed'),
'link': config.get('channel', {}).get('url', ''),
'description': config.get('rss', {}).get('description', ''),
'language': config.get('channel', {}).get('language', 'en')
}
generator = RSSGenerator(channel_info)
for video in processed_videos:
generator.add_video_entry(video)
output_file = config.get('rss', {}).get('output_file', 'transcript_feed.xml')
output_path = f"output/{output_file}"
os.makedirs('output', exist_ok=True)
generator.generate_rss(output_path)
print(f"RSS feed oluşturuldu: {output_path}")
db.close()
print("Tamamlandı!")
if __name__ == "__main__":
main()

10
requirements.txt Normal file
View File

@@ -0,0 +1,10 @@
youtube-transcript-api>=0.6.0
feedgen>=1.0.0
python-dateutil>=2.8.2
feedparser>=6.0.10
requests>=2.31.0
spacy>=3.7.0
pyyaml>=6.0.1
flask>=3.0.0
pytz>=2023.3

6
research.md Normal file

File diff suppressed because one or more lines are too long

80
rssbridge.md Normal file

File diff suppressed because one or more lines are too long

2
src/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
# YouTube Transcript RSS Feed Generator

174
src/database.py Normal file
View File

@@ -0,0 +1,174 @@
"""
SQLite veritabanı yönetimi modülü
"""
import sqlite3
import os
from datetime import datetime, timezone
from typing import Optional, List, Dict
class Database:
"""SQLite veritabanı yönetim sınıfı"""
def __init__(self, db_path: str = "data/videos.db"):
self.db_path = db_path
self.conn = None
def connect(self):
"""Veritabanı bağlantısı oluştur"""
# Dizin yoksa oluştur
os.makedirs(os.path.dirname(self.db_path), exist_ok=True)
self.conn = sqlite3.connect(self.db_path)
self.conn.row_factory = sqlite3.Row
return self.conn
def init_database(self):
"""Veritabanı şemasını oluştur"""
conn = self.connect()
cursor = conn.cursor()
# Channels tablosu
cursor.execute("""
CREATE TABLE IF NOT EXISTS channels (
channel_id TEXT PRIMARY KEY,
channel_name TEXT,
channel_url TEXT,
last_checked_utc TEXT,
created_at_utc TEXT DEFAULT (datetime('now'))
)
""")
cursor.execute("""
CREATE INDEX IF NOT EXISTS idx_channels_last_checked
ON channels(last_checked_utc)
""")
# Videos tablosu
cursor.execute("""
CREATE TABLE IF NOT EXISTS videos (
video_id TEXT PRIMARY KEY,
channel_id TEXT,
video_title TEXT,
video_url TEXT,
published_at_utc TEXT,
processed_at_utc TEXT,
transcript_status INTEGER DEFAULT 0,
transcript_language TEXT,
transcript_raw TEXT,
transcript_clean TEXT,
last_updated_utc TEXT DEFAULT (datetime('now'))
)
""")
# Index'ler
cursor.execute("""
CREATE INDEX IF NOT EXISTS idx_videos_channel_id
ON videos(channel_id)
""")
cursor.execute("""
CREATE INDEX IF NOT EXISTS idx_videos_published_at_utc
ON videos(published_at_utc)
""")
cursor.execute("""
CREATE INDEX IF NOT EXISTS idx_videos_transcript_status
ON videos(transcript_status)
""")
cursor.execute("""
CREATE INDEX IF NOT EXISTS idx_videos_processed_at_utc
ON videos(processed_at_utc)
""")
conn.commit()
print("Database initialized successfully")
def close(self):
"""Veritabanı bağlantısını kapat"""
if self.conn:
self.conn.close()
def is_video_processed(self, video_id: str) -> bool:
"""Video işlenmiş mi kontrol et"""
cursor = self.conn.cursor()
cursor.execute("SELECT video_id FROM videos WHERE video_id = ?", (video_id,))
return cursor.fetchone() is not None
def get_pending_videos(self) -> List[Dict]:
"""İşlenmeyi bekleyen videoları getir (status=0)"""
cursor = self.conn.cursor()
cursor.execute("""
SELECT * FROM videos
WHERE transcript_status = 0
ORDER BY published_at_utc DESC
""")
return [dict(row) for row in cursor.fetchall()]
def add_video(self, video_data: Dict):
"""Yeni video ekle (status=0 olarak)"""
cursor = self.conn.cursor()
cursor.execute("""
INSERT OR IGNORE INTO videos
(video_id, channel_id, video_title, video_url, published_at_utc, transcript_status)
VALUES (?, ?, ?, ?, ?, 0)
""", (
video_data['video_id'],
video_data.get('channel_id'),
video_data.get('video_title'),
video_data.get('video_url'),
video_data.get('published_at_utc')
))
self.conn.commit()
def update_video_transcript(self, video_id: str, raw: str, clean: str,
status: int, language: Optional[str] = None):
"""Video transcript'ini güncelle"""
cursor = self.conn.cursor()
now_utc = datetime.now(timezone.utc).isoformat()
cursor.execute("""
UPDATE videos
SET transcript_raw = ?,
transcript_clean = ?,
transcript_status = ?,
transcript_language = ?,
processed_at_utc = ?,
last_updated_utc = ?
WHERE video_id = ?
""", (raw, clean, status, language, now_utc, now_utc, video_id))
self.conn.commit()
def get_processed_videos(self, limit: Optional[int] = None,
channel_id: Optional[str] = None) -> List[Dict]:
"""İşlenmiş videoları getir (status=1)"""
cursor = self.conn.cursor()
query = """
SELECT * FROM videos
WHERE transcript_status = 1
"""
params = []
if channel_id:
query += " AND channel_id = ?"
params.append(channel_id)
query += " ORDER BY published_at_utc DESC"
if limit:
query += " LIMIT ?"
params.append(limit)
cursor.execute(query, params)
return [dict(row) for row in cursor.fetchall()]
def mark_video_failed(self, video_id: str, reason: Optional[str] = None):
"""Video'yu başarısız olarak işaretle (status=2)"""
cursor = self.conn.cursor()
cursor.execute("""
UPDATE videos
SET transcript_status = 2,
last_updated_utc = ?
WHERE video_id = ?
""", (datetime.now(timezone.utc).isoformat(), video_id))
self.conn.commit()

78
src/rss_generator.py Normal file
View File

@@ -0,0 +1,78 @@
"""
RSS feed oluşturma modülü
"""
from feedgen.feed import FeedGenerator
from datetime import datetime
from typing import List, Dict
import pytz
class RSSGenerator:
"""RSS feed generator sınıfı"""
def __init__(self, channel_info: Dict):
"""
Args:
channel_info: Kanal bilgileri (title, link, description, language)
"""
self.fg = FeedGenerator()
self.channel_info = channel_info
self._setup_channel()
def _setup_channel(self):
"""Channel metadata ayarla"""
self.fg.id(self.channel_info.get('id', ''))
self.fg.title(self.channel_info.get('title', ''))
self.fg.link(href=self.channel_info.get('link', ''))
self.fg.description(self.channel_info.get('description', ''))
self.fg.language(self.channel_info.get('language', 'en'))
self.fg.lastBuildDate(datetime.now(pytz.UTC))
def add_video_entry(self, video: Dict):
"""
Video entry ekle
Args:
video: Video metadata dict
"""
fe = self.fg.add_entry()
# GUID (video ID)
fe.id(video['video_id'])
# Title
fe.title(video.get('video_title', ''))
# Link
fe.link(href=video.get('video_url', ''))
# Published date (timezone-aware)
if video.get('published_at_utc'):
try:
pub_date = datetime.fromisoformat(
video['published_at_utc'].replace('Z', '+00:00')
)
fe.published(pub_date)
except:
pass
# Description (kısa özet)
fe.description(video.get('description', '')[:200])
# Content (tam transcript)
if video.get('transcript_clean'):
fe.content(content=video['transcript_clean'])
def generate_rss(self, output_path: str):
"""RSS feed'i dosyaya yaz"""
self.fg.rss_file(output_path, pretty=True, extensions=True)
print(f"RSS feed generated: {output_path}")
def generate_rss_string(self) -> str:
"""RSS feed'i string olarak döndür"""
return self.fg.rss_str(pretty=True, extensions=True)
def generate_atom_string(self) -> str:
"""Atom feed'i string olarak döndür"""
return self.fg.atom_str(pretty=True)

136
src/transcript_cleaner.py Normal file
View File

@@ -0,0 +1,136 @@
"""
Transcript temizleme ve NLP işleme modülü
"""
import re
import html
import spacy
from typing import List, Dict
class TranscriptCleaner:
"""Transcript temizleme ve SBD sınıfı"""
def __init__(self, model_name: str = "en_core_web_sm"):
"""
Args:
model_name: SpaCy model adı
"""
try:
self.nlp = spacy.load(model_name)
except OSError:
print(f"Model {model_name} not found. Loading default...")
self.nlp = spacy.load("en_core_web_sm")
def remove_artifacts(self, text: str) -> str:
"""Artifact'ları kaldır"""
# Zaman kodlarını kaldır [00:00:00]
text = re.sub(r'\[\d{2}:\d{2}:\d{2}\]', '', text)
# Konuşma dışı etiketleri kaldır
text = re.sub(r'\[(Music|Applause|Laughter|Music playing)\]', '', text, flags=re.IGNORECASE)
# Aşırı boşlukları temizle
text = re.sub(r'\s+', ' ', text)
return text.strip()
def detect_sentence_boundaries(self, fragments: List[Dict]) -> List[str]:
"""
Fragment'ları birleştir ve cümle sınırlarını tespit et
Args:
fragments: Transcript fragment listesi [{"text": "...", "start": 0.0, ...}]
Returns:
Cümle listesi
"""
# Fragment'ları birleştir
full_text = ' '.join([f['text'] for f in fragments])
# Artifact'ları kaldır
full_text = self.remove_artifacts(full_text)
# SpaCy ile işle
doc = self.nlp(full_text)
# Cümleleri çıkar
sentences = [sent.text.strip() for sent in doc.sents if sent.text.strip()]
return sentences
def create_paragraphs(self, sentences: List[str],
sentences_per_paragraph: int = 3) -> List[str]:
"""
Cümleleri paragraflara böl
Args:
sentences: Cümle listesi
sentences_per_paragraph: Paragraf başına cümle sayısı
Returns:
Paragraf listesi
"""
paragraphs = []
current_paragraph = []
for sentence in sentences:
current_paragraph.append(sentence)
if len(current_paragraph) >= sentences_per_paragraph:
paragraphs.append(' '.join(current_paragraph))
current_paragraph = []
# Kalan cümleleri ekle
if current_paragraph:
paragraphs.append(' '.join(current_paragraph))
return paragraphs
def wrap_html(self, paragraphs: List[str]) -> str:
"""Paragrafları HTML'e sar"""
html_paragraphs = [f"<p>{p}</p>" for p in paragraphs]
return '\n'.join(html_paragraphs)
def escape_xml_entities(self, text: str) -> str:
"""XML entity escaping (kritik!)"""
# Önce & karakterlerini escape et (diğerlerinden önce!)
text = text.replace('&', '&amp;')
text = text.replace('<', '&lt;')
text = text.replace('>', '&gt;')
text = text.replace('"', '&quot;')
text = text.replace("'", '&apos;')
# &amp;'yi tekrar düzelt (zaten escape edilmiş olanlar için)
text = text.replace('&amp;amp;', '&amp;')
text = text.replace('&amp;lt;', '&lt;')
text = text.replace('&amp;gt;', '&gt;')
text = text.replace('&amp;quot;', '&quot;')
text = text.replace('&amp;apos;', '&apos;')
return text
def clean_transcript(self, fragments: List[Dict],
sentences_per_paragraph: int = 3) -> tuple[str, str]:
"""
Tam transcript temizleme pipeline'ı
Returns:
(raw_text, clean_html) tuple
"""
# Raw text (sadece birleştirilmiş)
raw_text = ' '.join([f['text'] for f in fragments])
# SBD ile cümleleri çıkar
sentences = self.detect_sentence_boundaries(fragments)
# Paragraflara böl
paragraphs = self.create_paragraphs(sentences, sentences_per_paragraph)
# HTML'e sar
html_content = self.wrap_html(paragraphs)
# XML entity escaping
clean_html = self.escape_xml_entities(html_content)
return raw_text, clean_html

View File

@@ -0,0 +1,69 @@
"""
YouTube transcript çıkarımı modülü
"""
from youtube_transcript_api import YouTubeTranscriptApi
from typing import List, Dict, Optional
import time
class TranscriptExtractor:
"""YouTube transcript çıkarıcı sınıfı"""
def __init__(self, rate_limit: int = 5, time_window: int = 10):
"""
Args:
rate_limit: Zaman penceresi başına maksimum istek sayısı
time_window: Zaman penceresi (saniye)
"""
self.rate_limit = rate_limit
self.time_window = time_window
self.request_times = []
def _check_rate_limit(self):
"""Rate limiting kontrolü (basit implementasyon)"""
now = time.time()
# Son time_window saniyesindeki istekleri filtrele
self.request_times = [t for t in self.request_times if now - t < self.time_window]
# Rate limit aşıldıysa bekle
if len(self.request_times) >= self.rate_limit:
sleep_time = self.time_window - (now - self.request_times[0])
if sleep_time > 0:
time.sleep(sleep_time)
# Tekrar filtrele
now = time.time()
self.request_times = [t for t in self.request_times if now - t < self.time_window]
# İstek zamanını kaydet
self.request_times.append(time.time())
def fetch_transcript(self, video_id: str,
languages: List[str] = ['en']) -> Optional[List[Dict]]:
"""
Transcript çıkar (sync)
Args:
video_id: YouTube video ID
languages: Öncelik sırasına göre dil listesi
Returns:
Transcript listesi veya None
"""
# Rate limiting kontrolü
self._check_rate_limit()
try:
# YouTube Transcript API kullanımı (yeni versiyon)
# API instance oluştur ve fetch() metodunu kullan
api = YouTubeTranscriptApi()
fetched_transcript = api.fetch(video_id, languages=languages)
# Eski formatı döndürmek için to_raw_data() kullan
# Format: [{'text': '...', 'start': 1.36, 'duration': 1.68}, ...]
transcript = fetched_transcript.to_raw_data()
return transcript
except Exception as e:
print(f"Error fetching transcript for {video_id}: {e}")
return None

105
src/video_fetcher.py Normal file
View File

@@ -0,0 +1,105 @@
"""
RSS-Bridge kullanarak video metadata çıkarımı
"""
import feedparser
import re
import requests
from urllib.parse import urlencode
from typing import List, Dict, Optional
from datetime import datetime
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...
"""
try:
response = requests.get(handle_url)
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)
# 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)
return None
except requests.exceptions.RequestException as e:
raise Exception(f"Error fetching channel page: {e}")
def extract_video_id(url: str) -> Optional[str]:
"""YouTube URL'den video ID çıkar"""
patterns = [
r'youtube\.com/watch\?v=([a-zA-Z0-9_-]{11})',
r'youtu\.be/([a-zA-Z0-9_-]{11})',
r'youtube\.com/embed/([a-zA-Z0-9_-]{11})'
]
for pattern in patterns:
match = re.search(pattern, url)
if match:
return match.group(1)
return None
def fetch_videos_from_rss_bridge(base_url: str, channel_id: str,
format: str = "Atom", max_items: int = 100) -> List[Dict]:
"""
RSS-Bridge'den video listesini çek
Args:
base_url: RSS-Bridge base URL
channel_id: YouTube Channel ID (UC...)
format: Feed format (Atom veya Rss)
max_items: Maksimum video sayısı
Returns:
Video metadata listesi
"""
params = {
'action': 'display',
'bridge': 'YoutubeBridge',
'context': 'By channel id',
'c': channel_id,
'format': format
}
feed_url = f"{base_url}/?{urlencode(params)}"
try:
feed = feedparser.parse(feed_url)
videos = []
for entry in feed.entries[:max_items]:
video_id = extract_video_id(entry.link)
if not video_id:
continue
# Tarih parsing
published_date = None
if hasattr(entry, 'published_parsed') and entry.published_parsed:
published_date = datetime(*entry.published_parsed[:6]).isoformat() + 'Z'
videos.append({
'video_id': video_id,
'video_title': entry.title,
'video_url': entry.link,
'published_at_utc': published_date,
'description': getattr(entry, 'summary', '')
})
return videos
except Exception as e:
raise Exception(f"Error fetching RSS-Bridge feed: {e}")

286
src/web_server.py Normal file
View File

@@ -0,0 +1,286 @@
"""
Flask web server - RSS-Bridge benzeri URL template sistemi
"""
from flask import Flask, request, Response, jsonify
from typing import Optional
import sys
from pathlib import Path
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
app = Flask(__name__)
# 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
Args:
channel_id: Direkt Channel ID (UC...)
channel: Channel handle (@username) veya username
channel_url: Full YouTube channel URL
Returns:
Normalize edilmiş Channel ID veya None
"""
# Direkt Channel ID varsa
if channel_id:
if channel_id.startswith('UC') and len(channel_id) == 24:
return channel_id
# Eğer URL formatında ise parse et
if 'youtube.com/channel/' in channel_id:
parts = channel_id.split('/channel/')
if len(parts) > 1:
return parts[-1].split('?')[0].split('/')[0]
# Channel handle (@username)
if channel:
if not channel.startswith('@'):
channel = f"@{channel}"
handle_url = f"https://www.youtube.com/{channel}"
return get_channel_id_from_handle(handle_url)
# Channel URL
if channel_url:
# Handle URL
if '/@' in channel_url:
return get_channel_id_from_handle(channel_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]
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()
# RSS-Bridge'den videoları çek
try:
videos = fetch_videos_from_rss_bridge(
base_url="https://rss-bridge.org/bridge01",
channel_id=channel_id,
format="Atom",
max_items=max_items
)
except Exception as e:
raise Exception(f"RSS-Bridge hatası: {e}")
# Yeni videoları veritabanına ekle
for video in videos:
video['channel_id'] = channel_id
if not db.is_video_processed(video['video_id']):
db.add_video(video)
# Bekleyen videoları işle (ilk 20)
pending_videos = db.get_pending_videos()[:20]
for video in pending_videos:
if video['channel_id'] != channel_id:
continue
try:
# Transcript çıkar
transcript = extractor.fetch_transcript(
video['video_id'],
languages=['tr', 'en']
)
if transcript:
# Transcript temizle
raw, clean = cleaner.clean_transcript(transcript, sentences_per_paragraph=3)
# Veritabanına kaydet
db.update_video_transcript(
video['video_id'],
raw,
clean,
status=1,
language='tr'
)
except Exception as e:
print(f"Transcript çıkarım hatası {video['video_id']}: {e}")
db.mark_video_failed(video['video_id'], str(e))
# İşlenmiş videoları getir
processed_videos = db.get_processed_videos(
limit=max_items,
channel_id=channel_id
)
return {
'videos': processed_videos,
'channel_id': channel_id,
'count': len(processed_videos)
}
@app.route('/', methods=['GET'])
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
"""
# Query parametrelerini al
channel_id = request.args.get('channel_id')
channel = request.args.get('channel') # @username veya username
channel_url = request.args.get('channel_url')
format_type = request.args.get('format', 'Atom').lower() # Atom veya Rss
max_items = int(request.args.get('max_items', 50))
# Channel ID'yi normalize et
normalized_channel_id = normalize_channel_id(
channel_id=channel_id,
channel=channel,
channel_url=channel_url
)
if not normalized_channel_id:
return jsonify({
'error': 'Channel ID bulunamadı',
'usage': {
'channel_id': 'UC... (YouTube Channel ID)',
'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 video sayısı (varsayılan: 50)'
}
}), 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': 'Lütfen birkaç dakika sonra tekrar deneyin'
}), 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
if format_type == 'rss':
rss_content = generator.generate_rss_string()
return Response(
rss_content,
mimetype='application/rss+xml',
headers={'Content-Type': 'application/rss+xml; charset=utf-8'}
)
else: # Atom
# Feedgen Atom desteği
atom_content = generator.generate_atom_string()
return Response(
atom_content,
mimetype='application/atom+xml',
headers={'Content-Type': 'application/atom+xml; charset=utf-8'}
)
except Exception as e:
return jsonify({
'error': str(e),
'channel_id': normalized_channel_id
}), 500
@app.route('/health', methods=['GET'])
def health():
"""Health check endpoint"""
return jsonify({'status': 'ok', 'service': 'YouTube Transcript RSS Feed'})
@app.route('/info', methods=['GET'])
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': 'Maksimum video sayısı (varsayılan: 50)'
},
'examples': [
'/?channel_id=UC9h8BDcXwkhZtnqoQJ7PggA&format=Atom',
'/?channel=@tavakfi&format=Rss',
'/?channel_url=https://www.youtube.com/@tavakfi&format=Atom&max_items=100'
]
})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)

1
study.md Normal file

File diff suppressed because one or more lines are too long