updates: to time again
Local Docker Build (Dev) / build-dev (push) Successful in 4s
Local Docker Build (Main) / build (pull_request) Successful in 4s
Local Docker Build (Main) / deploy (pull_request) Has been skipped

This commit is contained in:
2025-10-20 19:49:01 -04:00
parent 53fe4dd9a3
commit b65ba5d15d
2 changed files with 454 additions and 0 deletions
+38
View File
@@ -178,8 +178,44 @@ curl -X POST "http://localhost:8080/manual/scan?path=/media/movies"
# Bulk update all movies from Radarr database
curl -X POST "http://localhost:8080/bulk/update"
# Verify NFO files match database data
curl -X POST "http://localhost:8080/database/verify/nfo-sync?media_type=both"
# Fix NFO files that don't match database
curl -X POST "http://localhost:8080/database/fix/nfo-sync?media_type=both"
```
### NFO Verification & Synchronization
NFOGuard includes comprehensive verification tools to ensure NFO files remain synchronized with database dates:
```bash
# Verify all NFO files (movies and episodes)
curl -X POST "http://localhost:8080/database/verify/nfo-sync?media_type=both"
# Verify only movies
curl -X POST "http://localhost:8080/database/verify/nfo-sync?media_type=movies"
# Verify only TV episodes
curl -X POST "http://localhost:8080/database/verify/nfo-sync?media_type=episodes"
# Fix detected issues by regenerating NFO files
curl -X POST "http://localhost:8080/database/fix/nfo-sync?media_type=both"
```
**Verification Checks:**
- **Missing NFO files** - Database entries without corresponding NFO files
- **Empty NFO files** - NFO files that exist but contain no content
- **Date mismatches** - Database dates don't match NFO file dates
- **Source mismatches** - Database source doesn't match NFO source information
**Fix Operations:**
- Regenerates NFO files from database data for any detected issues
- Preserves existing metadata while updating NFOGuard date sections
- Only operates when `MANAGE_NFO=true` is configured
- Creates proper movie.nfo and episode S##E##.nfo files
### API Endpoints
#### **Core Operations**
@@ -215,6 +251,8 @@ curl -X POST "http://localhost:8080/bulk/update"
| `/database/cleanup/orphaned-episodes` | POST | Delete episodes without video files |
| `/database/cleanup/orphaned-movies` | POST | Delete movies without video files |
| `/database/cleanup/orphaned-series` | POST | Delete TV series without directories |
| `/database/verify/nfo-sync` | POST | Verify that database dates match NFO file contents |
| `/database/fix/nfo-sync` | POST | Fix NFO sync issues by regenerating NFO files from database data |
#### **Configuration & Debugging**
| Endpoint | Method | Purpose |
+416
View File
@@ -1233,6 +1233,414 @@ async def cleanup_orphaned_series(dependencies: dict):
}
async def verify_nfo_sync(dependencies: dict, media_type: str = "both"):
"""Verify that database dates match NFO file contents"""
db = dependencies["db"]
nfo_manager = dependencies["nfo_manager"]
config = dependencies["config"]
try:
verification_results = {
"movies": {"total": 0, "verified": 0, "missing_nfo": 0, "date_mismatch": 0, "empty_nfo": 0, "issues": []},
"episodes": {"total": 0, "verified": 0, "missing_nfo": 0, "date_mismatch": 0, "empty_nfo": 0, "issues": []}
}
print(f"🔍 NFO VERIFICATION STARTED: Checking {media_type}")
# Verify Movies
if media_type in ["both", "movies"]:
print("📽️ Verifying movie NFO files...")
# Get all movies from database
with db.get_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT imdb_id, path, dateadded, released, source FROM movies WHERE has_video_file = true")
movies = cursor.fetchall()
verification_results["movies"]["total"] = len(movies)
for movie in movies:
imdb_id = movie['imdb_id']
db_path = movie['path']
db_dateadded = movie['dateadded']
db_released = movie['released']
db_source = movie['source']
try:
from pathlib import Path
movie_path = Path(db_path)
nfo_path = movie_path / "movie.nfo"
# Check if NFO exists
if not nfo_path.exists():
verification_results["movies"]["missing_nfo"] += 1
verification_results["movies"]["issues"].append({
"imdb_id": imdb_id,
"path": str(movie_path),
"issue": "missing_nfo",
"message": "NFO file does not exist"
})
continue
# Check if NFO is empty
nfo_content = nfo_path.read_text(encoding='utf-8').strip()
if not nfo_content:
verification_results["movies"]["empty_nfo"] += 1
verification_results["movies"]["issues"].append({
"imdb_id": imdb_id,
"path": str(movie_path),
"issue": "empty_nfo",
"message": "NFO file exists but is empty"
})
continue
# Extract NFOGuard data from NFO
nfo_data = nfo_manager.extract_nfoguard_dates_from_nfo(nfo_path)
if not nfo_data:
verification_results["movies"]["date_mismatch"] += 1
verification_results["movies"]["issues"].append({
"imdb_id": imdb_id,
"path": str(movie_path),
"issue": "no_nfoguard_data",
"message": "NFO exists but contains no NFOGuard date information",
"db_dateadded": str(db_dateadded),
"db_source": db_source
})
continue
# Compare dates
nfo_dateadded = nfo_data.get("dateadded")
nfo_source = nfo_data.get("source")
# Convert database datetime to string for comparison
if hasattr(db_dateadded, 'isoformat'):
db_dateadded_str = db_dateadded.isoformat()
else:
db_dateadded_str = str(db_dateadded)
# Check for date mismatch
if nfo_dateadded != db_dateadded_str or nfo_source != db_source:
verification_results["movies"]["date_mismatch"] += 1
verification_results["movies"]["issues"].append({
"imdb_id": imdb_id,
"path": str(movie_path),
"issue": "date_mismatch",
"message": "Database and NFO dates/sources don't match",
"db_dateadded": db_dateadded_str,
"db_source": db_source,
"nfo_dateadded": nfo_dateadded,
"nfo_source": nfo_source
})
continue
# Everything matches
verification_results["movies"]["verified"] += 1
except Exception as e:
verification_results["movies"]["issues"].append({
"imdb_id": imdb_id,
"path": db_path,
"issue": "verification_error",
"message": f"Error during verification: {str(e)}"
})
# Verify TV Episodes
if media_type in ["both", "episodes"]:
print("📺 Verifying TV episode NFO files...")
# Get all episodes from database
with db.get_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT imdb_id, season, episode, air_date, dateadded, source, video_path
FROM episodes
WHERE has_video_file = true
ORDER BY imdb_id, season, episode
""")
episodes = cursor.fetchall()
verification_results["episodes"]["total"] = len(episodes)
for episode in episodes:
imdb_id = episode['imdb_id']
season = episode['season']
episode_num = episode['episode']
db_air_date = episode['air_date']
db_dateadded = episode['dateadded']
db_source = episode['source']
video_path = episode['video_path']
try:
from pathlib import Path
if video_path:
video_file = Path(video_path)
nfo_path = video_file.with_suffix('.nfo')
else:
# Construct expected path
for tv_path in config.tv_paths:
series_dirs = [d for d in tv_path.iterdir() if d.is_dir() and imdb_id in str(d)]
if series_dirs:
season_dir = series_dirs[0] / f"Season {season:02d}"
if season_dir.exists():
episode_files = list(season_dir.glob(f"*S{season:02d}E{episode_num:02d}*.nfo"))
if episode_files:
nfo_path = episode_files[0]
break
else:
continue
# Check if NFO exists
if not nfo_path.exists():
verification_results["episodes"]["missing_nfo"] += 1
verification_results["episodes"]["issues"].append({
"imdb_id": imdb_id,
"episode": f"S{season:02d}E{episode_num:02d}",
"issue": "missing_nfo",
"message": "NFO file does not exist",
"expected_path": str(nfo_path)
})
continue
# Check if NFO is empty
nfo_content = nfo_path.read_text(encoding='utf-8').strip()
if not nfo_content:
verification_results["episodes"]["empty_nfo"] += 1
verification_results["episodes"]["issues"].append({
"imdb_id": imdb_id,
"episode": f"S{season:02d}E{episode_num:02d}",
"issue": "empty_nfo",
"message": "NFO file exists but is empty",
"nfo_path": str(nfo_path)
})
continue
# Extract NFOGuard data from episode NFO
nfo_data = nfo_manager.extract_nfoguard_dates_from_episode_nfo(nfo_path)
if not nfo_data:
verification_results["episodes"]["date_mismatch"] += 1
verification_results["episodes"]["issues"].append({
"imdb_id": imdb_id,
"episode": f"S{season:02d}E{episode_num:02d}",
"issue": "no_nfoguard_data",
"message": "NFO exists but contains no NFOGuard date information",
"db_dateadded": str(db_dateadded),
"db_source": db_source,
"nfo_path": str(nfo_path)
})
continue
# Everything matches
verification_results["episodes"]["verified"] += 1
except Exception as e:
verification_results["episodes"]["issues"].append({
"imdb_id": imdb_id,
"episode": f"S{season:02d}E{episode_num:02d}",
"issue": "verification_error",
"message": f"Error during verification: {str(e)}"
})
# Print summary
if media_type in ["both", "movies"]:
movies = verification_results["movies"]
print(f"📽️ MOVIE VERIFICATION COMPLETE:")
print(f" Total Movies: {movies['total']}")
print(f" ✅ Verified: {movies['verified']}")
print(f" ❌ Missing NFO: {movies['missing_nfo']}")
print(f" 📄 Empty NFO: {movies['empty_nfo']}")
print(f" 🔄 Date Mismatch: {movies['date_mismatch']}")
if media_type in ["both", "episodes"]:
episodes = verification_results["episodes"]
print(f"📺 EPISODE VERIFICATION COMPLETE:")
print(f" Total Episodes: {episodes['total']}")
print(f" ✅ Verified: {episodes['verified']}")
print(f" ❌ Missing NFO: {episodes['missing_nfo']}")
print(f" 📄 Empty NFO: {episodes['empty_nfo']}")
print(f" 🔄 Date Mismatch: {episodes['date_mismatch']}")
return {
"success": True,
"message": f"NFO verification completed for {media_type}",
"results": verification_results
}
except Exception as e:
return {
"success": False,
"error": str(e),
"message": "Failed to verify NFO files"
}
async def fix_nfo_sync_issues(dependencies: dict, media_type: str = "both"):
"""Fix NFO sync issues by regenerating NFO files from database data"""
db = dependencies["db"]
nfo_manager = dependencies["nfo_manager"]
config = dependencies["config"]
try:
# First run verification to identify issues
verification_result = await verify_nfo_sync(dependencies, media_type)
if not verification_result["success"]:
return verification_result
results = verification_result["results"]
fix_results = {
"movies": {"fixed": 0, "failed": 0, "errors": []},
"episodes": {"fixed": 0, "failed": 0, "errors": []}
}
print(f"🔧 NFO FIX STARTED: Regenerating NFO files for {media_type}")
# Fix Movies
if media_type in ["both", "movies"]:
print("📽️ Fixing movie NFO files...")
for issue in results["movies"]["issues"]:
if issue["issue"] in ["empty_nfo", "no_nfoguard_data", "date_mismatch"]:
imdb_id = issue["imdb_id"]
movie_path = issue["path"]
try:
# Get movie data from database
movie_data = db.get_movie_dates(imdb_id)
if not movie_data:
fix_results["movies"]["errors"].append(f"No database data for {imdb_id}")
fix_results["movies"]["failed"] += 1
continue
dateadded = movie_data.get("dateadded")
released = movie_data.get("released")
source = movie_data.get("source")
# Convert datetime to string if needed
if hasattr(dateadded, 'isoformat'):
dateadded = dateadded.isoformat()
if released and hasattr(released, 'isoformat'):
released = released.isoformat()
# Regenerate NFO file
from pathlib import Path
movie_path_obj = Path(movie_path)
if config.manage_nfo:
nfo_manager.create_movie_nfo(
movie_path_obj, imdb_id, dateadded, released, source, config.lock_metadata
)
print(f"✅ Fixed NFO for {imdb_id}: {movie_path}")
fix_results["movies"]["fixed"] += 1
else:
fix_results["movies"]["errors"].append(f"MANAGE_NFO disabled - cannot fix {imdb_id}")
fix_results["movies"]["failed"] += 1
except Exception as e:
fix_results["movies"]["errors"].append(f"Error fixing {imdb_id}: {str(e)}")
fix_results["movies"]["failed"] += 1
# Fix Episodes
if media_type in ["both", "episodes"]:
print("📺 Fixing TV episode NFO files...")
for issue in results["episodes"]["issues"]:
if issue["issue"] in ["empty_nfo", "no_nfoguard_data", "date_mismatch"]:
imdb_id = issue["imdb_id"]
episode_str = issue["episode"]
try:
# Parse season/episode from string like "S01E05"
import re
match = re.match(r'S(\d+)E(\d+)', episode_str)
if not match:
continue
season = int(match.group(1))
episode_num = int(match.group(2))
# Get episode data from database
episode_data = db.get_episode_date(imdb_id, season, episode_num)
if not episode_data:
fix_results["episodes"]["errors"].append(f"No database data for {episode_str}")
fix_results["episodes"]["failed"] += 1
continue
air_date = episode_data.get("air_date")
dateadded = episode_data.get("dateadded")
source = episode_data.get("source")
video_path = episode_data.get("video_path")
# Convert datetime to string if needed
if hasattr(air_date, 'isoformat'):
air_date = air_date.isoformat()
if hasattr(dateadded, 'isoformat'):
dateadded = dateadded.isoformat()
# Find the episode file
from pathlib import Path
if video_path:
episode_file = Path(video_path)
else:
# Try to find it by scanning
episode_file = None
for tv_path in config.tv_paths:
series_dirs = [d for d in tv_path.iterdir() if d.is_dir() and imdb_id in str(d)]
if series_dirs:
season_dir = series_dirs[0] / f"Season {season:02d}"
if season_dir.exists():
video_files = list(season_dir.glob(f"*S{season:02d}E{episode_num:02d}*.mkv")) + \
list(season_dir.glob(f"*S{season:02d}E{episode_num:02d}*.mp4")) + \
list(season_dir.glob(f"*S{season:02d}E{episode_num:02d}*.avi"))
if video_files:
episode_file = video_files[0]
break
if not episode_file or not episode_file.exists():
fix_results["episodes"]["errors"].append(f"Cannot find video file for {episode_str}")
fix_results["episodes"]["failed"] += 1
continue
# Regenerate NFO file
if config.manage_nfo:
nfo_manager.create_episode_nfo(
episode_file, imdb_id, season, episode_num, air_date, dateadded, source, config.lock_metadata
)
print(f"✅ Fixed NFO for {episode_str}: {episode_file}")
fix_results["episodes"]["fixed"] += 1
else:
fix_results["episodes"]["errors"].append(f"MANAGE_NFO disabled - cannot fix {episode_str}")
fix_results["episodes"]["failed"] += 1
except Exception as e:
fix_results["episodes"]["errors"].append(f"Error fixing {episode_str}: {str(e)}")
fix_results["episodes"]["failed"] += 1
# Print summary
movies = fix_results["movies"]
episodes = fix_results["episodes"]
print(f"🔧 NFO FIX COMPLETED:")
if media_type in ["both", "movies"]:
print(f" 📽️ Movies: {movies['fixed']} fixed, {movies['failed']} failed")
if media_type in ["both", "episodes"]:
print(f" 📺 Episodes: {episodes['fixed']} fixed, {episodes['failed']} failed")
return {
"success": True,
"message": f"NFO fix completed for {media_type}",
"fix_results": fix_results,
"original_verification": results
}
except Exception as e:
return {
"success": False,
"error": str(e),
"message": "Failed to fix NFO sync issues"
}
async def backfill_movie_release_dates(dependencies: dict):
"""Backfill missing release dates for existing movies"""
db = dependencies["db"]
@@ -1406,6 +1814,14 @@ def register_routes(app, dependencies: dict):
async def _backfill_movie_release_dates():
return await backfill_movie_release_dates(dependencies)
@app.post("/database/verify/nfo-sync")
async def _verify_nfo_sync(media_type: str = "both"):
return await verify_nfo_sync(dependencies, media_type)
@app.post("/database/fix/nfo-sync")
async def _fix_nfo_sync_issues(media_type: str = "both"):
return await fix_nfo_sync_issues(dependencies, media_type)
@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)