update to skiped files
Local Docker Build (Dev) / build-dev (push) Successful in 5s

This commit is contained in:
2025-11-03 16:37:42 -05:00
parent c9b099a218
commit 7932b9cfba
6 changed files with 209 additions and 54 deletions
+1 -1
View File
@@ -1 +1 @@
2.9.10-version-strings-fix 2.10.0-skipped-tracking
+33 -14
View File
@@ -82,13 +82,14 @@ def map_source_to_description(source: str) -> str:
# Database Query Endpoints # Database Query Endpoints
# --------------------------- # ---------------------------
async def get_movies_list(dependencies: dict, async def get_movies_list(dependencies: dict,
skip: int = Query(0, ge=0), skip: int = Query(0, ge=0),
limit: int = Query(100, le=1000), limit: int = Query(100, le=1000),
has_date: Optional[bool] = Query(None), has_date: Optional[bool] = Query(None),
source_filter: Optional[str] = Query(None), source_filter: Optional[str] = Query(None),
search: Optional[str] = Query(None), search: Optional[str] = Query(None),
imdb_search: Optional[str] = Query(None)): imdb_search: Optional[str] = Query(None),
skipped: Optional[bool] = Query(None)):
"""Get paginated list of movies with filtering options""" """Get paginated list of movies with filtering options"""
db = dependencies["db"] db = dependencies["db"]
@@ -98,7 +99,13 @@ async def get_movies_list(dependencies: dict,
# Build dynamic query # Build dynamic query
where_conditions = [] where_conditions = []
params = [] params = []
if skipped is not None:
if skipped:
where_conditions.append("skipped = TRUE")
else:
where_conditions.append("(skipped = FALSE OR skipped IS NULL)")
if has_date is not None: if has_date is not None:
if has_date: if has_date:
# PostgreSQL - NULL handling # PostgreSQL - NULL handling
@@ -106,15 +113,15 @@ async def get_movies_list(dependencies: dict,
else: else:
# PostgreSQL - NULL handling # PostgreSQL - NULL handling
where_conditions.append("dateadded IS NULL") where_conditions.append("dateadded IS NULL")
if source_filter: if source_filter:
where_conditions.append("source = %s") where_conditions.append("source = %s")
params.append(source_filter) params.append(source_filter)
if search: if search:
where_conditions.append("(imdb_id ILIKE %s OR path ILIKE %s)") where_conditions.append("(imdb_id ILIKE %s OR path ILIKE %s OR title ILIKE %s)")
params.extend([f"%{search}%", f"%{search}%"]) params.extend([f"%{search}%", f"%{search}%", f"%{search}%"])
if imdb_search: if imdb_search:
where_conditions.append("imdb_id ILIKE %s") where_conditions.append("imdb_id ILIKE %s")
params.append(f"%{imdb_search}%") params.append(f"%{imdb_search}%")
@@ -128,7 +135,7 @@ async def get_movies_list(dependencies: dict,
# Get paginated results - PostgreSQL # Get paginated results - PostgreSQL
query = f""" query = f"""
SELECT imdb_id, title, year, path, released, dateadded, source, has_video_file, last_updated SELECT imdb_id, title, year, path, released, dateadded, source, has_video_file, last_updated, skipped, skip_reason
FROM movies FROM movies
WHERE {where_clause} WHERE {where_clause}
ORDER BY last_updated DESC ORDER BY last_updated DESC
@@ -488,7 +495,14 @@ async def get_dashboard_stats(dependencies: dict):
cursor.execute("SELECT COUNT(*) FROM episodes WHERE source = 'no_valid_date_source'") cursor.execute("SELECT COUNT(*) FROM episodes WHERE source = 'no_valid_date_source'")
episodes_no_valid_source = db._get_first_value(cursor.fetchone()) episodes_no_valid_source = db._get_first_value(cursor.fetchone())
# Skipped items
cursor.execute("SELECT COUNT(*) FROM movies WHERE skipped = TRUE")
movies_skipped = db._get_first_value(cursor.fetchone())
cursor.execute("SELECT COUNT(*) FROM episodes WHERE skipped = TRUE")
episodes_skipped = db._get_first_value(cursor.fetchone())
# Recent activity (last 7 days) # Recent activity (last 7 days)
cursor.execute(""" cursor.execute("""
SELECT COUNT(*) FROM processing_history SELECT COUNT(*) FROM processing_history
@@ -518,7 +532,8 @@ async def get_dashboard_stats(dependencies: dict):
# Calculate total missing dates (movies + episodes) # Calculate total missing dates (movies + episodes)
total_missing_dates = movies_without_dates + episodes_without_dates total_missing_dates = movies_without_dates + episodes_without_dates
total_skipped = movies_skipped + episodes_skipped
# Combine with enhanced stats # Combine with enhanced stats
stats.update({ stats.update({
"movies_with_dates": movies_with_dates, "movies_with_dates": movies_with_dates,
@@ -530,6 +545,9 @@ async def get_dashboard_stats(dependencies: dict):
"total_missing_dates": total_missing_dates, "total_missing_dates": total_missing_dates,
"movies_no_valid_source": movies_no_valid_source, "movies_no_valid_source": movies_no_valid_source,
"episodes_no_valid_source": episodes_no_valid_source, "episodes_no_valid_source": episodes_no_valid_source,
"movies_skipped": movies_skipped,
"episodes_skipped": episodes_skipped,
"total_skipped": total_skipped,
"recent_activity_count": recent_activity, "recent_activity_count": recent_activity,
"movie_sources": movie_sources, "movie_sources": movie_sources,
"episode_sources": episode_sources "episode_sources": episode_sources
@@ -1461,9 +1479,10 @@ def register_web_routes(app, dependencies):
# Movies endpoints # Movies endpoints
@app.get("/api/movies") @app.get("/api/movies")
async def api_movies_list(skip: int = 0, limit: int = 100, has_date: bool = None, async def api_movies_list(skip: int = 0, limit: int = 100, has_date: bool = None,
source_filter: str = None, search: str = None, imdb_search: str = None): source_filter: str = None, search: str = None, imdb_search: str = None,
return await get_movies_list(dependencies, skip, limit, has_date, source_filter, search, imdb_search) skipped: bool = None):
return await get_movies_list(dependencies, skip, limit, has_date, source_filter, search, imdb_search, skipped)
@app.post("/api/movies/{imdb_id}/update-date") @app.post("/api/movies/{imdb_id}/update-date")
async def api_update_movie_date(imdb_id: str, dateadded: str = None, source: str = "manual"): async def api_update_movie_date(imdb_id: str, dateadded: str = None, source: str = "manual"):
+92 -1
View File
@@ -133,6 +133,29 @@ class NFOGuardDatabase:
END IF; END IF;
END $$; END $$;
""") """)
# Add skipped and skip_reason columns if they don't exist (migration for skip tracking)
cursor.execute("""
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name='movies' AND column_name='skipped') THEN
ALTER TABLE movies ADD COLUMN skipped BOOLEAN DEFAULT FALSE;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name='movies' AND column_name='skip_reason') THEN
ALTER TABLE movies ADD COLUMN skip_reason TEXT;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name='episodes' AND column_name='skipped') THEN
ALTER TABLE episodes ADD COLUMN skipped BOOLEAN DEFAULT FALSE;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name='episodes' AND column_name='skip_reason') THEN
ALTER TABLE episodes ADD COLUMN skip_reason TEXT;
END IF;
END $$;
""")
# Processing history table # Processing history table
cursor.execute(""" cursor.execute("""
@@ -301,7 +324,75 @@ class NFOGuardDatabase:
import os import os
if os.environ.get("DEBUG", "false").lower() == "true": if os.environ.get("DEBUG", "false").lower() == "true":
print(f"🔍 DATABASE VERIFY: After upsert, found dateadded={result['dateadded'] if result else 'NOT_FOUND'}, source={result['source'] 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 mark_movie_skipped(self, imdb_id: str, title: str, year: int, path: str, reason: str):
"""Mark a movie as skipped with reason"""
with self.get_connection() as conn:
cursor = conn.cursor()
timestamp = datetime.utcnow()
cursor.execute("""
INSERT INTO movies (imdb_id, title, year, path, skipped, skip_reason, has_video_file, last_updated)
VALUES (%s, %s, %s, %s, TRUE, %s, FALSE, %s)
ON CONFLICT (imdb_id) DO UPDATE SET
title = EXCLUDED.title,
year = EXCLUDED.year,
path = EXCLUDED.path,
skipped = TRUE,
skip_reason = EXCLUDED.skip_reason,
last_updated = EXCLUDED.last_updated
""", (imdb_id, title, year, path, reason, timestamp))
def mark_episode_skipped(self, imdb_id: str, season: int, episode: int, reason: str):
"""Mark an episode as skipped with reason"""
with self.get_connection() as conn:
cursor = conn.cursor()
timestamp = datetime.utcnow()
cursor.execute("""
INSERT INTO episodes (imdb_id, season, episode, skipped, skip_reason, has_video_file, last_updated)
VALUES (%s, %s, %s, TRUE, %s, FALSE, %s)
ON CONFLICT (imdb_id, season, episode) DO UPDATE SET
skipped = TRUE,
skip_reason = EXCLUDED.skip_reason,
last_updated = EXCLUDED.last_updated
""", (imdb_id, season, episode, reason, timestamp))
def clear_movie_skipped(self, imdb_id: str):
"""Clear skipped flag for a movie"""
with self.get_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
UPDATE movies SET skipped = FALSE, skip_reason = NULL
WHERE imdb_id = %s
""", (imdb_id,))
def clear_episode_skipped(self, imdb_id: str, season: int, episode: int):
"""Clear skipped flag for an episode"""
with self.get_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
UPDATE episodes SET skipped = FALSE, skip_reason = NULL
WHERE imdb_id = %s AND season = %s AND episode = %s
""", (imdb_id, season, episode))
def get_skipped_counts(self) -> Dict:
"""Get counts of skipped movies and episodes"""
with self.get_connection() as conn:
cursor = conn.cursor()
# Count skipped movies
cursor.execute("SELECT COUNT(*) as count FROM movies WHERE skipped = TRUE")
skipped_movies = cursor.fetchone()['count']
# Count skipped episodes
cursor.execute("SELECT COUNT(*) as count FROM episodes WHERE skipped = TRUE")
skipped_episodes = cursor.fetchone()['count']
return {
'movies': skipped_movies,
'episodes': skipped_episodes,
'total': skipped_movies + skipped_episodes
}
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:
+38 -30
View File
@@ -82,29 +82,24 @@ class DatabasePopulator:
_log("DEBUG", f"Extracted IMDb ID {imdb_id} from path for: {movie.get('title')}") _log("DEBUG", f"Extracted IMDb ID {imdb_id} from path for: {movie.get('title')}")
if not imdb_id: if not imdb_id:
skip_reason = 'No IMDb ID found'
skip_info = { skip_info = {
'title': movie.get('title', 'Unknown'), 'title': movie.get('title', 'Unknown'),
'year': movie.get('year'), 'year': movie.get('year'),
'path': path, 'path': path,
'reason': 'No IMDb ID found' 'reason': skip_reason
} }
stats['skipped_items'].append(skip_info) stats['skipped_items'].append(skip_info)
_log("DEBUG", f"Skipping movie without IMDb ID: {movie.get('title')} (path: {path})") _log("DEBUG", f"Skipping movie without IMDb ID: {movie.get('title')} (path: {path})")
# Note: Cannot mark in DB because no IMDb ID (primary key)
stats['skipped'] += 1 stats['skipped'] += 1
continue continue
# Check if movie already exists in database # Check if movie already exists in database
existing = self.db.get_movie_dates(imdb_id) existing = self.db.get_movie_dates(imdb_id)
if existing and existing.get('dateadded'): if existing and existing.get('dateadded'):
skip_info = { # Already in database - skip silently (not an error)
'title': movie.get('title', 'Unknown'),
'year': movie.get('year'),
'imdb_id': imdb_id,
'reason': 'Already in database'
}
stats['skipped_items'].append(skip_info)
_log("DEBUG", f"Movie {imdb_id} already in database, skipping") _log("DEBUG", f"Movie {imdb_id} already in database, skipping")
stats['skipped'] += 1
continue continue
# Get release date # Get release date
@@ -134,14 +129,24 @@ class DatabasePopulator:
dateadded = released dateadded = released
source = f'{source_type}_fallback' source = f'{source_type}_fallback'
else: else:
skip_reason = 'No import date in Radarr history and no release dates available'
skip_info = { skip_info = {
'title': movie.get('title', 'Unknown'), 'title': movie.get('title', 'Unknown'),
'year': movie.get('year'), 'year': movie.get('year'),
'imdb_id': imdb_id, 'imdb_id': imdb_id,
'reason': 'No import date in Radarr history and no release dates available' 'reason': skip_reason
} }
stats['skipped_items'].append(skip_info) stats['skipped_items'].append(skip_info)
_log("DEBUG", f"No date available for movie {imdb_id}, skipping") _log("DEBUG", f"No date available for movie {imdb_id}, skipping")
# Mark as skipped in database for troubleshooting
self.db.mark_movie_skipped(
imdb_id=imdb_id,
title=movie.get('title', 'Unknown'),
year=movie.get('year', 0),
path=path or 'unknown',
reason=skip_reason
)
stats['skipped'] += 1 stats['skipped'] += 1
continue continue
elif released: elif released:
@@ -149,14 +154,24 @@ class DatabasePopulator:
dateadded = released dateadded = released
source = f'{source_type}_fallback' source = f'{source_type}_fallback'
else: else:
skip_reason = 'No Radarr movie ID and no release dates available'
skip_info = { skip_info = {
'title': movie.get('title', 'Unknown'), 'title': movie.get('title', 'Unknown'),
'year': movie.get('year'), 'year': movie.get('year'),
'imdb_id': imdb_id, 'imdb_id': imdb_id,
'reason': 'No Radarr movie ID and no release dates available' 'reason': skip_reason
} }
stats['skipped_items'].append(skip_info) stats['skipped_items'].append(skip_info)
_log("DEBUG", f"No date available for movie {imdb_id}, skipping") _log("DEBUG", f"No date available for movie {imdb_id}, skipping")
# Mark as skipped in database for troubleshooting
self.db.mark_movie_skipped(
imdb_id=imdb_id,
title=movie.get('title', 'Unknown'),
year=movie.get('year', 0),
path=path or 'unknown',
reason=skip_reason
)
stats['skipped'] += 1 stats['skipped'] += 1
continue continue
@@ -278,29 +293,13 @@ class DatabasePopulator:
# Check if episode already exists # Check if episode already exists
existing = self.db.get_episode_date(imdb_id, season_num, episode_num) existing = self.db.get_episode_date(imdb_id, season_num, episode_num)
if existing and existing.get('dateadded'): if existing and existing.get('dateadded'):
skip_info = { # Already in database - skip silently (not an error)
'title': series_title,
'episode_title': episode_title,
'season': season_num,
'episode': episode_num,
'reason': 'Already in database'
}
stats['skipped_items'].append(skip_info)
stats['skipped'] += 1
continue continue
# Only process episodes that have video files # Only process episodes that have video files
has_file = episode.get('hasFile', False) has_file = episode.get('hasFile', False)
if not has_file: if not has_file:
skip_info = { # No video file - skip silently (intentionally filtered)
'title': series_title,
'episode_title': episode_title,
'season': season_num,
'episode': episode_num,
'reason': 'No video file'
}
stats['skipped_items'].append(skip_info)
stats['skipped'] += 1
continue continue
# Get air date # Get air date
@@ -328,14 +327,23 @@ class DatabasePopulator:
source = 'sonarr:aired_fallback' source = 'sonarr:aired_fallback'
elif not dateadded: elif not dateadded:
# No date available # No date available
skip_reason = 'No import date from Sonarr history and no air date available'
skip_info = { skip_info = {
'title': series_title, 'title': series_title,
'episode_title': episode_title, 'episode_title': episode_title,
'season': season_num, 'season': season_num,
'episode': episode_num, 'episode': episode_num,
'reason': 'No import date from Sonarr history and no air date available' 'reason': skip_reason
} }
stats['skipped_items'].append(skip_info) stats['skipped_items'].append(skip_info)
# Mark as skipped in database for troubleshooting
self.db.mark_episode_skipped(
imdb_id=imdb_id,
season=season_num,
episode=episode_num,
reason=skip_reason
)
stats['skipped'] += 1 stats['skipped'] += 1
continue continue
+15 -3
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NFOGuard - Database Management</title> <title>NFOGuard - Database Management</title>
<link rel="stylesheet" href="/static/css/styles.css?v=2.9.10-version-strings-fix"> <link rel="stylesheet" href="/static/css/styles.css?v=2.10.0-skipped-tracking">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet"> <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
</head> </head>
<body> <body>
@@ -12,7 +12,7 @@
<!-- Header --> <!-- Header -->
<header class="app-header"> <header class="app-header">
<div class="header-content"> <div class="header-content">
<h1><i class="fas fa-shield-alt"></i> NFOGuard <span style="font-size: 0.5em; color: #888;">v2.9.10-version-strings-fix</span></h1> <h1><i class="fas fa-shield-alt"></i> NFOGuard <span style="font-size: 0.5em; color: #888;">v2.10.0-skipped-tracking</span></h1>
<p>Database Management & Reporting</p> <p>Database Management & Reporting</p>
</div> </div>
<div class="auth-status" id="auth-status" style="display: none;"> <div class="auth-status" id="auth-status" style="display: none;">
@@ -93,6 +93,17 @@
<small>Last 7 days</small> <small>Last 7 days</small>
</div> </div>
</div> </div>
<div class="stat-card" style="cursor: pointer;" onclick="showSkippedItems()">
<div class="stat-icon" style="background: linear-gradient(135deg, #f39c12 0%, #e67e22 100%);">
<i class="fas fa-ban"></i>
</div>
<div class="stat-info">
<h3 id="skipped-total">-</h3>
<p>Skipped Items</p>
<small id="skipped-breakdown">- movies, - episodes</small>
</div>
</div>
</div> </div>
<div class="dashboard-charts"> <div class="dashboard-charts">
@@ -127,6 +138,7 @@
<option value="">All Movies</option> <option value="">All Movies</option>
<option value="true">With Dates</option> <option value="true">With Dates</option>
<option value="false">Missing Dates</option> <option value="false">Missing Dates</option>
<option value="skipped">Skipped</option>
</select> </select>
<select id="movies-filter-source"> <select id="movies-filter-source">
<option value="">All Sources</option> <option value="">All Sources</option>
@@ -711,6 +723,6 @@
<!-- Toast Notifications --> <!-- Toast Notifications -->
<div class="toast-container" id="toast-container"></div> <div class="toast-container" id="toast-container"></div>
<script src="/static/js/app.js?v=2.9.10-version-strings-fix"></script> <script src="/static/js/app.js?v=2.10.0-skipped-tracking">
</body> </body>
</html> </html>
+30 -5
View File
@@ -141,6 +141,24 @@ function updateDashboardStats() {
document.getElementById('no-valid-source-total').textContent = `${moviesWithoutDates} movies, ${episodesWithoutDates} episodes without dates`; document.getElementById('no-valid-source-total').textContent = `${moviesWithoutDates} movies, ${episodesWithoutDates} episodes without dates`;
document.getElementById('recent-activity').textContent = dashboardData.recent_activity_count || 0; document.getElementById('recent-activity').textContent = dashboardData.recent_activity_count || 0;
// Skipped items
const skippedMovies = dashboardData.movies_skipped || 0;
const skippedEpisodes = dashboardData.episodes_skipped || 0;
const skippedTotal = dashboardData.total_skipped || (skippedMovies + skippedEpisodes);
document.getElementById('skipped-total').textContent = skippedTotal;
document.getElementById('skipped-breakdown').textContent = `${skippedMovies} movies, ${skippedEpisodes} episodes`;
}
function showSkippedItems() {
// Switch to Movies tab and filter to show only skipped items
switchTab('movies');
// Set the filter dropdown to "skipped"
const moviesFilter = document.getElementById('movies-filter-date');
if (moviesFilter) {
moviesFilter.value = 'skipped';
refreshMovies();
}
} }
function updateDashboardCharts() { function updateDashboardCharts() {
@@ -196,20 +214,27 @@ async function loadMovies(page = 1) {
const search = document.getElementById('movies-search').value; const search = document.getElementById('movies-search').value;
const imdbSearch = document.getElementById('movies-imdb-search').value; const imdbSearch = document.getElementById('movies-imdb-search').value;
const hasDate = document.getElementById('movies-filter-date').value; const dateFilter = document.getElementById('movies-filter-date').value;
const sourceFilter = document.getElementById('movies-filter-source').value; const sourceFilter = document.getElementById('movies-filter-source').value;
const skip = (page - 1) * 100; const skip = (page - 1) * 100;
console.log(`DEBUG: loadMovies called with page=${page}, calculated skip=${skip}`); console.log(`DEBUG: loadMovies called with page=${page}, calculated skip=${skip}`);
const params = new URLSearchParams({ const params = new URLSearchParams({
skip: skip, skip: skip,
limit: 100 limit: 100
}); });
if (search) params.append('search', search); if (search) params.append('search', search);
if (imdbSearch) params.append('imdb_search', imdbSearch); if (imdbSearch) params.append('imdb_search', imdbSearch);
if (hasDate) params.append('has_date', hasDate);
// Handle different filter values
if (dateFilter === 'skipped') {
params.append('skipped', 'true');
} else if (dateFilter) {
params.append('has_date', dateFilter);
}
if (sourceFilter) params.append('source_filter', sourceFilter); if (sourceFilter) params.append('source_filter', sourceFilter);
try { try {