Compare commits
2 Commits
1ad2a3d945
...
57122769df
| Author | SHA1 | Date | |
|---|---|---|---|
| 57122769df | |||
| c66c19dc59 |
@@ -135,6 +135,15 @@ PATH_DEBUG=false
|
||||
# Suppress TVDB API warnings (true/false) - TVDB failures are common and non-critical
|
||||
SUPPRESS_TVDB_WARNINGS=true
|
||||
|
||||
# ===========================================
|
||||
# WEB INTERFACE AUTHENTICATION
|
||||
# ===========================================
|
||||
# Enable web interface authentication (default: false)
|
||||
WEB_AUTH_ENABLED=false
|
||||
|
||||
# Session timeout in seconds (default: 3600 = 1 hour)
|
||||
WEB_AUTH_SESSION_TIMEOUT=3600
|
||||
|
||||
# ===========================================
|
||||
# SERVER CONFIGURATION
|
||||
# ===========================================
|
||||
|
||||
@@ -38,4 +38,12 @@ JELLYSEERR_API_KEY=your_jellyseerr_api_key
|
||||
# TVDB API key (RECOMMENDED for v0.6.1+ Emby TV NFO Compatibility)
|
||||
# Required to convert IMDB IDs to TVDB IDs for proper Emby metadata loading
|
||||
# Get free API key at: https://thetvdb.com/api-information
|
||||
TVDB_API_KEY=your_tvdb_api_key
|
||||
TVDB_API_KEY=your_tvdb_api_key
|
||||
|
||||
# ===========================================
|
||||
# WEB INTERFACE AUTHENTICATION (OPTIONAL)
|
||||
# ===========================================
|
||||
# Web interface login credentials (only used if WEB_AUTH_ENABLED=true in .env)
|
||||
# Set WEB_AUTH_ENABLED=true in .env file to enable authentication
|
||||
WEB_AUTH_USERNAME=admin
|
||||
WEB_AUTH_PASSWORD=your_secure_web_password
|
||||
@@ -8,17 +8,11 @@
|
||||
|
||||
---
|
||||
|
||||
> **⚠️ ALPHA SOFTWARE NOTICE ⚠️**
|
||||
>
|
||||
> NFOGuard is currently in **Alpha** stage. While functional, it may have bugs or missing features.
|
||||
>
|
||||
> **🔌 Emby Plugin Included**: The Emby companion plugin is now bundled directly into the Docker image — no extra steps required.
|
||||
>
|
||||
> **💬 Community Feedback**: Join our Discord if you’d like to share feedback, test new features early, or discuss improvements with other users:
|
||||
> **💬 Community Feedback**: Join our Discord if you'd like to share feedback, test new features, or discuss improvements with other users:
|
||||
>
|
||||
> **[Join Discord: https://discord.gg/bbD9Pmtr](https://discord.gg/bbD9Pmtr)**
|
||||
>
|
||||
> *If the Discord link has expired, please [open an issue](https://github.com/sbcrumb/NFOguard/issues) and we'll provide an updated link.*
|
||||
> **[Join Discord: https://discord.gg/ZykJRGt72b](https://discord.gg/ZykJRGt72b)**
|
||||
|
||||
---
|
||||
|
||||
@@ -38,7 +32,7 @@ NFOGuard automatically updates movie and TV show NFO files with proper release d
|
||||
- **Batch Processing** - Efficient handling of multiple files simultaneously
|
||||
- **PostgreSQL Database** - Production-ready database with optimized queries
|
||||
- **Smart Skip Logic** - Database-first checking eliminates expensive filesystem scans
|
||||
- **88% Scan Optimization** - TV library scans reduced from hours to minutes
|
||||
- **88% Scan Optimization** - Subsequent scans reduced from hours to minutes via smart skip logic
|
||||
|
||||
### **Web Interface & Management**
|
||||
- **Complete Web UI** - Episode and movie management with filtering and search
|
||||
@@ -64,9 +58,9 @@ NFOGuard automatically updates movie and TV show NFO files with proper release d
|
||||
### 1. Download Configuration Files
|
||||
|
||||
```bash
|
||||
wget https://raw.githubusercontent.com/sbcrumb/NFOguard/main/.env.template
|
||||
wget https://raw.githubusercontent.com/sbcrumb/NFOguard/main/.env.secrets.template
|
||||
wget https://raw.githubusercontent.com/sbcrumb/NFOguard/main/docker-compose.example.yml
|
||||
wget https://raw.githubusercontent.com/sbcrumb/nfoguard/main/.env.template
|
||||
wget https://raw.githubusercontent.com/sbcrumb/nfoguard/main/.env.secrets.template
|
||||
wget https://raw.githubusercontent.com/sbcrumb/nfoguard/main/docker-compose.example.yml
|
||||
```
|
||||
|
||||
### 2. Configure Environment
|
||||
@@ -149,10 +143,10 @@ image: sbcrumb/nfoguard:dev
|
||||
|
||||
### Specific Version
|
||||
```yaml
|
||||
image: sbcrumb/nfoguard:v2.0.0 # Latest with monitoring & validation
|
||||
image: sbcrumb/nfoguard:v2.6.7 # Latest with PostgreSQL & optimization
|
||||
```
|
||||
|
||||
> **🚀 Version 2.0.0** includes major architecture improvements, async I/O performance enhancements, comprehensive monitoring, and configuration validation.
|
||||
> **🚀 Version 2.6.7** includes major architecture improvements, PostgreSQL database migration, 88% scan optimization, async I/O performance enhancements, comprehensive monitoring, and configuration validation.
|
||||
|
||||
## 🔗 Webhook Setup
|
||||
|
||||
@@ -499,7 +493,7 @@ NFOGuard will ignore:
|
||||
|
||||
## 🆘 Support
|
||||
|
||||
- **Issues**: [GitHub Issues](https://github.com/sbcrumb/NFOguard/issues)
|
||||
- **Issues**: [GitHub Issues](https://github.com/sbcrumb/nfoguard/issues)
|
||||
- **Documentation**: See `SETUP.md` for detailed instructions
|
||||
- **Docker Hub**: [`sbcrumb/nfoguard`](https://hub.docker.com/r/sbcrumb/nfoguard)
|
||||
|
||||
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
"""
|
||||
Simple authentication middleware for NFOGuard web interface
|
||||
Provides basic HTTP auth and session management for web interface protection
|
||||
"""
|
||||
import secrets
|
||||
import hashlib
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, Dict, Any
|
||||
from fastapi import HTTPException, status, Request, Response
|
||||
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
|
||||
|
||||
class AuthSession:
|
||||
"""Simple session management for web interface"""
|
||||
|
||||
def __init__(self, timeout_seconds: int = 3600):
|
||||
self.sessions: Dict[str, Dict[str, Any]] = {}
|
||||
self.timeout_seconds = timeout_seconds
|
||||
|
||||
def create_session(self, username: str) -> str:
|
||||
"""Create a new session and return session token"""
|
||||
session_token = secrets.token_urlsafe(32)
|
||||
self.sessions[session_token] = {
|
||||
"username": username,
|
||||
"created_at": datetime.utcnow(),
|
||||
"last_activity": datetime.utcnow()
|
||||
}
|
||||
return session_token
|
||||
|
||||
def validate_session(self, session_token: str) -> bool:
|
||||
"""Validate session token and update last activity"""
|
||||
if not session_token or session_token not in self.sessions:
|
||||
return False
|
||||
|
||||
session = self.sessions[session_token]
|
||||
now = datetime.utcnow()
|
||||
|
||||
# Check if session expired
|
||||
if (now - session["last_activity"]).seconds > self.timeout_seconds:
|
||||
del self.sessions[session_token]
|
||||
return False
|
||||
|
||||
# Update last activity
|
||||
session["last_activity"] = now
|
||||
return True
|
||||
|
||||
def get_session_user(self, session_token: str) -> Optional[str]:
|
||||
"""Get username from valid session"""
|
||||
if self.validate_session(session_token):
|
||||
return self.sessions[session_token]["username"]
|
||||
return None
|
||||
|
||||
def delete_session(self, session_token: str) -> None:
|
||||
"""Delete a session (logout)"""
|
||||
if session_token in self.sessions:
|
||||
del self.sessions[session_token]
|
||||
|
||||
def cleanup_expired_sessions(self) -> None:
|
||||
"""Remove expired sessions"""
|
||||
now = datetime.utcnow()
|
||||
expired_tokens = []
|
||||
|
||||
for token, session in self.sessions.items():
|
||||
if (now - session["last_activity"]).seconds > self.timeout_seconds:
|
||||
expired_tokens.append(token)
|
||||
|
||||
for token in expired_tokens:
|
||||
del self.sessions[token]
|
||||
|
||||
|
||||
class SimpleAuthMiddleware(BaseHTTPMiddleware):
|
||||
"""Simple authentication middleware for web interface routes"""
|
||||
|
||||
def __init__(self, app, config):
|
||||
super().__init__(app)
|
||||
self.config = config
|
||||
self.session_manager = AuthSession(config.web_auth_session_timeout)
|
||||
self.security = HTTPBasic()
|
||||
|
||||
# Routes that require authentication (web interface)
|
||||
self.protected_routes = [
|
||||
"/", # Main web interface
|
||||
"/static/", # Static files (CSS, JS)
|
||||
"/api/movies", # Web API endpoints
|
||||
"/api/series",
|
||||
"/api/episodes",
|
||||
"/api/dashboard",
|
||||
"/database/" # Database management endpoints
|
||||
]
|
||||
|
||||
# Routes that are always public (webhooks, health checks)
|
||||
self.public_routes = [
|
||||
"/webhook/",
|
||||
"/health",
|
||||
"/ping",
|
||||
"/api/v1/health",
|
||||
"/api/v1/metrics"
|
||||
]
|
||||
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
"""Process request through authentication middleware"""
|
||||
|
||||
# Skip authentication if disabled
|
||||
if not self.config.web_auth_enabled:
|
||||
return await call_next(request)
|
||||
|
||||
# Check if route requires authentication
|
||||
path = request.url.path
|
||||
needs_auth = any(path.startswith(route) for route in self.protected_routes)
|
||||
is_public = any(path.startswith(route) for route in self.public_routes)
|
||||
|
||||
if is_public or not needs_auth:
|
||||
return await call_next(request)
|
||||
|
||||
# Check for existing session
|
||||
session_token = request.cookies.get("nfoguard_session")
|
||||
if session_token and self.session_manager.validate_session(session_token):
|
||||
# Valid session, proceed
|
||||
return await call_next(request)
|
||||
|
||||
# Check for HTTP Basic Auth
|
||||
auth_header = request.headers.get("authorization")
|
||||
if auth_header and auth_header.startswith("Basic "):
|
||||
credentials = self._parse_basic_auth(auth_header)
|
||||
if credentials and self._validate_credentials(credentials.username, credentials.password):
|
||||
# Create session for successful login
|
||||
session_token = self.session_manager.create_session(credentials.username)
|
||||
response = await call_next(request)
|
||||
response.set_cookie(
|
||||
key="nfoguard_session",
|
||||
value=session_token,
|
||||
max_age=self.config.web_auth_session_timeout,
|
||||
httponly=True,
|
||||
secure=False # Set to True if using HTTPS
|
||||
)
|
||||
return response
|
||||
|
||||
# Authentication required
|
||||
return self._auth_required_response()
|
||||
|
||||
def _parse_basic_auth(self, auth_header: str) -> Optional[HTTPBasicCredentials]:
|
||||
"""Parse HTTP Basic Auth header"""
|
||||
try:
|
||||
import base64
|
||||
encoded_credentials = auth_header.split(" ")[1]
|
||||
decoded_credentials = base64.b64decode(encoded_credentials).decode('utf-8')
|
||||
username, password = decoded_credentials.split(":", 1)
|
||||
return HTTPBasicCredentials(username=username, password=password)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _validate_credentials(self, username: str, password: str) -> bool:
|
||||
"""Validate username and password"""
|
||||
return (username == self.config.web_auth_username and
|
||||
password == self.config.web_auth_password)
|
||||
|
||||
def _auth_required_response(self) -> Response:
|
||||
"""Return 401 response with WWW-Authenticate header"""
|
||||
return Response(
|
||||
content="Authentication required",
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
headers={"WWW-Authenticate": "Basic realm=\"NFOGuard Web Interface\""}
|
||||
)
|
||||
|
||||
|
||||
def create_auth_dependencies(config) -> Dict[str, Any]:
|
||||
"""Create authentication-related dependencies for dependency injection"""
|
||||
session_manager = AuthSession(config.web_auth_session_timeout)
|
||||
|
||||
return {
|
||||
"session_manager": session_manager,
|
||||
"auth_enabled": config.web_auth_enabled,
|
||||
"auth_config": {
|
||||
"username": config.web_auth_username,
|
||||
"timeout": config.web_auth_session_timeout
|
||||
}
|
||||
}
|
||||
+33
-1
@@ -7,7 +7,7 @@ import requests
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timezone
|
||||
from fastapi import HTTPException, BackgroundTasks, Request
|
||||
from fastapi import HTTPException, BackgroundTasks, Request, Response
|
||||
from typing import Optional
|
||||
|
||||
# Import models
|
||||
@@ -1396,6 +1396,38 @@ def register_routes(app, dependencies: dict):
|
||||
"""Update episode dateadded"""
|
||||
return await update_episode_date(dependencies, imdb_id, season, episode, request.dateadded, request.source)
|
||||
|
||||
@app.post("/api/auth/logout")
|
||||
async def _logout(request: Request, response: Response):
|
||||
"""Logout endpoint - clears session"""
|
||||
session_manager = dependencies.get("session_manager")
|
||||
if session_manager:
|
||||
session_token = request.cookies.get("nfoguard_session")
|
||||
if session_token:
|
||||
session_manager.delete_session(session_token)
|
||||
|
||||
response.delete_cookie("nfoguard_session")
|
||||
return {"status": "logged_out", "message": "Session cleared"}
|
||||
|
||||
@app.get("/api/auth/status")
|
||||
async def _auth_status(request: Request):
|
||||
"""Check authentication status"""
|
||||
auth_enabled = dependencies.get("auth_enabled", False)
|
||||
|
||||
if not auth_enabled:
|
||||
return {"authenticated": True, "auth_enabled": False, "message": "Authentication disabled"}
|
||||
|
||||
session_manager = dependencies.get("session_manager")
|
||||
if not session_manager:
|
||||
return {"authenticated": False, "auth_enabled": True, "message": "Session manager not available"}
|
||||
|
||||
session_token = request.cookies.get("nfoguard_session")
|
||||
if session_token:
|
||||
username = session_manager.get_session_user(session_token)
|
||||
if username:
|
||||
return {"authenticated": True, "auth_enabled": True, "username": username}
|
||||
|
||||
return {"authenticated": False, "auth_enabled": True, "message": "Not authenticated"}
|
||||
|
||||
@app.post("/api/bulk/update-source")
|
||||
async def _bulk_update_source(request: BulkUpdateRequest):
|
||||
"""Bulk update source for movies or episodes"""
|
||||
|
||||
@@ -79,6 +79,9 @@ class NFOGuardConfig:
|
||||
|
||||
# TV processing
|
||||
self._load_tv_settings()
|
||||
|
||||
# Web interface authentication
|
||||
self._load_auth_settings()
|
||||
|
||||
def _load_paths(self) -> None:
|
||||
"""Load and validate path configuration"""
|
||||
@@ -156,6 +159,13 @@ class NFOGuardConfig:
|
||||
self.tv_season_dir_pattern = os.environ.get("TV_SEASON_DIR_PATTERN", "season ").lower()
|
||||
self.tv_webhook_processing_mode = os.environ.get("TV_WEBHOOK_PROCESSING_MODE", "targeted").lower()
|
||||
|
||||
def _load_auth_settings(self) -> None:
|
||||
"""Load web interface authentication settings"""
|
||||
self.web_auth_enabled = _bool_env("WEB_AUTH_ENABLED", False)
|
||||
self.web_auth_username = os.environ.get("WEB_AUTH_USERNAME", "admin")
|
||||
self.web_auth_password = os.environ.get("WEB_AUTH_PASSWORD", "")
|
||||
self.web_auth_session_timeout = self._get_int_env("WEB_AUTH_SESSION_TIMEOUT", 3600, 300, 86400) # 1 hour default, 5min-24h range
|
||||
|
||||
def _get_int_env(self, name: str, default: int, min_val: int, max_val: int) -> int:
|
||||
"""Get integer environment variable with validation"""
|
||||
value_str = os.environ.get(name)
|
||||
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Debug script to check specific TV series/episode data in NFOGuard database
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add the project root to the path
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
from core.database import NFOGuardDatabase
|
||||
|
||||
def debug_series(imdb_id: str):
|
||||
"""Debug a specific TV series' data"""
|
||||
print(f"📺 DEBUG TV SERIES: {imdb_id}")
|
||||
print("=" * 60)
|
||||
|
||||
# Initialize database
|
||||
db = NFOGuardDatabase()
|
||||
|
||||
# Get series episodes
|
||||
episodes = db.get_series_episodes(imdb_id)
|
||||
if not episodes:
|
||||
print(f"❌ TV series {imdb_id} not found in database")
|
||||
return
|
||||
|
||||
print(f"📊 SERIES OVERVIEW:")
|
||||
print(f" IMDb ID: {imdb_id}")
|
||||
print(f" Total Episodes: {len(episodes)}")
|
||||
|
||||
# Count episodes by status
|
||||
with_dates = sum(1 for ep in episodes if ep.get('dateadded'))
|
||||
without_dates = len(episodes) - with_dates
|
||||
with_video = sum(1 for ep in episodes if ep.get('has_video_file'))
|
||||
|
||||
print(f" Episodes with dates: {with_dates}")
|
||||
print(f" Episodes without dates: {without_dates}")
|
||||
print(f" Episodes with video files: {with_video}")
|
||||
|
||||
# Group by season
|
||||
seasons = {}
|
||||
for ep in episodes:
|
||||
season = ep.get('season', 'Unknown')
|
||||
if season not in seasons:
|
||||
seasons[season] = []
|
||||
seasons[season].append(ep)
|
||||
|
||||
print(f" Seasons: {len(seasons)} ({', '.join(f'S{s}' if isinstance(s, int) else str(s) for s in sorted(seasons.keys()))})")
|
||||
|
||||
# Show sources breakdown
|
||||
sources = {}
|
||||
for ep in episodes:
|
||||
source = ep.get('source', 'None')
|
||||
sources[source] = sources.get(source, 0) + 1
|
||||
|
||||
print(f"\n📈 SOURCES BREAKDOWN:")
|
||||
for source, count in sorted(sources.items(), key=lambda x: x[1], reverse=True):
|
||||
print(f" {source}: {count} episodes")
|
||||
|
||||
# Show recent episodes (last 10 by date added)
|
||||
episodes_with_dates = [ep for ep in episodes if ep.get('dateadded')]
|
||||
recent_episodes = sorted(episodes_with_dates, key=lambda x: x.get('last_updated', ''), reverse=True)[:10]
|
||||
|
||||
if recent_episodes:
|
||||
print(f"\n🕒 RECENT EPISODES (by last_updated):")
|
||||
for ep in recent_episodes:
|
||||
season = ep.get('season', '?')
|
||||
episode = ep.get('episode', '?')
|
||||
dateadded = ep.get('dateadded', 'None')
|
||||
source = ep.get('source', 'None')
|
||||
video = "✅" if ep.get('has_video_file') else "❌"
|
||||
print(f" S{season:02d}E{episode:02d}: {dateadded} | {source} | Video: {video}")
|
||||
|
||||
def debug_episode(imdb_id: str, season: int, episode: int):
|
||||
"""Debug a specific episode's data"""
|
||||
print(f"📺 DEBUG TV EPISODE: {imdb_id} S{season:02d}E{episode:02d}")
|
||||
print("=" * 60)
|
||||
|
||||
# Initialize database
|
||||
db = NFOGuardDatabase()
|
||||
|
||||
# Get specific episode
|
||||
episode_data = db.get_episode_date(imdb_id, season, episode)
|
||||
if not episode_data:
|
||||
print(f"❌ Episode S{season:02d}E{episode:02d} for series {imdb_id} not found in database")
|
||||
return
|
||||
|
||||
print("📊 RAW EPISODE DATA:")
|
||||
for key, value in episode_data.items():
|
||||
print(f" {key}: {repr(value)}")
|
||||
|
||||
print("\n📺 FORMATTED EPISODE DATA:")
|
||||
print(f" Series IMDb: {episode_data.get('imdb_id', 'Unknown')}")
|
||||
print(f" Season/Episode: S{episode_data.get('season', '?'):02d}E{episode_data.get('episode', '?'):02d}")
|
||||
print(f" Title: {episode_data.get('title', 'Unknown')}")
|
||||
print(f" Air Date: {episode_data.get('air_date', 'None')}")
|
||||
print(f" Date Added: {episode_data.get('dateadded', 'None')}")
|
||||
print(f" Source: {episode_data.get('source', 'None')}")
|
||||
print(f" Has Video: {episode_data.get('has_video_file', False)}")
|
||||
print(f" Video Path: {episode_data.get('video_path', 'None')}")
|
||||
print(f" Last Updated: {episode_data.get('last_updated', 'None')}")
|
||||
|
||||
# Check if air date is valid
|
||||
air_date = episode_data.get('air_date')
|
||||
if air_date and air_date.strip():
|
||||
try:
|
||||
from datetime import datetime
|
||||
test_date = f"{air_date}T00:00:00"
|
||||
parsed = datetime.fromisoformat(test_date.replace('Z', '+00:00'))
|
||||
print(f"\n✅ Air date is valid: {parsed}")
|
||||
except Exception as e:
|
||||
print(f"\n❌ Air date is INVALID: {e}")
|
||||
else:
|
||||
print(f"\n⚠️ Air date is empty or None")
|
||||
|
||||
# Check if dateadded is valid
|
||||
dateadded = episode_data.get('dateadded')
|
||||
if dateadded and dateadded.strip():
|
||||
try:
|
||||
from datetime import datetime
|
||||
if isinstance(dateadded, str):
|
||||
test_date = f"{dateadded}T00:00:00" if 'T' not in dateadded else dateadded
|
||||
parsed = datetime.fromisoformat(test_date.replace('Z', '+00:00'))
|
||||
else:
|
||||
parsed = dateadded
|
||||
print(f"✅ Date added is valid: {parsed}")
|
||||
except Exception as e:
|
||||
print(f"❌ Date added is INVALID: {e}")
|
||||
else:
|
||||
print(f"⚠️ Date added is empty or None")
|
||||
|
||||
def debug_season(imdb_id: str, season: int):
|
||||
"""Debug all episodes in a specific season"""
|
||||
print(f"📺 DEBUG TV SEASON: {imdb_id} Season {season}")
|
||||
print("=" * 60)
|
||||
|
||||
# Initialize database
|
||||
db = NFOGuardDatabase()
|
||||
|
||||
# Get series episodes
|
||||
all_episodes = db.get_series_episodes(imdb_id)
|
||||
season_episodes = [ep for ep in all_episodes if ep.get('season') == season]
|
||||
|
||||
if not season_episodes:
|
||||
print(f"❌ No episodes found for season {season} of series {imdb_id}")
|
||||
return
|
||||
|
||||
print(f"📊 SEASON {season} OVERVIEW:")
|
||||
print(f" Total Episodes: {len(season_episodes)}")
|
||||
|
||||
# Sort by episode number
|
||||
season_episodes.sort(key=lambda x: x.get('episode', 0))
|
||||
|
||||
with_dates = sum(1 for ep in season_episodes if ep.get('dateadded'))
|
||||
without_dates = len(season_episodes) - with_dates
|
||||
with_video = sum(1 for ep in season_episodes if ep.get('has_video_file'))
|
||||
|
||||
print(f" Episodes with dates: {with_dates}")
|
||||
print(f" Episodes without dates: {without_dates}")
|
||||
print(f" Episodes with video files: {with_video}")
|
||||
|
||||
print(f"\n📋 EPISODE LIST:")
|
||||
for ep in season_episodes:
|
||||
episode_num = ep.get('episode', '?')
|
||||
title = ep.get('title', 'Unknown')[:30] + ('...' if len(ep.get('title', '')) > 30 else '')
|
||||
dateadded = ep.get('dateadded', 'None')
|
||||
source = ep.get('source', 'None')
|
||||
video = "✅" if ep.get('has_video_file') else "❌"
|
||||
air_date = ep.get('air_date', 'None')
|
||||
|
||||
print(f" E{episode_num:02d}: {title:<33} | Added: {dateadded} | Air: {air_date} | Video: {video}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage:")
|
||||
print(" python debug_tv.py <imdb_id> # Debug entire series")
|
||||
print(" python debug_tv.py <imdb_id> <season> # Debug specific season")
|
||||
print(" python debug_tv.py <imdb_id> <season> <episode> # Debug specific episode")
|
||||
print("\nExamples:")
|
||||
print(" python debug_tv.py tt0121955 # Debug South Park")
|
||||
print(" python debug_tv.py tt0121955 27 # Debug South Park Season 27")
|
||||
print(" python debug_tv.py tt0121955 27 6 # Debug South Park S27E06")
|
||||
sys.exit(1)
|
||||
|
||||
imdb_id = sys.argv[1]
|
||||
if not imdb_id.startswith('tt'):
|
||||
imdb_id = f'tt{imdb_id}'
|
||||
|
||||
if len(sys.argv) == 4:
|
||||
# Debug specific episode
|
||||
season = int(sys.argv[2])
|
||||
episode = int(sys.argv[3])
|
||||
debug_episode(imdb_id, season, episode)
|
||||
elif len(sys.argv) == 3:
|
||||
# Debug specific season
|
||||
season = int(sys.argv[2])
|
||||
debug_season(imdb_id, season)
|
||||
else:
|
||||
# Debug entire series
|
||||
debug_series(imdb_id)
|
||||
@@ -15,6 +15,9 @@ from fastapi import FastAPI
|
||||
|
||||
# Import configuration first
|
||||
from config.settings import config
|
||||
|
||||
# Import authentication
|
||||
from api.auth import SimpleAuthMiddleware, create_auth_dependencies
|
||||
from utils.logging import _log
|
||||
|
||||
# Import core components
|
||||
@@ -172,6 +175,17 @@ def main():
|
||||
# Initialize components
|
||||
dependencies = initialize_components()
|
||||
|
||||
# Add authentication dependencies
|
||||
auth_deps = create_auth_dependencies(config)
|
||||
dependencies.update(auth_deps)
|
||||
|
||||
# Add authentication middleware if enabled
|
||||
if config.web_auth_enabled:
|
||||
app.add_middleware(SimpleAuthMiddleware, config=config)
|
||||
_log("INFO", f"Web authentication enabled for user: {config.web_auth_username}")
|
||||
else:
|
||||
_log("INFO", "Web authentication disabled - web interface is public")
|
||||
|
||||
# Store dependencies globally for signal handler access
|
||||
signal_handler.dependencies = dependencies
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ body {
|
||||
color: white;
|
||||
padding: 1rem 0;
|
||||
box-shadow: var(--shadow-lg);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
@@ -63,6 +64,45 @@ body {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Authentication Status */
|
||||
.auth-status {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
color: white;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.auth-user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.auth-logout {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.auth-logout:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.nav-tabs {
|
||||
max-width: 1200px;
|
||||
margin: 1rem auto 0;
|
||||
|
||||
@@ -15,6 +15,14 @@
|
||||
<h1><i class="fas fa-shield-alt"></i> NFOGuard</h1>
|
||||
<p>Database Management & Reporting</p>
|
||||
</div>
|
||||
<div class="auth-status" id="auth-status" style="display: none;">
|
||||
<span class="auth-user">
|
||||
<i class="fas fa-user"></i> <span id="auth-username">Loading...</span>
|
||||
</span>
|
||||
<button class="auth-logout" id="logout-btn" onclick="logout()">
|
||||
<i class="fas fa-sign-out-alt"></i> Logout
|
||||
</button>
|
||||
</div>
|
||||
<nav class="nav-tabs">
|
||||
<button class="nav-tab active" data-tab="dashboard">
|
||||
<i class="fas fa-tachometer-alt"></i> Dashboard
|
||||
|
||||
@@ -10,6 +10,7 @@ let dashboardData = null;
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initializeTabs();
|
||||
initializeEventListeners();
|
||||
checkAuthStatus(); // Check authentication status on page load
|
||||
loadDashboard();
|
||||
loadSeriesSources();
|
||||
});
|
||||
@@ -1375,4 +1376,62 @@ function updateEpisodeModalCounts() {
|
||||
${videoCountText}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// Authentication Functions
|
||||
// ===========================
|
||||
|
||||
async function checkAuthStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/auth/status');
|
||||
const authStatus = await response.json();
|
||||
|
||||
const authStatusDiv = document.getElementById('auth-status');
|
||||
const authUsernameSpan = document.getElementById('auth-username');
|
||||
|
||||
if (authStatus.auth_enabled && authStatus.authenticated) {
|
||||
// Show authentication status with username
|
||||
authUsernameSpan.textContent = authStatus.username;
|
||||
authStatusDiv.style.display = 'flex';
|
||||
} else if (authStatus.auth_enabled && !authStatus.authenticated) {
|
||||
// This shouldn't happen if middleware is working, but handle it
|
||||
console.warn('Auth enabled but not authenticated - middleware may be misconfigured');
|
||||
} else {
|
||||
// Authentication disabled - hide auth status
|
||||
authStatusDiv.style.display = 'none';
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to check authentication status:', error);
|
||||
// Hide auth status on error
|
||||
document.getElementById('auth-status').style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
if (!confirm('Are you sure you want to logout?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/auth/logout', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showToast('✅ Logged out successfully', 'success');
|
||||
// Reload page to trigger authentication prompt
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1500);
|
||||
} else {
|
||||
showToast('❌ Logout failed', 'error');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Logout failed:', error);
|
||||
showToast('❌ Logout error', 'error');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user