Merge pull request 'fixes: web interface and movie processing' (#54) from dev into main
Reviewed-on: #54
This commit was merged in pull request #54.
This commit is contained in:
+134
-20
@@ -1,28 +1,142 @@
|
|||||||
# NFOGuard Environment Variables
|
# ===========================================
|
||||||
|
# NFOGuard Configuration - CORRECTED VERSION
|
||||||
|
# ===========================================
|
||||||
|
# Main configuration file - safe to share for debugging
|
||||||
|
# Sensitive data (API keys, passwords) are in .env.secrets
|
||||||
|
|
||||||
# User and Group IDs
|
# ===========================================
|
||||||
PUID=1000
|
# MEDIA PATHS (REQUIRED) - FIXED
|
||||||
PGID=1000
|
# ===========================================
|
||||||
|
# Container paths (what NFOGuard sees inside container)
|
||||||
|
MOVIE_PATHS=/media/Movies/movies,/media/Movies/movies6
|
||||||
|
TV_PATHS=/media/TV/tv,/media/TV/tv6
|
||||||
|
|
||||||
# Timezone
|
# Radarr paths (what Radarr sees on the host system)
|
||||||
TZ=UTC
|
RADARR_ROOT_FOLDERS=/mnt/unionfs/Media/Movies/movies,/mnt/unionfs/Media/Movies/movies6
|
||||||
|
|
||||||
# Media path (for read-only access to scan NFO files)
|
# Sonarr paths (what Sonarr sees on the host system) - FIXED VARIABLE NAME AND PATHS
|
||||||
MEDIA_PATH=/path/to/your/media
|
SONARR_ROOT_FOLDERS=/mnt/unionfs/Media/TV/tv,/mnt/unionfs/Media/TV/tv6
|
||||||
|
|
||||||
# Database settings (if using PostgreSQL)
|
# Download detection paths (for identifying downloads vs existing files)
|
||||||
# DB_USER=nfoguard
|
DOWNLOAD_PATH_INDICATORS=/downloads/,/nzbget/,/completed/,/sabnzbd/,/torrents/
|
||||||
# DB_PASSWORD=your_secure_password
|
|
||||||
|
|
||||||
# Emby Plugin Deployment - Bind Mount Method (Recommended)
|
# ===========================================
|
||||||
# Set this to the path where Emby plugins are installed on your host
|
# DATABASE CONFIGURATION
|
||||||
# Common paths:
|
# ===========================================
|
||||||
# - /var/lib/emby/plugins (native Emby install)
|
# NFOGuard Database Configuration (PostgreSQL - v2.6+)
|
||||||
# - /path/to/emby/config/plugins (Docker Emby)
|
# NOTE: For new installations, PostgreSQL is strongly recommended
|
||||||
# - /config/plugins (linuxserver.io Emby)
|
# Set these values in .env.secrets for security:
|
||||||
EMBY_PLUGINS_PATH=/path/to/emby/plugins
|
# - DB_PASSWORD=your_secure_password
|
||||||
|
# - DB_USER=nfoguard (default)
|
||||||
|
|
||||||
# NFOGuard specific settings
|
# PostgreSQL Database Settings
|
||||||
|
DB_TYPE=postgresql
|
||||||
|
DB_HOST=nfoguard-db # Container name from docker-compose
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_NAME=nfoguard
|
||||||
|
|
||||||
|
# Legacy SQLite Configuration (Deprecated in v2.6+)
|
||||||
|
# Only use for existing SQLite installations
|
||||||
|
# DB_PATH=/app/data/media_dates.db
|
||||||
|
|
||||||
|
# Log file directory
|
||||||
|
LOG_DIR=/app/data/logs
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# RADARR DATABASE CONNECTION (RECOMMENDED)
|
||||||
|
# ===========================================
|
||||||
|
# Direct database access for better performance
|
||||||
|
RADARR_DB_TYPE=postgresql
|
||||||
|
RADARR_DB_HOST=192.168.255.50
|
||||||
|
RADARR_DB_PORT=5432
|
||||||
|
RADARR_DB_NAME=radarr-main
|
||||||
|
RADARR_DB_USER=postgres
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# API CONNECTIONS (OPTIONAL)
|
||||||
|
# ===========================================
|
||||||
|
# API keys are stored in .env.secrets for security
|
||||||
|
RADARR_URL=http://radarr:7878
|
||||||
|
SONARR_URL=http://sonarr:8989
|
||||||
|
JELLYSEERR_URL=http://jellyseerr:5055
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# RELEASE DATE PROCESSING
|
||||||
|
# ===========================================
|
||||||
|
# Priority order for release date fallbacks (digital, physical, theatrical)
|
||||||
|
RELEASE_DATE_PRIORITY=digital,physical,theatrical
|
||||||
|
#RELEASE_DATE_PRIORITY=digital,theatrical,physical
|
||||||
|
ENABLE_SMART_DATE_VALIDATION=true
|
||||||
|
MAX_RELEASE_DATE_GAP_YEARS=10
|
||||||
|
|
||||||
|
# Prefer API release dates over file modification dates for manual imports
|
||||||
|
PREFER_RELEASE_DATES_OVER_FILE_DATES=true
|
||||||
|
|
||||||
|
# Disable file date fallback completely (recommended for clean imports)
|
||||||
|
ALLOW_FILE_DATE_FALLBACK=false
|
||||||
|
|
||||||
|
# TMDB country for regional release date preferences
|
||||||
|
TMDB_COUNTRY=US
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# NFO FILE MANAGEMENT
|
||||||
|
# ===========================================
|
||||||
|
# Create/update .nfo files
|
||||||
|
MANAGE_NFO=true
|
||||||
|
|
||||||
|
# Update file modification times to match import dates
|
||||||
|
FIX_DIR_MTIMES=true
|
||||||
|
|
||||||
|
# Add lockdata tags to prevent metadata overwrites
|
||||||
|
LOCK_METADATA=true
|
||||||
|
|
||||||
|
# Brand name in NFO comments
|
||||||
|
MANAGER_BRAND=NFOGuard
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# PROCESSING BEHAVIOR
|
||||||
|
# ===========================================
|
||||||
|
# Movie date update strategy
|
||||||
|
MOVIE_DATE_UPDATE_MODE=overwrite
|
||||||
|
MOVIE_PRESERVE_EXISTING=false
|
||||||
|
|
||||||
|
# When to update existing dates (always, missing_only, never)
|
||||||
|
UPDATE_MODE=always
|
||||||
|
|
||||||
|
# File modification time behavior (update, leave_alone)
|
||||||
|
MTIME_BEHAVIOR=update
|
||||||
|
|
||||||
|
# Manual scan priority: use existing NFO dates for speed vs check APIs for accuracy
|
||||||
|
# false (default) = Check external APIs first, use NFO as fallback (slower but accurate)
|
||||||
|
# true = Use NFO dates immediately without API checks (faster but may use wrong dates)
|
||||||
|
MANUAL_SCAN_PRIORITIZE_NFO=false
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# PERFORMANCE & BATCHING
|
||||||
|
# ===========================================
|
||||||
|
# Delay before processing batched events (seconds)
|
||||||
|
BATCH_DELAY=5.0
|
||||||
|
|
||||||
|
# Maximum concurrent series processing
|
||||||
|
MAX_CONCURRENT_SERIES=3
|
||||||
|
|
||||||
|
# API timeout in seconds
|
||||||
|
TIMEOUT_SECONDS=45
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# DEBUGGING
|
||||||
|
# ===========================================
|
||||||
|
# Enable verbose logging (true/false)
|
||||||
DEBUG=false
|
DEBUG=false
|
||||||
|
|
||||||
|
# Enable path mapping debug output (true/false)
|
||||||
PATH_DEBUG=false
|
PATH_DEBUG=false
|
||||||
SUPPRESS_TVDB_WARNINGS=true
|
|
||||||
|
# Suppress TVDB API warnings (true/false) - TVDB failures are common and non-critical
|
||||||
|
SUPPRESS_TVDB_WARNINGS=true
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# SERVER CONFIGURATION
|
||||||
|
# ===========================================
|
||||||
|
# Port for webhook server
|
||||||
|
PORT=8080
|
||||||
@@ -4,9 +4,17 @@
|
|||||||
# Add .env.secrets to your .gitignore
|
# Add .env.secrets to your .gitignore
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# RADARR DATABASE CREDENTIALS
|
# NFOGUARD DATABASE CREDENTIALS (REQUIRED)
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# Database password for PostgreSQL connection
|
# NFOGuard PostgreSQL database password (REQUIRED for v2.6+)
|
||||||
|
DB_PASSWORD=your_secure_nfoguard_password
|
||||||
|
# Optional: Override default database user (defaults to 'nfoguard')
|
||||||
|
# DB_USER=nfoguard
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# RADARR DATABASE CREDENTIALS (OPTIONAL)
|
||||||
|
# ===========================================
|
||||||
|
# Database password for external Radarr PostgreSQL connection (optional optimization)
|
||||||
RADARR_DB_PASSWORD=your_radarr_db_password
|
RADARR_DB_PASSWORD=your_radarr_db_password
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
-129
@@ -1,129 +0,0 @@
|
|||||||
# ===========================================
|
|
||||||
# NFOGuard Configuration - CORRECTED VERSION
|
|
||||||
# ===========================================
|
|
||||||
# Main configuration file - safe to share for debugging
|
|
||||||
# Sensitive data (API keys, passwords) are in .env.secrets
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# MEDIA PATHS (REQUIRED) - FIXED
|
|
||||||
# ===========================================
|
|
||||||
# Container paths (what NFOGuard sees inside container)
|
|
||||||
MOVIE_PATHS=/media/Movies/movies,/media/Movies/movies6
|
|
||||||
TV_PATHS=/media/TV/tv,/media/TV/tv6
|
|
||||||
|
|
||||||
# Radarr paths (what Radarr sees on the host system)
|
|
||||||
RADARR_ROOT_FOLDERS=/mnt/unionfs/Media/Movies/movies,/mnt/unionfs/Media/Movies/movies6
|
|
||||||
|
|
||||||
# Sonarr paths (what Sonarr sees on the host system) - FIXED VARIABLE NAME AND PATHS
|
|
||||||
SONARR_ROOT_FOLDERS=/mnt/unionfs/Media/TV/tv,/mnt/unionfs/Media/TV/tv6
|
|
||||||
|
|
||||||
# Download detection paths (for identifying downloads vs existing files)
|
|
||||||
DOWNLOAD_PATH_INDICATORS=/downloads/,/nzbget/,/completed/,/sabnzbd/,/torrents/
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# DATABASE CONFIGURATION
|
|
||||||
# ===========================================
|
|
||||||
# NFOGuard SQLite database location
|
|
||||||
DB_PATH=/app/data/media_dates.db
|
|
||||||
|
|
||||||
# Log file directory
|
|
||||||
LOG_DIR=/app/data/logs
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# RADARR DATABASE CONNECTION (RECOMMENDED)
|
|
||||||
# ===========================================
|
|
||||||
# Direct database access for better performance
|
|
||||||
RADARR_DB_TYPE=postgresql
|
|
||||||
RADARR_DB_HOST=192.168.255.50
|
|
||||||
RADARR_DB_PORT=5432
|
|
||||||
RADARR_DB_NAME=radarr-main
|
|
||||||
RADARR_DB_USER=postgres
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# API CONNECTIONS (OPTIONAL)
|
|
||||||
# ===========================================
|
|
||||||
# API keys are stored in .env.secrets for security
|
|
||||||
RADARR_URL=http://radarr:7878
|
|
||||||
SONARR_URL=http://sonarr:8989
|
|
||||||
JELLYSEERR_URL=http://jellyseerr:5055
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# RELEASE DATE PROCESSING
|
|
||||||
# ===========================================
|
|
||||||
# Priority order for release date fallbacks (digital, physical, theatrical)
|
|
||||||
RELEASE_DATE_PRIORITY=digital,physical,theatrical
|
|
||||||
#RELEASE_DATE_PRIORITY=digital,theatrical,physical
|
|
||||||
ENABLE_SMART_DATE_VALIDATION=true
|
|
||||||
MAX_RELEASE_DATE_GAP_YEARS=10
|
|
||||||
|
|
||||||
# Prefer API release dates over file modification dates for manual imports
|
|
||||||
PREFER_RELEASE_DATES_OVER_FILE_DATES=true
|
|
||||||
|
|
||||||
# Disable file date fallback completely (recommended for clean imports)
|
|
||||||
ALLOW_FILE_DATE_FALLBACK=false
|
|
||||||
|
|
||||||
# TMDB country for regional release date preferences
|
|
||||||
TMDB_COUNTRY=US
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# NFO FILE MANAGEMENT
|
|
||||||
# ===========================================
|
|
||||||
# Create/update .nfo files
|
|
||||||
MANAGE_NFO=true
|
|
||||||
|
|
||||||
# Update file modification times to match import dates
|
|
||||||
FIX_DIR_MTIMES=true
|
|
||||||
|
|
||||||
# Add lockdata tags to prevent metadata overwrites
|
|
||||||
LOCK_METADATA=true
|
|
||||||
|
|
||||||
# Brand name in NFO comments
|
|
||||||
MANAGER_BRAND=NFOGuard
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# PROCESSING BEHAVIOR
|
|
||||||
# ===========================================
|
|
||||||
# Movie date update strategy
|
|
||||||
MOVIE_DATE_UPDATE_MODE=overwrite
|
|
||||||
MOVIE_PRESERVE_EXISTING=false
|
|
||||||
|
|
||||||
# When to update existing dates (always, missing_only, never)
|
|
||||||
UPDATE_MODE=always
|
|
||||||
|
|
||||||
# File modification time behavior (update, leave_alone)
|
|
||||||
MTIME_BEHAVIOR=update
|
|
||||||
|
|
||||||
# Manual scan priority: use existing NFO dates for speed vs check APIs for accuracy
|
|
||||||
# false (default) = Check external APIs first, use NFO as fallback (slower but accurate)
|
|
||||||
# true = Use NFO dates immediately without API checks (faster but may use wrong dates)
|
|
||||||
MANUAL_SCAN_PRIORITIZE_NFO=false
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# PERFORMANCE & BATCHING
|
|
||||||
# ===========================================
|
|
||||||
# Delay before processing batched events (seconds)
|
|
||||||
BATCH_DELAY=5.0
|
|
||||||
|
|
||||||
# Maximum concurrent series processing
|
|
||||||
MAX_CONCURRENT_SERIES=3
|
|
||||||
|
|
||||||
# API timeout in seconds
|
|
||||||
TIMEOUT_SECONDS=45
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# DEBUGGING
|
|
||||||
# ===========================================
|
|
||||||
# Enable verbose logging (true/false)
|
|
||||||
DEBUG=false
|
|
||||||
|
|
||||||
# Enable path mapping debug output (true/false)
|
|
||||||
PATH_DEBUG=false
|
|
||||||
|
|
||||||
# Suppress TVDB API warnings (true/false) - TVDB failures are common and non-critical
|
|
||||||
SUPPRESS_TVDB_WARNINGS=true
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# SERVER CONFIGURATION
|
|
||||||
# ===========================================
|
|
||||||
# Port for webhook server
|
|
||||||
PORT=8080
|
|
||||||
Binary file not shown.
@@ -24,41 +24,42 @@
|
|||||||
|
|
||||||
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.
|
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
|
||||||
|
|
||||||
### **🎬 Core Media Management**
|
### **Core Media Management**
|
||||||
- **Movie & TV Support** - Works with both Radarr and Sonarr
|
- **Movie & TV Support** - Works with both Radarr and Sonarr
|
||||||
- **Smart Date Handling** - Prioritizes digital, physical, and theatrical release dates
|
- **Smart Date Handling** - Prioritizes digital, physical, and theatrical release dates
|
||||||
- **Webhook Integration** - Triggers automatically on import, upgrade, and rename
|
- **Webhook Integration** - Triggers automatically on import, upgrade, and rename
|
||||||
- **NFO Preservation** - Maintains existing metadata, adds fields cleanly at bottom
|
- **NFO Preservation** - Maintains existing metadata, adds fields cleanly at bottom
|
||||||
- **Metadata Locking** - Prevents overwrites with lockdata tags
|
- **Metadata Locking** - Prevents overwrites with lockdata tags
|
||||||
|
|
||||||
### **⚡ Performance & Scalability**
|
### **Performance & Scalability**
|
||||||
- **Async I/O Operations** - High-performance concurrent file processing
|
- **Async I/O Operations** - High-performance concurrent file processing
|
||||||
- **Batch Processing** - Efficient handling of multiple files simultaneously
|
- **Batch Processing** - Efficient handling of multiple files simultaneously
|
||||||
- **Database Integration** - Direct PostgreSQL/SQLite access for better performance
|
- **PostgreSQL Database** - Production-ready database with optimized queries
|
||||||
- **Smart Concurrency** - Controlled parallel processing with rate limiting
|
- **Smart Skip Logic** - Database-first checking eliminates expensive filesystem scans
|
||||||
|
- **88% Scan Optimization** - TV library scans reduced from hours to minutes
|
||||||
|
|
||||||
### **🔧 Configuration & Validation**
|
### **Web Interface & Management**
|
||||||
|
- **Complete Web UI** - Episode and movie management with filtering and search
|
||||||
|
- **Database Cleanup Tools** - Delete orphaned episodes with confirmation dialogs
|
||||||
|
- **Real-time Statistics** - Live episode counts and source mapping
|
||||||
|
- **Manual Scan Control** - Smart, full, and incomplete scan modes
|
||||||
|
- **Health Monitoring** - System status and performance metrics
|
||||||
|
|
||||||
|
### **Configuration & Validation**
|
||||||
- **Comprehensive Config Validation** - Validates all settings before startup
|
- **Comprehensive Config Validation** - Validates all settings before startup
|
||||||
- **Runtime Health Checks** - Monitors system health and dependencies
|
- **Runtime Health Checks** - Monitors system health and dependencies
|
||||||
- **Path Validation** - Ensures media directories are accessible
|
- **Path Validation** - Ensures media directories are accessible
|
||||||
- **Database Connectivity Tests** - Validates database connections
|
- **Database Connectivity Tests** - Validates database connections
|
||||||
|
|
||||||
### **📊 Monitoring & Observability**
|
### **Production Ready**
|
||||||
- **Health Check APIs** - `/api/v1/health`, `/api/v1/health/ready`, `/api/v1/health/live`
|
|
||||||
- **Prometheus Metrics** - `/api/v1/metrics` endpoint for monitoring integration
|
|
||||||
- **Performance Monitoring** - Operation timing, error rates, and bottleneck detection
|
|
||||||
- **Structured Logging** - JSON logs with correlation IDs for request tracing
|
|
||||||
- **System Status APIs** - Real-time performance and error metrics
|
|
||||||
|
|
||||||
### **🐳 Production Ready**
|
|
||||||
- **Docker & Kubernetes** - Health checks for orchestration platforms
|
- **Docker & Kubernetes** - Health checks for orchestration platforms
|
||||||
- **Grafana Compatible** - Metrics endpoints for dashboard integration
|
- **Graceful Shutdown** - Proper signal handling for container management
|
||||||
- **Configuration CLI** - Validation tools for troubleshooting
|
- **Configuration CLI** - Validation tools for troubleshooting
|
||||||
- **Modular Architecture** - Clean separation of concerns for maintainability
|
- **Modular Architecture** - Clean separation of concerns for maintainability
|
||||||
|
|
||||||
## 🚀 Quick Start
|
## Quick Start
|
||||||
|
|
||||||
### 1. Download Configuration Files
|
### 1. Download Configuration Files
|
||||||
|
|
||||||
@@ -101,7 +102,7 @@ docker-compose logs -f nfoguard
|
|||||||
curl http://localhost:8080/health
|
curl http://localhost:8080/health
|
||||||
```
|
```
|
||||||
|
|
||||||
## ⚙️ Configuration
|
## Configuration
|
||||||
|
|
||||||
### Environment Files
|
### Environment Files
|
||||||
|
|
||||||
@@ -134,7 +135,7 @@ DEBUG=false # Clean production logs
|
|||||||
SUPPRESS_TVDB_WARNINGS=true # Hide non-critical API failures
|
SUPPRESS_TVDB_WARNINGS=true # Hide non-critical API failures
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🐳 Docker Images
|
## Docker Images
|
||||||
|
|
||||||
### Production (Stable)
|
### Production (Stable)
|
||||||
```yaml
|
```yaml
|
||||||
@@ -257,7 +258,7 @@ volumes:
|
|||||||
- /home/user/media/tv:/media/TV/tv:rw
|
- /home/user/media/tv:/media/TV/tv:rw
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔧 Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### Check Logs
|
### Check Logs
|
||||||
```bash
|
```bash
|
||||||
@@ -276,7 +277,7 @@ PATH_DEBUG=true
|
|||||||
curl http://localhost:8080/health
|
curl http://localhost:8080/health
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📊 Monitoring & Observability
|
## Monitoring & Observability
|
||||||
|
|
||||||
NFOGuard provides comprehensive monitoring capabilities for production deployments:
|
NFOGuard provides comprehensive monitoring capabilities for production deployments:
|
||||||
|
|
||||||
@@ -356,7 +357,7 @@ LOG_LEVEL=INFO
|
|||||||
3. **Webhooks**: Check URLs and ensure port 8080 is accessible
|
3. **Webhooks**: Check URLs and ensure port 8080 is accessible
|
||||||
4. **Database**: Verify PostgreSQL credentials in `.env.secrets`
|
4. **Database**: Verify PostgreSQL credentials in `.env.secrets`
|
||||||
|
|
||||||
## 📊 What NFOGuard Does
|
## What NFOGuard Does
|
||||||
|
|
||||||
### Before
|
### Before
|
||||||
```xml
|
```xml
|
||||||
@@ -392,7 +393,7 @@ LOG_LEVEL=INFO
|
|||||||
|
|
||||||
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:
|
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**
|
### **Movies**
|
||||||
|
|
||||||
**Directory Structure:**
|
**Directory Structure:**
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -688,6 +688,9 @@ async def manual_scan(background_tasks: BackgroundTasks, path: Optional[str] = N
|
|||||||
movie_skipped += 1
|
movie_skipped += 1
|
||||||
elif result == "processed":
|
elif result == "processed":
|
||||||
movie_processed += 1
|
movie_processed += 1
|
||||||
|
elif result == "no_video_files":
|
||||||
|
print(f"INFO: Skipped empty directory: {item.name}")
|
||||||
|
movie_skipped += 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"ERROR: Failed processing movie {item}: {e}")
|
print(f"ERROR: Failed processing movie {item}: {e}")
|
||||||
movie_total += 1
|
movie_total += 1
|
||||||
@@ -1132,6 +1135,34 @@ async def delete_series_episodes(imdb_id: str, dependencies: dict):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_movie(imdb_id: str, dependencies: dict):
|
||||||
|
"""Delete a specific movie from the database"""
|
||||||
|
db = dependencies["db"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
deleted = db.delete_movie(imdb_id)
|
||||||
|
|
||||||
|
if deleted:
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": f"Deleted movie {imdb_id} from database",
|
||||||
|
"imdb_id": imdb_id
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": f"Movie {imdb_id} not found in database",
|
||||||
|
"imdb_id": imdb_id
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": str(e),
|
||||||
|
"imdb_id": imdb_id
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def cleanup_orphaned_episodes(dependencies: dict):
|
async def cleanup_orphaned_episodes(dependencies: dict):
|
||||||
"""Find and delete episodes that don't have corresponding video files"""
|
"""Find and delete episodes that don't have corresponding video files"""
|
||||||
db = dependencies["db"]
|
db = dependencies["db"]
|
||||||
@@ -1154,6 +1185,28 @@ async def cleanup_orphaned_episodes(dependencies: dict):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def cleanup_orphaned_movies(dependencies: dict):
|
||||||
|
"""Find and delete movies that don't have corresponding video files"""
|
||||||
|
db = dependencies["db"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
deleted_movies = db.delete_orphaned_movies()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": f"Cleaned up {len(deleted_movies)} orphaned movies",
|
||||||
|
"deleted_count": len(deleted_movies),
|
||||||
|
"deleted_movies": deleted_movies
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": str(e),
|
||||||
|
"message": "Failed to cleanup orphaned movies"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------
|
# ---------------------------
|
||||||
# Route Registration
|
# Route Registration
|
||||||
# ---------------------------
|
# ---------------------------
|
||||||
@@ -1212,10 +1265,18 @@ def register_routes(app, dependencies: dict):
|
|||||||
async def _delete_series_episodes(imdb_id: str):
|
async def _delete_series_episodes(imdb_id: str):
|
||||||
return await delete_series_episodes(imdb_id, dependencies)
|
return await delete_series_episodes(imdb_id, dependencies)
|
||||||
|
|
||||||
|
@app.delete("/database/movie/{imdb_id}")
|
||||||
|
async def _delete_movie(imdb_id: str):
|
||||||
|
return await delete_movie(imdb_id, dependencies)
|
||||||
|
|
||||||
@app.post("/database/cleanup/orphaned-episodes")
|
@app.post("/database/cleanup/orphaned-episodes")
|
||||||
async def _cleanup_orphaned_episodes():
|
async def _cleanup_orphaned_episodes():
|
||||||
return await cleanup_orphaned_episodes(dependencies)
|
return await cleanup_orphaned_episodes(dependencies)
|
||||||
|
|
||||||
|
@app.post("/database/cleanup/orphaned-movies")
|
||||||
|
async def _cleanup_orphaned_movies():
|
||||||
|
return await cleanup_orphaned_movies(dependencies)
|
||||||
|
|
||||||
@app.post("/manual/scan")
|
@app.post("/manual/scan")
|
||||||
async def _manual_scan(background_tasks: BackgroundTasks, path: Optional[str] = None, scan_type: str = "both", scan_mode: str = "smart"):
|
async def _manual_scan(background_tasks: BackgroundTasks, path: Optional[str] = None, scan_type: str = "both", scan_mode: str = "smart"):
|
||||||
return await manual_scan(background_tasks, path, scan_type, scan_mode, dependencies)
|
return await manual_scan(background_tasks, path, scan_type, scan_mode, dependencies)
|
||||||
|
|||||||
+91
-1
@@ -205,7 +205,7 @@ class NFOGuardDatabase:
|
|||||||
# Debug: Check what was actually saved
|
# Debug: Check what was actually saved
|
||||||
cursor.execute("SELECT dateadded, source FROM movies WHERE imdb_id = %s", (imdb_id,))
|
cursor.execute("SELECT dateadded, source FROM movies WHERE imdb_id = %s", (imdb_id,))
|
||||||
result = cursor.fetchone()
|
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'}")
|
print(f"🔍 DATABASE VERIFY: After upsert, found dateadded={result['dateadded'] if result else 'NOT_FOUND'}, source={result['source'] 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"""
|
||||||
@@ -411,6 +411,96 @@ class NFOGuardDatabase:
|
|||||||
|
|
||||||
return deleted_episodes
|
return deleted_episodes
|
||||||
|
|
||||||
|
def delete_movie(self, imdb_id: str) -> bool:
|
||||||
|
"""
|
||||||
|
Delete a specific movie from the database
|
||||||
|
|
||||||
|
Args:
|
||||||
|
imdb_id: Movie IMDb ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if movie was deleted, False if not found
|
||||||
|
"""
|
||||||
|
with self.get_connection() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
DELETE FROM movies
|
||||||
|
WHERE imdb_id = %s
|
||||||
|
""", (imdb_id,))
|
||||||
|
|
||||||
|
deleted_count = cursor.rowcount
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
return deleted_count > 0
|
||||||
|
|
||||||
|
def delete_orphaned_movies(self) -> List[Dict]:
|
||||||
|
"""
|
||||||
|
Find and delete movies that don't have corresponding video files on disk
|
||||||
|
This requires checking filesystem for each movie, so use carefully
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of deleted movies with their details
|
||||||
|
"""
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
deleted_movies = []
|
||||||
|
|
||||||
|
with self.get_connection() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Get all movies with their paths
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT imdb_id, path, dateadded, source
|
||||||
|
FROM movies
|
||||||
|
""")
|
||||||
|
|
||||||
|
movies_list = cursor.fetchall()
|
||||||
|
|
||||||
|
for movie in movies_list:
|
||||||
|
imdb_id = movie['imdb_id']
|
||||||
|
movie_path = Path(movie['path'])
|
||||||
|
|
||||||
|
if not movie_path.exists():
|
||||||
|
# Movie directory doesn't exist - delete it
|
||||||
|
cursor.execute("""
|
||||||
|
DELETE FROM movies
|
||||||
|
WHERE imdb_id = %s
|
||||||
|
""", (imdb_id,))
|
||||||
|
|
||||||
|
deleted_movies.append({
|
||||||
|
'imdb_id': imdb_id,
|
||||||
|
'reason': 'directory_not_found',
|
||||||
|
'path': str(movie_path),
|
||||||
|
'dateadded': movie['dateadded'],
|
||||||
|
'source': movie['source']
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check for video files
|
||||||
|
video_exts = (".mkv", ".mp4", ".avi", ".mov", ".m4v")
|
||||||
|
has_video = any(f.is_file() and f.suffix.lower() in video_exts
|
||||||
|
for f in movie_path.iterdir() if f.is_file())
|
||||||
|
|
||||||
|
if not has_video:
|
||||||
|
# No video files found - delete this movie
|
||||||
|
cursor.execute("""
|
||||||
|
DELETE FROM movies
|
||||||
|
WHERE imdb_id = %s
|
||||||
|
""", (imdb_id,))
|
||||||
|
|
||||||
|
deleted_movies.append({
|
||||||
|
'imdb_id': imdb_id,
|
||||||
|
'reason': 'no_video_files',
|
||||||
|
'path': str(movie_path),
|
||||||
|
'dateadded': movie['dateadded'],
|
||||||
|
'source': movie['source']
|
||||||
|
})
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
return deleted_movies
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""Close all database connections"""
|
"""Close all database connections"""
|
||||||
if hasattr(self._local, 'connection'):
|
if hasattr(self._local, 'connection'):
|
||||||
|
|||||||
+59
-6
@@ -6,13 +6,17 @@ services:
|
|||||||
image: sbcrumb/nfoguard:latest
|
image: sbcrumb/nfoguard:latest
|
||||||
|
|
||||||
# Alternative: Use specific version
|
# Alternative: Use specific version
|
||||||
# image: sbcrumb/nfoguard:v1.5.5
|
# image: sbcrumb/nfoguard:v2.6.5
|
||||||
|
|
||||||
# Alternative: Use development version
|
# Alternative: Use development version
|
||||||
# image: sbcrumb/nfoguard:dev
|
# image: sbcrumb/nfoguard:dev
|
||||||
|
|
||||||
container_name: nfoguard
|
container_name: nfoguard
|
||||||
|
|
||||||
|
# Database dependency
|
||||||
|
depends_on:
|
||||||
|
- nfoguard-postgres
|
||||||
|
|
||||||
# Restart policy
|
# Restart policy
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
@@ -87,6 +91,54 @@ services:
|
|||||||
max-size: "10m"
|
max-size: "10m"
|
||||||
max-file: "3"
|
max-file: "3"
|
||||||
|
|
||||||
|
# PostgreSQL Database (Required for v2.6+)
|
||||||
|
nfoguard-postgres:
|
||||||
|
image: postgres:15-alpine
|
||||||
|
container_name: nfoguard-db
|
||||||
|
|
||||||
|
# Restart policy
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# Environment variables for PostgreSQL
|
||||||
|
environment:
|
||||||
|
- POSTGRES_DB=nfoguard
|
||||||
|
- POSTGRES_USER=nfoguard
|
||||||
|
- POSTGRES_PASSWORD=${DB_PASSWORD} # Set in .env.secrets
|
||||||
|
- POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C
|
||||||
|
|
||||||
|
# PostgreSQL data persistence
|
||||||
|
volumes:
|
||||||
|
- ./postgres-data:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
# PostgreSQL port (optional - only needed for external access)
|
||||||
|
# ports:
|
||||||
|
# - "5432:5432"
|
||||||
|
|
||||||
|
# Health check for database
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U nfoguard -d nfoguard"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 60s
|
||||||
|
|
||||||
|
# Resource limits for database
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 256M
|
||||||
|
cpus: '0.3'
|
||||||
|
reservations:
|
||||||
|
memory: 128M
|
||||||
|
cpus: '0.1'
|
||||||
|
|
||||||
|
# Logging configuration
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
# Optional: Custom network for media services
|
# Optional: Custom network for media services
|
||||||
# networks:
|
# networks:
|
||||||
# media-network:
|
# media-network:
|
||||||
@@ -97,12 +149,13 @@ services:
|
|||||||
# ===========================================
|
# ===========================================
|
||||||
# 1. Copy this file to docker-compose.yml
|
# 1. Copy this file to docker-compose.yml
|
||||||
# 2. Update volume mounts to match your actual media paths
|
# 2. Update volume mounts to match your actual media paths
|
||||||
# 3. Copy .env.template to .env and configure
|
# 3. Copy .env.example to .env and configure all settings
|
||||||
# 4. Copy .env.secrets.template to .env.secrets and add credentials
|
# 4. Copy .env.secrets.example to .env.secrets and add credentials (especially DB_PASSWORD)
|
||||||
# 5. Create data directory: mkdir -p ./data
|
# 5. Create directories: mkdir -p ./data ./postgres-data
|
||||||
# 6. Run: docker-compose up -d
|
# 6. Run: docker-compose up -d
|
||||||
# 7. Check logs: docker-compose logs -f nfoguard
|
# 7. Check logs: docker-compose logs -f
|
||||||
# 8. Access health check: curl http://localhost:8080/health
|
# 8. Access web interface: http://localhost:8080
|
||||||
|
# 9. Access health check: curl http://localhost:8080/health
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# VOLUME MAPPING EXAMPLES
|
# VOLUME MAPPING EXAMPLES
|
||||||
|
|||||||
@@ -188,9 +188,8 @@ class MovieProcessor:
|
|||||||
has_video = any(f.is_file() and f.suffix.lower() in video_exts for f in movie_path.iterdir())
|
has_video = any(f.is_file() and f.suffix.lower() in video_exts for f in movie_path.iterdir())
|
||||||
|
|
||||||
if not has_video:
|
if not has_video:
|
||||||
_log("WARNING", f"No video files found in: {movie_path}")
|
_log("WARNING", f"No video files found in: {movie_path} - skipping database entry")
|
||||||
self.db.upsert_movie_dates(imdb_id, None, None, None, False)
|
return "no_video_files"
|
||||||
return "processed"
|
|
||||||
|
|
||||||
# TIER 1: Check database first (fastest - local lookup)
|
# TIER 1: Check database first (fastest - local lookup)
|
||||||
existing = self.db.get_movie_dates(imdb_id)
|
existing = self.db.get_movie_dates(imdb_id)
|
||||||
|
|||||||
@@ -289,6 +289,9 @@ function updateMoviesTable(data) {
|
|||||||
<button class="btn btn-sm btn-secondary" onclick="debugMovie('${movie.imdb_id}')" title="Debug Data">
|
<button class="btn btn-sm btn-secondary" onclick="debugMovie('${movie.imdb_id}')" title="Debug Data">
|
||||||
<i class="fas fa-bug"></i>
|
<i class="fas fa-bug"></i>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn btn-sm btn-danger" onclick="deleteMovie('${movie.imdb_id}')" style="margin-left: 5px;" title="Delete Movie">
|
||||||
|
<i class="fas fa-trash"></i> Delete
|
||||||
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
@@ -1315,6 +1318,40 @@ async function deleteEpisode(imdbId, season, episode) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Movie deletion functionality
|
||||||
|
async function deleteMovie(imdbId) {
|
||||||
|
// Confirmation dialog
|
||||||
|
if (!confirm(`⚠️ Delete Movie?\n\nThis will permanently remove the movie (${imdbId}) from the database.\n\nAre you sure you want to continue?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/database/movie/${imdbId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && result.success) {
|
||||||
|
showToast(`✅ Movie deleted successfully`, 'success');
|
||||||
|
|
||||||
|
// Refresh the movies table
|
||||||
|
loadMovies(currentMoviesPage);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
const errorMsg = result.message || result.error || 'Unknown error';
|
||||||
|
showToast(`❌ Failed to delete movie: ${errorMsg}`, 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Delete movie failed:', error);
|
||||||
|
showToast(`❌ Delete failed: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update episode counts in modal after deletion
|
// Update episode counts in modal after deletion
|
||||||
function updateEpisodeModalCounts() {
|
function updateEpisodeModalCounts() {
|
||||||
const remainingRows = document.querySelectorAll('#episodes-table-body tr');
|
const remainingRows = document.querySelectorAll('#episodes-table-body tr');
|
||||||
|
|||||||
Reference in New Issue
Block a user