From 148937ae0833f7aeb9f31e349e103897664930bc Mon Sep 17 00:00:00 2001 From: sbcrumb Date: Mon, 20 Oct 2025 09:32:28 -0400 Subject: [PATCH 1/2] chore: update to examples and readme --- .env.example | 154 +++++++++++++++--- .env.secrets.template => .env.secrets.example | 12 +- .env.template | 129 --------------- Emby-DLL/NFOGuard.Emby.Plugin.dll | Bin 1033216 -> 1042944 bytes README.md | 45 ++--- VERSION | 2 +- docker-compose.example | 65 +++++++- 7 files changed, 227 insertions(+), 180 deletions(-) rename .env.secrets.template => .env.secrets.example (68%) delete mode 100644 .env.template diff --git a/.env.example b/.env.example index 78ce602..7916a30 100644 --- a/.env.example +++ b/.env.example @@ -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 -PGID=1000 +# =========================================== +# 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 -# Timezone -TZ=UTC +# Radarr paths (what Radarr sees on the host system) +RADARR_ROOT_FOLDERS=/mnt/unionfs/Media/Movies/movies,/mnt/unionfs/Media/Movies/movies6 -# Media path (for read-only access to scan NFO files) -MEDIA_PATH=/path/to/your/media +# 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 -# Database settings (if using PostgreSQL) -# DB_USER=nfoguard -# DB_PASSWORD=your_secure_password +# Download detection paths (for identifying downloads vs existing files) +DOWNLOAD_PATH_INDICATORS=/downloads/,/nzbget/,/completed/,/sabnzbd/,/torrents/ -# Emby Plugin Deployment - Bind Mount Method (Recommended) -# Set this to the path where Emby plugins are installed on your host -# Common paths: -# - /var/lib/emby/plugins (native Emby install) -# - /path/to/emby/config/plugins (Docker Emby) -# - /config/plugins (linuxserver.io Emby) -EMBY_PLUGINS_PATH=/path/to/emby/plugins +# =========================================== +# DATABASE CONFIGURATION +# =========================================== +# NFOGuard Database Configuration (PostgreSQL - v2.6+) +# NOTE: For new installations, PostgreSQL is strongly recommended +# Set these values in .env.secrets for security: +# - 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 + +# Enable path mapping debug output (true/false) PATH_DEBUG=false -SUPPRESS_TVDB_WARNINGS=true \ No newline at end of file + +# 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 \ No newline at end of file diff --git a/.env.secrets.template b/.env.secrets.example similarity index 68% rename from .env.secrets.template rename to .env.secrets.example index 3efc95a..6653c39 100644 --- a/.env.secrets.template +++ b/.env.secrets.example @@ -4,9 +4,17 @@ # 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 # =========================================== diff --git a/.env.template b/.env.template deleted file mode 100644 index 189dc67..0000000 --- a/.env.template +++ /dev/null @@ -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 \ No newline at end of file diff --git a/Emby-DLL/NFOGuard.Emby.Plugin.dll b/Emby-DLL/NFOGuard.Emby.Plugin.dll index 8369710f27431feb01dcd83570eeac0e69133ca9..60fad74d62f99762e000dc27b2d5758b9912c8c4 100644 GIT binary patch delta 40806 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 delta 32320 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 Date: Mon, 20 Oct 2025 10:26:29 -0400 Subject: [PATCH 2/2] fix: update to not add empty dirs --- api/routes.py | 61 +++++++++++++++++++++++ core/database.py | 92 ++++++++++++++++++++++++++++++++++- processors/movie_processor.py | 5 +- static/js/app.js | 37 ++++++++++++++ 4 files changed, 191 insertions(+), 4 deletions(-) diff --git a/api/routes.py b/api/routes.py index 82c918d..35df7b8 100644 --- a/api/routes.py +++ b/api/routes.py @@ -688,6 +688,9 @@ async def manual_scan(background_tasks: BackgroundTasks, path: Optional[str] = N movie_skipped += 1 elif result == "processed": movie_processed += 1 + elif result == "no_video_files": + print(f"INFO: Skipped empty directory: {item.name}") + movie_skipped += 1 except Exception as e: print(f"ERROR: Failed processing movie {item}: {e}") 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): """Find and delete episodes that don't have corresponding video files""" 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 # --------------------------- @@ -1212,10 +1265,18 @@ def register_routes(app, dependencies: dict): async def _delete_series_episodes(imdb_id: str): 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") async def _cleanup_orphaned_episodes(): 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") 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) diff --git a/core/database.py b/core/database.py index a13235c..a80b15c 100644 --- a/core/database.py +++ b/core/database.py @@ -205,7 +205,7 @@ class NFOGuardDatabase: # Debug: Check what was actually saved cursor.execute("SELECT dateadded, source FROM movies WHERE imdb_id = %s", (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'}") + 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]: """Get all episodes for a series""" @@ -411,6 +411,96 @@ class NFOGuardDatabase: 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): """Close all database connections""" if hasattr(self._local, 'connection'): diff --git a/processors/movie_processor.py b/processors/movie_processor.py index 85db39e..b5daf61 100644 --- a/processors/movie_processor.py +++ b/processors/movie_processor.py @@ -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()) if not has_video: - _log("WARNING", f"No video files found in: {movie_path}") - self.db.upsert_movie_dates(imdb_id, None, None, None, False) - return "processed" + _log("WARNING", f"No video files found in: {movie_path} - skipping database entry") + return "no_video_files" # TIER 1: Check database first (fastest - local lookup) existing = self.db.get_movie_dates(imdb_id) diff --git a/static/js/app.js b/static/js/app.js index f550935..86b6118 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -289,6 +289,9 @@ function updateMoviesTable(data) { + `; @@ -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 function updateEpisodeModalCounts() { const remainingRows = document.querySelectorAll('#episodes-table-body tr');