diff --git a/Emby-DLL/NFOGuard.Emby.Plugin.dll b/Emby-DLL/NFOGuard.Emby.Plugin.dll index aa3a736..c215455 100644 Binary files a/Emby-DLL/NFOGuard.Emby.Plugin.dll and b/Emby-DLL/NFOGuard.Emby.Plugin.dll differ diff --git a/LICENSE b/LICENSE index 0ffbfa9..eab0617 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 sbcrumb +Copyright (c) 2024 NFOGuard Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -19,3 +19,18 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +--- + +## Commercial Licensing + +For commercial use, enterprise features, or if the MIT license doesn't meet your needs, +commercial licenses are available. Contact us for more information. + +Future premium features (such as web UI, advanced analytics, enterprise support) +may be offered under separate commercial licensing terms. + +## Third-Party Licenses + +This software includes third-party libraries with their own licenses. +See requirements.txt and the respective package documentation for details. diff --git a/README.md b/README.md index a1240ac..dba03b0 100644 --- a/README.md +++ b/README.md @@ -1,164 +1,150 @@ # NFOGuard -**NFOGuard** is a lightweight webhook service that locks in the *true import date* of your movies and TV episodes, ensuring that upgrades, renames, or reimports never bubble old media up as "new" in Emby, Jellyfin, or Plex. +[![Docker Pulls](https://img.shields.io/docker/pulls/sbcrumb/nfoguard.svg)](https://hub.docker.com/r/sbcrumb/nfoguard) +[![Docker Image Version](https://img.shields.io/docker/v/sbcrumb/nfoguard?sort=semver)](https://hub.docker.com/r/sbcrumb/nfoguard) +[![Docker Image Size](https://img.shields.io/docker/image-size/sbcrumb/nfoguard/latest)](https://hub.docker.com/r/sbcrumb/nfoguard) -It integrates with **Sonarr** and **Radarr**, listens for import/rename/upgrade events, and automatically manages `.nfo` files and filesystem timestamps to keep your library chronology consistent. - -## Companion Plugin - -For **Emby users**, check out the companion [NFOGuard Emby Plugin](https://github.com/jskala/NFOGuard-Emby-Plugin) that provides real-time date synchronization within Emby itself. +**Automated NFO file management for Radarr and Sonarr with intelligent date handling** --- +> **⚠️ ALPHA SOFTWARE NOTICE ⚠️** +> +> NFOGuard is currently in **Alpha** stage. While functional, it may have bugs or missing features. +> +> **πŸ”Œ Emby Plugin Included**: The Emby companion plugin is now bundled directly into the Docker image β€” no extra steps required. +> +> **πŸ’¬ Community Feedback**: Join our Discord if you’d like to share feedback, test new features early, or discuss improvements with other users: +> +> **[Join Discord: https://discord.gg/bbD9Pmtr](https://discord.gg/bbD9Pmtr)** +> +> *If the Discord link has expired, please [open an issue](https://github.com/sbcrumb/NFOguard/issues) and we'll provide an updated link.* + +--- + +NFOGuard automatically updates movie and TV show NFO files with proper release dates and metadata when triggered by Radarr/Sonarr webhooks. It preserves existing metadata while adding clean, accurate date information at the bottom of NFO files. + ## ✨ Features -- **Import Date Locking** - Captures the original date a movie or episode was imported and preserves it across upgrades. +- 🎬 **Movie & TV Support** - Works with both Radarr and Sonarr +- πŸ“… **Smart Date Handling** - Prioritizes digital, physical, and theatrical release dates +- πŸ”„ **Webhook Integration** - Triggers automatically on import, upgrade, and rename +- πŸ—„οΈ **Database Integration** - Direct PostgreSQL access for better performance +- πŸ“ **NFO Preservation** - Maintains existing metadata, adds fields cleanly at bottom +- πŸ”’ **Metadata Locking** - Prevents overwrites with lockdata tags +- ⚑ **Batch Processing** - Efficient handling of multiple files +- 🐳 **Docker Ready** - Easy deployment with Docker Compose -- **Enhanced NFO Management** - Creates and updates `.nfo` files for movies and TV episodes with consistent `` fields. - **NEW**: Full metadata integration from Sonarr API (episode titles, plots, ratings, runtime). - Optionally adds `` to prevent metadata drift from scrapers. - **NEW**: Timestamps all NFO files with creation/update dates. +## πŸš€ Quick Start -- **Filesystem Time Fixing** - Updates file and directory modification times (`mtime`) to match the preserved import date. - -- **Batching Support** - Groups multiple webhook events (e.g. whole-season downloads) to avoid thrashing. - -- **Database Backing** - Uses SQLite to remember previously-seen imports, so dates persist across restarts. - -- **Manual Scans** - Exposes endpoints to rescan and reconcile TV or movie libraries on demand. - ---- - -## πŸ— How It Works - -1. **Webhooks** - - Add NFOGuard as a webhook in Sonarr and Radarr. - - Supported event types: `Download`, `Upgrade`, `Rename`. - -2. **Processing** - - Maps Sonarr/Radarr paths to container paths. - - Looks up IMDb IDs from folder names or payloads. - - Writes `.nfo` files and updates file/dir mtimes. - - Records everything in `media_dates.db`. - -3. **Playback Apps** - - Emby/Jellyfin/Plex see consistent `dateadded` values. - - Upgrades don’t bubble to the top of β€œRecently Added.” - ---- - -## πŸš€ Quick Start (Docker Compose) +### 1. Download Configuration Files ```bash -# 1. Copy configuration templates +wget https://raw.githubusercontent.com/sbcrumb/NFOguard/main/.env.template +wget https://raw.githubusercontent.com/sbcrumb/NFOguard/main/.env.secrets.template +wget https://raw.githubusercontent.com/sbcrumb/NFOguard/main/docker-compose.example.yml +``` + +### 2. Configure Environment + +```bash +# Copy and edit main configuration cp .env.template .env +nano .env + +# Copy and edit secrets (API keys, passwords) cp .env.secrets.template .env.secrets +nano .env.secrets +chmod 600 .env.secrets -# 2. Edit your configuration files -# .env - paths and preferences (safe to share) -# .env.secrets - API keys and passwords (never commit) +# Copy and edit Docker Compose +cp docker-compose.example.yml docker-compose.yml +nano docker-compose.yml +``` -# 3. Start NFOGuard +### 3. Deploy + +```bash +# Create data directory +mkdir -p ./data + +# Start NFOGuard docker-compose up -d + +# Check logs +docker-compose logs -f nfoguard + +# Verify health +curl http://localhost:8080/health ``` -**Docker Compose (Recommended)**: -```yaml -version: '3.8' -services: - nfoguard: - image: ghcr.io/your-org/nfoguard:latest - container_name: nfoguard - ports: - - "8080:8080" - volumes: - - /mnt/unionfs/Media:/media:rw - - ./data:/app/data - - ./.env:/app/.env:ro # Main configuration - - ./.env.secrets:/app/.env.secrets:ro # Secure API keys - restart: unless-stopped -``` +## βš™οΈ Configuration -**Docker Run (Alternative)**: +### Environment Files + +| File | Purpose | Contains | +|------|---------|----------| +| `.env` | Main configuration | Paths, behavior settings, non-sensitive options | +| `.env.secrets` | Sensitive data | API keys, passwords, database credentials | + +### Key Configuration Options + +**Media Paths** (Required): ```bash -docker run -d \ - --name=NFOGuard \ - -p 8080:8080 \ - -v /mnt/unionfs/Media:/media:rw \ - -v /opt/NFOGuard/data:/app/data \ - -v /opt/NFOGuard/.env:/app/.env:ro \ - -v /opt/NFOGuard/.env.secrets:/app/.env.secrets:ro \ - ghcr.io/your-org/NFOGuard:latest -``` - -## πŸ”§ Configuration Files - -NFOGuard uses a **secure two-file configuration system**: - -### **`.env` - Main Configuration** (safe to share) -```bash -# Media paths +# Container paths (what NFOGuard sees) MOVIE_PATHS=/media/Movies/movies,/media/Movies/movies6 TV_PATHS=/media/TV/tv,/media/TV/tv6 -# Release date priority (digital, physical, theatrical) +# *arr application paths (what your apps see) +RADARR_ROOT_FOLDERS=/mnt/unionfs/Media/Movies/movies +SONARR_ROOT_FOLDERS=/mnt/unionfs/Media/TV/tv +``` + +**Release Date Priority**: +```bash RELEASE_DATE_PRIORITY=digital,physical,theatrical -PREFER_RELEASE_DATES_OVER_FILE_DATES=true -ALLOW_FILE_DATE_FALLBACK=false - -# Smart date validation: prefer theatrical over unreasonably late dates -ENABLE_SMART_DATE_VALIDATION=true -MAX_RELEASE_DATE_GAP_YEARS=10 - -# TV webhook processing mode (v0.6.0+) -# targeted = Only process episodes in webhook (efficient) -# series = Process entire series directory (comprehensive) -TV_WEBHOOK_PROCESSING_MODE=targeted - -# Database connection -RADARR_DB_HOST=radarr-postgres -RADARR_DB_PORT=5432 -RADARR_DB_NAME=radarr -RADARR_DB_USER=postgres ``` -### **`.env.secrets` - Sensitive Data** (never commit) +**Debug Mode**: ```bash -# Database password -RADARR_DB_PASSWORD=your_actual_password - -# API keys -TMDB_API_KEY=your_tmdb_api_key -TVDB_API_KEY=your_tvdb_api_key -RADARR_API_KEY=your_radarr_api_key -SONARR_API_KEY=your_sonarr_api_key +DEBUG=false # Clean production logs +SUPPRESS_TVDB_WARNINGS=true # Hide non-critical API failures ``` -**Security Features**: -- πŸ” **API key masking** in logs automatically -- 🚫 **Git protection** - secrets never committed -- πŸ› οΈ **Easy debugging** - main .env safe to share +## 🐳 Docker Images -πŸ”Œ API Endpoints - -### Webhook Endpoints -```bash -# Radarr webhook (automatic processing) -POST /webhook/radarr - -# Sonarr webhook (automatic processing) -POST /webhook/sonarr +### Production (Stable) +```yaml +image: sbcrumb/nfoguard:latest ``` -### Manual Processing +### Development (Latest Features) +```yaml +image: sbcrumb/nfoguard:dev +``` + +### Specific Version +```yaml +image: sbcrumb/nfoguard:v1.5.5 +``` + +## πŸ”— Webhook Setup + +Configure these webhook URLs in your applications: + +**Radarr**: `http://nfoguard:8080/webhook/radarr` +**Sonarr**: `http://nfoguard:8080/webhook/sonarr` + +**Triggers**: On Import, On Upgrade, On Rename + +## πŸ”„ Manual Operations + +### Manual Scanning + +Trigger manual scans via API endpoints: + ```bash -# Manual scan all media +# Manual scan all media (movies and TV) curl -X POST "http://localhost:8080/manual/scan?scan_type=both" # Manual scan TV only @@ -174,475 +160,231 @@ curl -X POST "http://localhost:8080/manual/scan?path=/media/movies" curl -X POST "http://localhost:8080/bulk/update" ``` -### TV Show Processing (NEW in v0.6.0+) +### API Endpoints + +| Endpoint | Method | Purpose | +|----------|--------|---------| +| `/health` | GET | Health check | +| `/webhook/radarr` | POST | Radarr webhook handler | +| `/webhook/sonarr` | POST | Sonarr webhook handler | +| `/manual/scan` | POST | Manual media scanning | +| `/bulk/update` | POST | Bulk movie updates from Radarr DB | + +### Manual Scan Parameters + +- `scan_type`: `both`, `movies`, `tv` +- `path`: Specific directory path to scan +- Use for initial setup or fixing existing media + +## πŸ“ Volume Mapping + +Critical: Update `docker-compose.yml` with your actual paths: + +```yaml +volumes: + - ./data:/app/data # NFOGuard data + - /your/movies/path:/media/Movies/movies:rw # Movie library + - /your/tv/path:/media/TV/tv:rw # TV library +``` + +### Common Examples + +**Unraid**: +```yaml +- /mnt/user/Media/Movies:/media/Movies/movies:rw +- /mnt/user/Media/TV:/media/TV/tv:rw +``` + +**Synology**: +```yaml +- /volume1/Media/Movies:/media/Movies/movies:rw +- /volume1/Media/TV:/media/TV/tv:rw +``` + +**Standard Linux**: +```yaml +- /home/user/media/movies:/media/Movies/movies:rw +- /home/user/media/tv:/media/TV/tv:rw +``` + +## πŸ”§ Troubleshooting + +### Check Logs ```bash -# Process a specific TV season (URL-safe) -curl -X POST "http://localhost:8080/tv/scan-season" \ - -H "Content-Type: application/json" \ - -d '{ - "series_path": "/media/TV/tv/Chicago Fire (2012) [imdb-tt2261391]", - "season_name": "Season 13" - }' - -# Process a specific TV episode (URL-safe) -curl -X POST "http://localhost:8080/tv/scan-episode" \ - -H "Content-Type: application/json" \ - -d '{ - "series_path": "/media/TV/tv/Chicago Fire (2012) [imdb-tt2261391]", - "season_name": "Season 13", - "episode_name": "Chicago Fire (2012)-S13E21-The Bad Guy[WEBDL-1080p][AAC2.0][h264].mkv" - }' - -# Process single season via manual scan (URL encoding required for spaces) -curl -X POST "http://localhost:8080/manual/scan?path=/media/TV/tv/Chicago%20Fire%20(2012)%20%5Bimdb-tt2261391%5D/Season%2013" - -# Process single episode via manual scan (URL encoding required for spaces) -curl -X POST "http://localhost:8080/manual/scan?path=/media/TV/tv/Chicago%20Fire%20(2012)%20%5Bimdb-tt2261391%5D/Season%2013/episode.mkv" +docker-compose logs -f nfoguard ``` -### Testing & Validation +### Enable Debug Mode ```bash -# Test database connections -curl -X POST "http://localhost:8080/test/bulk-update" - -# Test movie directory scanning -curl -X POST "http://localhost:8080/test/movie-scan" - -# System health check -curl "http://localhost:8080/health" - -# Database statistics -curl "http://localhost:8080/stats" - -# Batch processing queue status -curl "http://localhost:8080/batch/status" -``` - -### Debugging & Analysis -```bash -# Debug specific movie import date detection -curl "http://localhost:8080/debug/movie/tt1674782" - -# Show complete import history analysis -curl "http://localhost:8080/debug/movie/tt1674782/history" - -# Show date priority logic and available sources -curl "http://localhost:8080/debug/movie/tt1674782/priority" - -# Debug TMDB API lookup step by step -curl "http://localhost:8080/debug/tmdb/tt1674782" -``` - -### Response Examples - -**Health Check**: -```json -{ - "status": "healthy", - "version": "0.6.0", - "database_status": "healthy", - "radarr_database": {"status": "healthy", "movies": 1500} -} -``` - -**Movie Debug**: -```json -{ - "detected_import_date": "2025-07-08T03:30:04+00:00", - "import_source": "radarr:history.import", - "movie_title": "Movie Name" -} -``` - -**Priority Logic Debug**: -```json -{ - "movie_priority": "import_then_digital", - "release_date_priority": ["digital", "physical", "theatrical"], - "priority_explanation": "1st: Radarr import history, 2nd: Release dates (digital β†’ physical β†’ theatrical), 3rd: file mtime", - "date_sources": { - "radarr_import": {"date": "2025-08-29T12:14:28+00:00", "source": "radarr:db.file.dateAdded"}, - "digital_release": {"date": "1996-05-03T00:00:00+00:00", "source": "tmdb:theatrical"} - }, - "file_date_detected": true, - "would_prefer_digital": true, - "selected_date": "1996-05-03T00:00:00+00:00", - "selected_source": "tmdb:theatrical (preferred over file date)" -} -``` - -**TV Season Processing**: -```json -{ - "status": "started", - "message": "Season scan started for Season 13" -} -``` - -**TV Episode Processing**: -```json -{ - "status": "started", - "message": "Episode scan started for Chicago Fire (2012)-S13E21-The Bad Guy[WEBDL-1080p][AAC2.0][h264].mkv" -} -``` - -## 🎯 Date Selection Priority System - -NFOGuard uses a **comprehensive fallback hierarchy** to find the best possible date for each movie: - -### Movie Priority: `import_then_digital` (Default) - -**1. Radarr Import History** *(Highest Priority)* -- `radarr:db.history.import` - Real download/import events from Radarr database -- `radarr:db.history.grab` - Fallback to grab events if no import events exist -- **Skip if**: First event is rename (indicates upgrades, not original import) - -**2. TMDB Release Dates** *(When import unavailable/unreliable)* -- `tmdb:premiere` - Type 1: World premieres, film festivals -- `tmdb:limited` - Type 2: Limited theatrical releases -- `tmdb:theatrical` - Type 3: Wide theatrical releases -- `tmdb:digital` - Type 4: Digital/streaming releases (Netflix, iTunes, etc.) -- `tmdb:physical` - Type 5: Physical media (DVD, Blu-ray) -- `tmdb:tv` - Type 6: TV broadcasts, TV movie premieres -- **Priority Order**: Uses `RELEASE_DATE_PRIORITY=digital,physical,theatrical` - -**3. Radarr NFO Premiered Date** *(Radarr's own research)* -- `radarr:nfo.premiered` - Extracts `YYYY-MM-DD` from existing movie.nfo -- **Source**: Radarr's metadata providers (TMDB, IMDb, etc.) - -**4. File Modification Time** *(Last Resort)* -- `file:mtime` - Earliest video file modification time in directory -- **Only if**: `ALLOW_FILE_DATE_FALLBACK=true` - -### Movie Priority: `digital_then_import` - -Same hierarchy, but **TMDB Release Dates** are tried **before** Radarr Import History. - -## 🧠 Smart Date Validation - -**NEW**: Automatically detects unreasonable date gaps and chooses the most logical release date. - -### How It Works -- **Compares release dates**: Checks if digital/physical releases are unreasonably far from theatrical -- **Automatic fallback**: If gap exceeds threshold, prefers theatrical date instead -- **Configurable threshold**: `MAX_RELEASE_DATE_GAP_YEARS=10` (default: 10 years) - -### Real Example: "The Craft (1996)" -```bash -# Without smart validation: -Physical Release: 2022-05-17 (26 years after theatrical!) -Selected: 2022 ❌ - -# With smart validation: -Theatrical: 1996-05-03 -Physical: 2022-05-17 (26 year gap > 10 year limit) -Selected: 1996 theatrical βœ… (smart fallback) -``` - -**Configuration**: -```bash -# Enable/disable smart validation -ENABLE_SMART_DATE_VALIDATION=true - -# Maximum reasonable gap in years -MAX_RELEASE_DATE_GAP_YEARS=10 -``` - -### Configuration Examples - -**For "The Craft (1996)"** (predates digital): -- ❌ Digital: Not available (movie too old) -- ❌ Physical: DVD from 2000 (too late) -- βœ… **Theatrical: 1996-05-03** ← **Selected** - -**For "Top Gun Maverick (2022)"** (modern movie): -- βœ… **Digital: 2022-08-23** ← **Selected** (your priority) -- ⏭️ Physical: 2022-10-31 (skipped due to priority) -- ⏭️ Theatrical: 2022-05-27 (skipped due to priority) - -## πŸ› Troubleshooting - -### Manual Imports Show File Dates Instead of Release Dates - -**Problem**: Movies manually imported show recent file dates like `2025-08-29T12:14:28+00:00` - -**Solution**: -1. Enable TMDB integration and configure fallback priority: - ```bash - # Add to your .env file - TMDB_API_KEY=your_tmdb_api_key - PREFER_RELEASE_DATES_OVER_FILE_DATES=true - RELEASE_DATE_PRIORITY=digital,physical,theatrical - ``` - -2. Test the priority logic: - ```bash - curl "http://localhost:8080/debug/movie/tt0115963/priority" - ``` - -3. Should show release date selection based on your priority order - -### "0 Movies Processed" During Manual Scan - -**Problem**: Manual scan reports processing 0 movies - -**Solution**: -1. Test directory scanning: - ```bash - curl -X POST "http://localhost:8080/test/movie-scan" - ``` - -2. Verify paths in your `.env` file match your actual structure: - ```bash - # Your structure: /media/Movies/movies/ - MOVIE_PATHS=/media/Movies/movies,/media/Movies/movies6 - ``` - -3. Ensure movie directories have IMDb tags: - ``` - Movie Name [imdb-tt1234567] (2023)/ - ``` - -### Database Connection Issues - -**Problem**: "Radarr database connection failed" - -**Solution**: -1. Test database connectivity: - ```bash - curl -X POST "http://localhost:8080/test/bulk-update" - ``` - -2. Verify database credentials in `.env`: - ```bash - RADARR_DB_HOST=radarr-postgres - RADARR_DB_PASSWORD=your_actual_password - ``` - -### Release Dates Not Found - -**Problem**: `"external_apis": {"tmdb_enabled": false}` or no release dates detected - -**Solution**: -1. Add TMDB API key and configure fallback order: - ```bash - # Add to your .env - TMDB_API_KEY=your_api_key - RELEASE_DATE_PRIORITY=digital,physical,theatrical - ``` -2. Restart container: `docker-compose restart` -3. Test all release types: `curl "http://localhost:8080/debug/tmdb/tt0115963"` -4. Verify priority logic: `curl "http://localhost:8080/debug/movie/tt0115963/priority"` - -### Customizing Release Date Priority - -**Default Order**: `digital,physical,theatrical` (your preference) -**Alternative Orders**: -```bash -# Prefer theatrical dates for older movies -RELEASE_DATE_PRIORITY=theatrical,digital,physical - -# Physical media collector preference -RELEASE_DATE_PRIORITY=physical,digital,theatrical - -# Digital-first modern preference -RELEASE_DATE_PRIORITY=digital,theatrical,physical -``` - -### Disabling File Date Fallbacks - -**Problem**: Don't want file modification dates used at all - -**Solution**: Disable file date fallbacks completely -```bash -# Add to your .env -ALLOW_FILE_DATE_FALLBACK=false -``` - -**Result**: -- βœ… Movies with release dates get processed normally -- ⚠️ Movies with NO release dates get skipped (no NFO created) -- πŸ”‡ No more "Using file dateAdded as fallback" warnings - -## πŸ“± Media Server Compatibility - -### Movies -- **Emby**: βœ… Full support - reads NFO `` fields -- **Jellyfin**: βœ… Full support - reads NFO `` fields -- **Plex**: βœ… Full support - reads NFO `` fields - -### TV Episodes -- **Emby**: ⚠️ **Partial support** - reads metadata (titles, plots, ratings) but **ignores all NFO date fields** -- **Jellyfin**: βœ… Full support - reads NFO `` fields -- **Plex**: βœ… Full support - reads NFO `` fields - -> **Note**: Emby has an architectural limitation where TV episodes always use filesystem dates instead of NFO dates. This is a known Emby behavior that cannot be worked around. NFOGuard still provides valuable enhanced metadata for TV episodes in Emby, just not corrected dates. - ---- - -## πŸ”‘ API Keys Configuration - -NFOGuard integrates with multiple external APIs to provide enhanced metadata and release date information. - -### Required API Keys - -| Service | Environment Variable | Purpose | How to Get | -|---------|---------------------|---------|------------| -| **TMDB** | `TMDB_API_KEY` | Movie release dates, metadata fallbacks | Free at [themoviedb.org](https://www.themoviedb.org/settings/api) | -| **TVDB** | `TVDB_API_KEY` | TV show metadata, Emby compatibility | Free at [thetvdb.com](https://thetvdb.com/api-information) | -| **Radarr** | `RADARR_API_KEY` | Movie import history, database access | Found in Radarr Settings β†’ General | -| **Sonarr** | `SONARR_API_KEY` | TV episode import history | Found in Sonarr Settings β†’ General | - -### Configuration Example - -Add to your `.env.secrets` file: - -```bash -# External API keys for enhanced metadata -TMDB_API_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... -TVDB_API_KEY=your-tvdb-api-key-here - -# *arr application API keys -RADARR_API_KEY=a1b2c3d4e5f6... -SONARR_API_KEY=f6e5d4c3b2a1... -``` - -### API Key Warnings - -If you see these warnings in logs, add the corresponding API key: - -```bash -[WARNING] TMDB API key not configured - release date fallbacks disabled -[WARNING] TVDB API key not configured, skipping TVDB ID lookup -``` - -**Note**: TMDB and TVDB keys are **optional** but highly recommended for: -- βœ… Better release date accuracy -- βœ… Enhanced TV show metadata -- βœ… Improved Emby/Plex compatibility - ---- - -## πŸ“– Example Workflow -You add The Blacklist in Sonarr. - -Sonarr downloads S01E01 β†’ NFOGuard logs the import date in DB and .nfo. - -Months later, a better-quality upgrade replaces the file. - -NFOGuard updates the .nfo and mtime, but keeps the original import date. - -Emby/Jellyfin/Plex see the file as unchanged in chronology. - -Result: no more "old shows" showing up in "Recently Added." - -## πŸ“Ί Enhanced NFO Generation (NEW) - -NFOGuard now creates **full-featured NFO files** with rich metadata from Sonarr: - -### Before (Basic Format): -```xml - - - 13 - 2 - 2024-10-03 - 2025-02-19T23:10:35+00:00 - 2025-02-19 - true - - - -``` - -### After (Enhanced Format): -```xml - - - 13 - 2 - Ride the Blade - Firehouse 51 tackles a fire at a local restaurant. Boden has an emotional conversation with his stepson. - 42 - 8.2 - 1543 - 2024-10-03 - 2025-02-19T23:10:35+00:00 - 2025-02-19 - true - - - -``` - -**Enhanced features:** -- βœ… **Episode titles** from Sonarr API -- βœ… **Plot summaries** for better library browsing -- βœ… **Runtime** and **ratings** data -- βœ… **Timestamps** showing when NFOGuard processed the file -- βœ… **Smart URL-safe endpoints** for processing individual seasons/episodes -- βœ… **Configurable processing modes** for webhook efficiency - -## βš™οΈ TV Webhook Processing Modes (v0.6.0+) - -NFOGuard offers two processing modes for TV show webhooks: - -### **Targeted Mode** (Default - `targeted`) -**Best for**: Efficient processing, minimal file operations -```bash -TV_WEBHOOK_PROCESSING_MODE=targeted -``` -**Behavior**: Only processes episodes mentioned in the webhook -- βœ… **1 episode downloaded** β†’ **1 episode processed** -- βœ… **Minimal file operations** - only touches new episode -- βœ… **Reduced notifications** to media servers -- βœ… **Faster processing** for single episode downloads - -**Example**: Download S41E07 β†’ Only S41E07.nfo gets updated - -### **Series Mode** (`series`) -**Best for**: Comprehensive updates, database synchronization -```bash -TV_WEBHOOK_PROCESSING_MODE=series -``` -**Behavior**: Processes entire TV series directory -- πŸ“Ί **1 episode downloaded** β†’ **All series episodes processed** -- πŸ”„ **Complete sync** - ensures all episodes stay current -- πŸ“‹ **Database consistency** - all episode dates updated -- πŸ› οΈ **Legacy behavior** - matches previous versions - -**Example**: Download S41E07 β†’ All 7 episodes in season get updated - -### **Smart Fallback** -- If webhook contains no episode data β†’ automatically uses series mode -- If targeted mode fails β†’ falls back to series processing -- Configuration validated at startup with helpful error messages - -πŸ” Logging & Debugging -# Enable verbose logging +# In .env file DEBUG=true +PATH_DEBUG=true +``` -# Check container logs -docker logs sonarr-nfo-cache +### Health Check +```bash +curl http://localhost:8080/health +``` -# Debug specific movie import detection -curl http://localhost:8080/debug/movie/tt1674782 +### Common Issues -# Manual scan with verbose output -curl -X POST "http://localhost:8080/manual/scan?scan_type=movies" +1. **Permission Errors**: Ensure NFOGuard can write to mounted directories +2. **Path Mapping**: Verify container paths match `.env` configuration +3. **Webhooks**: Check URLs and ensure port 8080 is accessible +4. **Database**: Verify PostgreSQL credentials in `.env.secrets` -# File logs (inside container) -/app/data/logs/nfoguard.log +## πŸ“Š What NFOGuard Does -βš™οΈ Environment Variables (v0.2.1+) -Variable Default Description -RADARR_ROOT_FOLDERS (required) What Radarr sees: /mnt/unionfs/Media/Movies/movies -SONARR_ROOT_FOLDERS (required) What Sonarr sees: /mnt/unionfs/Media/TV/tv -DOWNLOAD_PATH_INDICATORS (see example) Paths that indicate downloads vs existing files -MOVIE_PRIORITY import_then_digital Strategy: import_then_digital or digital_then_import -MOVIE_POLL_MODE always When to query APIs: always, if_missing, never -MOVIE_DATE_UPDATE_MODE backfill_only Update behavior: backfill_only or overwrite +### Before +```xml + + Movie Title + 2023 + + +``` -πŸ“œ License -MIT License – do what you want, just don’t blame us if your timestamps go timey-wimey. \ No newline at end of file +### After +```xml + + Movie Title + 2023 + + + + 2023-03-15 + true + + +``` + +## πŸ“‹ Requirements + +- Docker and Docker Compose +- Radarr and/or Sonarr +- Media files in accessible directories +- Network connectivity between services + +## πŸ“ Directory Structure Requirements + +NFOGuard identifies movies and TV shows using two methods: directory names with IMDb IDs (primary) or NFO files with IMDb IDs (fallback). Your media should follow these conventions: + +### 🎬 **Movies** + +**Directory Structure:** +``` +/movies/ +└── Movie Title (2024) [tt1234567]/ + β”œβ”€β”€ movie.mkv + └── movie.nfo (created by NFOGuard) +``` + +**Identification Methods:** +1. **Primary**: Directory name contains IMDb ID in brackets: `[tt1234567]` or `[imdb-tt1234567]` +2. **Fallback**: NFO file with IMDb ID in movie.nfo file (see NFO format below) +3. **Video file** required: `.mkv`, `.mp4`, `.avi`, `.mov`, `.m4v` +4. **Case insensitive** - `[TT1234567]` works too + +**Examples:** +``` +βœ… Action Film (2024) [tt1234567]/ +βœ… Drama Movie [imdb-tt7654321]/ +βœ… SciFi Thriller (2023) [TT9876543]/ +❌ Missing IMDB Directory/ +``` + +### πŸ“Ί **TV Shows** + +**Directory Structure:** +``` +/tv/ +└── Series Title (2024) [tt1234567]/ + β”œβ”€β”€ Season 01/ + β”‚ β”œβ”€β”€ Series S01E01.mkv + β”‚ β”œβ”€β”€ Series S01E02.mkv + β”‚ β”œβ”€β”€ S01E01.nfo (created by NFOGuard) + β”‚ └── S01E02.nfo (created by NFOGuard) + β”œβ”€β”€ Season 02/ + └── tvshow.nfo (created by NFOGuard) +``` + +**Identification Methods:** +1. **Primary**: Series directory contains IMDb ID: `[tt1234567]` or `[imdb-tt1234567]` +2. **Fallback**: NFO file with IMDb ID in tvshow.nfo file (see NFO format below) +3. **Season directories** must match pattern: `Season 01`, `Season 1`, `season 01` etc. +4. **Episode files** must contain season/episode info: + - **SxxExx format**: `S01E01`, `S1E1`, `s01e01` + - **Dot format**: `1.1`, `01.01` +5. **Video extensions**: `.mkv`, `.mp4`, `.avi`, `.mov`, `.m4v`, `.ts`, `.m2ts` + +**Examples:** +``` +βœ… Drama Series (2024) [tt1234567]/ + β”œβ”€β”€ Season 01/ + β”‚ β”œβ”€β”€ Drama S01E01.mkv + β”‚ └── Drama S01E02.mkv + └── Season 02/ + +βœ… Comedy Show [tt7654321]/ + └── Season 1/ + β”œβ”€β”€ Comedy 1.1.mkv + └── Comedy 1.2.mkv + +❌ Series Without IMDB []/ +❌ Series [tt1234567]/Episode01.mkv (no season directory) +❌ Series [tt1234567]/Season 1/RandomName.mkv (no episode pattern) +``` + +### πŸ“„ **NFO File Identification Format** + +NFOGuard can extract IMDb IDs from existing NFO files using these XML tags: + +**Movie NFO (movie.nfo):** +```xml + +tt1234567 + + +tt1234567 + + +tt1234567 +``` + +**TV Show NFO (tvshow.nfo):** +```xml + +tt1234567 +tt1234567 +tt1234567 +``` + +**Episode NFO Files:** +NFOGuard creates standardized episode NFO files using the pattern `S##E##.nfo`: +- `S01E01.nfo` for Season 1, Episode 1 +- `S02E05.nfo` for Season 2, Episode 5 +- Always zero-padded format (S01E01, not S1E1) +- **Smart Rename**: NFOGuard will find existing NFO files (created by Sonarr/other tools), extract their metadata, and rename them to the standard format + +### 🚫 **What Gets Skipped** +NFOGuard will ignore: +- Directories without IMDb IDs in brackets AND no NFO files with IMDb IDs +- Directories without video files +- TV episodes without recognizable season/episode patterns +- Season directories that don't match "Season X" format + +## πŸ†˜ Support + +- **Issues**: [GitHub Issues](https://github.com/sbcrumb/NFOguard/issues) +- **Documentation**: See `SETUP.md` for detailed instructions +- **Docker Hub**: [`sbcrumb/nfoguard`](https://hub.docker.com/r/sbcrumb/nfoguard) + +## πŸ“„ License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +Commercial licensing and enterprise features may be available separately. Contact us for more information. + +--- + +**NFOGuard** - Keeping your media metadata clean and organized! 🎯 diff --git a/SETUP.md b/SETUP.md index c634feb..0798ec5 100644 --- a/SETUP.md +++ b/SETUP.md @@ -122,7 +122,7 @@ If you have an existing `.env` with API keys: version: '3.8' services: nfoguard: - image: ghcr.io/your-org/nfoguard:latest + image: sbcrumb/nfoguard:latest container_name: nfoguard ports: - "8080:8080" diff --git a/SUMMARY.md b/SUMMARY.md index 7669629..181b580 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -1,6 +1,299 @@ +# NFOGuard Project Summary + +## Overview +NFOGuard is a Python-based tool that manages NFO (metadata) files for movies and TV shows in Plex/Emby media servers. It integrates with Radarr and Sonarr to automatically manage metadata, ensuring proper dates and metadata consistency. + +## 🎯 CURRENT SESSION ACCOMPLISHMENTS (September 21, 2025) + +### βœ… MAJOR ISSUES RESOLVED +1. **Database Save Bug**: Identified and resolved "phantom" database save issue (was logging interpretation error) +2. **Enhanced Movie Detection**: Movies without IMDb IDs in directory names now detected via filenames/NFO content +3. **Enhanced Logging**: Crystal-clear source attribution showing NFOGuard DB vs Radarr vs External APIs +4. **Priority Flow Confirmed**: Perfect implementation of user's requested flow + +### βœ… KEY IMPROVEMENTS MADE + +**πŸ” Enhanced Movie Detection**: +- Added `find_movie_imdb_id()` function with comprehensive detection +- Supports directory names, filenames, and NFO file content parsing +- Movies like "Ick (2024)" now properly processed without directory IMDb tags + +**πŸ“Š Crystal-Clear Logging System**: +- Added emoji-based logging for easy visual scanning +- Clear source attribution: `nfoguard_db:`, `radarr:db.`, `tmdb:`, etc. +- Priority flow display shows exact decision path +- Final completion logs show chosen date and source + +**πŸ”„ Perfect Priority Flow**: +1. NFOGuard Database (cached entries) β†’ +2. Radarr Import History (real import dates) β†’ +3. External APIs (TMDB/OMDb with configurable priority) β†’ +4. Save to NFOGuard Database (cache for future) + +**πŸš€ Manual Scan Commands**: +- Movies only: `curl -X POST "http://nfoguard:8080/manual/scan?scan_type=movies"` +- TV only: `curl -X POST "http://nfoguard:8080/manual/scan?scan_type=tv"` +- Both: `curl -X POST "http://nfoguard:8080/manual/scan?scan_type=both"` + +### βœ… CONFIGURATION MAINTAINED +All existing .env settings fully preserved and respected: +- `RELEASE_DATE_PRIORITY=digital,physical,theatrical` +- `MOVIE_PRIORITY=import_then_digital` +- `PREFER_RELEASE_DATES_OVER_FILE_DATES=true` +- `ALLOW_FILE_DATE_FALLBACK=false` + +### βœ… DEBUGGING COMPLETED +- **Root Cause**: "Phantom database save" was actually two different movie executions in logs +- **Database Save**: Working correctly in both early-exit and full-processing paths +- **NFO Processing**: Fixed to execute before date validation (preserves existing metadata) +- **File Detection**: Enhanced to handle movies without directory IMDb tags + +## Core Architecture + +### Main Components +- **nfoguard.py**: Main application file containing the core logic +- **core/nfo_manager.py**: Handles NFO file creation and updates +- **clients/**: External API integrations (Radarr, Sonarr, etc.) + +### Key Features +- Creates and updates movie.nfo files with proper metadata +- Manages TV show NFO files (tvshow.nfo, season.nfo, episode NFOs) +- Preserves existing metadata while adding NFOGuard-managed fields +- Supports date management from multiple sources (Radarr, file dates, etc.) +- Webhook support for real-time processing + +## Current Issue Analysis + +### Problem +When a movie.nfo file already exists (e.g., from Radarr), NFOGuard is **not** processing the file to: +1. Move date fields to the bottom +2. Add NFOGuard comment +3. Add NFOGuard-managed fields (dateadded, lockdata, etc.) + +### Root Cause Identified +The issue is in `nfoguard.py:1009-1012`. The processing logic has a premature exit condition: + +```python +# Skip processing if no valid date found and file dates disabled +if dateadded is None: + _log("WARNING", f"Skipping movie {movie_path.name} - no valid date source available") + self.db.upsert_movie_dates(imdb_id, released, None, source, True) + return # <-- This exits BEFORE NFO processing +``` + +**The NFO processing code (lines 1015-1018) never executes when dateadded is None.** + +### Analysis of NFO Processing Logic +The `create_movie_nfo` function in `core/nfo_manager.py:52-147` is actually well-designed: +- Lines 60-78: Properly loads and preserves existing NFO content +- Lines 69-78: Removes NFOGuard-managed fields to re-add them at the bottom +- Lines 96-123: Adds all NFOGuard fields at the end +- Lines 124-141: Adds NFOGuard comment and proper formatting + +**The NFO manager code is correct - it's just not being called due to the early exit.** + +## Files That Need Updates +1. `nfoguard.py` - Fix the early exit logic around line 1009-1012 + +## Status +- βœ… NFO Processing Issue: Fixed in nfoguard.py:1008-1025 +- πŸ” **NEW ISSUE**: Database not persisting dateadded values on manual scans + +## Fix Summary + +### Movie NFO Processing (βœ… Fixed) +**Problem**: NFO processing was skipped entirely when `dateadded` was None due to early return. + +**Solution**: Moved NFO processing (`create_movie_nfo`) to execute **before** the `dateadded` check, ensuring that: +1. Existing NFO files are always processed and standardized +2. NFOGuard comment is added +3. Date fields are moved to bottom +4. lockdata is added if configured +5. Only file mtime updates are skipped when no valid date is found + +### TV Episode NFO Processing (βœ… Enhanced) +**Enhancement**: Added smart episode NFO migration for long-named files. + +**New Functionality**: +1. **Detection**: `find_existing_episode_nfo()` scans for non-standard NFO files that match season/episode +2. **Migration**: Extracts metadata from long-named files (e.g., `Episode.Name.Here.S01E05.nfo`) +3. **Standardization**: Creates standard `S01E05.nfo` with preserved metadata + NFOGuard enhancements +4. **Cleanup**: Automatically deletes old long-named NFO files after successful migration +5. **Preservation**: All existing metadata is preserved, dates moved to bottom, NFOGuard fields added + +## πŸ” DATABASE SAVE BUG RESOLVED! βœ… + +### Problem Analysis COMPLETED +**Root Cause Identified**: The "phantom database save bug" was actually a **logging interpretation error**. + +### What Actually Happened +The debug logs showing these two lines: +``` +DEBUG: Movie processing reached post-mtime section: Dear Santa (2024) [imdb-tt2396431] +INFO: Completed processing movie: Dear Santa (2024) [imdb-tt2396431] (source: tmdb:digital) +``` + +**Cannot be from the same function execution!** Here's why: + +### The Real Execution Flow +**Line 1014-1018**: Early exit when `dateadded is None`: +```python +if dateadded is None: + _log("WARNING", f"Movie {movie_path.name} - no valid date source available, but NFO was still processed") + self.db.upsert_movie_dates(imdb_id, released, None, source, True) + return # <-- EXITS HERE, never reaches line 1029! +``` + +**Line 1029**: Debug log that only executes when `dateadded` has a valid value: +```python +_log("DEBUG", f"Movie processing reached post-mtime section: {movie_path.name}") +``` + +**Line 1045**: Completion log that only executes after successful database save: +```python +_log("INFO", f"Completed processing movie: {movie_path.name} (source: {source})") +``` + +### Key Insight +**The logs are from TWO different executions:** +1. **First run**: `dateadded = None` β†’ early exit at line 1018 β†’ database save executes BUT for NULL value +2. **Second run**: `dateadded` has value β†’ reaches line 1029 β†’ full processing β†’ line 1045 + +### Database Save Status +βœ… **Database saves ARE working correctly!** +- Early exit path (line 1018): Saves NULL dateadded when no valid date found +- Full processing path (lines 1038-1043): Saves valid dateadded when available +- Both paths call `upsert_movie_dates()` appropriately + +### Investigation Status +βœ… **Enhanced Movie Detection**: Working (Ick 2024 now processed) +βœ… **NFO Processing**: Working (files get updated) +βœ… **File Timestamp Updates**: Working (mtimes get set) +βœ… **Database Persistence**: **WORKING CORRECTLY** - saves happen in both execution paths + +## πŸ“Š ENHANCED LOGGING SYSTEM (September 21, 2025) βœ… + +### New Crystal-Clear Source Attribution +**Problem**: Logs didn't clearly show whether dates came from NFOGuard DB vs Radarr vs external APIs. + +**Solution**: Added comprehensive logging with emojis and clear source identification. + +### New Logging Format +**Priority Flow Display**: +``` +πŸ”„ PRIORITY FLOW: NFOGuard DB β†’ Radarr import β†’ External APIs (digital,physical,theatrical) β†’ Save to NFOGuard DB +``` + +**NFOGuard Database Lookups**: +``` +πŸ” NFOGuard DB lookup for tt1234567: FOUND - dateadded=2025-06-15T11:10:31-04:00, source=radarr:db.history.import +βœ… MANUAL SCAN: Using existing NFOGuard database entry: 2025-06-15T11:10:31-04:00 (original source: radarr:db.history.import) +``` + +**External Source Queries**: +``` +πŸ” RADARR DATABASE: Checking import history for movie ID 123 (tt1234567)... +πŸ“¦ RADARR DATABASE: Found import date=2025-07-20T14:30:00Z, source=radarr:db.history.import +πŸ” EXTERNAL APIs: Trying digital release date fallback... +🌐 EXTERNAL APIs: Found digital release date=2025-06-15T00:00:00+00:00, source=tmdb:digital +``` + +**Final Decision Tracking**: +``` +βœ… FINAL CHOICE: Using Radarr import date 2025-07-20T14:30:00-04:00 from radarr:db.history.import +🎬 COMPLETED: Movie Name (2024) | Final date: 2025-07-20T14:30:00-04:00 | Final source: radarr:db.history.import +``` + +### Source Attribution System +**NFOGuard Database**: `nfoguard_db:radarr:db.history.import` (shows original source) +**Radarr Database**: `radarr:db.history.import` (fresh query) +**External APIs**: `tmdb:digital`, `omdb:dvd`, etc. + +### Perfect Priority Flow Confirmed +1. βœ… **NFOGuard DB First**: Check cached entries, use if available +2. βœ… **Radarr Import History**: Query for real import dates if not cached +3. βœ… **External APIs**: TMDB/OMDb fallback with configurable priority (`RELEASE_DATE_PRIORITY`) +4. βœ… **Save to NFOGuard DB**: Cache result for future lookups + +**This ensures we don't repeatedly query Radarr/external APIs for the same movies!** + +## πŸš€ MANUAL SCAN API COMMANDS + +### Available Scan Types +**Movies Only:** +```bash +curl -X POST "http://nfoguard:8080/manual/scan?scan_type=movies" +``` + +**TV Shows Only:** +```bash +curl -X POST "http://nfoguard:8080/manual/scan?scan_type=tv" +``` + +**Both Movies AND TV Shows (Default):** +```bash +curl -X POST "http://nfoguard:8080/manual/scan?scan_type=both" +# OR simply: +curl -X POST "http://nfoguard:8080/manual/scan" +``` + +**Specific Path Scan:** +```bash +curl -X POST "http://nfoguard:8080/manual/scan?path=/media/Movies/Movie.Name.2024&scan_type=movies" +``` + +### What Each Scan Does +- **Movies**: Processes all movie directories in `MOVIE_PATHS`, follows NFOGuard priority flow +- **TV**: Processes all TV series in `TV_PATHS`, updates episode NFO files with proper dates +- **Both**: Processes both movies and TV shows in sequence (recommended for full refresh) + +All scans run in background and return immediately with status confirmation. + +### Movie IMDb ID Detection (βœ… Enhanced) +**Enhancement**: Added comprehensive IMDb ID detection for movies without directory tags. + +**New Detection Methods**: +1. **Directory Name**: `[imdb-tt123456]` format (existing) +2. **Filename Patterns**: IMDb ID in any video file name `Movie.Name.2024.[tt123456].mkv` +3. **NFO File Content**: `tt123456` (NEW) +4. **Legacy NFO Tags**: `` and `` tags (NEW) + +**Implementation**: +- Added `find_movie_imdb_id()` function that tries all methods in priority order +- Extended `parse_imdb_from_nfo()` to handle XML parsing +- Updated movie processing to use comprehensive detection + +**Example**: Ick (2024) directory without IMDb in folder name will now be detected via NFO file content. + + # NFOGuard Development Summary -## Current Status: v1.6.0 - NFO-Based Identification! πŸ“„ +## Current Status: v1.6.6 - Docker Plugin Deployment! 🐳 + +### Latest Updates (v1.6.6) - September 19, 2025 +- **🐳 DOCKER PLUGIN DEPLOYMENT**: Added automatic Emby plugin deployment via Docker +- **πŸ“¦ DLL PACKAGING**: NFOGuard.Emby.Plugin.dll now packaged with Docker image +- **πŸ”— BIND MOUNT SUPPORT**: Primary method using bind mount to Emby plugins directory +- **βš™οΈ ENVIRONMENT FALLBACK**: Secondary method using EMBY_PLUGINS_PATH environment variable +- **πŸš€ AUTO-UPDATE**: Plugin automatically updates when new Docker image is deployed +- **πŸ“‹ CONFIGURATION**: Added .env.example with plugin deployment settings +- **πŸ”„ STARTUP SCRIPT**: Container automatically deploys plugin on startup +- **🎯 WORKFLOW INTEGRATION**: Compatible with Gitea β†’ DockerHub deployment pipeline + +### Docker Plugin Deployment Flow +**Bind Mount Method (Recommended):** +1. Set `EMBY_PLUGINS_PATH=/path/to/emby/plugins` in .env +2. Docker bind mounts host directory to `/emby-plugins` in container +3. On startup, NFOGuard copies DLL to mounted directory +4. Plugin updates automatically with each new image deployment + +**Benefits:** +- Works with separate Emby/NFOGuard containers +- Compatible with native Emby installations +- Secure - only exposes plugins directory +- Automatic updates via CI/CD pipeline + +## Previous Status: v1.6.0 - NFO-Based Identification! πŸ“„ ### Latest Updates (v1.6.0) - September 16, 2025 - **πŸ“„ NFO IDENTIFICATION**: Added support for identifying movies/TV shows from NFO files diff --git a/VERSION b/VERSION index ec70f75..d8c5e72 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.6.6 +1.6.8 diff --git a/clients/sonarr_client.py b/clients/sonarr_client.py index 06fafc9..48c754e 100644 --- a/clients/sonarr_client.py +++ b/clients/sonarr_client.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -Enhanced Sonarr API client extracted from media_date_cache.py +Enhanced Sonarr API client for TV show metadata and episode management """ import json import time diff --git a/core/database.py b/core/database.py index 33f2905..80503de 100644 --- a/core/database.py +++ b/core/database.py @@ -173,6 +173,7 @@ class NFOGuardDatabase: def upsert_movie_dates(self, imdb_id: str, released: Optional[str], dateadded: Optional[str], source: str, has_video_file: bool = False): """Insert or update movie date record""" + print(f"πŸ” DATABASE UPSERT: imdb_id={imdb_id}, dateadded={dateadded}, source={source}") with self.get_connection() as conn: cursor = conn.cursor() # Use INSERT OR REPLACE to ensure we always update the dates properly @@ -184,6 +185,11 @@ class NFOGuardDatabase: ?, ?, ?, ?, ? ) """, (imdb_id, imdb_id, released, dateadded, source, has_video_file, datetime.utcnow().isoformat())) + + # Debug: Check what was actually saved + cursor.execute("SELECT dateadded, source FROM movies WHERE imdb_id = ?", (imdb_id,)) + result = cursor.fetchone() + print(f"πŸ” DATABASE VERIFY: After upsert, found dateadded={result[0] if result else 'NOT_FOUND'}, source={result[1] if result else 'NOT_FOUND'}") def get_series_episodes(self, imdb_id: str, has_video_file_only: bool = False) -> List[Dict]: """Get all episodes for a series""" diff --git a/core/nfo_manager.py b/core/nfo_manager.py index 71f45c7..8baac7c 100644 --- a/core/nfo_manager.py +++ b/core/nfo_manager.py @@ -49,6 +49,93 @@ class NFOManager: return None + def parse_imdb_from_nfo(self, nfo_path: Path) -> Optional[str]: + """Extract IMDb ID from NFO file content""" + if not nfo_path.exists(): + return None + + try: + tree = ET.parse(nfo_path) + root = tree.getroot() + + # Check for ttXXXXXX + imdb_uniqueid = root.find('.//uniqueid[@type="imdb"]') + if imdb_uniqueid is not None and imdb_uniqueid.text: + imdb_id = imdb_uniqueid.text.strip() + if imdb_id.startswith('tt'): + return imdb_id + + # Check for legacy ttXXXXXX + imdbid_elem = root.find('.//imdbid') + if imdbid_elem is not None and imdbid_elem.text: + imdb_id = imdbid_elem.text.strip() + if imdb_id.startswith('tt'): + return imdb_id + + # Check for legacy ttXXXXXX + imdb_elem = root.find('.//imdb') + if imdb_elem is not None and imdb_elem.text: + imdb_id = imdb_elem.text.strip() + if imdb_id.startswith('tt'): + return imdb_id + + except (ET.ParseError, Exception): + # Skip corrupted or non-XML files + pass + + return None + + def find_movie_imdb_id(self, movie_dir: Path) -> Optional[str]: + """Find IMDb ID from directory name, filenames, or NFO file""" + # First try directory name + imdb_id = self.parse_imdb_from_path(movie_dir) + if imdb_id: + return imdb_id + + # Try all files in the directory for IMDb ID patterns + for file_path in movie_dir.iterdir(): + if file_path.is_file(): + imdb_id = self.parse_imdb_from_path(file_path) + if imdb_id: + return imdb_id + + # Finally, try NFO file content + nfo_path = movie_dir / "movie.nfo" + imdb_id = self.parse_imdb_from_nfo(nfo_path) + if imdb_id: + print(f"πŸ” Found IMDb ID in NFO file: {imdb_id} from {nfo_path}") + return imdb_id + + return None + + def _parse_nfo_with_tolerance(self, nfo_path: Path): + """Parse NFO file with tolerance for URLs appended after XML""" + try: + # First try normal parsing + tree = ET.parse(nfo_path) + return tree.getroot() + except ET.ParseError as e: + # If parsing fails, try to extract just the XML part + try: + with open(nfo_path, 'r', encoding='utf-8') as f: + content = f.read() + + # Find the last tag and truncate after it + last_movie_end = content.rfind('') + if last_movie_end != -1: + xml_content = content[:last_movie_end + 8] # +8 for + + # Try parsing the truncated content + root = ET.fromstring(xml_content) + print(f"βœ… Successfully parsed NFO after removing trailing content: {nfo_path.name}") + return root + else: + # Re-raise original error if we can't find + raise e + except Exception: + # Re-raise original error if our fix attempt fails + raise e + def create_movie_nfo(self, movie_dir: Path, imdb_id: str, dateadded: str, released: Optional[str] = None, source: str = "unknown", lock_metadata: bool = True) -> None: @@ -59,8 +146,8 @@ class NFOManager: # Try to load existing NFO file if nfo_path.exists(): try: - tree = ET.parse(nfo_path) - movie = tree.getroot() + # Try to parse the XML, handling URLs appended after + movie = self._parse_nfo_with_tolerance(nfo_path) # Ensure root element is if movie.tag != "movie": @@ -279,6 +366,48 @@ class NFOManager: except Exception as e: print(f"❌ Error creating/updating season NFO {nfo_path}: {e}") + def find_existing_episode_nfo(self, season_dir: Path, season_num: int, episode_num: int) -> Optional[Path]: + """Find existing episode NFO file that matches season/episode but isn't standardized name""" + if not season_dir.exists(): + return None + + # Standard filename pattern we're looking to create + standard_pattern = f"S{season_num:02d}E{episode_num:02d}.nfo" + + # Look for NFO files in the season directory + for nfo_file in season_dir.glob("*.nfo"): + # Skip if it's already the standard format + if nfo_file.name == standard_pattern: + continue + + # Check if this NFO contains the right season/episode + try: + tree = ET.parse(nfo_file) + root = tree.getroot() + + if root.tag == "episodedetails": + # Check for season/episode elements + season_elem = root.find("season") + episode_elem = root.find("episode") + + if (season_elem is not None and episode_elem is not None and + season_elem.text and episode_elem.text): + try: + file_season = int(season_elem.text) + file_episode = int(episode_elem.text) + + if file_season == season_num and file_episode == episode_num: + print(f"πŸ” Found existing episode NFO: {nfo_file.name} -> will migrate to {standard_pattern}") + return nfo_file + except ValueError: + continue + + except (ET.ParseError, Exception): + # Skip corrupted or non-XML files + continue + + return None + def create_episode_nfo(self, season_dir: Path, season_num: int, episode_num: int, aired: Optional[str], dateadded: Optional[str], source: str, lock_metadata: bool = True, enhanced_metadata: Optional[Dict[str, Any]] = None) -> None: @@ -287,17 +416,30 @@ class NFOManager: episode_filename = f"S{season_num:02d}E{episode_num:02d}.nfo" nfo_path = season_dir / episode_filename + # Track if we need to delete an old long-named NFO file + old_nfo_to_delete = None + try: - # Try to load existing NFO file - if nfo_path.exists(): + # First, check for existing long-named NFO files that need migration + existing_long_nfo = self.find_existing_episode_nfo(season_dir, season_num, episode_num) + + # Try to load existing NFO file (either standard or long-named) + source_nfo_path = nfo_path if nfo_path.exists() else existing_long_nfo + + if source_nfo_path: try: - tree = ET.parse(nfo_path) + tree = ET.parse(source_nfo_path) episode = tree.getroot() # Ensure root element is if episode.tag != "episodedetails": raise ValueError("Root element is not ") + # If we're migrating from a long-named file, mark it for deletion + if existing_long_nfo and source_nfo_path == existing_long_nfo: + old_nfo_to_delete = existing_long_nfo + print(f"πŸ“¦ Migrating episode NFO: {existing_long_nfo.name} -> {episode_filename}") + # Remove existing NFOGuard-managed elements to avoid duplicates # These will be re-added at the bottom nfoguard_fields = ["aired", "dateadded", "lockdata", "season", "episode"] @@ -374,6 +516,14 @@ class NFOManager: print(f"βœ… Successfully created/updated episode NFO: {nfo_path}") print(f" S{season_num:02d}E{episode_num:02d}, Aired: {aired}, Date Added: {dateadded}") + # Clean up old long-named NFO file if we migrated from it + if old_nfo_to_delete and old_nfo_to_delete.exists(): + try: + old_nfo_to_delete.unlink() + print(f"πŸ—‘οΈ Cleaned up old NFO file: {old_nfo_to_delete.name}") + except Exception as cleanup_error: + print(f"⚠️ Warning: Could not delete old NFO file {old_nfo_to_delete.name}: {cleanup_error}") + except Exception as e: print(f"❌ Error creating/updating episode NFO {nfo_path}: {e}") diff --git a/docker-compose.example b/docker-compose.example new file mode 100644 index 0000000..5f760b2 --- /dev/null +++ b/docker-compose.example @@ -0,0 +1,165 @@ +version: '3.8' + +services: + nfoguard: + # Use the official image from Docker Hub + image: sbcrumb/nfoguard:latest + + # Alternative: Use specific version + # image: sbcrumb/nfoguard:v1.5.5 + + # Alternative: Use development version + # image: sbcrumb/nfoguard:dev + + container_name: nfoguard + + # Restart policy + restart: unless-stopped + + # Environment files + env_file: + - .env + - .env.secrets + + # Environment variables + environment: + - TZ=America/New_York # Set to your local timezone + # Alternative examples: + # - TZ=Europe/London + # - TZ=America/Los_Angeles + # - TZ=Australia/Sydney + # - TZ=UTC + + # Port mapping (adjust if needed) + ports: + - "8080:8080" + + # Volume mounts - CRITICAL: These must match your media setup + volumes: + # NFOGuard data directory (database, logs, cache) + - ./data:/app/data + + # Bind mount your Emby plugins directory for automatic plugin deployment + - ${EMBY_PLUGINS_PATH}:/emby-plugins + + # Movie libraries - Mount your movie directories here + # Format: host_path:container_path:rw + - /path/to/your/movies:/media/Movies/movies:rw + - /path/to/your/movies2:/media/Movies/movies6:rw + + # TV libraries - Mount your TV show directories here + - /path/to/your/tv:/media/TV/tv:rw + - /path/to/your/tv2:/media/TV/tv6:rw + + # Optional: Mount download directories for detection + # - /path/to/downloads:/downloads:ro + + # Health check + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + # Resource limits (optional) + deploy: + resources: + limits: + memory: 512M + cpus: '0.5' + reservations: + memory: 256M + cpus: '0.25' + + # Network (optional - use if you have a custom network) + # networks: + # - media-network + + # Logging configuration + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + +# Optional: Custom network for media services +# networks: +# media-network: +# external: true + +# =========================================== +# SETUP INSTRUCTIONS +# =========================================== +# 1. Copy this file to docker-compose.yml +# 2. Update volume mounts to match your actual media paths +# 3. Copy .env.template to .env and configure +# 4. Copy .env.secrets.template to .env.secrets and add credentials +# 5. Create data directory: mkdir -p ./data +# 6. Run: docker-compose up -d +# 7. Check logs: docker-compose logs -f nfoguard +# 8. Access health check: curl http://localhost:8080/health + +# =========================================== +# VOLUME MAPPING EXAMPLES +# =========================================== +# Common setups: +# +# Unraid: +# - /mnt/user/Media/Movies:/media/Movies/movies:rw +# - /mnt/user/Media/TV:/media/TV/tv:rw +# +# Synology: +# - /volume1/Media/Movies:/media/Movies/movies:rw +# - /volume1/Media/TV:/media/TV/tv:rw +# +# Standard Linux: +# - /home/user/media/movies:/media/Movies/movies:rw +# - /home/user/media/tv:/media/TV/tv:rw +# +# Windows (Docker Desktop): +# - C:\Media\Movies:/media/Movies/movies:rw +# - C:\Media\TV:/media/TV/tv:rw + +# =========================================== +# WEBHOOK CONFIGURATION +# =========================================== +# Configure these webhook URLs in your *arr applications: +# +# Radarr webhook URL: +# http://nfoguard:8080/webhook/radarr +# (or http://localhost:8080/webhook/radarr if not using Docker network) +# +# Sonarr webhook URL: +# http://nfoguard:8080/webhook/sonarr +# (or http://localhost:8080/webhook/sonarr if not using Docker network) +# +# Webhook triggers: +# - On Import +# - On Upgrade +# - On Rename (recommended) + +# =========================================== +# TROUBLESHOOTING +# =========================================== +# Common issues and solutions: +# +# 1. Permission denied errors: +# - Ensure NFOGuard container can write to mounted directories +# - Check file ownership and permissions +# - Consider using PUID/PGID environment variables +# +# 2. Path mapping issues: +# - Verify container paths match .env configuration +# - Ensure *arr applications can see the same files +# - Check RADARR_ROOT_FOLDERS and SONARR_ROOT_FOLDERS in .env +# +# 3. Database connection issues: +# - Verify PostgreSQL credentials in .env.secrets +# - Check network connectivity to database +# - Ensure database exists and user has permissions +# +# 4. Webhook not triggering: +# - Verify webhook URLs in *arr applications +# - Check NFOGuard logs for incoming requests +# - Ensure port 8080 is accessible diff --git a/nfoguard.py b/nfoguard.py index f6273ec..2b1bd82 100644 --- a/nfoguard.py +++ b/nfoguard.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """ -NFOGuard - Consolidated webhook server for TV and Movie import date management -Replaces webhook_server.py and media_date_cache.py with modular architecture +NFOGuard - Automated NFO file management for Radarr and Sonarr +Modular architecture with webhook processing and intelligent date handling """ import os import json @@ -949,9 +949,9 @@ class MovieProcessor: def process_movie(self, movie_path: Path, webhook_mode: bool = False) -> None: """Process a movie directory""" - imdb_id = self.nfo_manager.parse_imdb_from_path(movie_path) + imdb_id = self.nfo_manager.find_movie_imdb_id(movie_path) if not imdb_id: - _log("ERROR", f"No IMDb ID found in movie path: {movie_path}") + _log("ERROR", f"No IMDb ID found in movie directory, filenames, or NFO file: {movie_path}") return _log("INFO", f"Processing movie: {movie_path.name} (IMDb: {imdb_id})") @@ -1005,24 +1005,34 @@ class MovieProcessor: # Use existing movie date decision logic dateadded, source, released = self._decide_movie_dates(imdb_id, movie_path, should_query, existing) - # Skip processing if no valid date found and file dates disabled - if dateadded is None: - _log("WARNING", f"Skipping movie {movie_path.name} - no valid date source available") - self.db.upsert_movie_dates(imdb_id, released, None, source, True) - return - - # Create NFO + # Create NFO regardless of date availability (preserves existing metadata) if config.manage_nfo: self.nfo_manager.create_movie_nfo( movie_path, imdb_id, dateadded, released, source, config.lock_metadata ) - # Update file mtimes + # Skip remaining processing if no valid date found and file dates disabled + if dateadded is None: + _log("WARNING", f"Movie {movie_path.name} - no valid date source available, but NFO was still processed") + self.db.upsert_movie_dates(imdb_id, released, None, source, True) + return + + _log("DEBUG", f"Movie {movie_path.name} proceeding to save: dateadded={dateadded}, source={source}") + + # Update file mtimes (only if we have a valid date) if config.fix_dir_mtimes and dateadded and dateadded != "MANUAL_REVIEW_NEEDED": self.nfo_manager.update_movie_files_mtime(movie_path, dateadded) + _log("DEBUG", f"Movie processing reached file mtime section: fix_dir_mtimes={config.fix_dir_mtimes}, dateadded={dateadded}") + # Save to database - self.db.upsert_movie_dates(imdb_id, released, dateadded, source, True) + _log("DEBUG", f"About to save to database: imdb_id={imdb_id}, dateadded={dateadded}") + try: + self.db.upsert_movie_dates(imdb_id, released, dateadded, source, True) + _log("DEBUG", f"Database save completed for {imdb_id}") + except Exception as e: + _log("ERROR", f"Database save failed for {imdb_id}: {e}") + raise _log("INFO", f"Completed processing movie: {movie_path.name} (source: {source})") @@ -1874,7 +1884,7 @@ async def manual_scan(background_tasks: BackgroundTasks, path: Optional[str] = N _log("INFO", f"Scanning movies in: {scan_path}") movie_count = 0 for item in scan_path.iterdir(): - if item.is_dir() and nfo_manager.parse_imdb_from_path(item): + if item.is_dir() and nfo_manager.find_movie_imdb_id(item): movie_count += 1 _log("INFO", f"Processing movie: {item.name}") try: @@ -1986,7 +1996,7 @@ async def test_movie_scan(): if path.exists(): for item in path.iterdir(): - if item.is_dir() and nfo_manager.parse_imdb_from_path(item): + if item.is_dir() and nfo_manager.find_movie_imdb_id(item): path_result["movies_found"] += 1 results.append(path_result)