first commit

This commit is contained in:
2026-02-28 02:44:41 +03:00
commit 97fb289fe7
70 changed files with 11928 additions and 0 deletions

256
doc/api.md Normal file
View File

@@ -0,0 +1,256 @@
# Netflix Scraper API - API Dokümantasyonu
## Base URL
```
Development: http://localhost:3000
Production: https://api.yourdomain.com
```
## Authentication
Tüm API istekleri `X-API-Key` header'ı gerektirir.
```http
X-API-Key: your-api-key-here
```
### API Key Tipleri
| Key | Kullanım |
|-----|----------|
| `API_KEY_WEB` | Web frontend |
| `API_KEY_MOBILE` | Mobil uygulama |
| `API_KEY_ADMIN` | Admin panel |
---
## Endpoints
### POST /api/getinfo
Netflix URL'sinden içerik bilgisi getirir.
**Request**
```http
POST /api/getinfo
Content-Type: application/json
X-API-Key: your-api-key
{
"url": "https://www.netflix.com/tr/title/81616256"
}
```
**Response (Success)**
```json
{
"success": true,
"data": {
"title": "Hayata Röveşata Çeken Adam",
"year": 2022,
"plot": "Dünyaya karşı duyduğu öfke ve yaşadığı kederin katılaştırdığı huysuz bir emekli, yaşamına son vermeyi planlar. Ancak hayatına neşeli bir genç aile girince tüm planları suya düşer.",
"genres": ["18+", "Komedi"],
"cast": ["Tom Hanks", "Mariana Treviño", "Rachel Keller"],
"backdrop": "https://occ-0-7335-3467.1.nflxso.net/dnm/api/v6/..."
}
}
```
**Response (Error)**
```json
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request parameters",
"details": {
"errors": [
{
"field": "url",
"message": "URL must be a valid Netflix title URL"
}
]
}
}
}
```
**Status Codes**
| Code | Açıklama |
|------|----------|
| 200 | Başarılı |
| 400 | Geçersiz istek |
| 401 | API key eksik |
| 403 | Geçersiz API key |
| 429 | Rate limit aşıldı |
| 500 | Sunucu hatası |
---
### POST /api/getinfo/async
Asenkron scraping job'u oluşturur. Büyük ölçekli kullanım için uygundur.
**Request**
```http
POST /api/getinfo/async
Content-Type: application/json
X-API-Key: your-api-key
{
"url": "https://www.netflix.com/tr/title/81616256"
}
```
**Response**
```json
{
"success": true,
"data": {
"jobId": "550e8400-e29b-41d4-a716-446655440000",
"status": "pending"
}
}
```
**Socket ile İzleme**
Job durumunu Socket.IO ile izleyebilirsiniz:
```javascript
socket.emit('job:subscribe', jobId);
socket.on('job:progress', (data) => console.log(data));
socket.on('job:completed', (data) => console.log(data));
socket.on('job:error', (data) => console.error(data));
```
---
### GET /api/jobs/:jobId
Job durumunu sorgular.
**Request**
```http
GET /api/jobs/550e8400-e29b-41d4-a716-446655440000
X-API-Key: your-api-key
```
**Response**
```json
{
"success": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"url": "https://www.netflix.com/tr/title/81616256",
"status": "completed",
"progress": 100,
"step": "completed",
"result": {
"title": "Hayata Röveşata Çeken Adam",
"year": 2022,
"plot": "...",
"genres": ["18+", "Komedi"],
"cast": ["Tom Hanks", "Mariana Treviño", "Rachel Keller"],
"backdrop": "https://..."
},
"createdAt": "2025-02-27T10:00:00.000Z",
"updatedAt": "2025-02-27T10:00:05.000Z"
}
}
```
---
### GET /health
Basit sağlık kontrolü.
**Response**
```json
{
"status": "ok",
"timestamp": "2025-02-27T10:00:00.000Z",
"uptime": 3600
}
```
---
### GET /ready
Tüm bağımlılıkların hazır olup olmadığını kontrol eder.
**Response**
```json
{
"status": "ready",
"timestamp": "2025-02-27T10:00:00.000Z",
"checks": {
"database": "healthy",
"redis": "healthy"
},
"env": "production"
}
```
---
## Error Codes
| Code | Açıklama |
|------|----------|
| `MISSING_API_KEY` | API key header'ı eksik |
| `INVALID_API_KEY` | Geçersiz API key |
| `VALIDATION_ERROR` | İstek parametreleri geçersiz |
| `RATE_LIMIT_EXCEEDED` | Genel rate limit aşıldı |
| `SCRAPE_RATE_LIMIT_EXCEEDED` | Scraping rate limit aşıldı |
| `SCRAPE_ERROR` | Netflix'ten veri çekilemedi |
| `JOB_NOT_FOUND` | Job bulunamadı |
| `INTERNAL_ERROR` | Beklenmeyen sunucu hatası |
---
## Rate Limiting
### Genel Rate Limit
- **Window**: 1 dakika
- **Max İstek**: 30 istek/dakika/IP+APIKey
### Scraping Rate Limit
- **Window**: 1 dakika
- **Max İstek**: 10 istek/dakika/IP+APIKey
Rate limit değerleri `.env` dosyasından yapılandırılabilir.
---
## Request/Response Formatları
### Content Data (GetInfoResponse)
| Alan | Tip | Açıklama |
|------|-----|----------|
| `title` | string | İçerik başlığı |
| `year` | number \| null | Yayın yılı |
| `plot` | string \| null | Açıklama/özet |
| `genres` | string[] | Tür listesi |
| `cast` | string[] | Oyuncu listesi |
| `backdrop` | string \| null | Arka plan görseli URL |
---
## OpenAPI / Swagger
OpenAPI spesifikasyonu için: `/api/docs` (yakında)

View File

@@ -0,0 +1,53 @@
# ADR-001: Cheerio Scraping Kütüphanesi Seçimi
## Durum
Kabul edildi
## Bağlam
Netflix içerik sayfalarından HTML parsing ile veri çekmemiz gerekiyor. İki ana seçenek var:
1. **Cheerio**: Lightweight HTML parser
2. **Playwright/Puppeteer**: Headless browser automation
## Karar
**Cheerio** seçildi.
## Gerekçe
### Cheerio Avantajları
- Hafif ve hızlı
- Düşük kaynak kullanımı
- Basit API
- Daha az bağımlılık
### Playwright Avantajları
- JavaScript rendering desteği
- Daha güçlü scraping
- Dinamik içerik desteği
### Seçim Nedeni
1. Netflix sayfalarının HTML'inde temel veriler mevcut
2. Client-side rendering gerektiren kritik veri yok
3. Performans öncelikli
4. Başlangıç için Cheerio yeterli
## Sonuçlar
### Olumlu
- Düşük kaynak kullanımı
- Hızlı yanıt süresi
- Basit bakım
### Olumsuz
- JavaScript rendering gerektiren sayfalar için çalışmayabilir
- Netflix client-side rendering'e geçerse güncelleme gerekir
### Alternatif Plan
Eğer Cheerio yetersiz kalırsa Playwright'a geçiş yapılabilir. Altyapı buna uygun hazır.
## Tarih
2025-02-27

View File

@@ -0,0 +1,61 @@
# ADR-002: Hybrid Cache Stratejisi
## Durum
Kabul edildi
## Bağlam
Netflix'ten scraped verileri nasıl saklayacağımıza karar vermemiz gerekiyor. Seçenekler:
1. **Sadece Cache**: Redis'te TTL ile sakla
2. **Sadece Database**: PostgreSQL'de kalıcı sakla
3. **Hybrid**: Cache + Database birlikte
## Karar
**Hybrid Cache Stratejisi** (Cache → DB → Netflix) seçildi.
## Akış
```
İstek → Redis Cache → Varsa dön
→ Yoksa → PostgreSQL → Varsa dön, Cache'e yaz
→ Yoksa → Netflix'ten çek → DB'ye yaz → Cache'e yaz → Dön
```
## Gerekçe
### Sadece Cache Eksikleri
- TTL dolduğunda veri kaybı
- Yeniden scraping maliyeti
### Sadece Database Eksikleri
- Her istekte DB sorgusu
- Daha yavaş yanıt
### Hybrid Avantajları
1. **Hız**: Cache hit durumunda anlık yanıt
2. **Kalıcılık**: Veriler DB'de saklanır
3. **Verimlilik**: Netflix'e gereksiz istek atılmaz
4. **TTL Esnekliği**: Cache süresi ayarlanabilir
## Sonuçlar
### Olumlu
- En hızlı yanıt süresi (cache hit)
- Veri kalıcılığı
- Netflix üzerinde minimal yük
### Olumsuz
- İki sistem senkronizasyonu
- Daha fazla kod karmaşıklığı
### Yapılandırma
```env
REDIS_TTL_SECONDS=604800 # 7 gün
```
## Tarih
2025-02-27

View File

@@ -0,0 +1,63 @@
# ADR-003: Named API Keys Stratejisi
## Durum
Kabul edildi
## Bağlam
API güvenliği için authentication yöntemi belirlememiz gerekiyor. Birden fazla frontend olacak (Web, Mobile, Admin).
## Seçenekler
1. **Tek API Key**: Tüm frontend'ler aynı key'i kullanır
2. **Named API Keys**: Her frontend için ayrı key
3. **Database API Keys**: Key'ler DB'de tutulur, yönetim paneli ile
## Karar
**Named API Keys** (.env'de tanımlı) seçildi.
## Yapılandırma
```env
API_KEY_WEB=web-frontend-key-xxx
API_KEY_MOBILE=mobile-app-key-yyy
API_KEY_ADMIN=admin-key-zzz
```
## Gerekçe
### Tek Key Eksikleri
- Hangi frontend'in istek attığı belli değil
- Tek bir frontend engellenemez
- Audit trail yok
### Database Key Eksikleri
- Ekstra kompleksite
- Yönetim paneli gerektirir
- Başlangıç için overkill
### Named Keys Avantajları
1. **İzlenebilirlik**: Hangi frontend'in istek attığı bilinir
2. **Kontrol**: Tek bir frontend'in erişimi kapatılabilir
3. **Basitlik**: .env ile yönetim
4. **Güvenlik**: Her frontend için ayrı secret
## Sonuçlar
### Olumlu
- Frontend bazlı rate limiting
- Kolay key rotasyonu
- Basit yapılandırma
### Olumsuz
- Yeni frontend için .env güncellemesi gerekir
- Key yönetimi manuel
### Gelecek
Gerekirse Database API Keys sistemine geçiş yapılabilir.
## Tarih
2025-02-27

291
doc/ops.md Normal file
View File

@@ -0,0 +1,291 @@
# Netflix Scraper API - Operasyon Dokümantasyonu
## Hızlı Başlangıç
### Gereksinimler
- Docker & Docker Compose
- Node.js 20+ (local development için)
### Tek Komut ile Başlatma (Development)
```bash
# .env dosyasını oluştur
cp .env.example .env
# .env dosyasını düzenle
nano .env
# Başlat
docker compose -f docker-compose.dev.yml up --build
```
**Hepsi bu kadar!** Uygulama şu adreste çalışacak: `http://localhost:3000`
---
## Ortamlar
### Development
```bash
docker compose -f docker-compose.dev.yml up --build
```
Özellikler:
- Hot reload aktif
- Tüm loglar görünür
- Debug modu
### Production
```bash
docker compose up --build -d
```
Özellikler:
- Multi-stage build
- Non-root user
- Minimal image
- Production optimizations
---
## Environment Değişkenleri
### .env Dosyası
```env
# === Server ===
PORT=3000
NODE_ENV=development
# === PostgreSQL ===
POSTGRES_HOST=postgres
POSTGRES_PORT=5432
POSTGRES_USER=postgres
POSTGRES_PASSWORD=your-secure-password
POSTGRES_DB=netflix_scraper
# === Redis ===
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_TTL_SECONDS=604800
# === Rate Limiting ===
RATE_LIMIT_WINDOW_MS=60000
RATE_LIMIT_MAX_REQUESTS=30
# === API Keys ===
API_KEY_WEB=web-key-change-me
API_KEY_MOBILE=mobile-key-change-me
API_KEY_ADMIN=admin-key-secret
```
### Değişken Açıklamaları
| Değişken | Açıklama | Varsayılan |
|----------|----------|------------|
| `PORT` | Sunucu portu | 3000 |
| `NODE_ENV` | Ortam (development/production) | development |
| `REDIS_TTL_SECONDS` | Cache süresi (saniye) | 604800 (7 gün) |
| `RATE_LIMIT_WINDOW_MS` | Rate limit penceresi (ms) | 60000 (1 dk) |
| `RATE_LIMIT_MAX_REQUESTS` | Max istek sayısı | 30 |
---
## Migration
Migration'lar otomatik olarak container başlatılırken çalışır. Manuel çalıştırmak için:
```bash
# Container içinde
docker compose exec app npx prisma migrate deploy
# Local
npx prisma migrate deploy
```
### Yeni Migration Oluşturma
```bash
npx prisma migrate dev --name description_of_change
```
---
## Loglama
Tüm loglar JSON formatında structured logging ile yazılır.
### Log Seviyeleri
| Seviye | Açıklama |
|--------|----------|
| `debug` | Detaylı debug bilgisi |
| `info` | Genel bilgi |
| `warn` | Uyarı |
| `error` | Hata |
### Log Formatı
```json
{
"timestamp": "2025-02-27T10:00:00.000Z",
"level": "info",
"message": "Server started",
"service": "netflix-scraper-api",
"port": 3000
}
```
### Log Seviyesi Ayarlama
```env
LOG_LEVEL=debug
```
---
## Health Check
### Endpoints
- `GET /health` - Basit sağlık kontrolü
- `GET /ready` - Bağımlılık kontrolü (DB + Redis)
### Docker Health Check
Container'lar otomatik health check ile izlenir:
```yaml
healthcheck:
test: ["CMD", "wget", "--spider", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
```
---
## Troubleshooting
### Database Bağlantı Hatası
```
Error: Can't reach database server
```
**Çözüm:**
1. PostgreSQL container'ın çalıştığını kontrol et
2. `POSTGRES_HOST` değerini kontrol et (docker'da `postgres` olmalı)
3. Health check'i bekle: `docker compose logs postgres`
### Redis Bağlantı Hatası
```
Error: Redis connection failed
```
**Çözüm:**
1. Redis container'ın çalıştığını kontrol et
2. `REDIS_HOST` değerini kontrol et
### Migration Hatası
```
Error: P3005: The database schema is not empty
```
**Çözüm:**
```bash
# Veritabanını sıfırla
docker compose down -v
docker compose up --build
```
### Port Kullanımda Hatası
```
Error: port is already allocated
```
**Çözüm:**
```bash
# Hangi process kullanıyor
lsof -i :3000
# Process'i durdur
kill -9 <PID>
```
---
## Yararlı Komutlar
### Container Yönetimi
```bash
# Container'ları durdur
docker compose down
# Container'ları sil (volumes dahil)
docker compose down -v
# Logları görüntüle
docker compose logs -f app
# Container içine gir
docker compose exec app sh
```
### Database İşlemleri
```bash
# Prisma studio aç
docker compose exec app npx prisma studio
# Database schema görüntüle
docker compose exec app npx prisma db pull
# Seed çalıştır
docker compose exec app npm run prisma:seed
```
### Redis İşlemleri
```bash
# Redis CLI
docker compose exec redis redis-cli
# Tüm cache'i temizle
docker compose exec redis redis-cli FLUSHALL
```
---
## Backup & Restore
### PostgreSQL Backup
```bash
docker compose exec postgres pg_dump -U postgres netflix_scraper > backup.sql
```
### PostgreSQL Restore
```bash
cat backup.sql | docker compose exec -T postgres psql -U postgres netflix_scraper
```
---
## Monitoring (Opsiyonel)
### Metrics Endpoint (Yakında)
```
GET /metrics
```
Prometheus uyumlu metrics döndürür.

82
doc/overview.md Normal file
View File

@@ -0,0 +1,82 @@
# Netflix Scraper API - Proje Özeti
## Proje Hakkında
Netflix Scraper API, Netflix içerik sayfalarından film/dizi bilgilerini çeken bir backend servisidir. URL gönderilerek içerik bilgileri çekilir, önbelleğe alınır ve geri döndürülür.
## Neden Oluşturuldu?
- **Otomatik İçerik Toplama**: Netflix URL'lerinden otomatik olarak içerik bilgisi çekmek
- **Performans**: Redis cache ile tekrarlayan isteklerde hızlı yanıt
- **Kalıcılık**: PostgreSQL ile verilerin kalıcı olarak saklanması
- **Gerçek Zamanlı**: Socket.IO ile canlı ilerleme bildirimleri
## Teknoloji Yığını
| Katman | Teknoloji |
|--------|-----------|
| Runtime | Node.js 20+ |
| Framework | Express.js |
| Database | PostgreSQL 16 |
| Cache | Redis 7 |
| Real-time | Socket.IO |
| Scraper | Cheerio |
| ORM | Prisma |
## Mimari
```
┌─────────────────┐
│ Frontend │
│ (Web/Mobile) │
└────────┬────────┘
│ HTTP + API Key
┌─────────────────┐ ┌─────────────┐
│ Express API │────▶│ Redis │
│ (Port 3000) │ │ (Cache) │
└────────┬────────┘ └─────────────┘
┌─────────────────┐ ┌─────────────┐
│ Scraper Service │────▶│ PostgreSQL │
│ (Cheerio) │ │ (Data) │
└─────────────────┘ └─────────────┘
```
## Veri Akışı
1. **İstek Alınır**: `POST /api/getinfo` endpoint'ine URL gönderilir
2. **Cache Kontrolü**: Redis'te veri var mı diye bakılır
3. **DB Kontrolü**: PostgreSQL'de veri var mı diye bakılır
4. **Scraping**: Netflix'ten veri çekilir (cache/DB'de yoksa)
5. **Kaydetme**: Veri DB'ye ve cache'e yazılır
6. **Yanıt**: JSON formatında veri döndürülür
## Önemli Kararlar
### ADR-001: Cheerio Seçimi
Playwright yerine Cheerio seçildi. Başlangıç için yeterli, daha hafif ve hızlı. Gerekirse Playwright'a geçiş yapılabilir.
### ADR-002: Hybrid Cache Stratejisi
Cache → DB → Netflix sıralaması ile veri alınır. Bu sayede:
- En hızlı yanıt cache'ten gelir
- Cache'te yoksa DB'den gelir
- İlk istek hariç Netflix'e istek atılmaz
### ADR-003: Named API Keys
Her frontend için ayrı API key kullanılır. Bu sayede:
- Hangi frontend'in istek attığı takip edilebilir
- Gerekirse tek bir frontend'in erişimi kapatılabilir
## Güncellemeler
| Tarih | Güncelleme |
|-------|-----------|
| 2025-02-27 | Proje oluşturuldu |
## İlgili Dokümanlar
- [API Dokümantasyonu](./api.md)
- [Operasyon Notları](./ops.md)
- [Socket Events](./socket-events.md)

256
doc/socket-events.md Normal file
View File

@@ -0,0 +1,256 @@
# Netflix Scraper API - Socket.IO Events
## Bağlantı
### URL
```
ws://localhost:3000
```
### Transport
- WebSocket
- Polling (fallback)
---
## Client → Server Events
### `job:subscribe`
Bir job'ın güncellemelerine abone ol.
**Payload:**
```typescript
jobId: string
```
**Örnek:**
```javascript
socket.emit('job:subscribe', '550e8400-e29b-41d4-a716-446655440000');
```
---
### `job:unsubscribe`
Job aboneliğini iptal et.
**Payload:**
```typescript
jobId: string
```
**Örnek:**
```javascript
socket.emit('job:unsubscribe', '550e8400-e29b-41d4-a716-446655440000');
```
---
## Server → Client Events
### `job:progress`
Job ilerleme durumu güncellendiğinde gönderilir.
**Payload:**
```typescript
{
jobId: string;
progress: number; // 0-100 arası
status: string; // "pending" | "processing" | "completed" | "failed"
step: string; // Mevcut adım açıklaması
}
```
**Örnek:**
```json
{
"jobId": "550e8400-e29b-41d4-a716-446655440000",
"progress": 50,
"status": "processing",
"step": "Scraping Netflix"
}
```
**Adımlar:**
| Step | Progress | Açıklama |
|------|----------|----------|
| `created` | 0 | Job oluşturuldu |
| `checking_cache` | 10 | Cache kontrol ediliyor |
| `checking_database` | 30 | Database kontrol ediliyor |
| `scraping_netflix` | 50 | Netflix'ten veri çekiliyor |
| `saving_to_database` | 80 | Veritabanına kaydediliyor |
| `completed` | 100 | İşlem tamamlandı |
---
### `job:completed`
Job başarıyla tamamlandığında gönderilir.
**Payload:**
```typescript
{
jobId: string;
data: GetInfoResponse;
source: "cache" | "database" | "netflix";
}
```
**Örnek:**
```json
{
"jobId": "550e8400-e29b-41d4-a716-446655440000",
"data": {
"title": "Hayata Röveşata Çeken Adam",
"year": 2022,
"plot": "...",
"genres": ["18+", "Komedi"],
"cast": ["Tom Hanks", "Mariana Treviño", "Rachel Keller"],
"backdrop": "https://..."
},
"source": "netflix"
}
```
---
### `job:error`
Job sırasında hata oluştuğunda gönderilir.
**Payload:**
```typescript
{
jobId: string;
error: {
code: string;
message: string;
}
}
```
**Örnek:**
```json
{
"jobId": "550e8400-e29b-41d4-a716-446655440000",
"error": {
"code": "SCRAPE_ERROR",
"message": "Failed to fetch Netflix page: 403"
}
}
```
---
## Kullanım Örneği
### JavaScript (Browser)
```javascript
import { io } from 'socket.io-client';
// Bağlan
const socket = io('http://localhost:3000');
// Bağlantı başarılı
socket.on('connect', () => {
console.log('Connected:', socket.id);
});
// Job oluştur (API üzerinden)
const response = await fetch('http://localhost:3000/api/getinfo/async', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'your-api-key'
},
body: JSON.stringify({
url: 'https://www.netflix.com/tr/title/81616256'
})
});
const { data: { jobId } } = await response.json();
// Job'a abone ol
socket.emit('job:subscribe', jobId);
// İlerleme dinle
socket.on('job:progress', (data) => {
console.log(`Progress: ${data.progress}% - ${data.step}`);
});
// Tamamlanma dinle
socket.on('job:completed', (data) => {
console.log('Completed:', data.data);
socket.emit('job:unsubscribe', jobId);
});
// Hata dinle
socket.on('job:error', (data) => {
console.error('Error:', data.error);
});
```
### React Hook
```typescript
import { useEffect, useState } from 'react';
import { io, Socket } from 'socket.io-client';
interface JobProgress {
jobId: string;
progress: number;
status: string;
step: string;
}
export function useJobProgress(jobId: string | null) {
const [progress, setProgress] = useState<JobProgress | null>(null);
const [data, setData] = useState<any>(null);
const [error, setError] = useState<any>(null);
useEffect(() => {
if (!jobId) return;
const socket = io('http://localhost:3000');
socket.emit('job:subscribe', jobId);
socket.on('job:progress', setProgress);
socket.on('job:completed', (result) => {
setData(result.data);
socket.emit('job:unsubscribe', jobId);
});
socket.on('job:error', (err) => {
setError(err.error);
socket.emit('job:unsubscribe', jobId);
});
return () => {
socket.emit('job:unsubscribe', jobId);
socket.disconnect();
};
}, [jobId]);
return { progress, data, error };
}
```
---
## CORS Yapılandırması
Production'da CORS ayarlarını yapılandırın:
```typescript
// src/config/socket.ts
io = new Server(httpServer, {
cors: {
origin: ['https://yourdomain.com', 'https://admin.yourdomain.com'],
methods: ['GET', 'POST'],
credentials: true,
},
});
```