🎯 Problem Fixed: - TV episodes downloaded today showed historical air dates (1951) as dateadded - Episodes appeared as old content instead of "recently added" in media servers - Incorrect fallback logic used air dates instead of actual download times ⚡ Revolutionary Changes: - Webhook-first processing: When Sonarr webhook fires → use current timestamp - Database priority: Existing NFOguard entries preserved (no re-processing) - Smart backfill: Manual scans check Sonarr import history first - Clear separation: Webhook vs backfill use different logic paths 🔧 Technical Implementation: - New method: _get_webhook_episode_date() for webhook-specific handling - Enhanced method: _get_single_episode_date() for proper backfill priority - Database-first approach: Always check NFOguard DB before external APIs 📋 Correct Processing Flow: 1. New episode download → Sonarr webhook → current time as dateadded 2. Re-download same episode → use existing stored date (preserve original) 3. Backfill scan → NFOguard DB → Sonarr history → air dates (last resort) 4. No bulk import - database populated through webhooks and manual scans 🏆 Results: - Episodes downloaded today now show today's date as dateadded - Historical air dates preserved in aired field for accuracy - "Recently Added" functionality restored in media servers - No regression for existing properly-dated episodes 🐛 Bug Fixes: - Fixed f-string syntax error in core/nfo_manager.py:324 - Resolved backslash escaping issue preventing startup
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.
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 that provides real-time date synchronization within Emby itself.
✨ Features
-
Import Date Locking
Captures the original date a movie or episode was imported and preserves it across upgrades. -
Enhanced NFO Management
Creates and updates.nfofiles for movies and TV episodes with consistent<dateadded>fields.
NEW: Full metadata integration from Sonarr API (episode titles, plots, ratings, runtime).
Optionally adds<lockdata>to prevent metadata drift from scrapers.
NEW: Timestamps all NFO files with creation/update dates. -
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
-
Webhooks
- Add NFOGuard as a webhook in Sonarr and Radarr.
- Supported event types:
Download,Upgrade,Rename.
-
Processing
- Maps Sonarr/Radarr paths to container paths.
- Looks up IMDb IDs from folder names or payloads.
- Writes
.nfofiles and updates file/dir mtimes. - Records everything in
media_dates.db.
-
Playback Apps
- Emby/Jellyfin/Plex see consistent
dateaddedvalues. - Upgrades don’t bubble to the top of “Recently Added.”
- Emby/Jellyfin/Plex see consistent
🚀 Quick Start (Docker Compose)
# 1. Copy configuration templates
cp .env.template .env
cp .env.secrets.template .env.secrets
# 2. Edit your configuration files
# .env - paths and preferences (safe to share)
# .env.secrets - API keys and passwords (never commit)
# 3. Start NFOGuard
docker-compose up -d
Docker Compose (Recommended):
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
Docker Run (Alternative):
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)
# Media paths
MOVIE_PATHS=/media/Movies/movies,/media/Movies/movies6
TV_PATHS=/media/TV/tv,/media/TV/tv6
# Release date priority (digital, physical, theatrical)
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)
# Database password
RADARR_DB_PASSWORD=your_actual_password
# API keys
TMDB_API_KEY=your_tmdb_api_key
RADARR_API_KEY=your_radarr_api_key
SONARR_API_KEY=your_sonarr_api_key
Security Features:
- 🔐 API key masking in logs automatically
- 🚫 Git protection - secrets never committed
- 🛠️ Easy debugging - main .env safe to share
🔌 API Endpoints
Webhook Endpoints
# Radarr webhook (automatic processing)
POST /webhook/radarr
# Sonarr webhook (automatic processing)
POST /webhook/sonarr
Manual Processing
# Manual scan all media
curl -X POST "http://localhost:8080/manual/scan?scan_type=both"
# Manual scan TV only
curl -X POST "http://localhost:8080/manual/scan?scan_type=tv"
# Manual scan movies only
curl -X POST "http://localhost:8080/manual/scan?scan_type=movies"
# Manual scan specific path
curl -X POST "http://localhost:8080/manual/scan?path=/media/movies"
# Bulk update all movies from Radarr database
curl -X POST "http://localhost:8080/bulk/update"
TV Show Processing (NEW in v0.6.0+)
# 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"
Testing & Validation
# 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
# 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:
{
"status": "healthy",
"version": "0.6.0",
"database_status": "healthy",
"radarr_database": {"status": "healthy", "movies": 1500}
}
Movie Debug:
{
"detected_import_date": "2025-07-08T03:30:04+00:00",
"import_source": "radarr:history.import",
"movie_title": "Movie Name"
}
Priority Logic Debug:
{
"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:
{
"status": "started",
"message": "Season scan started for Season 13"
}
TV Episode Processing:
{
"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 databaseradarr: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 festivalstmdb:limited- Type 2: Limited theatrical releasestmdb:theatrical- Type 3: Wide theatrical releasestmdb: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<premiered>YYYY-MM-DD</premiered>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)"
# 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:
# 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:
-
Enable TMDB integration and configure fallback priority:
# 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 -
Test the priority logic:
curl "http://localhost:8080/debug/movie/tt0115963/priority" -
Should show release date selection based on your priority order
"0 Movies Processed" During Manual Scan
Problem: Manual scan reports processing 0 movies
Solution:
-
Test directory scanning:
curl -X POST "http://localhost:8080/test/movie-scan" -
Verify paths in your
.envfile match your actual structure:# Your structure: /media/Movies/movies/ MOVIE_PATHS=/media/Movies/movies,/media/Movies/movies6 -
Ensure movie directories have IMDb tags:
Movie Name [imdb-tt1234567] (2023)/
Database Connection Issues
Problem: "Radarr database connection failed"
Solution:
-
Test database connectivity:
curl -X POST "http://localhost:8080/test/bulk-update" -
Verify database credentials in
.env: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:
- Add TMDB API key and configure fallback order:
# Add to your .env TMDB_API_KEY=your_api_key RELEASE_DATE_PRIORITY=digital,physical,theatrical - Restart container:
docker-compose restart - Test all release types:
curl "http://localhost:8080/debug/tmdb/tt0115963" - Verify priority logic:
curl "http://localhost:8080/debug/movie/tt0115963/priority"
Customizing Release Date Priority
Default Order: digital,physical,theatrical (your preference)
Alternative Orders:
# 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
# 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
<dateadded>fields - Jellyfin: ✅ Full support - reads NFO
<dateadded>fields - Plex: ✅ Full support - reads NFO
<dateadded>fields
TV Episodes
- Emby: ⚠️ Partial support - reads metadata (titles, plots, ratings) but ignores all NFO date fields
- Jellyfin: ✅ Full support - reads NFO
<dateadded>fields - Plex: ✅ Full support - reads NFO
<dateadded>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.
📖 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 version="1.0" encoding="UTF-8" standalone="yes"?>
<episodedetails>
<season>13</season>
<episode>2</episode>
<aired>2024-10-03</aired>
<dateadded>2025-02-19T23:10:35+00:00</dateadded>
<premiered>2025-02-19</premiered>
<lockdata>true</lockdata>
<!-- source: TV Episode; sonarr:history.import -->
<!-- managed by NFOGuard -->
</episodedetails>
After (Enhanced Format):
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<episodedetails>
<season>13</season>
<episode>2</episode>
<title>Ride the Blade</title>
<plot>Firehouse 51 tackles a fire at a local restaurant. Boden has an emotional conversation with his stepson.</plot>
<runtime>42</runtime>
<rating>8.2</rating>
<votes>1543</votes>
<aired>2024-10-03</aired>
<dateadded>2025-02-19T23:10:35+00:00</dateadded>
<premiered>2025-02-19</premiered>
<lockdata>true</lockdata>
<!-- source: TV Episode; sonarr:history.import -->
<!-- managed by NFOGuard at 2025-09-11T13:45:22+00:00 -->
</episodedetails>
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
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
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
DEBUG=true
Check container logs
docker logs sonarr-nfo-cache
Debug specific movie import detection
curl http://localhost:8080/debug/movie/tt1674782
Manual scan with verbose output
curl -X POST "http://localhost:8080/manual/scan?scan_type=movies"
File logs (inside container)
/app/data/logs/nfoguard.log
⚙️ 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
📜 License MIT License – do what you want, just don’t blame us if your timestamps go timey-wimey.