{"id":43654725,"url":"https://github.com/andronics/tvmaze-sync","last_synced_at":"2026-02-04T20:31:44.265Z","repository":{"id":330919675,"uuid":"1124432818","full_name":"andronics/tvmaze-sync","owner":"andronics","description":"Automated TV show discovery and Sonarr integration powered by TVMaze API","archived":false,"fork":false,"pushed_at":"2025-12-29T22:28:11.000Z","size":166,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-12-31T22:26:08.152Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/andronics.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-12-29T02:37:34.000Z","updated_at":"2025-12-29T19:32:20.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/andronics/tvmaze-sync","commit_stats":null,"previous_names":["andronics/tvmaze-sync"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/andronics/tvmaze-sync","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andronics%2Ftvmaze-sync","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andronics%2Ftvmaze-sync/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andronics%2Ftvmaze-sync/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andronics%2Ftvmaze-sync/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/andronics","download_url":"https://codeload.github.com/andronics/tvmaze-sync/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andronics%2Ftvmaze-sync/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29095391,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-04T20:17:23.003Z","status":"ssl_error","status_checked_at":"2026-02-04T20:16:36.396Z","response_time":62,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-02-04T20:31:43.310Z","updated_at":"2026-02-04T20:31:44.255Z","avatar_url":"https://github.com/andronics.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# TVMaze-Sync\n\n\u003e Automated TV show discovery and Sonarr integration powered by TVMaze API\n\nTVMaze-Sync is a Docker-native service that automatically discovers TV shows from TVMaze and adds them to Sonarr based on configurable filters. It maintains a local SQLite cache for efficient syncing and instant filter re-evaluation.\n\n[![Docker Image](https://img.shields.io/badge/docker-ghcr.io-blue)](https://github.com/andronics/tvmaze-sync/pkgs/container/tvmaze-sync)\n[![Docker Pulls](https://img.shields.io/docker/pulls/andronics/tvmaze-sync)](https://github.com/andronics/tvmaze-sync/pkgs/container/tvmaze-sync)\n\n## Features\n\n- ✅ **Automated Discovery**: Automatically sync TV shows from TVMaze to Sonarr\n- 🎯 **Smart Selections**: Multiple selection rules with global excludes - filter by genre, language, country, type, status, dates, rating, and runtime\n- 💾 **Efficient Caching**: SQLite database cache (~70k shows, ~15-20MB)\n- 🔄 **Incremental Syncing**: Initial full sync, then efficient incremental updates\n- 🧪 **Dry Run Mode**: Test filters without actually adding shows to Sonarr\n- 📊 **Prometheus Metrics**: Built-in metrics for monitoring\n- 🔍 **HTTP API**: RESTful endpoints for status, manual triggers, and queries\n- 🐳 **Docker Native**: Full Docker and docker-compose support\n- 🔐 **Secrets Support**: Docker secrets for API keys\n- ⚡ **Rate Limiting**: Built-in TVMaze API rate limiting (20 req/10s)\n\n## Quick Start\n\n### Docker Compose (Recommended)\n\n```bash\n# Clone the repository\ngit clone https://github.com/andronics/tvmaze-sync.git\ncd tvmaze-sync\n\n# Create configuration\ncp config.example.yaml config.yaml\n# Edit config.yaml with your Sonarr details\n\n# Run with Docker Compose\ndocker-compose up -d\n\n# Check logs\ndocker-compose logs -f\n```\n\n### Docker\n\n```bash\ndocker run -d \\\n  --name tvmaze-sync \\\n  -v /path/to/config:/config \\\n  -v /path/to/data:/data \\\n  -e SONARR_URL=http://sonarr:8989 \\\n  -e SONARR_API_KEY=your-api-key \\\n  -e SONARR_ROOT_FOLDER=/tv \\\n  -e SONARR_QUALITY_PROFILE=HD-1080p \\\n  -p 8080:8080 \\\n  ghcr.io/andronics/tvmaze-sync:latest\n```\n\n### Local Development\n\n```bash\n# Create virtual environment\npython -m venv venv\nsource venv/bin/activate  # On Windows: venv\\Scripts\\activate\n\n# Install package with dependencies\npip install -e .\n\n# Or install development dependencies\npip install -e \".[dev]\"\n\n# Run the application\nCONFIG_PATH=./config.yaml tvmaze-sync\n\n# Or use the module directly\npython -m src.main\n```\n\n## Configuration\n\nConfiguration is done via `config.yaml` or environment variables. All config values support `${VAR}` and `${VAR_FILE}` (Docker secrets) patterns.\n\n### Minimal Configuration\n\n```yaml\nsonarr:\n  url: \"${SONARR_URL}\"\n  api_key: \"${SONARR_API_KEY}\"\n  root_folder: \"/tv\"\n  quality_profile: \"HD-1080p\"\n\n# At least one selection is required\nselections:\n  - name: \"English Shows\"\n    languages: [\"English\"]\n```\n\n### Full Configuration\n\nSee `config.example.yaml` for all available options.\n\n```yaml\ntvmaze:\n  api_key: \"${TVMAZE_API_KEY}\"      # Optional premium key for higher rate limits\n  rate_limit: 20                     # Requests per 10 seconds (20 for free, 100 for premium)\n  update_window: \"week\"              # day, week, or month\n\nsync:\n  poll_interval: \"6h\"                # How often to sync (s, m, h, d, w, y)\n  retry_delay: \"1w\"                  # Retry pending_tvdb shows after this delay\n  abandon_after: \"1y\"                # Abandon pending_tvdb shows after this time\n\n# Global exclusions - shows matching ANY criteria are rejected\nexclude:\n  genres: [\"Reality\", \"Talk Show\", \"Game Show\", \"News\", \"Sports\"]\n  types: []\n  languages: []\n  countries: []\n  networks: [\"Home Shopping Network\"]\n\n# Selections - show must match ALL criteria within a selection,\n# but only needs to match ONE selection overall (OR logic)\n# Empty selections list = all shows rejected\nselections:\n  - name: \"English Scripted\"\n    languages: [\"English\"]\n    countries: [\"US\", \"GB\", \"CA\", \"AU\"]     # ISO 3166-1 codes (GB, not UK)\n    types: [\"Scripted\", \"Animation\"]\n    # status: [\"Running\", \"In Development\"]  # Optional status filter\n    premiered:\n      after: \"2010-01-01\"                    # ISO format YYYY-MM-DD\n      # before: \"2025-12-31\"\n    rating:\n      min: 6.0                               # TVMaze rating (0-10)\n      # max: 10.0\n    runtime:\n      min: 20                                # Minutes\n      # max: 90\n\n  - name: \"Quality Documentaries\"\n    types: [\"Documentary\"]\n    rating:\n      min: 7.5\n    premiered:\n      after: \"2015-01-01\"\n\nsonarr:\n  url: \"${SONARR_URL}\"\n  api_key: \"${SONARR_API_KEY}\"\n  root_folder: \"/tv\"                 # Path or folder ID\n  quality_profile: \"HD-1080p\"        # Name or profile ID\n  language_profile: \"English\"        # Sonarr v3 only (auto-detected)\n  monitor: \"all\"                     # all, future, missing, existing, pilot, etc.\n  search_on_add: true\n  tags: [\"tvmaze\", \"auto\"]           # Optional tags\n\nstorage:\n  path: \"/data\"                      # Data directory for database and state\n\nlogging:\n  level: \"INFO\"                      # DEBUG, INFO, WARNING, ERROR, CRITICAL\n  format: \"json\"                     # json or text\n\nserver:\n  enabled: true\n  port: 8080\n\ndry_run: false                       # Defaults to true - set false when ready to add shows\n```\n\n## Environment Variables\n\nAll configuration options can be set via environment variables using `SECTION_KEY_SUBKEY` naming convention:\n\n### Required Variables\n\n| Variable | Description |\n|----------|-------------|\n| `SONARR_URL` | Sonarr base URL (e.g., `http://localhost:8989`) |\n| `SONARR_API_KEY` | Sonarr API key |\n| `SONARR_ROOT_FOLDER` | Root folder path or ID |\n| `SONARR_QUALITY_PROFILE` | Quality profile name or ID |\n| `SERVER_API_KEY` | API key for protected HTTP endpoints |\n\n### Optional Variables\n\n| Variable | Description | Default |\n|----------|-------------|---------|\n| `CONFIG_PATH` | Path to config file | `/config/config.yaml` |\n| `TVMAZE_API_KEY` | TVMaze premium API key | (none) |\n| `TVMAZE_RATE_LIMIT` | Requests per 10 seconds | `20` |\n| `TVMAZE_UPDATE_WINDOW` | Update check window | `week` |\n| `SYNC_POLL_INTERVAL` | Sync frequency | `6h` |\n| `SYNC_RETRY_DELAY` | Retry pending_tvdb after | `1w` |\n| `SYNC_ABANDON_AFTER` | Abandon pending_tvdb after | `1y` |\n| `EXCLUDE_GENRES` | Comma-separated excluded genres | (none) |\n| `EXCLUDE_TYPES` | Comma-separated excluded types | (none) |\n| `EXCLUDE_LANGUAGES` | Comma-separated excluded languages | (none) |\n| `EXCLUDE_COUNTRIES` | Comma-separated excluded countries | (none) |\n| `EXCLUDE_NETWORKS` | Comma-separated excluded networks | (none) |\n| `SONARR_LANGUAGE_PROFILE` | Language profile (v3 only) | (none) |\n| `SONARR_MONITOR` | Monitor mode | `all` |\n| `SONARR_SEARCH_ON_ADD` | Search after adding | `true` |\n| `SONARR_TAGS` | Comma-separated tags | (none) |\n| `STORAGE_PATH` | Data directory | `/data` |\n| `LOGGING_LEVEL` | Log level | `INFO` |\n| `LOGGING_FORMAT` | Log format (json/text) | `json` |\n| `SERVER_ENABLED` | Enable HTTP server | `true` |\n| `SERVER_PORT` | HTTP server port | `8080` |\n| `DRY_RUN` | Don't actually add to Sonarr | `true` |\n\n\u003e **Note:** Selections cannot be configured via environment variables - use `config.yaml` for complex selection rules.\n\n### Docker Secrets Support\n\nFor sensitive values, use the `_FILE` suffix to read from a file (useful for Docker secrets):\n\n```yaml\nsonarr:\n  api_key: \"${SONARR_API_KEY_FILE}\"\n```\n\n```bash\n# Docker Compose with secrets\ndocker-compose.yml:\n  secrets:\n    - sonarr_api_key\n  environment:\n    - SONARR_API_KEY_FILE=/run/secrets/sonarr_api_key\n```\n\n## API Endpoints\n\nThe built-in HTTP server exposes several endpoints for monitoring and control.\n\n### Authentication\n\nProtected endpoints require an API key via:\n- Header: `X-API-Key: your-api-key`\n- Query param: `?api_key=your-api-key`\n\n| Endpoint | Method | Auth | Description |\n|----------|--------|------|-------------|\n| `/health` | GET | Public | Liveness probe (always returns 200 if running) |\n| `/ready` | GET | Public | Readiness probe (checks Sonarr connectivity) |\n| `/metrics` | GET | Public | Prometheus metrics |\n| `/config` | GET | 🔒 | Current running configuration |\n| `/trigger` | POST | 🔒 | Manually trigger a sync cycle |\n| `/state` | GET | 🔒 | Current sync state (last run times, page info) |\n| `/shows` | GET | 🔒 | Query shows from database |\n| `/refilter` | POST | 🔒 | Re-evaluate filters for all filtered shows |\n\n### API Examples\n\n```bash\n# Public endpoints (no auth required)\ncurl http://localhost:8080/health\ncurl http://localhost:8080/ready\ncurl http://localhost:8080/metrics\n\n# Protected endpoints (require API key)\n# Trigger manual sync\ncurl -X POST -H \"X-API-Key: your-api-key\" http://localhost:8080/trigger\n\n# Check current state\ncurl -H \"X-API-Key: your-api-key\" http://localhost:8080/state\n\n# View running configuration\ncurl -H \"X-API-Key: your-api-key\" http://localhost:8080/config\n\n# Query filtered shows\ncurl -H \"X-API-Key: your-api-key\" \"http://localhost:8080/shows?status=filtered\u0026limit=10\"\n\n# Query added shows\ncurl -H \"X-API-Key: your-api-key\" \"http://localhost:8080/shows?status=added\"\n\n# Re-evaluate filters after config change\ncurl -X POST -H \"X-API-Key: your-api-key\" http://localhost:8080/refilter\n\n# Alternative: use query parameter\ncurl \"http://localhost:8080/state?api_key=your-api-key\"\n```\n\n## Dry Run Mode\n\nTest your filters without actually adding shows to Sonarr:\n\n```bash\n# Via environment variable\nDRY_RUN=true docker-compose up\n\n# Via config.yaml\ndry_run: true\n\n# Check logs to see what would be added\ndocker logs tvmaze-sync | grep \"DRY RUN\"\n```\n\nDry run mode will:\n- Fetch shows from TVMaze\n- Apply all filters\n- Log decisions (add/filter/skip)\n- NOT add shows to Sonarr\n- Still update the database\n\n## Monitoring\n\n### Prometheus Metrics\n\nMetrics are exposed at `/metrics` in Prometheus format:\n\n```\n# Sync health and timing\ntvmaze_sync_last_run_timestamp         # Unix timestamp of last sync\ntvmaze_sync_healthy                    # 1 if healthy, 0 if unhealthy\ntvmaze_sync_initial_complete           # 1 if initial sync completed\n\n# Show counts by status\ntvmaze_shows_total{status=\"added\"}     # Shows added to Sonarr\ntvmaze_shows_total{status=\"filtered\"}  # Shows filtered out\ntvmaze_shows_total{status=\"pending\"}   # Shows pending TVDB ID\ntvmaze_shows_total{status=\"failed\"}    # Shows that failed to add\ntvmaze_shows_total{status=\"exists\"}    # Shows already in Sonarr\n\n# External service health\ntvmaze_sonarr_healthy                  # 1 if Sonarr reachable, 0 otherwise\n```\n\n### Grafana Dashboard\n\nExample Prometheus queries:\n\n```promql\n# Shows added in last 24h\nincrease(tvmaze_shows_total{status=\"added\"}[24h])\n\n# Filter rejection rate\nrate(tvmaze_shows_total{status=\"filtered\"}[1h])\n\n# Time since last successful sync\ntime() - tvmaze_sync_last_run_timestamp\n```\n\n### Database Inspection\n\n```bash\n# Access database\ndocker exec -it tvmaze-sync sqlite3 /data/shows.db\n\n# Show counts by status\nSELECT processing_status, COUNT(*) FROM shows GROUP BY processing_status;\n\n# Find filtered shows by reason\nSELECT title, filter_reason FROM shows WHERE processing_status='filtered' LIMIT 20;\n\n# Shows by genre\nSELECT title, genres FROM shows WHERE genres LIKE '%Drama%' LIMIT 10;\n\n# Check pending retries\nSELECT title, retry_after, retry_count FROM shows WHERE processing_status='pending_tvdb';\n```\n\n### State Inspection\n\n```bash\n# View current state\ncat data/state.json | jq .\n\n# Or via API\ncurl http://localhost:8080/state | jq .\n```\n\n### Log Analysis\n\n```bash\n# Follow logs in Docker\ndocker logs -f tvmaze-sync\n\n# JSON log filtering (if using json format)\ndocker logs tvmaze-sync 2\u003e\u00261 | jq 'select(.level==\"ERROR\")'\n\n# Filter for added shows\ndocker logs tvmaze-sync | grep \"Added:\"\n```\n\n## Troubleshooting\n\n### Initial sync taking long?\n- **Normal**: ~70k shows with 20 req/10s rate limit takes several hours\n- **Progress is checkpointed**: Safe to restart - resumes from last page\n- **Premium API key**: Upgrade to 100 req/10s for faster sync\n\n### Show not added?\nCheck filter reason:\n```bash\ncurl \"http://localhost:8080/shows?status=filtered\" | jq '.[] | {title, filter_reason}'\n```\n\nTest with dry run mode:\n```bash\nDRY_RUN=true docker-compose restart tvmaze-sync\ndocker logs -f tvmaze-sync\n```\n\n### Missing TVDB ID?\nSome shows don't have TVDB IDs in TVMaze:\n- Marked as `pending_tvdb`\n- Retried weekly (configurable via `SYNC_RETRY_DELAY`)\n- Abandoned after 1 year (configurable via `SYNC_ABANDON_AFTER`)\n- After abandon time, marked as `failed`\n\n```bash\n# Check pending shows\nsqlite3 /data/shows.db \"SELECT title, retry_count FROM shows WHERE processing_status='pending_tvdb';\"\n```\n\n### Filters not working?\nRe-evaluate filters after config change:\n```bash\ncurl -X POST http://localhost:8080/refilter\n```\n\nThis will:\n- Re-process all filtered shows\n- Apply new filter criteria\n- Add shows that now pass filters\n- Fast (local SQLite queries, no API calls)\n\n### Sonarr connection errors?\nCheck validation at startup:\n```bash\ndocker logs tvmaze-sync | grep -i sonarr\n```\n\nValidates:\n- Sonarr connectivity\n- Root folder exists\n- Quality profile exists\n- Tags exist (if configured)\n\n### Country codes?\nTVMaze uses ISO 3166-1 country codes:\n- ✅ Use `GB` (not `UK`)\n- ✅ Use `US` (not `USA`)\n- See: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2\n\n## Architecture\n\n### Project Structure\n\n```\ntvmaze-sync/\n├── pyproject.toml           # Package configuration\n├── config.example.yaml      # Example configuration\n├── Dockerfile\n├── docker-compose.yml\n├── src/\n│   ├── main.py              # Entry point, orchestration\n│   ├── config.py            # Config loading, env var resolution\n│   ├── models.py            # Dataclasses (Show, ProcessingResult, etc.)\n│   ├── database.py          # SQLite operations\n│   ├── state.py             # JSON state management\n│   ├── processor.py         # Show filtering logic\n│   ├── scheduler.py         # Sync scheduling\n│   ├── server.py            # Flask HTTP server\n│   ├── metrics.py           # Prometheus metrics\n│   └── clients/\n│       ├── tvmaze.py        # TVMaze API client\n│       └── sonarr.py        # Sonarr client (pyarr wrapper)\n├── tests/\n└── data/                    # Runtime data (not in repo)\n    ├── state.json           # Sync state\n    ├── state.json.bak       # State backup\n    └── shows.db             # SQLite cache\n```\n\n### Tech Stack\n\n- **Python 3.12+**\n- **SQLite** - Show metadata cache (~70k shows, ~15-20MB)\n- **Flask** - HTTP server for health/metrics/API\n- **pyarr** - Sonarr API client library\n- **prometheus_client** - Metrics exposition\n- **requests** - TVMaze API client\n- **PyYAML** - Configuration parsing\n\n### Storage Architecture\n\n**Hybrid Storage (SQLite + JSON)**\n\n- **SQLite (shows.db)**: All show metadata and processing state\n  - ~70k shows\n  - ~15-20MB on disk\n  - Indexed queries for fast filter re-evaluation\n  - Handles large datasets efficiently\n\n- **JSON (state.json)**: Lightweight operational state\n  - Sync progress (last page, highest ID)\n  - Last run timestamps\n  - Filter hash (for change detection)\n  - Human-readable, easy to inspect\n\n### Sync Process\n\n1. **Initial Full Sync** (first run)\n   - Paginate through entire TVMaze index (~70k shows)\n   - 250 shows per page\n   - Progress checkpointed every page\n   - Rate limited (20 req/10s default)\n   - Resumes from last page on restart\n\n2. **Incremental Sync** (subsequent runs)\n   - Fetch updated shows since last sync (day/week/month window)\n   - Check for new shows beyond highest known ID\n   - Much faster than full sync\n\n3. **Filter Re-evaluation** (on filter config change)\n   - Detects filter changes via hash comparison\n   - Re-processes all filtered shows\n   - Local SQLite queries (no API calls)\n   - Fast (~67k shows in seconds)\n\n4. **Pending TVDB Retries**\n   - Shows without TVDB IDs marked `pending_tvdb`\n   - Retried weekly (configurable)\n   - Max 4 retries (configurable)\n   - Marked `failed` after max retries\n\n### Filter Processing\n\nShows are processed through a two-stage filter:\n\n**Stage 1: Global Excludes**\n- Shows matching ANY global exclude criteria are rejected\n- Checked first before any selection evaluation\n\n**Stage 2: Selections (OR logic)**\n- Show must match at least ONE selection to pass\n- Within each selection, ALL criteria must match (AND logic)\n- Empty selections list = all shows rejected\n\n**Selection Criteria:**\n- `languages`: Show language must be in list\n- `countries`: Show country must be in list\n- `genres`: Show must have at least one matching genre\n- `types`: Show type must be in list\n- `networks`: Show network must be in list\n- `status`: Show status must be in list\n- `premiered`: Show premiered date within range\n- `ended`: Show ended date within range\n- `rating`: TVMaze rating within range (0-10)\n- `runtime`: Episode runtime within range (minutes)\n\n**Processing Order:**\n1. Check TVDB ID (must exist or marked pending_tvdb)\n2. Check global excludes (reject if ANY match)\n3. Check selections (accept if ANY selection matches all its criteria)\n\n### External APIs\n\n**TVMaze API**\n- Base URL: `https://api.tvmaze.com`\n- Rate limit: 20 req/10s (100 with premium key)\n- No auth required (optional premium key)\n- Endpoints used:\n  - `GET /shows?page=N` - Paginated index\n  - `GET /shows/:id` - Single show details\n  - `GET /updates/shows?since=week` - Updated shows\n\n**Sonarr API**\n- Library: pyarr\n- Auth: API key in header\n- Version detection: Auto-detects v3 vs v4\n- Operations:\n  - `get_system_status()` - Health check\n  - `get_root_folder()` - Validate root folder\n  - `get_quality_profile()` - Validate quality profile\n  - `lookup_series(term=\"tvdb:123\")` - Find show\n  - `add_series(...)` - Add to library\n\n## Development\n\n### Setup\n\n```bash\n# Clone repository\ngit clone https://github.com/andronics/tvmaze-sync.git\ncd tvmaze-sync\n\n# Create virtual environment\npython -m venv venv\nsource venv/bin/activate\n\n# Install with dev dependencies\npip install -e \".[dev]\"\n```\n\n### Testing\n\n```bash\n# Run all tests\npytest\n\n# Run with coverage\npytest --cov=src --cov-report=html\n\n# Run only unit tests\npytest -m unit\n\n# Run specific test file\npytest tests/test_processor.py\n\n# Watch mode (requires pytest-watch)\nptw\n```\n\n### Code Style\n\nThe codebase follows these conventions:\n\n**Type Hints** - Use modern Python 3.12+ syntax:\n```python\ndef get_show(self, tvmaze_id: int) -\u003e Show | None:\n    ...\n\ndef get_shows(self, status: str | None = None) -\u003e list[Show]:\n    ...\n```\n\n**Dataclasses** - For data structures:\n```python\n@dataclass(frozen=True)\nclass SonarrConfig:\n    url: str\n    api_key: str\n```\n\n**Error Handling**:\n- Custom exception classes\n- Fail fast on startup\n- Resilient during runtime\n- Log errors with context\n\n**Database**:\n- Parameterized queries (never string formatting)\n- Context managers for transactions\n- Index columns used in WHERE clauses\n\n### Contributing\n\n1. Fork the repository\n2. Create a feature branch\n3. Make your changes\n4. Add tests\n5. Run test suite\n6. Submit pull request\n\n### Building Docker Image\n\n```bash\n# Build\ndocker build -t tvmaze-sync:latest .\n\n# Build with build args\ndocker build --build-arg PYTHON_VERSION=3.12 -t tvmaze-sync:latest .\n\n# Multi-platform build\ndocker buildx build --platform linux/amd64,linux/arm64 -t tvmaze-sync:latest .\n```\n\n## Known Issues\n\n### Sonarr v3 vs v4\nLanguage profiles are required in v3 but don't exist in v4. The client auto-detects the version and adjusts accordingly. If using v3, you must specify `language_profile` in config.\n\n### Filter Re-evaluation Logging\nChanging filters triggers re-evaluation of ~67k filtered shows. This is fast (SQLite indexed queries) but generates many log entries. This is normal and expected behavior.\n\n### TVMaze Rate Limiting\n- Free tier: 20 requests per 10 seconds\n- Initial sync of 70k shows takes several hours\n- Premium key increases to 100 req/10s (5x faster)\n- Progress is checkpointed - safe to restart\n\n### Docker Volume Permissions\nIf running into permission issues:\n```bash\n# Fix ownership\nchown -R 1000:1000 /path/to/data /path/to/config\n\n# Or run as root (not recommended)\ndocker run --user root ...\n```\n\n## Documentation\n\n- [ARCHITECTURE.md](.github/ARCHITECTURE.md) - Detailed system design and data flow\n- [MODULES.md](.github/MODULES.md) - Module specifications and interfaces\n- [config.example.yaml](config.example.yaml) - Configuration reference\n\n## Support\n\n- **Issues**: https://github.com/andronics/tvmaze-sync/issues\n- **Discussions**: https://github.com/andronics/tvmaze-sync/discussions\n\n## License\n\nMIT License - see LICENSE file for details\n\n## Acknowledgments\n\n- [TVMaze](https://www.tvmaze.com/) - TV show data API\n- [Sonarr](https://sonarr.tv/) - PVR for TV shows\n- [pyarr](https://github.com/totaldebug/pyarr) - Python API client for Sonarr\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandronics%2Ftvmaze-sync","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fandronics%2Ftvmaze-sync","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandronics%2Ftvmaze-sync/lists"}