From 0efbe9683df3f1860e4b57247376bcae5af70f5b Mon Sep 17 00:00:00 2001 From: ewgsta <159681870+ewgsta@users.noreply.github.com> Date: Sun, 29 Mar 2026 11:35:13 +0300 Subject: [PATCH 1/4] feat(serve): add RESTful API server mode --- Dockerfile.restful | 40 +++ README.md | 32 ++- docker-compose.restful.yml | 23 ++ docs/cli/restful-api.md | 433 +++++++++++++++++++++++++++++ docs/cli/restful-api.tr.md | 433 +++++++++++++++++++++++++++++ md/tr/README.md | 32 +++ pyproject.toml | 1 + tests/test_restful_api.py | 227 +++++++++++++++ weeb_cli/commands/serve.py | 9 +- weeb_cli/commands/serve_restful.py | 373 +++++++++++++++++++++++++ 10 files changed, 1601 insertions(+), 2 deletions(-) create mode 100644 Dockerfile.restful create mode 100644 docker-compose.restful.yml create mode 100644 docs/cli/restful-api.md create mode 100644 docs/cli/restful-api.tr.md create mode 100644 tests/test_restful_api.py create mode 100644 weeb_cli/commands/serve_restful.py diff --git a/Dockerfile.restful b/Dockerfile.restful new file mode 100644 index 0000000..6b1ca4e --- /dev/null +++ b/Dockerfile.restful @@ -0,0 +1,40 @@ +FROM python:3.12-slim + +LABEL maintainer="ewgsta " +LABEL description="Weeb CLI RESTful API Server" + +# Install system dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc \ + && rm -rf /var/lib/apt/lists/* + +# Create app directory +WORKDIR /app + +# Copy project files +COPY pyproject.toml README.md ./ +COPY weeb_cli ./weeb_cli + +# Install weeb-cli with serve-restful dependencies +RUN pip install --no-cache-dir -e ".[serve-restful]" + +# Create non-root user +RUN useradd -m -u 1000 weeb && chown -R weeb:weeb /app +USER weeb + +# Expose default port +EXPOSE 8080 + +# Environment variables +ENV RESTFUL_PORT=8080 +ENV RESTFUL_HOST=0.0.0.0 +ENV RESTFUL_PROVIDERS=animecix,hianime,aniworld,docchi +ENV RESTFUL_CORS=true +ENV RESTFUL_DEBUG=false + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD python -c "import requests; requests.get('http://localhost:8080/health', timeout=5)" + +# Run the server +CMD ["weeb-cli", "serve", "restful"] diff --git a/README.md b/README.md index 2cc2ebf..82ee4fc 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,7 @@ - Automatic update checks - Non-interactive JSON API for scripts and AI agents - Torznab server mode for Sonarr/*arr integration +- RESTful API server for web/mobile applications --- @@ -152,7 +153,34 @@ weeb-cli serve --port 9876 \ Then add `http://weeb-cli-host:9876` as a Torznab indexer in Sonarr with category 5070 (TV/Anime). The server includes a blackhole download worker that automatically processes grabbed episodes. -#### Docker +### RESTful API Server + +For web/mobile applications and custom integrations, weeb-cli provides a RESTful API server: + +```bash +pip install weeb-cli[serve-restful] + +weeb-cli serve restful --port 8080 \ + --providers animecix,hianime,aniworld,docchi \ + --cors +``` + +**API Endpoints:** +- `GET /health` - Health check +- `GET /api/providers` - List available providers +- `GET /api/search?q=naruto&provider=animecix` - Search anime +- `GET /api/anime/{id}?provider=animecix` - Get anime details +- `GET /api/anime/{id}/episodes?season=1` - List episodes +- `GET /api/anime/{id}/episodes/{ep_id}/streams` - Get stream URLs + +**Docker Support:** +```bash +docker-compose -f docker-compose.restful.yml up -d +``` + +See [RESTful API Documentation](https://ewgsta.github.io/weeb-cli/cli/restful-api/) for full details. + +#### Docker (Torznab) ```dockerfile FROM python:3.13-slim @@ -249,6 +277,7 @@ All settings can be modified through the interactive Settings menu. - [x] Keyboard shortcuts - [x] Non-interactive API mode (JSON output) - [x] Torznab server for Sonarr/*arr integration +- [x] RESTful API server for web/mobile apps ### Planned @@ -272,6 +301,7 @@ weeb-cli/ │ │ ├── downloads.py # Download management commands │ │ ├── search.py # Anime search functionality │ │ ├── serve.py # Torznab server for *arr integration +│ │ ├── serve_restful.py # RESTful API server │ │ ├── settings.py # Settings menu and configuration │ │ ├── setup.py # Initial setup wizard │ │ └── watchlist.py # Watch history and progress diff --git a/docker-compose.restful.yml b/docker-compose.restful.yml new file mode 100644 index 0000000..de936b5 --- /dev/null +++ b/docker-compose.restful.yml @@ -0,0 +1,23 @@ +version: '3.8' + +services: + weeb-cli-restful: + build: + context: . + dockerfile: Dockerfile.restful + container_name: weeb-cli-restful + ports: + - "8080:8080" + environment: + - RESTFUL_PORT=8080 + - RESTFUL_HOST=0.0.0.0 + - RESTFUL_PROVIDERS=animecix,hianime,aniworld,docchi + - RESTFUL_CORS=true + - RESTFUL_DEBUG=false + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import requests; requests.get('http://localhost:8080/health', timeout=5)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s diff --git a/docs/cli/restful-api.md b/docs/cli/restful-api.md new file mode 100644 index 0000000..8267751 --- /dev/null +++ b/docs/cli/restful-api.md @@ -0,0 +1,433 @@ +# RESTful API Mode + +Weeb CLI can run as a RESTful API server, providing HTTP endpoints for all provider operations including search, episode listing, stream extraction, and anime details. + +## Installation + +Install with RESTful API dependencies: + +```bash +pip install weeb-cli[serve-restful] +``` + +## Usage + +### Basic Usage + +Start the server with default settings: + +```bash +weeb-cli serve restful +``` + +The server will start on `http://0.0.0.0:8080` with all available providers. + +### Custom Configuration + +```bash +weeb-cli serve restful \ + --port 9000 \ + --host 127.0.0.1 \ + --providers animecix,hianime \ + --no-cors \ + --debug +``` + +### Command Options + +| Option | Environment Variable | Default | Description | +|--------|---------------------|---------|-------------| +| `--port` | `RESTFUL_PORT` | `8080` | HTTP port to bind | +| `--host` | `RESTFUL_HOST` | `0.0.0.0` | Host address to bind | +| `--providers` | `RESTFUL_PROVIDERS` | `animecix,hianime,aniworld,docchi` | Comma-separated provider names | +| `--cors/--no-cors` | `RESTFUL_CORS` | `true` | Enable/disable CORS | +| `--debug` | `RESTFUL_DEBUG` | `false` | Enable debug mode | + +## API Endpoints + +### Health Check + +Check if the server is running: + +```http +GET /health +``` + +**Response:** +```json +{ + "status": "ok", + "service": "weeb-cli-restful", + "providers": ["animecix", "hianime", "aniworld", "docchi"] +} +``` + +### List Providers + +Get all available providers: + +```http +GET /api/providers +``` + +**Response:** +```json +{ + "success": true, + "providers": [ + { + "name": "animecix", + "lang": "tr", + "region": "TR", + "class": "AnimecixProvider" + } + ], + "loaded": ["animecix", "hianime"] +} +``` + +### Search Anime + +Search for anime across providers: + +```http +GET /api/search?q=naruto&provider=animecix +``` + +**Query Parameters:** +- `q` (required): Search query +- `provider` (optional): Provider name (defaults to first loaded) + +**Response:** +```json +{ + "success": true, + "provider": "animecix", + "query": "naruto", + "count": 10, + "results": [ + { + "id": "12345", + "title": "Naruto", + "type": "series", + "cover": "https://example.com/cover.jpg", + "year": 2002 + } + ] +} +``` + +### Get Anime Details + +Get detailed information about an anime: + +```http +GET /api/anime/{anime_id}?provider=animecix +``` + +**Query Parameters:** +- `provider` (optional): Provider name (defaults to first loaded) + +**Response:** +```json +{ + "success": true, + "provider": "animecix", + "anime": { + "id": "12345", + "title": "Naruto", + "type": "series", + "cover": "https://example.com/cover.jpg", + "year": 2002, + "description": "Anime description...", + "genres": ["Action", "Adventure"], + "status": "completed", + "episodes": [...] + } +} +``` + +### Get Episodes + +List all episodes for an anime: + +```http +GET /api/anime/{anime_id}/episodes?provider=animecix&season=1 +``` + +**Query Parameters:** +- `provider` (optional): Provider name (defaults to first loaded) +- `season` (optional): Filter by season number + +**Response:** +```json +{ + "success": true, + "provider": "animecix", + "anime_id": "12345", + "count": 220, + "episodes": [ + { + "id": "ep-1", + "number": 1, + "title": "Enter: Naruto Uzumaki!", + "season": 1, + "url": "https://example.com/episode/1" + } + ] +} +``` + +### Get Streams + +Get stream URLs for an episode: + +```http +GET /api/anime/{anime_id}/episodes/{episode_id}/streams?provider=animecix&sort=desc +``` + +**Query Parameters:** +- `provider` (optional): Provider name (defaults to first loaded) +- `sort` (optional): Sort by quality (`asc` or `desc`, defaults to `desc`) + +**Response:** +```json +{ + "success": true, + "provider": "animecix", + "anime_id": "12345", + "episode_id": "ep-1", + "count": 3, + "streams": [ + { + "url": "https://example.com/stream.m3u8", + "quality": "1080p", + "server": "default", + "headers": { + "Referer": "https://example.com" + }, + "subtitles": null + } + ] +} +``` + +## Docker Deployment + +### Using Docker Compose + +```bash +docker-compose -f docker-compose.restful.yml up -d +``` + +### Using Dockerfile + +Build the image: + +```bash +docker build -f Dockerfile.restful -t weeb-cli-restful . +``` + +Run the container: + +```bash +docker run -d \ + --name weeb-cli-restful \ + -p 8080:8080 \ + -e RESTFUL_PROVIDERS=animecix,hianime \ + weeb-cli-restful +``` + +### Environment Variables + +All command options can be configured via environment variables: + +```bash +docker run -d \ + --name weeb-cli-restful \ + -p 9000:9000 \ + -e RESTFUL_PORT=9000 \ + -e RESTFUL_HOST=0.0.0.0 \ + -e RESTFUL_PROVIDERS=animecix,hianime,aniworld \ + -e RESTFUL_CORS=true \ + -e RESTFUL_DEBUG=false \ + weeb-cli-restful +``` + +## Error Handling + +All endpoints return consistent error responses: + +```json +{ + "success": false, + "error": "Error message description" +} +``` + +**Common HTTP Status Codes:** +- `200`: Success +- `400`: Bad request (missing/invalid parameters) +- `404`: Resource not found +- `500`: Internal server error + +## CORS Support + +CORS is enabled by default, allowing requests from any origin. To disable: + +```bash +weeb-cli serve restful --no-cors +``` + +Or via environment variable: + +```bash +export RESTFUL_CORS=false +``` + +## Example Usage + +### cURL + +```bash +# Search anime +curl "http://localhost:8080/api/search?q=naruto&provider=animecix" + +# Get episodes +curl "http://localhost:8080/api/anime/12345/episodes?season=1" + +# Get streams +curl "http://localhost:8080/api/anime/12345/episodes/ep-1/streams?sort=desc" +``` + +### Python + +```python +import requests + +# Search anime +response = requests.get( + "http://localhost:8080/api/search", + params={"q": "naruto", "provider": "animecix"} +) +results = response.json() + +# Get streams +response = requests.get( + f"http://localhost:8080/api/anime/{anime_id}/episodes/{episode_id}/streams", + params={"provider": "animecix", "sort": "desc"} +) +streams = response.json() +``` + +### JavaScript + +```javascript +// Search anime +const response = await fetch( + 'http://localhost:8080/api/search?q=naruto&provider=animecix' +); +const data = await response.json(); + +// Get streams +const streamResponse = await fetch( + `http://localhost:8080/api/anime/${animeId}/episodes/${episodeId}/streams?sort=desc` +); +const streams = await streamResponse.json(); +``` + +## Production Deployment + +### Reverse Proxy (Nginx) + +```nginx +server { + listen 80; + server_name api.example.com; + + location / { + proxy_pass http://localhost:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` + +### Systemd Service + +Create `/etc/systemd/system/weeb-cli-restful.service`: + +```ini +[Unit] +Description=Weeb CLI RESTful API +After=network.target + +[Service] +Type=simple +User=weeb +WorkingDirectory=/opt/weeb-cli +Environment="RESTFUL_PORT=8080" +Environment="RESTFUL_PROVIDERS=animecix,hianime" +ExecStart=/usr/local/bin/weeb-cli serve restful +Restart=always + +[Install] +WantedBy=multi-user.target +``` + +Enable and start: + +```bash +sudo systemctl enable weeb-cli-restful +sudo systemctl start weeb-cli-restful +``` + +## Security Considerations + +1. **Authentication**: The API does not include built-in authentication. Use a reverse proxy (Nginx, Traefik) with authentication if exposing publicly. + +2. **Rate Limiting**: Implement rate limiting at the reverse proxy level to prevent abuse. + +3. **HTTPS**: Always use HTTPS in production. Configure SSL/TLS at the reverse proxy level. + +4. **Firewall**: Restrict access to trusted IPs if possible. + +## Troubleshooting + +### Port Already in Use + +```bash +# Check what's using the port +lsof -i :8080 + +# Use a different port +weeb-cli serve restful --port 9000 +``` + +### Provider Not Found + +Ensure the provider name is correct: + +```bash +# List available providers +weeb-cli api providers + +# Use correct provider name +weeb-cli serve restful --providers animecix,hianime +``` + +### CORS Issues + +If experiencing CORS issues, ensure CORS is enabled: + +```bash +weeb-cli serve restful --cors +``` + +## See Also + +- [Torznab Server Mode](serve-mode.md) +- [API Commands](api-mode.md) +- [Available Providers](../api/providers/registry.md) diff --git a/docs/cli/restful-api.tr.md b/docs/cli/restful-api.tr.md new file mode 100644 index 0000000..b8590ac --- /dev/null +++ b/docs/cli/restful-api.tr.md @@ -0,0 +1,433 @@ +# RESTful API Modu + +Weeb CLI, arama, bölüm listeleme, stream çıkarma ve anime detayları dahil tüm provider işlemleri için HTTP endpoint'leri sağlayan bir RESTful API sunucusu olarak çalıştırılabilir. + +## Kurulum + +RESTful API bağımlılıklarıyla birlikte kurun: + +```bash +pip install weeb-cli[serve-restful] +``` + +## Kullanım + +### Temel Kullanım + +Sunucuyu varsayılan ayarlarla başlatın: + +```bash +weeb-cli serve restful +``` + +Sunucu `http://0.0.0.0:8080` adresinde tüm mevcut provider'larla başlayacaktır. + +### Özel Yapılandırma + +```bash +weeb-cli serve restful \ + --port 9000 \ + --host 127.0.0.1 \ + --providers animecix,hianime \ + --no-cors \ + --debug +``` + +### Komut Seçenekleri + +| Seçenek | Ortam Değişkeni | Varsayılan | Açıklama | +|---------|----------------|-----------|----------| +| `--port` | `RESTFUL_PORT` | `8080` | Bağlanılacak HTTP portu | +| `--host` | `RESTFUL_HOST` | `0.0.0.0` | Bağlanılacak host adresi | +| `--providers` | `RESTFUL_PROVIDERS` | `animecix,hianime,aniworld,docchi` | Virgülle ayrılmış provider isimleri | +| `--cors/--no-cors` | `RESTFUL_CORS` | `true` | CORS'u etkinleştir/devre dışı bırak | +| `--debug` | `RESTFUL_DEBUG` | `false` | Debug modunu etkinleştir | + +## API Endpoint'leri + +### Sağlık Kontrolü + +Sunucunun çalışıp çalışmadığını kontrol edin: + +```http +GET /health +``` + +**Yanıt:** +```json +{ + "status": "ok", + "service": "weeb-cli-restful", + "providers": ["animecix", "hianime", "aniworld", "docchi"] +} +``` + +### Provider'ları Listele + +Tüm mevcut provider'ları alın: + +```http +GET /api/providers +``` + +**Yanıt:** +```json +{ + "success": true, + "providers": [ + { + "name": "animecix", + "lang": "tr", + "region": "TR", + "class": "AnimecixProvider" + } + ], + "loaded": ["animecix", "hianime"] +} +``` + +### Anime Ara + +Provider'lar arasında anime arayın: + +```http +GET /api/search?q=naruto&provider=animecix +``` + +**Sorgu Parametreleri:** +- `q` (zorunlu): Arama sorgusu +- `provider` (opsiyonel): Provider adı (varsayılan olarak ilk yüklenen) + +**Yanıt:** +```json +{ + "success": true, + "provider": "animecix", + "query": "naruto", + "count": 10, + "results": [ + { + "id": "12345", + "title": "Naruto", + "type": "series", + "cover": "https://example.com/cover.jpg", + "year": 2002 + } + ] +} +``` + +### Anime Detaylarını Al + +Bir anime hakkında detaylı bilgi alın: + +```http +GET /api/anime/{anime_id}?provider=animecix +``` + +**Sorgu Parametreleri:** +- `provider` (opsiyonel): Provider adı (varsayılan olarak ilk yüklenen) + +**Yanıt:** +```json +{ + "success": true, + "provider": "animecix", + "anime": { + "id": "12345", + "title": "Naruto", + "type": "series", + "cover": "https://example.com/cover.jpg", + "year": 2002, + "description": "Anime açıklaması...", + "genres": ["Aksiyon", "Macera"], + "status": "completed", + "episodes": [...] + } +} +``` + +### Bölümleri Al + +Bir anime için tüm bölümleri listeleyin: + +```http +GET /api/anime/{anime_id}/episodes?provider=animecix&season=1 +``` + +**Sorgu Parametreleri:** +- `provider` (opsiyonel): Provider adı (varsayılan olarak ilk yüklenen) +- `season` (opsiyonel): Sezon numarasına göre filtrele + +**Yanıt:** +```json +{ + "success": true, + "provider": "animecix", + "anime_id": "12345", + "count": 220, + "episodes": [ + { + "id": "ep-1", + "number": 1, + "title": "Giriş: Naruto Uzumaki!", + "season": 1, + "url": "https://example.com/episode/1" + } + ] +} +``` + +### Stream'leri Al + +Bir bölüm için stream URL'lerini alın: + +```http +GET /api/anime/{anime_id}/episodes/{episode_id}/streams?provider=animecix&sort=desc +``` + +**Sorgu Parametreleri:** +- `provider` (opsiyonel): Provider adı (varsayılan olarak ilk yüklenen) +- `sort` (opsiyonel): Kaliteye göre sırala (`asc` veya `desc`, varsayılan `desc`) + +**Yanıt:** +```json +{ + "success": true, + "provider": "animecix", + "anime_id": "12345", + "episode_id": "ep-1", + "count": 3, + "streams": [ + { + "url": "https://example.com/stream.m3u8", + "quality": "1080p", + "server": "default", + "headers": { + "Referer": "https://example.com" + }, + "subtitles": null + } + ] +} +``` + +## Docker Dağıtımı + +### Docker Compose Kullanarak + +```bash +docker-compose -f docker-compose.restful.yml up -d +``` + +### Dockerfile Kullanarak + +Image'ı oluşturun: + +```bash +docker build -f Dockerfile.restful -t weeb-cli-restful . +``` + +Container'ı çalıştırın: + +```bash +docker run -d \ + --name weeb-cli-restful \ + -p 8080:8080 \ + -e RESTFUL_PROVIDERS=animecix,hianime \ + weeb-cli-restful +``` + +### Ortam Değişkenleri + +Tüm komut seçenekleri ortam değişkenleri ile yapılandırılabilir: + +```bash +docker run -d \ + --name weeb-cli-restful \ + -p 9000:9000 \ + -e RESTFUL_PORT=9000 \ + -e RESTFUL_HOST=0.0.0.0 \ + -e RESTFUL_PROVIDERS=animecix,hianime,aniworld \ + -e RESTFUL_CORS=true \ + -e RESTFUL_DEBUG=false \ + weeb-cli-restful +``` + +## Hata Yönetimi + +Tüm endpoint'ler tutarlı hata yanıtları döndürür: + +```json +{ + "success": false, + "error": "Hata mesajı açıklaması" +} +``` + +**Yaygın HTTP Durum Kodları:** +- `200`: Başarılı +- `400`: Hatalı istek (eksik/geçersiz parametreler) +- `404`: Kaynak bulunamadı +- `500`: Sunucu hatası + +## CORS Desteği + +CORS varsayılan olarak etkindir ve herhangi bir origin'den gelen isteklere izin verir. Devre dışı bırakmak için: + +```bash +weeb-cli serve restful --no-cors +``` + +Veya ortam değişkeni ile: + +```bash +export RESTFUL_CORS=false +``` + +## Örnek Kullanım + +### cURL + +```bash +# Anime ara +curl "http://localhost:8080/api/search?q=naruto&provider=animecix" + +# Bölümleri al +curl "http://localhost:8080/api/anime/12345/episodes?season=1" + +# Stream'leri al +curl "http://localhost:8080/api/anime/12345/episodes/ep-1/streams?sort=desc" +``` + +### Python + +```python +import requests + +# Anime ara +response = requests.get( + "http://localhost:8080/api/search", + params={"q": "naruto", "provider": "animecix"} +) +results = response.json() + +# Stream'leri al +response = requests.get( + f"http://localhost:8080/api/anime/{anime_id}/episodes/{episode_id}/streams", + params={"provider": "animecix", "sort": "desc"} +) +streams = response.json() +``` + +### JavaScript + +```javascript +// Anime ara +const response = await fetch( + 'http://localhost:8080/api/search?q=naruto&provider=animecix' +); +const data = await response.json(); + +// Stream'leri al +const streamResponse = await fetch( + `http://localhost:8080/api/anime/${animeId}/episodes/${episodeId}/streams?sort=desc` +); +const streams = await streamResponse.json(); +``` + +## Üretim Dağıtımı + +### Reverse Proxy (Nginx) + +```nginx +server { + listen 80; + server_name api.example.com; + + location / { + proxy_pass http://localhost:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` + +### Systemd Servisi + +`/etc/systemd/system/weeb-cli-restful.service` oluşturun: + +```ini +[Unit] +Description=Weeb CLI RESTful API +After=network.target + +[Service] +Type=simple +User=weeb +WorkingDirectory=/opt/weeb-cli +Environment="RESTFUL_PORT=8080" +Environment="RESTFUL_PROVIDERS=animecix,hianime" +ExecStart=/usr/local/bin/weeb-cli serve restful +Restart=always + +[Install] +WantedBy=multi-user.target +``` + +Etkinleştir ve başlat: + +```bash +sudo systemctl enable weeb-cli-restful +sudo systemctl start weeb-cli-restful +``` + +## Güvenlik Hususları + +1. **Kimlik Doğrulama**: API yerleşik kimlik doğrulama içermez. Herkese açık olarak sunuluyorsa, kimlik doğrulama ile bir reverse proxy (Nginx, Traefik) kullanın. + +2. **Hız Sınırlama**: Kötüye kullanımı önlemek için reverse proxy seviyesinde hız sınırlama uygulayın. + +3. **HTTPS**: Üretimde her zaman HTTPS kullanın. SSL/TLS'yi reverse proxy seviyesinde yapılandırın. + +4. **Güvenlik Duvarı**: Mümkünse erişimi güvenilir IP'lerle sınırlayın. + +## Sorun Giderme + +### Port Zaten Kullanımda + +```bash +# Portu kullanan uygulamayı kontrol et +lsof -i :8080 + +# Farklı bir port kullan +weeb-cli serve restful --port 9000 +``` + +### Provider Bulunamadı + +Provider adının doğru olduğundan emin olun: + +```bash +# Mevcut provider'ları listele +weeb-cli api providers + +# Doğru provider adını kullan +weeb-cli serve restful --providers animecix,hianime +``` + +### CORS Sorunları + +CORS sorunları yaşıyorsanız, CORS'un etkin olduğundan emin olun: + +```bash +weeb-cli serve restful --cors +``` + +## Ayrıca Bakınız + +- [Torznab Sunucu Modu](serve-mode.tr.md) +- [API Komutları](api-mode.tr.md) +- [Mevcut Provider'lar](../api/providers/registry.tr.md) diff --git a/md/tr/README.md b/md/tr/README.md index 1385792..233fa37 100644 --- a/md/tr/README.md +++ b/md/tr/README.md @@ -74,6 +74,7 @@ - Otomatik güncelleme kontrolü - Scriptler ve yapay zeka ajanları için etkileşimsiz JSON API - Sonarr/*arr entegrasyonu için Torznab sunucu modu +- Web/mobil uygulamalar için RESTful API sunucusu --- @@ -151,6 +152,37 @@ weeb-cli serve --port 9876 \ Ardından Sonarr'da `http://weeb-cli-host:9876` adresini 5070 (TV/Anime) kategorisiyle Torznab indexer olarak ekleyin. Sunucu, yakalanan bölümleri otomatik olarak işleyen bir blackhole indirme worker'ı içerir. +### RESTful API Sunucusu + +Web/mobil uygulamalar ve özel entegrasyonlar için weeb-cli bir RESTful API sunucusu sağlar: + +```bash +pip install weeb-cli[serve-restful] + +weeb-cli serve restful --port 8080 \ + --providers animecix,hianime,aniworld,docchi \ + --cors +``` + +**API Endpoint'leri:** +- `GET /health` - Sağlık kontrolü +- `GET /api/providers` - Mevcut provider'ları listele +- `GET /api/search?q=naruto&provider=animecix` - Anime ara +- `GET /api/anime/{id}?provider=animecix` - Anime detaylarını al +- `GET /api/anime/{id}/episodes?season=1` - Bölümleri listele +- `GET /api/anime/{id}/episodes/{ep_id}/streams` - Stream URL'lerini al + +**Docker Desteği:** +```bash +docker-compose -f docker-compose.restful.yml up -d +``` + +Tüm detaylar için [RESTful API Dokümantasyonu](https://ewgsta.github.io/weeb-cli/cli/restful-api.tr/)'na bakın. + +#### Docker (Torznab) + +Ardından Sonarr'da `http://weeb-cli-host:9876` adresini 5070 (TV/Anime) kategorisiyle Torznab indexer olarak ekleyin. Sunucu, yakalanan bölümleri otomatik olarak işleyen bir blackhole indirme worker'ı içerir. + #### Docker ```dockerfile diff --git a/pyproject.toml b/pyproject.toml index 53674f4..9976d9f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ dependencies = [ [project.optional-dependencies] serve = ["flask>=3.0", "pyyaml>=6.0"] +serve-restful = ["flask>=3.0", "flask-cors>=4.0"] dev = ["build", "pyinstaller", "pytest>=7.0.0", "pytest-cov", "pytest-asyncio", "pytest-mock"] [tool.setuptools.packages.find] diff --git a/tests/test_restful_api.py b/tests/test_restful_api.py new file mode 100644 index 0000000..ae43584 --- /dev/null +++ b/tests/test_restful_api.py @@ -0,0 +1,227 @@ +"""Tests for RESTful API server.""" +import pytest +from unittest.mock import Mock, patch, MagicMock +from weeb_cli.providers.base import AnimeResult, Episode, StreamLink + + +@pytest.fixture +def mock_provider(): + """Create a mock provider.""" + provider = Mock() + provider.name = "test_provider" + provider.search.return_value = [ + AnimeResult( + id="test123", + title="Test Anime", + type="series", + cover="https://example.com/cover.jpg", + year=2024, + ) + ] + provider.get_episodes.return_value = [ + Episode(id="ep1", number=1, title="Episode 1", season=1), + Episode(id="ep2", number=2, title="Episode 2", season=1), + ] + provider.get_streams.return_value = [ + StreamLink( + url="https://example.com/stream.m3u8", + quality="1080p", + server="default", + headers={"Referer": "https://example.com"}, + ) + ] + return provider + + +@pytest.fixture +def mock_flask_app(mock_provider): + """Create a mock Flask app with routes.""" + with patch("weeb_cli.commands.serve_restful.get_provider") as mock_get_provider: + mock_get_provider.return_value = mock_provider + + with patch("weeb_cli.commands.serve_restful.list_all_providers") as mock_list: + mock_list.return_value = [ + {"name": "test_provider", "lang": "en", "region": "US"} + ] + + # Import after patching + from flask import Flask + app = Flask(__name__) + + # Mock the routes (simplified for testing) + @app.route("/health") + def health(): + from flask import jsonify + return jsonify({ + "status": "ok", + "service": "weeb-cli-restful", + "providers": ["test_provider"], + }) + + yield app + + +def test_health_endpoint(mock_flask_app): + """Test health check endpoint.""" + client = mock_flask_app.test_client() + response = client.get("/health") + + assert response.status_code == 200 + data = response.get_json() + assert data["status"] == "ok" + assert data["service"] == "weeb-cli-restful" + assert "test_provider" in data["providers"] + + +def test_serialize_anime_result(): + """Test AnimeResult serialization.""" + from weeb_cli.commands.serve_restful import _serialize_anime_result + + result = AnimeResult( + id="test123", + title="Test Anime", + type="series", + cover="https://example.com/cover.jpg", + year=2024, + ) + + serialized = _serialize_anime_result(result) + + assert serialized["id"] == "test123" + assert serialized["title"] == "Test Anime" + assert serialized["type"] == "series" + assert serialized["cover"] == "https://example.com/cover.jpg" + assert serialized["year"] == 2024 + + +def test_serialize_episode(): + """Test Episode serialization.""" + from weeb_cli.commands.serve_restful import _serialize_episode + + episode = Episode( + id="ep1", + number=1, + title="Episode 1", + season=1, + url="https://example.com/ep1", + ) + + serialized = _serialize_episode(episode) + + assert serialized["id"] == "ep1" + assert serialized["number"] == 1 + assert serialized["title"] == "Episode 1" + assert serialized["season"] == 1 + assert serialized["url"] == "https://example.com/ep1" + + +def test_serialize_stream(): + """Test StreamLink serialization.""" + from weeb_cli.commands.serve_restful import _serialize_stream + + stream = StreamLink( + url="https://example.com/stream.m3u8", + quality="1080p", + server="default", + headers={"Referer": "https://example.com"}, + subtitles="https://example.com/subs.vtt", + ) + + serialized = _serialize_stream(stream) + + assert serialized["url"] == "https://example.com/stream.m3u8" + assert serialized["quality"] == "1080p" + assert serialized["server"] == "default" + assert serialized["headers"]["Referer"] == "https://example.com" + assert serialized["subtitles"] == "https://example.com/subs.vtt" + + +def test_quality_score(): + """Test quality scoring function.""" + from weeb_cli.commands.serve_restful import _quality_score + + assert _quality_score("4k") == 5 + assert _quality_score("2160p") == 5 + assert _quality_score("1080p") == 4 + assert _quality_score("720p") == 3 + assert _quality_score("480p") == 2 + assert _quality_score("360p") == 1 + assert _quality_score("unknown") == 0 + assert _quality_score(None) == 0 + + +@patch("weeb_cli.commands.serve_restful.get_provider") +def test_search_with_provider(mock_get_provider, mock_provider): + """Test search functionality with provider selection.""" + mock_get_provider.return_value = mock_provider + + # Simulate search + results = mock_provider.search("test query") + + assert len(results) == 1 + assert results[0].title == "Test Anime" + assert results[0].id == "test123" + + +@patch("weeb_cli.commands.serve_restful.get_provider") +def test_get_episodes_with_season_filter(mock_get_provider, mock_provider): + """Test episode listing with season filter.""" + mock_get_provider.return_value = mock_provider + + # Add episodes from different seasons + mock_provider.get_episodes.return_value = [ + Episode(id="ep1", number=1, season=1), + Episode(id="ep2", number=2, season=1), + Episode(id="ep3", number=1, season=2), + ] + + episodes = mock_provider.get_episodes("test123") + season_1_episodes = [ep for ep in episodes if ep.season == 1] + + assert len(season_1_episodes) == 2 + assert all(ep.season == 1 for ep in season_1_episodes) + + +@patch("weeb_cli.commands.serve_restful.get_provider") +def test_get_streams_sorted(mock_get_provider, mock_provider): + """Test stream retrieval with quality sorting.""" + from weeb_cli.commands.serve_restful import _quality_score + + mock_get_provider.return_value = mock_provider + + # Add streams with different qualities + mock_provider.get_streams.return_value = [ + StreamLink(url="url1", quality="480p"), + StreamLink(url="url2", quality="1080p"), + StreamLink(url="url3", quality="720p"), + ] + + streams = mock_provider.get_streams("test123", "ep1") + sorted_streams = sorted(streams, key=lambda s: _quality_score(s.quality), reverse=True) + + assert sorted_streams[0].quality == "1080p" + assert sorted_streams[1].quality == "720p" + assert sorted_streams[2].quality == "480p" + + +def test_missing_flask_import(): + """Test graceful handling when Flask is not installed.""" + with patch("builtins.__import__", side_effect=ImportError("No module named 'flask'")): + with pytest.raises(ImportError): + import flask + + +@pytest.mark.parametrize("quality,expected_score", [ + ("4K", 5), + ("2160p", 5), + ("1080p", 4), + ("720p", 3), + ("480p", 2), + ("360p", 1), + ("auto", 0), + ("", 0), +]) +def test_quality_score_parametrized(quality, expected_score): + """Test quality scoring with various inputs.""" + from weeb_cli.commands.serve_restful import _quality_score + assert _quality_score(quality) == expected_score diff --git a/weeb_cli/commands/serve.py b/weeb_cli/commands/serve.py index bcd93ee..407a586 100644 --- a/weeb_cli/commands/serve.py +++ b/weeb_cli/commands/serve.py @@ -32,10 +32,17 @@ def _sanitize_for_release(name: str) -> str: serve_app = typer.Typer( name="serve", - help="Start a Torznab-compatible server for Sonarr/*arr integration.", + help="Start a Torznab-compatible server for Sonarr/*arr integration or RESTful API server.", add_completion=False, ) +# Import restful subcommand +try: + from weeb_cli.commands.serve_restful import restful_app + serve_app.add_typer(restful_app, name="restful") +except ImportError: + pass # Flask not installed, restful mode unavailable + # -- Helpers ------------------------------------------------------------------ diff --git a/weeb_cli/commands/serve_restful.py b/weeb_cli/commands/serve_restful.py new file mode 100644 index 0000000..b43af5f --- /dev/null +++ b/weeb_cli/commands/serve_restful.py @@ -0,0 +1,373 @@ +"""RESTful API server for weeb-cli. + +Provides a REST API interface for all provider operations including search, +episode listing, stream extraction, and anime details. + +Requires: pip install weeb-cli[serve-restful] +Usage: weeb-cli serve restful --port 8080 --providers animecix,hianime +""" +import json +import logging +import sys +from typing import Optional, List + +import typer +from flask import Flask, request, jsonify, Response +from flask_cors import CORS + +log = logging.getLogger("weeb-cli-restful") + +restful_app = typer.Typer( + name="restful", + help="Start a RESTful API server for provider operations.", + add_completion=False, +) + + +def _quality_score(q: str) -> int: + """Calculate quality score for stream sorting.""" + q = (q or "").lower() + if "4k" in q or "2160" in q: + return 5 + if "1080" in q: + return 4 + if "720" in q: + return 3 + if "480" in q: + return 2 + if "360" in q: + return 1 + return 0 + + +def _serialize_anime_result(result) -> dict: + """Serialize AnimeResult to dict.""" + return { + "id": result.id, + "title": result.title, + "type": result.type, + "cover": result.cover, + "year": result.year, + } + + +def _serialize_episode(episode) -> dict: + """Serialize Episode to dict.""" + return { + "id": episode.id, + "number": episode.number, + "title": episode.title, + "season": episode.season, + "url": episode.url, + } + + +def _serialize_stream(stream) -> dict: + """Serialize StreamLink to dict.""" + return { + "url": stream.url, + "quality": stream.quality, + "server": stream.server, + "headers": stream.headers, + "subtitles": stream.subtitles, + } + + +def _serialize_anime_details(details) -> dict: + """Serialize AnimeDetails to dict.""" + return { + "id": details.id, + "title": details.title, + "type": details.type, + "cover": details.cover, + "year": details.year, + "description": details.description, + "genres": details.genres, + "status": details.status, + "episodes": [_serialize_episode(ep) for ep in details.episodes] if details.episodes else [], + } + + +@restful_app.callback(invoke_without_command=True) +def serve_restful( + ctx: typer.Context, + port: int = typer.Option(8080, "--port", envvar="RESTFUL_PORT", help="HTTP port"), + host: str = typer.Option("0.0.0.0", "--host", envvar="RESTFUL_HOST", help="Bind host"), + provider_names: str = typer.Option( + "animecix,hianime,aniworld,docchi", + "--providers", + envvar="RESTFUL_PROVIDERS", + help="Comma-separated provider names", + ), + enable_cors: bool = typer.Option(True, "--cors/--no-cors", envvar="RESTFUL_CORS", help="Enable CORS"), + debug: bool = typer.Option(False, "--debug", envvar="RESTFUL_DEBUG", help="Enable debug mode"), +): + """Start RESTful API server.""" + try: + from flask import Flask + except ImportError: + typer.echo( + "Flask is required for serve restful mode. Install with: pip install weeb-cli[serve-restful]", + err=True, + ) + raise typer.Exit(1) + + # Setup headless mode + from weeb_cli.config import config as weeb_config + weeb_config.set_headless(True) + + # Setup logging + logging.basicConfig( + level=logging.DEBUG if debug else logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + stream=sys.stdout, + ) + + # Load providers + from weeb_cli.providers.registry import get_provider, list_providers as list_all_providers + + providers = {} + for name in provider_names.split(","): + name = name.strip() + p = get_provider(name) + if p: + providers[name] = p + log.info(f"Loaded provider: {name}") + else: + log.warning(f"Provider not found: {name}") + + if not providers: + log.error("No providers loaded, exiting.") + raise typer.Exit(1) + + # Create Flask app + flask_app = Flask(__name__) + + if enable_cors: + CORS(flask_app) + log.info("CORS enabled") + + # Health check endpoint + @flask_app.route("/health", methods=["GET"]) + def health(): + """Health check endpoint.""" + return jsonify({ + "status": "ok", + "service": "weeb-cli-restful", + "providers": list(providers.keys()), + }) + + # List all available providers + @flask_app.route("/api/providers", methods=["GET"]) + def api_providers(): + """List all available providers.""" + try: + all_providers = list_all_providers() + return jsonify({ + "success": True, + "providers": all_providers, + "loaded": list(providers.keys()), + }) + except Exception as e: + log.error(f"Error listing providers: {e}") + return jsonify({"success": False, "error": str(e)}), 500 + + # Search anime + @flask_app.route("/api/search", methods=["GET"]) + def api_search(): + """Search anime across providers. + + Query params: + q: Search query (required) + provider: Provider name (optional, defaults to first loaded) + """ + query = request.args.get("q", "").strip() + provider_name = request.args.get("provider", "").strip() + + if not query: + return jsonify({"success": False, "error": "Missing query parameter 'q'"}), 400 + + # Select provider + if provider_name: + if provider_name not in providers: + return jsonify({ + "success": False, + "error": f"Provider '{provider_name}' not loaded", + "available": list(providers.keys()), + }), 404 + provider = providers[provider_name] + else: + provider = next(iter(providers.values())) + provider_name = provider.name + + try: + log.info(f"Search: query='{query}' provider={provider_name}") + results = provider.search(query) + + return jsonify({ + "success": True, + "provider": provider_name, + "query": query, + "count": len(results), + "results": [_serialize_anime_result(r) for r in results], + }) + except Exception as e: + log.error(f"Search error: {e}") + return jsonify({"success": False, "error": str(e)}), 500 + + # Get anime details + @flask_app.route("/api/anime/", methods=["GET"]) + def api_anime_details(anime_id: str): + """Get anime details. + + Query params: + provider: Provider name (optional, defaults to first loaded) + """ + provider_name = request.args.get("provider", "").strip() + + # Select provider + if provider_name: + if provider_name not in providers: + return jsonify({ + "success": False, + "error": f"Provider '{provider_name}' not loaded", + "available": list(providers.keys()), + }), 404 + provider = providers[provider_name] + else: + provider = next(iter(providers.values())) + provider_name = provider.name + + try: + log.info(f"Details: anime_id='{anime_id}' provider={provider_name}") + details = provider.get_details(anime_id) + + if not details: + return jsonify({ + "success": False, + "error": "Anime not found", + }), 404 + + return jsonify({ + "success": True, + "provider": provider_name, + "anime": _serialize_anime_details(details), + }) + except Exception as e: + log.error(f"Details error: {e}") + return jsonify({"success": False, "error": str(e)}), 500 + + # Get episodes + @flask_app.route("/api/anime//episodes", methods=["GET"]) + def api_episodes(anime_id: str): + """Get anime episodes. + + Query params: + provider: Provider name (optional, defaults to first loaded) + season: Filter by season number (optional) + """ + provider_name = request.args.get("provider", "").strip() + season_filter = request.args.get("season") + + # Select provider + if provider_name: + if provider_name not in providers: + return jsonify({ + "success": False, + "error": f"Provider '{provider_name}' not loaded", + "available": list(providers.keys()), + }), 404 + provider = providers[provider_name] + else: + provider = next(iter(providers.values())) + provider_name = provider.name + + try: + log.info(f"Episodes: anime_id='{anime_id}' provider={provider_name} season={season_filter}") + episodes = provider.get_episodes(anime_id) + + # Filter by season if requested + if season_filter: + try: + season_num = int(season_filter) + episodes = [ep for ep in episodes if ep.season == season_num] + except ValueError: + return jsonify({"success": False, "error": "Invalid season number"}), 400 + + return jsonify({ + "success": True, + "provider": provider_name, + "anime_id": anime_id, + "count": len(episodes), + "episodes": [_serialize_episode(ep) for ep in episodes], + }) + except Exception as e: + log.error(f"Episodes error: {e}") + return jsonify({"success": False, "error": str(e)}), 500 + + # Get streams + @flask_app.route("/api/anime//episodes//streams", methods=["GET"]) + def api_streams(anime_id: str, episode_id: str): + """Get episode streams. + + Query params: + provider: Provider name (optional, defaults to first loaded) + sort: Sort by quality (asc/desc, optional) + """ + provider_name = request.args.get("provider", "").strip() + sort_order = request.args.get("sort", "desc").lower() + + # Select provider + if provider_name: + if provider_name not in providers: + return jsonify({ + "success": False, + "error": f"Provider '{provider_name}' not loaded", + "available": list(providers.keys()), + }), 404 + provider = providers[provider_name] + else: + provider = next(iter(providers.values())) + provider_name = provider.name + + try: + log.info(f"Streams: anime_id='{anime_id}' episode_id='{episode_id}' provider={provider_name}") + streams = provider.get_streams(anime_id, episode_id) + + # Sort by quality + if sort_order == "desc": + streams = sorted(streams, key=lambda s: _quality_score(s.quality), reverse=True) + elif sort_order == "asc": + streams = sorted(streams, key=lambda s: _quality_score(s.quality)) + + return jsonify({ + "success": True, + "provider": provider_name, + "anime_id": anime_id, + "episode_id": episode_id, + "count": len(streams), + "streams": [_serialize_stream(s) for s in streams], + }) + except Exception as e: + log.error(f"Streams error: {e}") + return jsonify({"success": False, "error": str(e)}), 500 + + # Error handlers + @flask_app.errorhandler(404) + def not_found(e): + return jsonify({"success": False, "error": "Endpoint not found"}), 404 + + @flask_app.errorhandler(500) + def internal_error(e): + return jsonify({"success": False, "error": "Internal server error"}), 500 + + # Start server + log.info("weeb-cli RESTful API starting") + log.info(f" Host: {host}") + log.info(f" Port: {port}") + log.info(f" Providers: {', '.join(providers.keys())}") + log.info(f" CORS: {'enabled' if enable_cors else 'disabled'}") + log.info(f" Debug: {'enabled' if debug else 'disabled'}") + + flask_app.run(host=host, port=port, debug=debug) From cda6b5bc661d8426cd4e659b49a873c57cd8ee09 Mon Sep 17 00:00:00 2001 From: ewgsta <159681870+ewgsta@users.noreply.github.com> Date: Sun, 29 Mar 2026 11:36:58 +0300 Subject: [PATCH 2/4] docs(restful): add German and Polish documentations --- docs/cli/restful-api.de.md | 433 +++++++++++++++++++++++++++++++++++++ docs/cli/restful-api.pl.md | 433 +++++++++++++++++++++++++++++++++++++ 2 files changed, 866 insertions(+) create mode 100644 docs/cli/restful-api.de.md create mode 100644 docs/cli/restful-api.pl.md diff --git a/docs/cli/restful-api.de.md b/docs/cli/restful-api.de.md new file mode 100644 index 0000000..abf0ee0 --- /dev/null +++ b/docs/cli/restful-api.de.md @@ -0,0 +1,433 @@ +# RESTful API Modus + +Weeb CLI kann als RESTful API-Server ausgeführt werden und bietet HTTP-Endpunkte für alle Provider-Operationen, einschließlich Suche, Episodenliste, Stream-Extraktion und Anime-Details. + +## Installation + +Mit RESTful API-Abhängigkeiten installieren: + +```bash +pip install weeb-cli[serve-restful] +``` + +## Verwendung + +### Grundlegende Verwendung + +Server mit Standardeinstellungen starten: + +```bash +weeb-cli serve restful +``` + +Der Server startet auf `http://0.0.0.0:8080` mit allen verfügbaren Providern. + +### Benutzerdefinierte Konfiguration + +```bash +weeb-cli serve restful \ + --port 9000 \ + --host 127.0.0.1 \ + --providers animecix,hianime \ + --no-cors \ + --debug +``` + +### Befehlsoptionen + +| Option | Umgebungsvariable | Standard | Beschreibung | +|--------|------------------|----------|--------------| +| `--port` | `RESTFUL_PORT` | `8080` | HTTP-Port zum Binden | +| `--host` | `RESTFUL_HOST` | `0.0.0.0` | Host-Adresse zum Binden | +| `--providers` | `RESTFUL_PROVIDERS` | `animecix,hianime,aniworld,docchi` | Kommagetrennte Provider-Namen | +| `--cors/--no-cors` | `RESTFUL_CORS` | `true` | CORS aktivieren/deaktivieren | +| `--debug` | `RESTFUL_DEBUG` | `false` | Debug-Modus aktivieren | + +## API-Endpunkte + +### Gesundheitsprüfung + +Überprüfen, ob der Server läuft: + +```http +GET /health +``` + +**Antwort:** +```json +{ + "status": "ok", + "service": "weeb-cli-restful", + "providers": ["animecix", "hianime", "aniworld", "docchi"] +} +``` + +### Provider auflisten + +Alle verfügbaren Provider abrufen: + +```http +GET /api/providers +``` + +**Antwort:** +```json +{ + "success": true, + "providers": [ + { + "name": "animecix", + "lang": "tr", + "region": "TR", + "class": "AnimecixProvider" + } + ], + "loaded": ["animecix", "hianime"] +} +``` + +### Anime suchen + +Anime über Provider suchen: + +```http +GET /api/search?q=naruto&provider=animecix +``` + +**Abfrageparameter:** +- `q` (erforderlich): Suchanfrage +- `provider` (optional): Provider-Name (Standard: erster geladener) + +**Antwort:** +```json +{ + "success": true, + "provider": "animecix", + "query": "naruto", + "count": 10, + "results": [ + { + "id": "12345", + "title": "Naruto", + "type": "series", + "cover": "https://example.com/cover.jpg", + "year": 2002 + } + ] +} +``` + +### Anime-Details abrufen + +Detaillierte Informationen über einen Anime abrufen: + +```http +GET /api/anime/{anime_id}?provider=animecix +``` + +**Abfrageparameter:** +- `provider` (optional): Provider-Name (Standard: erster geladener) + +**Antwort:** +```json +{ + "success": true, + "provider": "animecix", + "anime": { + "id": "12345", + "title": "Naruto", + "type": "series", + "cover": "https://example.com/cover.jpg", + "year": 2002, + "description": "Anime-Beschreibung...", + "genres": ["Action", "Abenteuer"], + "status": "completed", + "episodes": [...] + } +} +``` + +### Episoden abrufen + +Alle Episoden für einen Anime auflisten: + +```http +GET /api/anime/{anime_id}/episodes?provider=animecix&season=1 +``` + +**Abfrageparameter:** +- `provider` (optional): Provider-Name (Standard: erster geladener) +- `season` (optional): Nach Staffelnummer filtern + +**Antwort:** +```json +{ + "success": true, + "provider": "animecix", + "anime_id": "12345", + "count": 220, + "episodes": [ + { + "id": "ep-1", + "number": 1, + "title": "Enter: Naruto Uzumaki!", + "season": 1, + "url": "https://example.com/episode/1" + } + ] +} +``` + +### Streams abrufen + +Stream-URLs für eine Episode abrufen: + +```http +GET /api/anime/{anime_id}/episodes/{episode_id}/streams?provider=animecix&sort=desc +``` + +**Abfrageparameter:** +- `provider` (optional): Provider-Name (Standard: erster geladener) +- `sort` (optional): Nach Qualität sortieren (`asc` oder `desc`, Standard: `desc`) + +**Antwort:** +```json +{ + "success": true, + "provider": "animecix", + "anime_id": "12345", + "episode_id": "ep-1", + "count": 3, + "streams": [ + { + "url": "https://example.com/stream.m3u8", + "quality": "1080p", + "server": "default", + "headers": { + "Referer": "https://example.com" + }, + "subtitles": null + } + ] +} +``` + +## Docker-Bereitstellung + +### Mit Docker Compose + +```bash +docker-compose -f docker-compose.restful.yml up -d +``` + +### Mit Dockerfile + +Image erstellen: + +```bash +docker build -f Dockerfile.restful -t weeb-cli-restful . +``` + +Container ausführen: + +```bash +docker run -d \ + --name weeb-cli-restful \ + -p 8080:8080 \ + -e RESTFUL_PROVIDERS=animecix,hianime \ + weeb-cli-restful +``` + +### Umgebungsvariablen + +Alle Befehlsoptionen können über Umgebungsvariablen konfiguriert werden: + +```bash +docker run -d \ + --name weeb-cli-restful \ + -p 9000:9000 \ + -e RESTFUL_PORT=9000 \ + -e RESTFUL_HOST=0.0.0.0 \ + -e RESTFUL_PROVIDERS=animecix,hianime,aniworld \ + -e RESTFUL_CORS=true \ + -e RESTFUL_DEBUG=false \ + weeb-cli-restful +``` + +## Fehlerbehandlung + +Alle Endpunkte geben konsistente Fehlerantworten zurück: + +```json +{ + "success": false, + "error": "Fehlermeldungsbeschreibung" +} +``` + +**Häufige HTTP-Statuscodes:** +- `200`: Erfolg +- `400`: Ungültige Anfrage (fehlende/ungültige Parameter) +- `404`: Ressource nicht gefunden +- `500`: Interner Serverfehler + +## CORS-Unterstützung + +CORS ist standardmäßig aktiviert und erlaubt Anfragen von jedem Origin. Zum Deaktivieren: + +```bash +weeb-cli serve restful --no-cors +``` + +Oder über Umgebungsvariable: + +```bash +export RESTFUL_CORS=false +``` + +## Beispielverwendung + +### cURL + +```bash +# Anime suchen +curl "http://localhost:8080/api/search?q=naruto&provider=animecix" + +# Episoden abrufen +curl "http://localhost:8080/api/anime/12345/episodes?season=1" + +# Streams abrufen +curl "http://localhost:8080/api/anime/12345/episodes/ep-1/streams?sort=desc" +``` + +### Python + +```python +import requests + +# Anime suchen +response = requests.get( + "http://localhost:8080/api/search", + params={"q": "naruto", "provider": "animecix"} +) +results = response.json() + +# Streams abrufen +response = requests.get( + f"http://localhost:8080/api/anime/{anime_id}/episodes/{episode_id}/streams", + params={"provider": "animecix", "sort": "desc"} +) +streams = response.json() +``` + +### JavaScript + +```javascript +// Anime suchen +const response = await fetch( + 'http://localhost:8080/api/search?q=naruto&provider=animecix' +); +const data = await response.json(); + +// Streams abrufen +const streamResponse = await fetch( + `http://localhost:8080/api/anime/${animeId}/episodes/${episodeId}/streams?sort=desc` +); +const streams = await streamResponse.json(); +``` + +## Produktionsbereitstellung + +### Reverse Proxy (Nginx) + +```nginx +server { + listen 80; + server_name api.example.com; + + location / { + proxy_pass http://localhost:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` + +### Systemd-Dienst + +`/etc/systemd/system/weeb-cli-restful.service` erstellen: + +```ini +[Unit] +Description=Weeb CLI RESTful API +After=network.target + +[Service] +Type=simple +User=weeb +WorkingDirectory=/opt/weeb-cli +Environment="RESTFUL_PORT=8080" +Environment="RESTFUL_PROVIDERS=animecix,hianime" +ExecStart=/usr/local/bin/weeb-cli serve restful +Restart=always + +[Install] +WantedBy=multi-user.target +``` + +Aktivieren und starten: + +```bash +sudo systemctl enable weeb-cli-restful +sudo systemctl start weeb-cli-restful +``` + +## Sicherheitsüberlegungen + +1. **Authentifizierung**: Die API enthält keine integrierte Authentifizierung. Verwenden Sie einen Reverse Proxy (Nginx, Traefik) mit Authentifizierung, wenn Sie öffentlich verfügbar machen. + +2. **Rate Limiting**: Implementieren Sie Rate Limiting auf Reverse-Proxy-Ebene, um Missbrauch zu verhindern. + +3. **HTTPS**: Verwenden Sie in der Produktion immer HTTPS. Konfigurieren Sie SSL/TLS auf Reverse-Proxy-Ebene. + +4. **Firewall**: Beschränken Sie den Zugriff auf vertrauenswürdige IPs, wenn möglich. + +## Fehlerbehebung + +### Port bereits in Verwendung + +```bash +# Prüfen, was den Port verwendet +lsof -i :8080 + +# Anderen Port verwenden +weeb-cli serve restful --port 9000 +``` + +### Provider nicht gefunden + +Stellen Sie sicher, dass der Provider-Name korrekt ist: + +```bash +# Verfügbare Provider auflisten +weeb-cli api providers + +# Korrekten Provider-Namen verwenden +weeb-cli serve restful --providers animecix,hianime +``` + +### CORS-Probleme + +Wenn CORS-Probleme auftreten, stellen Sie sicher, dass CORS aktiviert ist: + +```bash +weeb-cli serve restful --cors +``` + +## Siehe auch + +- [Torznab-Servermodus](serve-mode.de.md) +- [API-Befehle](api-mode.de.md) +- [Verfügbare Provider](../api/providers/registry.de.md) diff --git a/docs/cli/restful-api.pl.md b/docs/cli/restful-api.pl.md new file mode 100644 index 0000000..22f24e4 --- /dev/null +++ b/docs/cli/restful-api.pl.md @@ -0,0 +1,433 @@ +# Tryb RESTful API + +Weeb CLI może działać jako serwer RESTful API, zapewniając punkty końcowe HTTP dla wszystkich operacji dostawców, w tym wyszukiwania, listy odcinków, ekstrakcji strumieni i szczegółów anime. + +## Instalacja + +Zainstaluj z zależnościami RESTful API: + +```bash +pip install weeb-cli[serve-restful] +``` + +## Użycie + +### Podstawowe użycie + +Uruchom serwer z domyślnymi ustawieniami: + +```bash +weeb-cli serve restful +``` + +Serwer uruchomi się na `http://0.0.0.0:8080` ze wszystkimi dostępnymi dostawcami. + +### Niestandardowa konfiguracja + +```bash +weeb-cli serve restful \ + --port 9000 \ + --host 127.0.0.1 \ + --providers animecix,hianime \ + --no-cors \ + --debug +``` + +### Opcje polecenia + +| Opcja | Zmienna środowiskowa | Domyślna | Opis | +|-------|---------------------|----------|------| +| `--port` | `RESTFUL_PORT` | `8080` | Port HTTP do powiązania | +| `--host` | `RESTFUL_HOST` | `0.0.0.0` | Adres hosta do powiązania | +| `--providers` | `RESTFUL_PROVIDERS` | `animecix,hianime,aniworld,docchi` | Nazwy dostawców oddzielone przecinkami | +| `--cors/--no-cors` | `RESTFUL_CORS` | `true` | Włącz/wyłącz CORS | +| `--debug` | `RESTFUL_DEBUG` | `false` | Włącz tryb debugowania | + +## Punkty końcowe API + +### Sprawdzenie stanu + +Sprawdź, czy serwer działa: + +```http +GET /health +``` + +**Odpowiedź:** +```json +{ + "status": "ok", + "service": "weeb-cli-restful", + "providers": ["animecix", "hianime", "aniworld", "docchi"] +} +``` + +### Lista dostawców + +Pobierz wszystkich dostępnych dostawców: + +```http +GET /api/providers +``` + +**Odpowiedź:** +```json +{ + "success": true, + "providers": [ + { + "name": "animecix", + "lang": "tr", + "region": "TR", + "class": "AnimecixProvider" + } + ], + "loaded": ["animecix", "hianime"] +} +``` + +### Wyszukaj anime + +Wyszukaj anime u dostawców: + +```http +GET /api/search?q=naruto&provider=animecix +``` + +**Parametry zapytania:** +- `q` (wymagane): Zapytanie wyszukiwania +- `provider` (opcjonalne): Nazwa dostawcy (domyślnie pierwszy załadowany) + +**Odpowiedź:** +```json +{ + "success": true, + "provider": "animecix", + "query": "naruto", + "count": 10, + "results": [ + { + "id": "12345", + "title": "Naruto", + "type": "series", + "cover": "https://example.com/cover.jpg", + "year": 2002 + } + ] +} +``` + +### Pobierz szczegóły anime + +Pobierz szczegółowe informacje o anime: + +```http +GET /api/anime/{anime_id}?provider=animecix +``` + +**Parametry zapytania:** +- `provider` (opcjonalne): Nazwa dostawcy (domyślnie pierwszy załadowany) + +**Odpowiedź:** +```json +{ + "success": true, + "provider": "animecix", + "anime": { + "id": "12345", + "title": "Naruto", + "type": "series", + "cover": "https://example.com/cover.jpg", + "year": 2002, + "description": "Opis anime...", + "genres": ["Akcja", "Przygoda"], + "status": "completed", + "episodes": [...] + } +} +``` + +### Pobierz odcinki + +Wyświetl wszystkie odcinki anime: + +```http +GET /api/anime/{anime_id}/episodes?provider=animecix&season=1 +``` + +**Parametry zapytania:** +- `provider` (opcjonalne): Nazwa dostawcy (domyślnie pierwszy załadowany) +- `season` (opcjonalne): Filtruj według numeru sezonu + +**Odpowiedź:** +```json +{ + "success": true, + "provider": "animecix", + "anime_id": "12345", + "count": 220, + "episodes": [ + { + "id": "ep-1", + "number": 1, + "title": "Enter: Naruto Uzumaki!", + "season": 1, + "url": "https://example.com/episode/1" + } + ] +} +``` + +### Pobierz strumienie + +Pobierz adresy URL strumieni dla odcinka: + +```http +GET /api/anime/{anime_id}/episodes/{episode_id}/streams?provider=animecix&sort=desc +``` + +**Parametry zapytania:** +- `provider` (opcjonalne): Nazwa dostawcy (domyślnie pierwszy załadowany) +- `sort` (opcjonalne): Sortuj według jakości (`asc` lub `desc`, domyślnie `desc`) + +**Odpowiedź:** +```json +{ + "success": true, + "provider": "animecix", + "anime_id": "12345", + "episode_id": "ep-1", + "count": 3, + "streams": [ + { + "url": "https://example.com/stream.m3u8", + "quality": "1080p", + "server": "default", + "headers": { + "Referer": "https://example.com" + }, + "subtitles": null + } + ] +} +``` + +## Wdrożenie Docker + +### Używając Docker Compose + +```bash +docker-compose -f docker-compose.restful.yml up -d +``` + +### Używając Dockerfile + +Zbuduj obraz: + +```bash +docker build -f Dockerfile.restful -t weeb-cli-restful . +``` + +Uruchom kontener: + +```bash +docker run -d \ + --name weeb-cli-restful \ + -p 8080:8080 \ + -e RESTFUL_PROVIDERS=animecix,hianime \ + weeb-cli-restful +``` + +### Zmienne środowiskowe + +Wszystkie opcje poleceń można skonfigurować za pomocą zmiennych środowiskowych: + +```bash +docker run -d \ + --name weeb-cli-restful \ + -p 9000:9000 \ + -e RESTFUL_PORT=9000 \ + -e RESTFUL_HOST=0.0.0.0 \ + -e RESTFUL_PROVIDERS=animecix,hianime,aniworld \ + -e RESTFUL_CORS=true \ + -e RESTFUL_DEBUG=false \ + weeb-cli-restful +``` + +## Obsługa błędów + +Wszystkie punkty końcowe zwracają spójne odpowiedzi błędów: + +```json +{ + "success": false, + "error": "Opis komunikatu o błędzie" +} +``` + +**Typowe kody stanu HTTP:** +- `200`: Sukces +- `400`: Złe żądanie (brakujące/nieprawidłowe parametry) +- `404`: Zasób nie znaleziony +- `500`: Wewnętrzny błąd serwera + +## Obsługa CORS + +CORS jest domyślnie włączony, zezwalając na żądania z dowolnego źródła. Aby wyłączyć: + +```bash +weeb-cli serve restful --no-cors +``` + +Lub za pomocą zmiennej środowiskowej: + +```bash +export RESTFUL_CORS=false +``` + +## Przykładowe użycie + +### cURL + +```bash +# Wyszukaj anime +curl "http://localhost:8080/api/search?q=naruto&provider=animecix" + +# Pobierz odcinki +curl "http://localhost:8080/api/anime/12345/episodes?season=1" + +# Pobierz strumienie +curl "http://localhost:8080/api/anime/12345/episodes/ep-1/streams?sort=desc" +``` + +### Python + +```python +import requests + +# Wyszukaj anime +response = requests.get( + "http://localhost:8080/api/search", + params={"q": "naruto", "provider": "animecix"} +) +results = response.json() + +# Pobierz strumienie +response = requests.get( + f"http://localhost:8080/api/anime/{anime_id}/episodes/{episode_id}/streams", + params={"provider": "animecix", "sort": "desc"} +) +streams = response.json() +``` + +### JavaScript + +```javascript +// Wyszukaj anime +const response = await fetch( + 'http://localhost:8080/api/search?q=naruto&provider=animecix' +); +const data = await response.json(); + +// Pobierz strumienie +const streamResponse = await fetch( + `http://localhost:8080/api/anime/${animeId}/episodes/${episodeId}/streams?sort=desc` +); +const streams = await streamResponse.json(); +``` + +## Wdrożenie produkcyjne + +### Reverse Proxy (Nginx) + +```nginx +server { + listen 80; + server_name api.example.com; + + location / { + proxy_pass http://localhost:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` + +### Usługa Systemd + +Utwórz `/etc/systemd/system/weeb-cli-restful.service`: + +```ini +[Unit] +Description=Weeb CLI RESTful API +After=network.target + +[Service] +Type=simple +User=weeb +WorkingDirectory=/opt/weeb-cli +Environment="RESTFUL_PORT=8080" +Environment="RESTFUL_PROVIDERS=animecix,hianime" +ExecStart=/usr/local/bin/weeb-cli serve restful +Restart=always + +[Install] +WantedBy=multi-user.target +``` + +Włącz i uruchom: + +```bash +sudo systemctl enable weeb-cli-restful +sudo systemctl start weeb-cli-restful +``` + +## Kwestie bezpieczeństwa + +1. **Uwierzytelnianie**: API nie zawiera wbudowanego uwierzytelniania. Użyj reverse proxy (Nginx, Traefik) z uwierzytelnianiem, jeśli udostępniasz publicznie. + +2. **Ograniczanie szybkości**: Zaimplementuj ograniczanie szybkości na poziomie reverse proxy, aby zapobiec nadużyciom. + +3. **HTTPS**: Zawsze używaj HTTPS w produkcji. Skonfiguruj SSL/TLS na poziomie reverse proxy. + +4. **Firewall**: Ogranicz dostęp do zaufanych adresów IP, jeśli to możliwe. + +## Rozwiązywanie problemów + +### Port już w użyciu + +```bash +# Sprawdź, co używa portu +lsof -i :8080 + +# Użyj innego portu +weeb-cli serve restful --port 9000 +``` + +### Dostawca nie znaleziony + +Upewnij się, że nazwa dostawcy jest poprawna: + +```bash +# Wyświetl dostępnych dostawców +weeb-cli api providers + +# Użyj poprawnej nazwy dostawcy +weeb-cli serve restful --providers animecix,hianime +``` + +### Problemy z CORS + +Jeśli występują problemy z CORS, upewnij się, że CORS jest włączony: + +```bash +weeb-cli serve restful --cors +``` + +## Zobacz także + +- [Tryb serwera Torznab](serve-mode.pl.md) +- [Polecenia API](api-mode.pl.md) +- [Dostępni dostawcy](../api/providers/registry.pl.md) From aa236a62c72ee023f5aa52875b637336419e555b Mon Sep 17 00:00:00 2001 From: ewgsta <159681870+ewgsta@users.noreply.github.com> Date: Sun, 29 Mar 2026 11:38:57 +0300 Subject: [PATCH 3/4] docs(examples): add RESTful API client examples - Add Python client example with comprehensive usage - Add JavaScript/Node.js client example - Create examples README with integration guides - Include usage examples for: * Web applications (React) * Mobile apps (React Native) * CLI tools (Bash) * Docker deployment --- examples/README.md | 324 +++++++++++++++++++++++++++++++++ examples/restful_api_client.js | 190 +++++++++++++++++++ examples/restful_api_client.py | 224 +++++++++++++++++++++++ 3 files changed, 738 insertions(+) create mode 100644 examples/README.md create mode 100755 examples/restful_api_client.js create mode 100755 examples/restful_api_client.py diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..4599db8 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,324 @@ +# Weeb CLI Examples + +This directory contains example scripts demonstrating how to use weeb-cli's various features. + +## RESTful API Client Examples + +### Python Client + +**File:** `restful_api_client.py` + +A comprehensive Python client demonstrating all RESTful API endpoints. + +**Requirements:** +```bash +pip install requests +``` + +**Usage:** +```bash +# Start the RESTful API server first +weeb-cli serve restful --port 8080 + +# Run the example client +python examples/restful_api_client.py +``` + +**Features:** +- Health check +- List providers +- Search anime +- Get anime details +- List episodes +- Get stream URLs + +### JavaScript/Node.js Client + +**File:** `restful_api_client.js` + +A Node.js client demonstrating RESTful API usage in JavaScript. + +**Requirements:** +- Node.js 18+ (for native fetch support) + +**Usage:** +```bash +# Start the RESTful API server first +weeb-cli serve restful --port 8080 + +# Run the example client +node examples/restful_api_client.js +``` + +**Environment Variables:** +```bash +# Custom server URL +WEEB_CLI_URL=http://localhost:9000 node examples/restful_api_client.js +``` + +## Creating Your Own Client + +### Basic Structure + +```python +import requests + +class WeebCLIClient: + def __init__(self, base_url="http://localhost:8080"): + self.base_url = base_url + + def search(self, query, provider=None): + params = {"q": query} + if provider: + params["provider"] = provider + response = requests.get(f"{self.base_url}/api/search", params=params) + return response.json() + +# Usage +client = WeebCLIClient() +results = client.search("naruto", provider="animecix") +``` + +### Error Handling + +```python +try: + results = client.search("naruto") +except requests.exceptions.ConnectionError: + print("Server not running") +except requests.exceptions.HTTPError as e: + print(f"HTTP error: {e}") +``` + +### Async Support (Python) + +```python +import aiohttp + +class AsyncWeebCLIClient: + def __init__(self, base_url="http://localhost:8080"): + self.base_url = base_url + + async def search(self, query, provider=None): + params = {"q": query} + if provider: + params["provider"] = provider + async with aiohttp.ClientSession() as session: + async with session.get( + f"{self.base_url}/api/search", + params=params + ) as response: + return await response.json() + +# Usage +import asyncio +client = AsyncWeebCLIClient() +results = asyncio.run(client.search("naruto")) +``` + +## API Endpoints Reference + +### Health Check +```http +GET /health +``` + +### List Providers +```http +GET /api/providers +``` + +### Search Anime +```http +GET /api/search?q=naruto&provider=animecix +``` + +### Get Anime Details +```http +GET /api/anime/{anime_id}?provider=animecix +``` + +### List Episodes +```http +GET /api/anime/{anime_id}/episodes?season=1&provider=animecix +``` + +### Get Streams +```http +GET /api/anime/{anime_id}/episodes/{episode_id}/streams?provider=animecix&sort=desc +``` + +## Integration Examples + +### Web Application (React) + +```javascript +// api/weebcli.js +export class WeebCLIAPI { + constructor(baseUrl = 'http://localhost:8080') { + this.baseUrl = baseUrl; + } + + async search(query, provider) { + const params = new URLSearchParams({ q: query }); + if (provider) params.append('provider', provider); + + const response = await fetch(`${this.baseUrl}/api/search?${params}`); + const data = await response.json(); + return data.results; + } +} + +// Component usage +import { WeebCLIAPI } from './api/weebcli'; + +function SearchComponent() { + const [results, setResults] = useState([]); + const api = new WeebCLIAPI(); + + const handleSearch = async (query) => { + const data = await api.search(query, 'animecix'); + setResults(data); + }; + + return ( +
+ handleSearch(e.target.value)} /> + {results.map(anime => ( +
{anime.title}
+ ))} +
+ ); +} +``` + +### Mobile App (React Native) + +```javascript +// services/weebcli.js +export const searchAnime = async (query, provider = 'animecix') => { + const response = await fetch( + `http://your-server:8080/api/search?q=${encodeURIComponent(query)}&provider=${provider}` + ); + const data = await response.json(); + return data.results; +}; + +// Usage in component +import { searchAnime } from './services/weebcli'; + +const SearchScreen = () => { + const [results, setResults] = useState([]); + + useEffect(() => { + searchAnime('naruto').then(setResults); + }, []); + + return ( + {item.title}} + /> + ); +}; +``` + +### CLI Tool (Bash) + +```bash +#!/bin/bash +# weeb-search.sh + +API_URL="http://localhost:8080" +QUERY="$1" +PROVIDER="${2:-animecix}" + +# Search anime +curl -s "${API_URL}/api/search?q=${QUERY}&provider=${PROVIDER}" | jq '.results[] | "\(.title) (\(.year))"' +``` + +## Docker Deployment + +### docker-compose.yml + +```yaml +version: '3.8' + +services: + weeb-cli-api: + image: weeb-cli-restful + ports: + - "8080:8080" + environment: + - RESTFUL_PROVIDERS=animecix,hianime,aniworld + - RESTFUL_CORS=true + restart: unless-stopped + + web-app: + image: your-web-app + depends_on: + - weeb-cli-api + environment: + - WEEB_CLI_API_URL=http://weeb-cli-api:8080 +``` + +## Testing + +### Unit Tests (Python) + +```python +import pytest +from restful_api_client import WeebCLIClient + +@pytest.fixture +def client(): + return WeebCLIClient("http://localhost:8080") + +def test_health_check(client): + health = client.health_check() + assert health["status"] == "ok" + +def test_search(client): + results = client.search("naruto", provider="animecix") + assert len(results) > 0 + assert "id" in results[0] + assert "title" in results[0] +``` + +### Integration Tests (JavaScript) + +```javascript +const { WeebCLIClient } = require('./restful_api_client'); + +describe('WeebCLI API', () => { + let client; + + beforeAll(() => { + client = new WeebCLIClient('http://localhost:8080'); + }); + + test('health check returns ok', async () => { + const health = await client.healthCheck(); + expect(health.status).toBe('ok'); + }); + + test('search returns results', async () => { + const results = await client.search('naruto', 'animecix'); + expect(results.length).toBeGreaterThan(0); + expect(results[0]).toHaveProperty('id'); + expect(results[0]).toHaveProperty('title'); + }); +}); +``` + +## Contributing + +Feel free to contribute more examples! Submit a pull request with: +- Clear documentation +- Working code +- Usage instructions +- Error handling + +## License + +These examples are part of weeb-cli and are licensed under GPL-3.0. diff --git a/examples/restful_api_client.js b/examples/restful_api_client.js new file mode 100755 index 0000000..3fbdc2f --- /dev/null +++ b/examples/restful_api_client.js @@ -0,0 +1,190 @@ +#!/usr/bin/env node +/** + * Example RESTful API client for weeb-cli (Node.js/JavaScript) + * + * This script demonstrates how to interact with the weeb-cli RESTful API server. + * + * Usage: + * node restful_api_client.js + */ + +const BASE_URL = process.env.WEEB_CLI_URL || 'http://localhost:8080'; + +/** + * WeebCLI API Client + */ +class WeebCLIClient { + constructor(baseUrl = BASE_URL) { + this.baseUrl = baseUrl.replace(/\/$/, ''); + } + + /** + * Make HTTP GET request + */ + async get(endpoint, params = {}) { + const url = new URL(`${this.baseUrl}${endpoint}`); + Object.keys(params).forEach(key => { + if (params[key] !== null && params[key] !== undefined) { + url.searchParams.append(key, params[key]); + } + }); + + const response = await fetch(url.toString()); + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + return response.json(); + } + + /** + * Health check + */ + async healthCheck() { + return this.get('/health'); + } + + /** + * List all providers + */ + async listProviders() { + return this.get('/api/providers'); + } + + /** + * Search anime + */ + async search(query, provider = null) { + const data = await this.get('/api/search', { q: query, provider }); + return data.results || []; + } + + /** + * Get anime details + */ + async getAnimeDetails(animeId, provider = null) { + const data = await this.get(`/api/anime/${animeId}`, { provider }); + return data.anime || {}; + } + + /** + * Get episodes + */ + async getEpisodes(animeId, season = null, provider = null) { + const data = await this.get(`/api/anime/${animeId}/episodes`, { season, provider }); + return data.episodes || []; + } + + /** + * Get streams + */ + async getStreams(animeId, episodeId, provider = null, sort = 'desc') { + const data = await this.get( + `/api/anime/${animeId}/episodes/${episodeId}/streams`, + { provider, sort } + ); + return data.streams || []; + } +} + +/** + * Main example function + */ +async function main() { + const client = new WeebCLIClient(); + + try { + // Health check + console.log('=== Health Check ==='); + const health = await client.healthCheck(); + console.log(`Status: ${health.status}`); + console.log(`Providers: ${health.providers.join(', ')}`); + console.log(); + + // List providers + console.log('=== Available Providers ==='); + const providersData = await client.listProviders(); + providersData.providers.forEach(provider => { + console.log(`- ${provider.name} (${provider.lang}/${provider.region})`); + }); + console.log(); + + // Search anime + console.log('=== Search Results ==='); + const query = 'naruto'; + const results = await client.search(query, 'animecix'); + console.log(`Found ${results.length} results for '${query}':`); + results.slice(0, 5).forEach((anime, i) => { + console.log(`${i + 1}. ${anime.title} (${anime.year}) - ID: ${anime.id}`); + }); + console.log(); + + if (results.length === 0) { + console.log('No results found. Exiting.'); + return; + } + + // Get anime details + console.log('=== Anime Details ==='); + const animeId = results[0].id; + const details = await client.getAnimeDetails(animeId, 'animecix'); + console.log(`Title: ${details.title}`); + console.log(`Type: ${details.type}`); + console.log(`Year: ${details.year}`); + console.log(`Status: ${details.status || 'N/A'}`); + if (details.genres && details.genres.length > 0) { + console.log(`Genres: ${details.genres.join(', ')}`); + } + console.log(); + + // Get episodes + console.log('=== Episodes (Season 1) ==='); + const episodes = await client.getEpisodes(animeId, 1, 'animecix'); + console.log(`Found ${episodes.length} episodes:`); + episodes.slice(0, 5).forEach(ep => { + const title = ep.title || `Episode ${ep.number}`; + const season = String(ep.season).padStart(2, '0'); + const number = String(ep.number).padStart(2, '0'); + console.log(`- S${season}E${number}: ${title}`); + }); + console.log(); + + if (episodes.length === 0) { + console.log('No episodes found. Exiting.'); + return; + } + + // Get streams for first episode + console.log('=== Streams (First Episode) ==='); + const episodeId = episodes[0].id; + const streams = await client.getStreams(animeId, episodeId, 'animecix'); + console.log(`Found ${streams.length} streams:`); + streams.forEach(stream => { + console.log(`- Quality: ${stream.quality}`); + console.log(` Server: ${stream.server}`); + console.log(` URL: ${stream.url.substring(0, 60)}...`); + if (stream.subtitles) { + console.log(` Subtitles: ${stream.subtitles}`); + } + }); + console.log(); + + } catch (error) { + if (error.code === 'ECONNREFUSED') { + console.error('Error: Could not connect to weeb-cli RESTful API server.'); + console.error('Make sure the server is running: weeb-cli serve restful'); + } else { + console.error(`Error: ${error.message}`); + } + process.exit(1); + } +} + +// Run main function +if (require.main === module) { + main().catch(error => { + console.error('Unhandled error:', error); + process.exit(1); + }); +} + +module.exports = { WeebCLIClient }; diff --git a/examples/restful_api_client.py b/examples/restful_api_client.py new file mode 100755 index 0000000..1dde139 --- /dev/null +++ b/examples/restful_api_client.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python3 +"""Example RESTful API client for weeb-cli. + +This script demonstrates how to interact with the weeb-cli RESTful API server. +""" +import requests +from typing import List, Dict, Optional + + +class WeebCLIClient: + """Client for weeb-cli RESTful API.""" + + def __init__(self, base_url: str = "http://localhost:8080"): + """Initialize client with base URL. + + Args: + base_url: Base URL of the weeb-cli RESTful API server + """ + self.base_url = base_url.rstrip("/") + self.session = requests.Session() + + def health_check(self) -> Dict: + """Check if the server is running. + + Returns: + Health status dictionary + """ + response = self.session.get(f"{self.base_url}/health") + response.raise_for_status() + return response.json() + + def list_providers(self) -> Dict: + """List all available providers. + + Returns: + Dictionary with providers list + """ + response = self.session.get(f"{self.base_url}/api/providers") + response.raise_for_status() + return response.json() + + def search(self, query: str, provider: Optional[str] = None) -> List[Dict]: + """Search for anime. + + Args: + query: Search query + provider: Provider name (optional) + + Returns: + List of anime results + """ + params = {"q": query} + if provider: + params["provider"] = provider + + response = self.session.get(f"{self.base_url}/api/search", params=params) + response.raise_for_status() + data = response.json() + return data.get("results", []) + + def get_anime_details(self, anime_id: str, provider: Optional[str] = None) -> Dict: + """Get anime details. + + Args: + anime_id: Anime ID + provider: Provider name (optional) + + Returns: + Anime details dictionary + """ + params = {} + if provider: + params["provider"] = provider + + response = self.session.get( + f"{self.base_url}/api/anime/{anime_id}", + params=params + ) + response.raise_for_status() + data = response.json() + return data.get("anime", {}) + + def get_episodes( + self, + anime_id: str, + season: Optional[int] = None, + provider: Optional[str] = None + ) -> List[Dict]: + """Get anime episodes. + + Args: + anime_id: Anime ID + season: Season number (optional) + provider: Provider name (optional) + + Returns: + List of episodes + """ + params = {} + if season: + params["season"] = season + if provider: + params["provider"] = provider + + response = self.session.get( + f"{self.base_url}/api/anime/{anime_id}/episodes", + params=params + ) + response.raise_for_status() + data = response.json() + return data.get("episodes", []) + + def get_streams( + self, + anime_id: str, + episode_id: str, + provider: Optional[str] = None, + sort: str = "desc" + ) -> List[Dict]: + """Get episode streams. + + Args: + anime_id: Anime ID + episode_id: Episode ID + provider: Provider name (optional) + sort: Sort order ('asc' or 'desc') + + Returns: + List of streams + """ + params = {"sort": sort} + if provider: + params["provider"] = provider + + response = self.session.get( + f"{self.base_url}/api/anime/{anime_id}/episodes/{episode_id}/streams", + params=params + ) + response.raise_for_status() + data = response.json() + return data.get("streams", []) + + +def main(): + """Example usage of the WeebCLIClient.""" + # Initialize client + client = WeebCLIClient("http://localhost:8080") + + # Health check + print("=== Health Check ===") + health = client.health_check() + print(f"Status: {health['status']}") + print(f"Providers: {', '.join(health['providers'])}") + print() + + # List providers + print("=== Available Providers ===") + providers_data = client.list_providers() + for provider in providers_data["providers"]: + print(f"- {provider['name']} ({provider['lang']}/{provider['region']})") + print() + + # Search anime + print("=== Search Results ===") + query = "naruto" + results = client.search(query, provider="animecix") + print(f"Found {len(results)} results for '{query}':") + for i, anime in enumerate(results[:5], 1): + print(f"{i}. {anime['title']} ({anime['year']}) - ID: {anime['id']}") + print() + + if not results: + print("No results found. Exiting.") + return + + # Get anime details + print("=== Anime Details ===") + anime_id = results[0]["id"] + details = client.get_anime_details(anime_id, provider="animecix") + print(f"Title: {details['title']}") + print(f"Type: {details['type']}") + print(f"Year: {details['year']}") + print(f"Status: {details.get('status', 'N/A')}") + if details.get('genres'): + print(f"Genres: {', '.join(details['genres'])}") + print() + + # Get episodes + print("=== Episodes (Season 1) ===") + episodes = client.get_episodes(anime_id, season=1, provider="animecix") + print(f"Found {len(episodes)} episodes:") + for ep in episodes[:5]: + title = ep.get('title') or f"Episode {ep['number']}" + print(f"- S{ep['season']:02d}E{ep['number']:02d}: {title}") + print() + + if not episodes: + print("No episodes found. Exiting.") + return + + # Get streams for first episode + print("=== Streams (First Episode) ===") + episode_id = episodes[0]["id"] + streams = client.get_streams(anime_id, episode_id, provider="animecix") + print(f"Found {len(streams)} streams:") + for stream in streams: + print(f"- Quality: {stream['quality']}") + print(f" Server: {stream['server']}") + print(f" URL: {stream['url'][:60]}...") + if stream.get('subtitles'): + print(f" Subtitles: {stream['subtitles']}") + print() + + +if __name__ == "__main__": + try: + main() + except requests.exceptions.ConnectionError: + print("Error: Could not connect to weeb-cli RESTful API server.") + print("Make sure the server is running: weeb-cli serve restful") + except requests.exceptions.HTTPError as e: + print(f"HTTP Error: {e}") + except KeyboardInterrupt: + print("\nInterrupted by user.") From 747a825373aa6b440fc568844fdc454f68017a62 Mon Sep 17 00:00:00 2001 From: ewgsta <159681870+ewgsta@users.noreply.github.com> Date: Sun, 29 Mar 2026 12:18:12 +0300 Subject: [PATCH 4/4] feat(serve): implement RESTful API server with proper structure --- Dockerfile.restful | 1 - README.md | 6 +- docker-compose.restful.yml | 1 - docs/cli/restful-api.md | 19 +- docs/cli/restful-api.tr.md | 19 +- md/tr/README.md | 6 +- weeb_cli/commands/serve.py | 31 +- weeb_cli/commands/serve_restful.py | 475 +++++++++++++++-------------- weeb_cli/providers/en/hianime.py | 2 +- weeb_cli/providers/registry.py | 22 +- 10 files changed, 312 insertions(+), 270 deletions(-) diff --git a/Dockerfile.restful b/Dockerfile.restful index 6b1ca4e..99aa111 100644 --- a/Dockerfile.restful +++ b/Dockerfile.restful @@ -28,7 +28,6 @@ EXPOSE 8080 # Environment variables ENV RESTFUL_PORT=8080 ENV RESTFUL_HOST=0.0.0.0 -ENV RESTFUL_PROVIDERS=animecix,hianime,aniworld,docchi ENV RESTFUL_CORS=true ENV RESTFUL_DEBUG=false diff --git a/README.md b/README.md index 82ee4fc..a64d5f6 100644 --- a/README.md +++ b/README.md @@ -160,9 +160,7 @@ For web/mobile applications and custom integrations, weeb-cli provides a RESTful ```bash pip install weeb-cli[serve-restful] -weeb-cli serve restful --port 8080 \ - --providers animecix,hianime,aniworld,docchi \ - --cors +weeb-cli serve restful --port 8080 --cors ``` **API Endpoints:** @@ -173,6 +171,8 @@ weeb-cli serve restful --port 8080 \ - `GET /api/anime/{id}/episodes?season=1` - List episodes - `GET /api/anime/{id}/episodes/{ep_id}/streams` - Get stream URLs +All available providers are loaded automatically. Select which provider to use via the `provider` query parameter. + **Docker Support:** ```bash docker-compose -f docker-compose.restful.yml up -d diff --git a/docker-compose.restful.yml b/docker-compose.restful.yml index de936b5..973a67f 100644 --- a/docker-compose.restful.yml +++ b/docker-compose.restful.yml @@ -11,7 +11,6 @@ services: environment: - RESTFUL_PORT=8080 - RESTFUL_HOST=0.0.0.0 - - RESTFUL_PROVIDERS=animecix,hianime,aniworld,docchi - RESTFUL_CORS=true - RESTFUL_DEBUG=false restart: unless-stopped diff --git a/docs/cli/restful-api.md b/docs/cli/restful-api.md index 8267751..9b180d3 100644 --- a/docs/cli/restful-api.md +++ b/docs/cli/restful-api.md @@ -20,7 +20,7 @@ Start the server with default settings: weeb-cli serve restful ``` -The server will start on `http://0.0.0.0:8080` with all available providers. +The server will start on `http://0.0.0.0:8080` and automatically load all available providers. ### Custom Configuration @@ -28,21 +28,23 @@ The server will start on `http://0.0.0.0:8080` with all available providers. weeb-cli serve restful \ --port 9000 \ --host 127.0.0.1 \ - --providers animecix,hianime \ --no-cors \ --debug ``` +All available providers are loaded automatically. Select which provider to use via the `provider` query parameter in API requests. + ### Command Options | Option | Environment Variable | Default | Description | |--------|---------------------|---------|-------------| | `--port` | `RESTFUL_PORT` | `8080` | HTTP port to bind | | `--host` | `RESTFUL_HOST` | `0.0.0.0` | Host address to bind | -| `--providers` | `RESTFUL_PROVIDERS` | `animecix,hianime,aniworld,docchi` | Comma-separated provider names | | `--cors/--no-cors` | `RESTFUL_CORS` | `true` | Enable/disable CORS | | `--debug` | `RESTFUL_DEBUG` | `false` | Enable debug mode | +**Note:** All available providers are loaded automatically. Use the `provider` query parameter in API requests to select which provider to use. + ## API Endpoints ### Health Check @@ -234,13 +236,12 @@ Run the container: docker run -d \ --name weeb-cli-restful \ -p 8080:8080 \ - -e RESTFUL_PROVIDERS=animecix,hianime \ weeb-cli-restful ``` ### Environment Variables -All command options can be configured via environment variables: +Configure the server via environment variables: ```bash docker run -d \ @@ -248,7 +249,6 @@ docker run -d \ -p 9000:9000 \ -e RESTFUL_PORT=9000 \ -e RESTFUL_HOST=0.0.0.0 \ - -e RESTFUL_PROVIDERS=animecix,hianime,aniworld \ -e RESTFUL_CORS=true \ -e RESTFUL_DEBUG=false \ weeb-cli-restful @@ -369,7 +369,6 @@ Type=simple User=weeb WorkingDirectory=/opt/weeb-cli Environment="RESTFUL_PORT=8080" -Environment="RESTFUL_PROVIDERS=animecix,hianime" ExecStart=/usr/local/bin/weeb-cli serve restful Restart=always @@ -408,14 +407,14 @@ weeb-cli serve restful --port 9000 ### Provider Not Found -Ensure the provider name is correct: +Ensure the provider name is correct and available: ```bash # List available providers weeb-cli api providers -# Use correct provider name -weeb-cli serve restful --providers animecix,hianime +# Use correct provider name in API request +curl "http://localhost:8080/api/search?q=naruto&provider=animecix" ``` ### CORS Issues diff --git a/docs/cli/restful-api.tr.md b/docs/cli/restful-api.tr.md index b8590ac..d16323b 100644 --- a/docs/cli/restful-api.tr.md +++ b/docs/cli/restful-api.tr.md @@ -20,7 +20,7 @@ Sunucuyu varsayılan ayarlarla başlatın: weeb-cli serve restful ``` -Sunucu `http://0.0.0.0:8080` adresinde tüm mevcut provider'larla başlayacaktır. +Sunucu `http://0.0.0.0:8080` adresinde başlayacak ve tüm mevcut provider'ları otomatik olarak yükleyecektir. ### Özel Yapılandırma @@ -28,21 +28,23 @@ Sunucu `http://0.0.0.0:8080` adresinde tüm mevcut provider'larla başlayacaktı weeb-cli serve restful \ --port 9000 \ --host 127.0.0.1 \ - --providers animecix,hianime \ --no-cors \ --debug ``` +Tüm mevcut provider'lar otomatik olarak yüklenir. API isteklerinde `provider` sorgu parametresi ile hangi provider'ı kullanacağınızı seçin. + ### Komut Seçenekleri | Seçenek | Ortam Değişkeni | Varsayılan | Açıklama | |---------|----------------|-----------|----------| | `--port` | `RESTFUL_PORT` | `8080` | Bağlanılacak HTTP portu | | `--host` | `RESTFUL_HOST` | `0.0.0.0` | Bağlanılacak host adresi | -| `--providers` | `RESTFUL_PROVIDERS` | `animecix,hianime,aniworld,docchi` | Virgülle ayrılmış provider isimleri | | `--cors/--no-cors` | `RESTFUL_CORS` | `true` | CORS'u etkinleştir/devre dışı bırak | | `--debug` | `RESTFUL_DEBUG` | `false` | Debug modunu etkinleştir | +**Not:** Tüm mevcut provider'lar otomatik olarak yüklenir. API isteklerinde `provider` sorgu parametresi ile hangi provider'ı kullanacağınızı seçin. + ## API Endpoint'leri ### Sağlık Kontrolü @@ -234,13 +236,12 @@ Container'ı çalıştırın: docker run -d \ --name weeb-cli-restful \ -p 8080:8080 \ - -e RESTFUL_PROVIDERS=animecix,hianime \ weeb-cli-restful ``` ### Ortam Değişkenleri -Tüm komut seçenekleri ortam değişkenleri ile yapılandırılabilir: +Sunucuyu ortam değişkenleri ile yapılandırın: ```bash docker run -d \ @@ -248,7 +249,6 @@ docker run -d \ -p 9000:9000 \ -e RESTFUL_PORT=9000 \ -e RESTFUL_HOST=0.0.0.0 \ - -e RESTFUL_PROVIDERS=animecix,hianime,aniworld \ -e RESTFUL_CORS=true \ -e RESTFUL_DEBUG=false \ weeb-cli-restful @@ -369,7 +369,6 @@ Type=simple User=weeb WorkingDirectory=/opt/weeb-cli Environment="RESTFUL_PORT=8080" -Environment="RESTFUL_PROVIDERS=animecix,hianime" ExecStart=/usr/local/bin/weeb-cli serve restful Restart=always @@ -408,14 +407,14 @@ weeb-cli serve restful --port 9000 ### Provider Bulunamadı -Provider adının doğru olduğundan emin olun: +Provider adının doğru ve mevcut olduğundan emin olun: ```bash # Mevcut provider'ları listele weeb-cli api providers -# Doğru provider adını kullan -weeb-cli serve restful --providers animecix,hianime +# API isteğinde doğru provider adını kullan +curl "http://localhost:8080/api/search?q=naruto&provider=animecix" ``` ### CORS Sorunları diff --git a/md/tr/README.md b/md/tr/README.md index 233fa37..648e880 100644 --- a/md/tr/README.md +++ b/md/tr/README.md @@ -159,9 +159,7 @@ Web/mobil uygulamalar ve özel entegrasyonlar için weeb-cli bir RESTful API sun ```bash pip install weeb-cli[serve-restful] -weeb-cli serve restful --port 8080 \ - --providers animecix,hianime,aniworld,docchi \ - --cors +weeb-cli serve restful --port 8080 --cors ``` **API Endpoint'leri:** @@ -172,6 +170,8 @@ weeb-cli serve restful --port 8080 \ - `GET /api/anime/{id}/episodes?season=1` - Bölümleri listele - `GET /api/anime/{id}/episodes/{ep_id}/streams` - Stream URL'lerini al +Tüm mevcut provider'lar otomatik olarak yüklenir. `provider` sorgu parametresi ile hangi provider'ı kullanacağınızı seçin. + **Docker Desteği:** ```bash docker-compose -f docker-compose.restful.yml up -d diff --git a/weeb_cli/commands/serve.py b/weeb_cli/commands/serve.py index 407a586..4b8dead 100644 --- a/weeb_cli/commands/serve.py +++ b/weeb_cli/commands/serve.py @@ -32,10 +32,23 @@ def _sanitize_for_release(name: str) -> str: serve_app = typer.Typer( name="serve", - help="Start a Torznab-compatible server for Sonarr/*arr integration or RESTful API server.", + help="Start server modes: Torznab for Sonarr/*arr integration or RESTful API server.", add_completion=False, + invoke_without_command=True, ) +@serve_app.callback() +def serve_callback(ctx: typer.Context): + """Server modes for weeb-cli. + + Available modes: + - torznab: Torznab-compatible server for Sonarr/*arr integration + - restful: RESTful API server for web/mobile applications + """ + if ctx.invoked_subcommand is None: + typer.echo(ctx.get_help()) + raise typer.Exit(0) + # Import restful subcommand try: from weeb_cli.commands.serve_restful import restful_app @@ -213,10 +226,16 @@ def _get_fallback_streams(fallback_providers, sonarr_title, season, episode_num) return [] -# -- Main serve command ------------------------------------------------------- +# -- Torznab serve command ---------------------------------------------------- + +torznab_app = typer.Typer( + name="torznab", + help="Start Torznab-compatible server for Sonarr/*arr integration.", + add_completion=False, +) -@serve_app.callback(invoke_without_command=True) -def serve( +@torznab_app.callback(invoke_without_command=True) +def serve_torznab( ctx: typer.Context, port: int = typer.Option(9876, "--port", envvar="FLASK_PORT", help="HTTP port"), watch_dir: str = typer.Option("/downloads/weeb-cli/watch", "--watch-dir", envvar="WATCH_DIR", help="Blackhole watch directory"), @@ -477,3 +496,7 @@ def blackhole_worker(): worker.start() flask_app.run(host="0.0.0.0", port=port, debug=False) + + +# Register torznab subcommand +serve_app.add_typer(torznab_app, name="torznab") diff --git a/weeb_cli/commands/serve_restful.py b/weeb_cli/commands/serve_restful.py index b43af5f..1123ac0 100644 --- a/weeb_cli/commands/serve_restful.py +++ b/weeb_cli/commands/serve_restful.py @@ -1,26 +1,24 @@ """RESTful API server for weeb-cli. -Provides a REST API interface for all provider operations including search, -episode listing, stream extraction, and anime details. +Provides HTTP endpoints that mirror the 'weeb-cli api' commands. +All endpoints return JSON responses identical to the CLI API mode. Requires: pip install weeb-cli[serve-restful] -Usage: weeb-cli serve restful --port 8080 --providers animecix,hianime +Usage: weeb-cli serve restful --port 8080 """ -import json import logging import sys -from typing import Optional, List +from typing import Optional import typer -from flask import Flask, request, jsonify, Response -from flask_cors import CORS log = logging.getLogger("weeb-cli-restful") restful_app = typer.Typer( name="restful", - help="Start a RESTful API server for provider operations.", + help="Start a RESTful API server (HTTP version of 'weeb-cli api' commands).", add_completion=False, + invoke_without_command=True, ) @@ -40,71 +38,26 @@ def _quality_score(q: str) -> int: return 0 -def _serialize_anime_result(result) -> dict: - """Serialize AnimeResult to dict.""" - return { - "id": result.id, - "title": result.title, - "type": result.type, - "cover": result.cover, - "year": result.year, - } - - -def _serialize_episode(episode) -> dict: - """Serialize Episode to dict.""" - return { - "id": episode.id, - "number": episode.number, - "title": episode.title, - "season": episode.season, - "url": episode.url, - } - - -def _serialize_stream(stream) -> dict: - """Serialize StreamLink to dict.""" - return { - "url": stream.url, - "quality": stream.quality, - "server": stream.server, - "headers": stream.headers, - "subtitles": stream.subtitles, - } - - -def _serialize_anime_details(details) -> dict: - """Serialize AnimeDetails to dict.""" - return { - "id": details.id, - "title": details.title, - "type": details.type, - "cover": details.cover, - "year": details.year, - "description": details.description, - "genres": details.genres, - "status": details.status, - "episodes": [_serialize_episode(ep) for ep in details.episodes] if details.episodes else [], - } - - @restful_app.callback(invoke_without_command=True) def serve_restful( ctx: typer.Context, port: int = typer.Option(8080, "--port", envvar="RESTFUL_PORT", help="HTTP port"), host: str = typer.Option("0.0.0.0", "--host", envvar="RESTFUL_HOST", help="Bind host"), - provider_names: str = typer.Option( - "animecix,hianime,aniworld,docchi", - "--providers", - envvar="RESTFUL_PROVIDERS", - help="Comma-separated provider names", - ), enable_cors: bool = typer.Option(True, "--cors/--no-cors", envvar="RESTFUL_CORS", help="Enable CORS"), debug: bool = typer.Option(False, "--debug", envvar="RESTFUL_DEBUG", help="Enable debug mode"), ): - """Start RESTful API server.""" + """Start RESTful API server. + + Provides HTTP endpoints that mirror 'weeb-cli api' commands: + - GET /api/providers -> weeb-cli api providers + - GET /api/search -> weeb-cli api search + - GET /api/episodes -> weeb-cli api episodes + - GET /api/streams -> weeb-cli api streams + - GET /api/details -> weeb-cli api details + - POST /api/download -> weeb-cli api download + """ try: - from flask import Flask + from flask import Flask, request, jsonify except ImportError: typer.echo( "Flask is required for serve restful mode. Install with: pip install weeb-cli[serve-restful]", @@ -124,250 +77,308 @@ def serve_restful( stream=sys.stdout, ) - # Load providers + # Import provider functions from weeb_cli.providers.registry import get_provider, list_providers as list_all_providers - - providers = {} - for name in provider_names.split(","): - name = name.strip() - p = get_provider(name) - if p: - providers[name] = p - log.info(f"Loaded provider: {name}") - else: - log.warning(f"Provider not found: {name}") - - if not providers: - log.error("No providers loaded, exiting.") - raise typer.Exit(1) # Create Flask app flask_app = Flask(__name__) if enable_cors: - CORS(flask_app) - log.info("CORS enabled") + try: + from flask_cors import CORS + CORS(flask_app) + log.info("CORS enabled") + except ImportError: + log.warning("flask-cors not installed, CORS disabled") + + # Helper function + def _get_provider(name: str): + provider = get_provider(name) + if provider is None: + return None + return provider # Health check endpoint @flask_app.route("/health", methods=["GET"]) def health(): """Health check endpoint.""" + all_providers = list_all_providers() + # Only show enabled providers in health check + enabled_providers = [p["name"] for p in all_providers if not p.get("disabled", False)] return jsonify({ "status": "ok", "service": "weeb-cli-restful", - "providers": list(providers.keys()), + "providers": enabled_providers, }) - # List all available providers + # GET /api/providers - List all available providers @flask_app.route("/api/providers", methods=["GET"]) def api_providers(): - """List all available providers.""" + """List all available providers. + + Equivalent to: weeb-cli api providers + """ try: - all_providers = list_all_providers() - return jsonify({ - "success": True, - "providers": all_providers, - "loaded": list(providers.keys()), - }) + providers = list_all_providers() + return jsonify(providers) except Exception as e: log.error(f"Error listing providers: {e}") - return jsonify({"success": False, "error": str(e)}), 500 + return jsonify({"error": str(e)}), 500 - # Search anime + # GET /api/search?query=...&provider=... - Search anime @flask_app.route("/api/search", methods=["GET"]) def api_search(): - """Search anime across providers. + """Search anime. Query params: - q: Search query (required) - provider: Provider name (optional, defaults to first loaded) + query: Search query (required) + provider: Provider name (default: animecix) + + Equivalent to: weeb-cli api search "query" --provider animecix """ - query = request.args.get("q", "").strip() - provider_name = request.args.get("provider", "").strip() + query = request.args.get("query", "").strip() + provider_name = request.args.get("provider", "animecix").strip() if not query: - return jsonify({"success": False, "error": "Missing query parameter 'q'"}), 400 - - # Select provider - if provider_name: - if provider_name not in providers: - return jsonify({ - "success": False, - "error": f"Provider '{provider_name}' not loaded", - "available": list(providers.keys()), - }), 404 - provider = providers[provider_name] - else: - provider = next(iter(providers.values())) - provider_name = provider.name + return jsonify({"error": "Missing query parameter"}), 400 + + provider = _get_provider(provider_name) + if provider is None: + return jsonify({"error": f"Unknown provider: {provider_name}"}), 404 try: - log.info(f"Search: query='{query}' provider={provider_name}") results = provider.search(query) - - return jsonify({ - "success": True, - "provider": provider_name, - "query": query, - "count": len(results), - "results": [_serialize_anime_result(r) for r in results], - }) + return jsonify([ + {"id": r.id, "title": r.title, "type": r.type, "cover": r.cover, "year": r.year} + for r in results + ]) except Exception as e: log.error(f"Search error: {e}") - return jsonify({"success": False, "error": str(e)}), 500 - - # Get anime details - @flask_app.route("/api/anime/", methods=["GET"]) - def api_anime_details(anime_id: str): - """Get anime details. - - Query params: - provider: Provider name (optional, defaults to first loaded) - """ - provider_name = request.args.get("provider", "").strip() - - # Select provider - if provider_name: - if provider_name not in providers: - return jsonify({ - "success": False, - "error": f"Provider '{provider_name}' not loaded", - "available": list(providers.keys()), - }), 404 - provider = providers[provider_name] - else: - provider = next(iter(providers.values())) - provider_name = provider.name - - try: - log.info(f"Details: anime_id='{anime_id}' provider={provider_name}") - details = provider.get_details(anime_id) - - if not details: - return jsonify({ - "success": False, - "error": "Anime not found", - }), 404 - - return jsonify({ - "success": True, - "provider": provider_name, - "anime": _serialize_anime_details(details), - }) - except Exception as e: - log.error(f"Details error: {e}") - return jsonify({"success": False, "error": str(e)}), 500 + return jsonify({"error": str(e)}), 500 - # Get episodes - @flask_app.route("/api/anime//episodes", methods=["GET"]) - def api_episodes(anime_id: str): + # GET /api/episodes?anime_id=...&provider=...&season=... - Get episodes + @flask_app.route("/api/episodes", methods=["GET"]) + def api_episodes(): """Get anime episodes. Query params: - provider: Provider name (optional, defaults to first loaded) + anime_id: Anime ID (required) + provider: Provider name (default: animecix) season: Filter by season number (optional) + + Equivalent to: weeb-cli api episodes --provider animecix --season 1 """ - provider_name = request.args.get("provider", "").strip() + anime_id = request.args.get("anime_id", "").strip() + provider_name = request.args.get("provider", "animecix").strip() season_filter = request.args.get("season") - # Select provider - if provider_name: - if provider_name not in providers: - return jsonify({ - "success": False, - "error": f"Provider '{provider_name}' not loaded", - "available": list(providers.keys()), - }), 404 - provider = providers[provider_name] - else: - provider = next(iter(providers.values())) - provider_name = provider.name + if not anime_id: + return jsonify({"error": "Missing anime_id parameter"}), 400 + + provider = _get_provider(provider_name) + if provider is None: + return jsonify({"error": f"Unknown provider: {provider_name}"}), 404 try: - log.info(f"Episodes: anime_id='{anime_id}' provider={provider_name} season={season_filter}") - episodes = provider.get_episodes(anime_id) + eps = provider.get_episodes(anime_id) - # Filter by season if requested - if season_filter: + if season_filter is not None: try: season_num = int(season_filter) - episodes = [ep for ep in episodes if ep.season == season_num] + eps = [e for e in eps if e.season == season_num] except ValueError: - return jsonify({"success": False, "error": "Invalid season number"}), 400 + return jsonify({"error": "Invalid season number"}), 400 - return jsonify({ - "success": True, - "provider": provider_name, - "anime_id": anime_id, - "count": len(episodes), - "episodes": [_serialize_episode(ep) for ep in episodes], - }) + return jsonify([ + {"id": e.id, "number": e.number, "title": e.title, "season": e.season, "url": e.url} + for e in eps + ]) except Exception as e: log.error(f"Episodes error: {e}") - return jsonify({"success": False, "error": str(e)}), 500 + return jsonify({"error": str(e)}), 500 - # Get streams - @flask_app.route("/api/anime//episodes//streams", methods=["GET"]) - def api_streams(anime_id: str, episode_id: str): + # GET /api/streams?anime_id=...&season=...&episode=...&provider=... - Get streams + @flask_app.route("/api/streams", methods=["GET"]) + def api_streams(): """Get episode streams. Query params: - provider: Provider name (optional, defaults to first loaded) - sort: Sort by quality (asc/desc, optional) + anime_id: Anime ID (required) + season: Season number (default: 1) + episode: Episode number (required) + provider: Provider name (default: animecix) + + Equivalent to: weeb-cli api streams --season 1 --episode 1 --provider animecix """ - provider_name = request.args.get("provider", "").strip() - sort_order = request.args.get("sort", "desc").lower() - - # Select provider - if provider_name: - if provider_name not in providers: - return jsonify({ - "success": False, - "error": f"Provider '{provider_name}' not loaded", - "available": list(providers.keys()), - }), 404 - provider = providers[provider_name] - else: - provider = next(iter(providers.values())) - provider_name = provider.name + anime_id = request.args.get("anime_id", "").strip() + provider_name = request.args.get("provider", "animecix").strip() + + try: + season = int(request.args.get("season", "1")) + episode = int(request.args.get("episode", "0")) + except ValueError: + return jsonify({"error": "Invalid season or episode number"}), 400 + + if not anime_id: + return jsonify({"error": "Missing anime_id parameter"}), 400 + if episode == 0: + return jsonify({"error": "Missing episode parameter"}), 400 + + provider = _get_provider(provider_name) + if provider is None: + return jsonify({"error": f"Unknown provider: {provider_name}"}), 404 try: - log.info(f"Streams: anime_id='{anime_id}' episode_id='{episode_id}' provider={provider_name}") - streams = provider.get_streams(anime_id, episode_id) + eps = provider.get_episodes(anime_id) + target = [e for e in eps if e.season == season and e.number == episode] + + if not target: + return jsonify({"error": f"Episode S{season:02d}E{episode:02d} not found"}), 404 + + ep = target[0] + links = provider.get_streams(anime_id, ep.id) - # Sort by quality - if sort_order == "desc": - streams = sorted(streams, key=lambda s: _quality_score(s.quality), reverse=True) - elif sort_order == "asc": - streams = sorted(streams, key=lambda s: _quality_score(s.quality)) + return jsonify([ + {"url": s.url, "quality": s.quality, "server": s.server, "headers": s.headers, "subtitles": s.subtitles} + for s in links + ]) + except Exception as e: + log.error(f"Streams error: {e}") + return jsonify({"error": str(e)}), 500 + + # GET /api/details?anime_id=...&provider=... - Get anime details + @flask_app.route("/api/details", methods=["GET"]) + def api_details(): + """Get anime details. + + Query params: + anime_id: Anime ID (required) + provider: Provider name (default: animecix) + + Equivalent to: weeb-cli api details --provider animecix + """ + anime_id = request.args.get("anime_id", "").strip() + provider_name = request.args.get("provider", "animecix").strip() + + if not anime_id: + return jsonify({"error": "Missing anime_id parameter"}), 400 + + provider = _get_provider(provider_name) + if provider is None: + return jsonify({"error": f"Unknown provider: {provider_name}"}), 404 + + try: + d = provider.get_details(anime_id) + if d is None: + return jsonify({"error": "Not found"}), 404 return jsonify({ - "success": True, - "provider": provider_name, - "anime_id": anime_id, - "episode_id": episode_id, - "count": len(streams), - "streams": [_serialize_stream(s) for s in streams], + "id": d.id, "title": d.title, "description": d.description, "cover": d.cover, + "genres": d.genres, "year": d.year, "status": d.status, "total_episodes": d.total_episodes, + "episodes": [ + {"id": e.id, "number": e.number, "title": e.title, "season": e.season} + for e in d.episodes + ], }) except Exception as e: - log.error(f"Streams error: {e}") - return jsonify({"success": False, "error": str(e)}), 500 + log.error(f"Details error: {e}") + return jsonify({"error": str(e)}), 500 + + # POST /api/download - Download episode + @flask_app.route("/api/download", methods=["POST"]) + def api_download(): + """Download episode. + + JSON body: + anime_id: Anime ID (required) + season: Season number (default: 1) + episode: Episode number (required) + provider: Provider name (default: animecix) + output: Output directory (default: ".") + + Equivalent to: weeb-cli api download --season 1 --episode 1 --provider animecix --output . + """ + from weeb_cli.services.headless_downloader import download_episode + + data = request.get_json() or {} + + anime_id = data.get("anime_id", "").strip() + provider_name = data.get("provider", "animecix").strip() + season = data.get("season", 1) + episode_num = data.get("episode", 0) + output = data.get("output", ".") + + if not anime_id: + return jsonify({"status": "error", "message": "Missing anime_id"}), 400 + if episode_num == 0: + return jsonify({"status": "error", "message": "Missing episode"}), 400 + + provider = _get_provider(provider_name) + if provider is None: + return jsonify({"status": "error", "message": f"Unknown provider: {provider_name}"}), 404 + + try: + eps = provider.get_episodes(anime_id) + target = [e for e in eps if e.season == season and e.number == episode_num] + + if not target: + return jsonify({"status": "error", "message": f"Episode S{season:02d}E{episode_num:02d} not found"}), 404 + + ep = target[0] + stream_links = provider.get_streams(anime_id, ep.id) + + if not stream_links: + return jsonify({"status": "error", "message": "No streams available"}), 404 + + stream_links.sort(key=lambda s: _quality_score(s.quality), reverse=True) + + # Try to get the anime title for the filename + d = provider.get_details(anime_id) + title = d.title if d else anime_id + + for stream in stream_links: + result = download_episode( + stream_url=stream.url, + series_title=title, + season=season, + episode=episode_num, + download_dir=output, + ) + if result: + return jsonify({"status": "ok", "path": result, "anime": title, "quality": stream.quality}) + + return jsonify({"status": "error", "message": "All streams failed"}), 500 + + except Exception as e: + log.error(f"Download error: {e}") + return jsonify({"status": "error", "message": str(e)}), 500 # Error handlers @flask_app.errorhandler(404) def not_found(e): - return jsonify({"success": False, "error": "Endpoint not found"}), 404 + return jsonify({"error": "Endpoint not found"}), 404 @flask_app.errorhandler(500) def internal_error(e): - return jsonify({"success": False, "error": "Internal server error"}), 500 + return jsonify({"error": "Internal server error"}), 500 # Start server + log.info("=" * 60) log.info("weeb-cli RESTful API starting") + log.info("=" * 60) log.info(f" Host: {host}") log.info(f" Port: {port}") - log.info(f" Providers: {', '.join(providers.keys())}") log.info(f" CORS: {'enabled' if enable_cors else 'disabled'}") log.info(f" Debug: {'enabled' if debug else 'disabled'}") + log.info("=" * 60) + log.info(" Endpoints (mirror 'weeb-cli api' commands):") + log.info(" GET /health") + log.info(" GET /api/providers") + log.info(" GET /api/search?query=...&provider=...") + log.info(" GET /api/episodes?anime_id=...&provider=...&season=...") + log.info(" GET /api/streams?anime_id=...&season=...&episode=...&provider=...") + log.info(" GET /api/details?anime_id=...&provider=...") + log.info(" POST /api/download (JSON body)") + log.info("=" * 60) flask_app.run(host=host, port=port, debug=debug) diff --git a/weeb_cli/providers/en/hianime.py b/weeb_cli/providers/en/hianime.py index 9f5522b..d9cb520 100644 --- a/weeb_cli/providers/en/hianime.py +++ b/weeb_cli/providers/en/hianime.py @@ -47,7 +47,7 @@ def _get_html(url: str, headers: dict = None) -> str: return "" -@register_provider("hianime", lang="en", region="US") +@register_provider("hianime", lang="en", region="US", disabled=True) class HiAnimeProvider(BaseProvider): def __init__(self): diff --git a/weeb_cli/providers/registry.py b/weeb_cli/providers/registry.py index f94b6a7..53ab20a 100644 --- a/weeb_cli/providers/registry.py +++ b/weeb_cli/providers/registry.py @@ -52,16 +52,17 @@ class MyProvider(BaseProvider): _initialized: bool = False -def register_provider(name: str, lang: str = "tr", region: str = "TR"): +def register_provider(name: str, lang: str = "tr", region: str = "TR", disabled: bool = False): """Decorator for registering provider classes. Registers a provider class with metadata in the global registry. - Sets class attributes (name, lang, region) for easy access. + Sets class attributes (name, lang, region, disabled) for easy access. Args: name: Unique provider identifier (e.g., 'animecix', 'hianime'). lang: Language code (e.g., 'en', 'tr', 'de', 'pl'). region: Region code (e.g., 'US', 'TR', 'DE', 'PL'). + disabled: If True, provider is registered but not available for use. Returns: Decorator function that registers the class. @@ -70,18 +71,24 @@ def register_provider(name: str, lang: str = "tr", region: str = "TR"): >>> @register_provider("myprovider", lang="en", region="US") ... class MyProvider(BaseProvider): ... pass + + >>> @register_provider("oldprovider", lang="en", region="US", disabled=True) + ... class OldProvider(BaseProvider): + ... pass # Registered but not usable """ def decorator(cls: Type[BaseProvider]) -> Type[BaseProvider]: cls.name = name cls.lang = lang cls.region = region + cls.disabled = disabled _providers[name] = cls _provider_meta[name] = { "name": name, "lang": lang, "region": region, - "class": cls.__name__ + "class": cls.__name__, + "disabled": disabled } return cls @@ -124,13 +131,14 @@ def get_provider(name: str) -> Optional[BaseProvider]: """Get provider instance by name. Triggers provider discovery if not already done, then returns - a new instance of the requested provider. + a new instance of the requested provider. Returns None if provider + is disabled or not found. Args: name: Provider identifier (e.g., 'animecix', 'hianime'). Returns: - Provider instance, or None if not found. + Provider instance, or None if not found or disabled. Example: >>> provider = get_provider("animecix") @@ -139,6 +147,10 @@ def get_provider(name: str) -> Optional[BaseProvider]: """ _discover_providers() if name in _providers: + # Check if provider is disabled + if _provider_meta.get(name, {}).get("disabled", False): + debug(f"[Registry] Provider '{name}' is disabled") + return None return _providers[name]() return None