initial update
This commit is contained in:
Binary file not shown.
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2025 sbcrumb
|
Copyright (c) 2024 NFOGuard
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
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,
|
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
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
SOFTWARE.
|
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.
|
||||||
|
|||||||
@@ -1,164 +1,150 @@
|
|||||||
# NFOGuard
|
# 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.
|
[](https://hub.docker.com/r/sbcrumb/nfoguard)
|
||||||
|
[](https://hub.docker.com/r/sbcrumb/nfoguard)
|
||||||
|
[](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.
|
**Automated NFO file management for Radarr and Sonarr with intelligent date handling**
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
> **⚠️ 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
|
## ✨ Features
|
||||||
|
|
||||||
- **Import Date Locking**
|
- 🎬 **Movie & TV Support** - Works with both Radarr and Sonarr
|
||||||
Captures the original date a movie or episode was imported and preserves it across upgrades.
|
- 📅 **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**
|
## 🚀 Quick Start
|
||||||
Creates and updates `.nfo` files 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**
|
### 1. Download Configuration Files
|
||||||
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)
|
|
||||||
|
|
||||||
```bash
|
```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
|
cp .env.template .env
|
||||||
|
nano .env
|
||||||
|
|
||||||
|
# Copy and edit secrets (API keys, passwords)
|
||||||
cp .env.secrets.template .env.secrets
|
cp .env.secrets.template .env.secrets
|
||||||
|
nano .env.secrets
|
||||||
|
chmod 600 .env.secrets
|
||||||
|
|
||||||
# 2. Edit your configuration files
|
# Copy and edit Docker Compose
|
||||||
# .env - paths and preferences (safe to share)
|
cp docker-compose.example.yml docker-compose.yml
|
||||||
# .env.secrets - API keys and passwords (never commit)
|
nano docker-compose.yml
|
||||||
|
```
|
||||||
|
|
||||||
# 3. Start NFOGuard
|
### 3. Deploy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create data directory
|
||||||
|
mkdir -p ./data
|
||||||
|
|
||||||
|
# Start NFOGuard
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Check logs
|
||||||
|
docker-compose logs -f nfoguard
|
||||||
|
|
||||||
|
# Verify health
|
||||||
|
curl http://localhost:8080/health
|
||||||
```
|
```
|
||||||
|
|
||||||
**Docker Compose (Recommended)**:
|
## ⚙️ Configuration
|
||||||
```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
|
|
||||||
```
|
|
||||||
|
|
||||||
**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
|
```bash
|
||||||
docker run -d \
|
# Container paths (what NFOGuard sees)
|
||||||
--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
|
|
||||||
MOVIE_PATHS=/media/Movies/movies,/media/Movies/movies6
|
MOVIE_PATHS=/media/Movies/movies,/media/Movies/movies6
|
||||||
TV_PATHS=/media/TV/tv,/media/TV/tv6
|
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
|
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
|
```bash
|
||||||
# Database password
|
DEBUG=false # Clean production logs
|
||||||
RADARR_DB_PASSWORD=your_actual_password
|
SUPPRESS_TVDB_WARNINGS=true # Hide non-critical API failures
|
||||||
|
|
||||||
# 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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Security Features**:
|
## 🐳 Docker Images
|
||||||
- 🔐 **API key masking** in logs automatically
|
|
||||||
- 🚫 **Git protection** - secrets never committed
|
|
||||||
- 🛠️ **Easy debugging** - main .env safe to share
|
|
||||||
|
|
||||||
🔌 API Endpoints
|
### Production (Stable)
|
||||||
|
```yaml
|
||||||
### Webhook Endpoints
|
image: sbcrumb/nfoguard:latest
|
||||||
```bash
|
|
||||||
# Radarr webhook (automatic processing)
|
|
||||||
POST /webhook/radarr
|
|
||||||
|
|
||||||
# Sonarr webhook (automatic processing)
|
|
||||||
POST /webhook/sonarr
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 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
|
```bash
|
||||||
# Manual scan all media
|
# Manual scan all media (movies and TV)
|
||||||
curl -X POST "http://localhost:8080/manual/scan?scan_type=both"
|
curl -X POST "http://localhost:8080/manual/scan?scan_type=both"
|
||||||
|
|
||||||
# Manual scan TV only
|
# 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"
|
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
|
```bash
|
||||||
# Process a specific TV season (URL-safe)
|
docker-compose logs -f nfoguard
|
||||||
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
|
### Enable Debug Mode
|
||||||
```bash
|
```bash
|
||||||
# Test database connections
|
# In .env file
|
||||||
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 `<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)"
|
|
||||||
```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 `<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.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔑 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
|
|
||||||
<?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
|
|
||||||
<?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
|
|
||||||
```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
|
|
||||||
DEBUG=true
|
DEBUG=true
|
||||||
|
PATH_DEBUG=true
|
||||||
|
```
|
||||||
|
|
||||||
# Check container logs
|
### Health Check
|
||||||
docker logs sonarr-nfo-cache
|
```bash
|
||||||
|
curl http://localhost:8080/health
|
||||||
|
```
|
||||||
|
|
||||||
# Debug specific movie import detection
|
### Common Issues
|
||||||
curl http://localhost:8080/debug/movie/tt1674782
|
|
||||||
|
|
||||||
# Manual scan with verbose output
|
1. **Permission Errors**: Ensure NFOGuard can write to mounted directories
|
||||||
curl -X POST "http://localhost:8080/manual/scan?scan_type=movies"
|
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)
|
## 📊 What NFOGuard Does
|
||||||
/app/data/logs/nfoguard.log
|
|
||||||
|
|
||||||
⚙️ Environment Variables (v0.2.1+)
|
### Before
|
||||||
Variable Default Description
|
```xml
|
||||||
RADARR_ROOT_FOLDERS (required) What Radarr sees: /mnt/unionfs/Media/Movies/movies
|
<movie>
|
||||||
SONARR_ROOT_FOLDERS (required) What Sonarr sees: /mnt/unionfs/Media/TV/tv
|
<title>Movie Title</title>
|
||||||
DOWNLOAD_PATH_INDICATORS (see example) Paths that indicate downloads vs existing files
|
<year>2023</year>
|
||||||
MOVIE_PRIORITY import_then_digital Strategy: import_then_digital or digital_then_import
|
<!-- Existing Radarr metadata -->
|
||||||
MOVIE_POLL_MODE always When to query APIs: always, if_missing, never
|
</movie>
|
||||||
MOVIE_DATE_UPDATE_MODE backfill_only Update behavior: backfill_only or overwrite
|
```
|
||||||
|
|
||||||
📜 License
|
### After
|
||||||
MIT License – do what you want, just don’t blame us if your timestamps go timey-wimey.
|
```xml
|
||||||
|
<movie>
|
||||||
|
<title>Movie Title</title>
|
||||||
|
<year>2023</year>
|
||||||
|
<!-- Existing Radarr metadata preserved -->
|
||||||
|
|
||||||
|
<!-- NFOGuard additions at bottom -->
|
||||||
|
<digital_release_date>2023-03-15</digital_release_date>
|
||||||
|
<lockdata>true</lockdata>
|
||||||
|
<!-- Manager: NFOGuard -->
|
||||||
|
</movie>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 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
|
||||||
|
<!-- Method 1: uniqueid tag (preferred) -->
|
||||||
|
<uniqueid type="imdb">tt1234567</uniqueid>
|
||||||
|
|
||||||
|
<!-- Method 2: imdbid tag -->
|
||||||
|
<imdbid>tt1234567</imdbid>
|
||||||
|
|
||||||
|
<!-- Method 3: imdb tag -->
|
||||||
|
<imdb>tt1234567</imdb>
|
||||||
|
```
|
||||||
|
|
||||||
|
**TV Show NFO (tvshow.nfo):**
|
||||||
|
```xml
|
||||||
|
<!-- Same format as movies -->
|
||||||
|
<uniqueid type="imdb">tt1234567</uniqueid>
|
||||||
|
<imdbid>tt1234567</imdbid>
|
||||||
|
<imdb>tt1234567</imdb>
|
||||||
|
```
|
||||||
|
|
||||||
|
**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! 🎯
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ If you have an existing `.env` with API keys:
|
|||||||
version: '3.8'
|
version: '3.8'
|
||||||
services:
|
services:
|
||||||
nfoguard:
|
nfoguard:
|
||||||
image: ghcr.io/your-org/nfoguard:latest
|
image: sbcrumb/nfoguard:latest
|
||||||
container_name: nfoguard
|
container_name: nfoguard
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
|
|||||||
+294
-1
@@ -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**: `<uniqueid type="imdb">tt123456</uniqueid>` (NEW)
|
||||||
|
4. **Legacy NFO Tags**: `<imdbid>` and `<imdb>` 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
|
# 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
|
### Latest Updates (v1.6.0) - September 16, 2025
|
||||||
- **📄 NFO IDENTIFICATION**: Added support for identifying movies/TV shows from NFO files
|
- **📄 NFO IDENTIFICATION**: Added support for identifying movies/TV shows from NFO files
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/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 json
|
||||||
import time
|
import time
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ class NFOGuardDatabase:
|
|||||||
def upsert_movie_dates(self, imdb_id: str, released: Optional[str],
|
def upsert_movie_dates(self, imdb_id: str, released: Optional[str],
|
||||||
dateadded: Optional[str], source: str, has_video_file: bool = False):
|
dateadded: Optional[str], source: str, has_video_file: bool = False):
|
||||||
"""Insert or update movie date record"""
|
"""Insert or update movie date record"""
|
||||||
|
print(f"🔍 DATABASE UPSERT: imdb_id={imdb_id}, dateadded={dateadded}, source={source}")
|
||||||
with self.get_connection() as conn:
|
with self.get_connection() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
# Use INSERT OR REPLACE to ensure we always update the dates properly
|
# Use INSERT OR REPLACE to ensure we always update the dates properly
|
||||||
@@ -185,6 +186,11 @@ class NFOGuardDatabase:
|
|||||||
)
|
)
|
||||||
""", (imdb_id, imdb_id, released, dateadded, source, has_video_file, datetime.utcnow().isoformat()))
|
""", (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]:
|
def get_series_episodes(self, imdb_id: str, has_video_file_only: bool = False) -> List[Dict]:
|
||||||
"""Get all episodes for a series"""
|
"""Get all episodes for a series"""
|
||||||
with self.get_connection() as conn:
|
with self.get_connection() as conn:
|
||||||
|
|||||||
+155
-5
@@ -49,6 +49,93 @@ class NFOManager:
|
|||||||
|
|
||||||
return None
|
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 <uniqueid type="imdb">ttXXXXXX</uniqueid>
|
||||||
|
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 <imdbid>ttXXXXXX</imdbid>
|
||||||
|
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 <imdb>ttXXXXXX</imdb>
|
||||||
|
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 </movie> tag and truncate after it
|
||||||
|
last_movie_end = content.rfind('</movie>')
|
||||||
|
if last_movie_end != -1:
|
||||||
|
xml_content = content[:last_movie_end + 8] # +8 for </movie>
|
||||||
|
|
||||||
|
# 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 </movie>
|
||||||
|
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,
|
def create_movie_nfo(self, movie_dir: Path, imdb_id: str, dateadded: str,
|
||||||
released: Optional[str] = None, source: str = "unknown",
|
released: Optional[str] = None, source: str = "unknown",
|
||||||
lock_metadata: bool = True) -> None:
|
lock_metadata: bool = True) -> None:
|
||||||
@@ -59,8 +146,8 @@ class NFOManager:
|
|||||||
# Try to load existing NFO file
|
# Try to load existing NFO file
|
||||||
if nfo_path.exists():
|
if nfo_path.exists():
|
||||||
try:
|
try:
|
||||||
tree = ET.parse(nfo_path)
|
# Try to parse the XML, handling URLs appended after </movie>
|
||||||
movie = tree.getroot()
|
movie = self._parse_nfo_with_tolerance(nfo_path)
|
||||||
|
|
||||||
# Ensure root element is <movie>
|
# Ensure root element is <movie>
|
||||||
if movie.tag != "movie":
|
if movie.tag != "movie":
|
||||||
@@ -279,6 +366,48 @@ class NFOManager:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Error creating/updating season NFO {nfo_path}: {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,
|
def create_episode_nfo(self, season_dir: Path, season_num: int, episode_num: int,
|
||||||
aired: Optional[str], dateadded: Optional[str], source: str,
|
aired: Optional[str], dateadded: Optional[str], source: str,
|
||||||
lock_metadata: bool = True, enhanced_metadata: Optional[Dict[str, Any]] = None) -> None:
|
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"
|
episode_filename = f"S{season_num:02d}E{episode_num:02d}.nfo"
|
||||||
nfo_path = season_dir / episode_filename
|
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:
|
||||||
# Try to load existing NFO file
|
# First, check for existing long-named NFO files that need migration
|
||||||
if nfo_path.exists():
|
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:
|
try:
|
||||||
tree = ET.parse(nfo_path)
|
tree = ET.parse(source_nfo_path)
|
||||||
episode = tree.getroot()
|
episode = tree.getroot()
|
||||||
|
|
||||||
# Ensure root element is <episodedetails>
|
# Ensure root element is <episodedetails>
|
||||||
if episode.tag != "episodedetails":
|
if episode.tag != "episodedetails":
|
||||||
raise ValueError("Root element is not <episodedetails>")
|
raise ValueError("Root element is not <episodedetails>")
|
||||||
|
|
||||||
|
# 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
|
# Remove existing NFOGuard-managed elements to avoid duplicates
|
||||||
# These will be re-added at the bottom
|
# These will be re-added at the bottom
|
||||||
nfoguard_fields = ["aired", "dateadded", "lockdata", "season", "episode"]
|
nfoguard_fields = ["aired", "dateadded", "lockdata", "season", "episode"]
|
||||||
@@ -374,6 +516,14 @@ class NFOManager:
|
|||||||
print(f"✅ Successfully created/updated episode NFO: {nfo_path}")
|
print(f"✅ Successfully created/updated episode NFO: {nfo_path}")
|
||||||
print(f" S{season_num:02d}E{episode_num:02d}, Aired: {aired}, Date Added: {dateadded}")
|
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:
|
except Exception as e:
|
||||||
print(f"❌ Error creating/updating episode NFO {nfo_path}: {e}")
|
print(f"❌ Error creating/updating episode NFO {nfo_path}: {e}")
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
+24
-14
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
NFOGuard - Consolidated webhook server for TV and Movie import date management
|
NFOGuard - Automated NFO file management for Radarr and Sonarr
|
||||||
Replaces webhook_server.py and media_date_cache.py with modular architecture
|
Modular architecture with webhook processing and intelligent date handling
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
@@ -949,9 +949,9 @@ class MovieProcessor:
|
|||||||
|
|
||||||
def process_movie(self, movie_path: Path, webhook_mode: bool = False) -> None:
|
def process_movie(self, movie_path: Path, webhook_mode: bool = False) -> None:
|
||||||
"""Process a movie directory"""
|
"""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:
|
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
|
return
|
||||||
|
|
||||||
_log("INFO", f"Processing movie: {movie_path.name} (IMDb: {imdb_id})")
|
_log("INFO", f"Processing movie: {movie_path.name} (IMDb: {imdb_id})")
|
||||||
@@ -1005,24 +1005,34 @@ class MovieProcessor:
|
|||||||
# Use existing movie date decision logic
|
# Use existing movie date decision logic
|
||||||
dateadded, source, released = self._decide_movie_dates(imdb_id, movie_path, should_query, existing)
|
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
|
# Create NFO regardless of date availability (preserves existing metadata)
|
||||||
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
|
|
||||||
if config.manage_nfo:
|
if config.manage_nfo:
|
||||||
self.nfo_manager.create_movie_nfo(
|
self.nfo_manager.create_movie_nfo(
|
||||||
movie_path, imdb_id, dateadded, released, source, config.lock_metadata
|
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":
|
if config.fix_dir_mtimes and dateadded and dateadded != "MANUAL_REVIEW_NEEDED":
|
||||||
self.nfo_manager.update_movie_files_mtime(movie_path, dateadded)
|
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
|
# Save to database
|
||||||
|
_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)
|
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})")
|
_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}")
|
_log("INFO", f"Scanning movies in: {scan_path}")
|
||||||
movie_count = 0
|
movie_count = 0
|
||||||
for item in scan_path.iterdir():
|
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
|
movie_count += 1
|
||||||
_log("INFO", f"Processing movie: {item.name}")
|
_log("INFO", f"Processing movie: {item.name}")
|
||||||
try:
|
try:
|
||||||
@@ -1986,7 +1996,7 @@ async def test_movie_scan():
|
|||||||
|
|
||||||
if path.exists():
|
if path.exists():
|
||||||
for item in path.iterdir():
|
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
|
path_result["movies_found"] += 1
|
||||||
|
|
||||||
results.append(path_result)
|
results.append(path_result)
|
||||||
|
|||||||
Reference in New Issue
Block a user