dev #66

Merged
sbcrumb merged 20 commits from dev into main 2025-11-02 12:41:59 -05:00
6 changed files with 610 additions and 1469 deletions
Showing only changes of commit afdbe1f755 - Show all commits
+28 -467
View File
@@ -49,32 +49,10 @@ body {
text-align: center;
}
/* Header Logo and Text Layout */
.header-logo {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
}
.header-logo .logo {
height: 60px;
width: auto;
/* Clean display for new logo */
object-fit: contain;
}
.header-text {
display: flex;
flex-direction: column;
align-items: flex-start;
text-align: left;
}
.header-content h1 {
font-size: 2rem;
font-weight: 300;
margin-bottom: 0.25rem;
margin-bottom: 0.5rem;
}
.header-content h1 i {
@@ -84,28 +62,6 @@ body {
.header-content p {
opacity: 0.9;
font-size: 1rem;
margin: 0;
}
/* Responsive logo layout */
@media (max-width: 768px) {
.header-logo {
flex-direction: column;
gap: 0.5rem;
}
.header-text {
align-items: center;
text-align: center;
}
.header-logo .logo {
height: 45px;
}
.header-content h1 {
font-size: 1.5rem;
}
}
/* Authentication Status */
@@ -888,454 +844,59 @@ body {
.justify-content-between { justify-content: space-between; }
.align-items-center { align-items: center; }
/* Database Admin Tools Styles */
.query-results {
/* Manual Scan Styles */
.scan-status {
margin-top: 1rem;
padding: 1rem;
background: #f8f9fa;
background-color: var(--light-color);
border: 1px solid var(--border-color);
border-radius: 0.375rem;
}
.query-info {
padding: 1rem;
border-radius: 0.375rem;
.scan-progress {
margin-bottom: 1rem;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 0.875rem;
}
.query-info.success {
background-color: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
}
.query-info.error {
background-color: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
}
.results-table {
.progress-bar {
width: 100%;
border-collapse: collapse;
font-size: 0.875rem;
background: white;
height: 1.5rem;
background-color: #e9ecef;
border-radius: 0.375rem;
overflow: hidden;
box-shadow: var(--shadow);
}
.results-table th {
background-color: var(--dark-color);
color: white;
padding: 0.75rem;
text-align: left;
font-weight: 600;
}
.results-table td {
padding: 0.75rem;
border-bottom: 1px solid var(--border-color);
vertical-align: top;
}
.results-table tr:hover {
background-color: #f8f9fa;
}
.results-table td em {
color: var(--text-muted);
font-style: italic;
}
.query-results .table-container,
.admin-results .table-container {
max-height: 400px;
overflow-y: auto;
border: 1px solid var(--border-color);
border-radius: 0.375rem;
}
/* NFO Repair Table Styling */
.nfo-repair-table-container {
background: white;
border-radius: 0.5rem;
box-shadow: var(--shadow);
overflow-x: auto;
overflow-y: auto;
max-height: 600px;
margin-bottom: 1rem;
border: 1px solid var(--border-color);
}
.nfo-repair-table {
width: 100%;
min-width: 800px;
border-collapse: collapse;
font-size: 0.875rem;
background: white;
}
.nfo-repair-table th {
background-color: var(--dark-color);
color: white;
padding: 0.75rem 0.5rem;
text-align: left;
font-weight: 600;
position: sticky;
top: 0;
z-index: 10;
}
.nfo-repair-table td {
padding: 0.75rem 0.5rem;
border-bottom: 1px solid var(--border-color);
vertical-align: top;
}
.nfo-repair-table tr:hover {
background-color: #f8f9fa;
}
.media-type-badge {
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.media-type-badge.episode {
background-color: #e3f2fd;
color: #1976d2;
}
.media-type-badge.movie {
background-color: #f3e5f5;
color: #7b1fa2;
}
.status-ready {
color: #2e7d32;
font-weight: 600;
font-size: 0.75rem;
}
.status-missing {
color: #d32f2f;
font-weight: 600;
font-size: 0.75rem;
}
.text-center {
text-align: center;
}
.text-muted {
color: #6c757d;
}
.nfo-fix-actions {
margin-top: 1rem;
padding: 1rem;
background-color: #f8f9fa;
border-radius: 0.5rem;
text-align: center;
}
.nfo-fix-actions .btn {
margin-bottom: 0.5rem;
}
.admin-tools {
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--primary-color), var(--success-color));
transition: width 0.3s ease;
width: 0%;
}
.scan-info {
display: flex;
flex-direction: column;
gap: 0.5rem;
justify-content: space-between;
align-items: center;
font-size: 0.875rem;
}
.admin-tools .btn {
text-align: left;
justify-content: flex-start;
}
.admin-results {
margin-top: 1rem;
padding: 1rem;
background: #f8f9fa;
border: 1px solid var(--border-color);
border-radius: 0.375rem;
}
.scan-summary,
.fix-summary {
padding: 1rem;
border-radius: 0.375rem;
margin-bottom: 1rem;
}
.scan-summary {
background-color: #e7f3ff;
border: 1px solid #b3d9ff;
}
.fix-summary.success {
background-color: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
}
.fix-errors {
background-color: #fff3cd;
border: 1px solid #ffeaa7;
padding: 1rem;
border-radius: 0.375rem;
margin-top: 1rem;
}
.fix-errors ul {
margin: 0.5rem 0 0 1rem;
}
.missing-episodes {
margin-top: 1rem;
}
.loading {
text-align: center;
padding: 2rem;
.scan-info span:first-child {
color: var(--text-muted);
font-style: italic;
}
.loading::before {
content: "⏳ ";
}
.success {
background-color: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
padding: 1rem;
border-radius: 0.375rem;
margin: 1rem 0;
}
.error {
background-color: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
padding: 1rem;
border-radius: 0.375rem;
margin: 1rem 0;
}
/* Make text areas more distinctive */
textarea {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 0.875rem;
resize: vertical;
}
/* Button variations for admin tools */
.btn.btn-info {
background-color: #17a2b8;
border-color: #17a2b8;
color: white;
}
.btn.btn-info:hover {
background-color: #138496;
border-color: #117a8b;
}
/* Missing IMDb Items Styles */
.missing-imdb-table-container {
overflow-x: auto;
max-height: 600px;
border: 1px solid var(--border-color);
border-radius: 8px;
margin: 1rem 0;
}
.missing-imdb-table {
width: 100%;
border-collapse: collapse;
min-width: 800px;
}
.missing-imdb-table thead {
background-color: var(--bg-secondary);
position: sticky;
top: 0;
z-index: 1;
}
.missing-imdb-table th,
.missing-imdb-table td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid var(--border-color);
vertical-align: top;
}
.missing-imdb-table th {
.scan-info span:last-child {
font-weight: 600;
color: var(--text-primary);
white-space: nowrap;
}
.missing-imdb-table td {
color: var(--text-secondary);
}
.missing-imdb-table .file-path {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 0.85rem;
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.media-type-badge {
display: inline-block;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.media-type-badge.tv-series {
background-color: #e3f2fd;
color: #1976d2;
}
.media-type-badge.movie {
background-color: #fff3e0;
color: #f57c00;
}
.missing-imdb-actions {
background-color: var(--bg-secondary);
padding: 1rem;
border-radius: 8px;
margin-top: 1rem;
}
.missing-imdb-actions ul {
margin: 0.5rem 0 0 1rem;
padding: 0;
}
.missing-imdb-actions li {
margin-bottom: 0.5rem;
}
.missing-imdb-actions code {
background-color: var(--bg-primary);
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 0.85rem;
border: 1px solid var(--border-color);
}
.btn-small {
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
min-width: auto;
}
/* NFO Statistics Styling */
.nfo-stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin: 1rem 0;
}
.stat-box {
background-color: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 1rem;
text-align: center;
}
.stat-number {
font-size: 2rem;
font-weight: 700;
color: var(--primary-color);
margin-bottom: 0.5rem;
}
.stat-label {
.form-group small {
display: block;
margin-top: 0.25rem;
color: var(--text-muted);
font-size: 0.875rem;
color: var(--text-secondary);
font-weight: 500;
}
.recommendation-box {
background-color: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 8px;
padding: 1rem;
margin: 1rem 0;
}
.recommendation-box ul {
margin: 0.5rem 0 0 1rem;
padding: 0;
}
.recommendation-box li {
margin-bottom: 0.5rem;
}
.success-box {
background-color: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
border-radius: 8px;
padding: 1rem;
margin: 1rem 0;
text-align: center;
font-weight: 600;
}
/* Responsive adjustments for admin tools */
@media (max-width: 768px) {
.table-container {
max-height: 300px;
}
.results-table {
font-size: 0.75rem;
}
.results-table th,
.results-table td {
padding: 0.5rem;
}
.admin-tools {
gap: 0.75rem;
}
.missing-imdb-table {
min-width: 600px;
}
.missing-imdb-table th,
.missing-imdb-table td {
padding: 0.5rem;
font-size: 0.85rem;
}
.missing-imdb-table .file-path {
max-width: 200px;
}
.btn-sm {
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
}
+52 -139
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NFOGuard - Database Management</title>
<link rel="stylesheet" href="/static/css/styles.css?v=2.8.2-20241026-nfo-table-redesign">
<link rel="stylesheet" href="/static/css/styles.css?v=manual-scan-ui">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
</head>
<body>
@@ -12,13 +12,9 @@
<!-- Header -->
<header class="app-header">
<div class="header-content">
<div class="header-logo">
<div class="header-text">
<h1>NFOGuard</h1>
<h1><i class="fas fa-shield-alt"></i> NFOGuard</h1>
<p>Database Management & Reporting</p>
</div>
</div>
</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>
@@ -50,24 +46,6 @@
<main class="main-content">
<!-- Dashboard Tab -->
<div class="tab-content active" id="dashboard">
<!-- Scan Status Banner -->
<div class="scan-status-banner" id="dashboard-scan-status" style="display: none;">
<div class="scan-status-content">
<div class="scan-status-icon">
<i class="fas fa-sync fa-spin"></i>
</div>
<div class="scan-status-info">
<h4>Scan in Progress</h4>
<p id="dashboard-scan-text">Processing media files...</p>
<div class="scan-progress-mini">
<div class="progress-bar-mini">
<div class="progress-fill-mini" id="dashboard-scan-progress"></div>
</div>
</div>
</div>
</div>
</div>
<div class="dashboard-grid">
<div class="stat-card">
<div class="stat-icon movies">
@@ -315,31 +293,50 @@
<h2><i class="fas fa-tools"></i> Database Tools</h2>
</div>
<!-- Scan Status Display -->
<div class="scan-status-card" id="scan-status" style="display: none;">
<div class="scan-status-header">
<h3><i class="fas fa-sync fa-spin"></i> Scan in Progress</h3>
<div class="scan-progress">
<div class="progress-bar">
<div class="progress-fill" id="scan-progress"></div>
<div class="tools-grid">
<div class="tool-card">
<h3><i class="fas fa-exchange-alt"></i> Bulk Source Update</h3>
<p>Change source for multiple items at once</p>
<form id="bulk-update-form">
<div class="form-group">
<label>Media Type:</label>
<select id="bulk-media-type" required>
<option value="">Select type...</option>
<option value="movies">Movies</option>
<option value="episodes">Episodes</option>
</select>
</div>
<span class="progress-text" id="scan-progress-text">Initializing...</span>
<div class="form-group">
<label>From Source:</label>
<input type="text" id="bulk-old-source" placeholder="e.g., no_valid_date_source" required>
</div>
<div class="form-group">
<label>To Source:</label>
<select id="bulk-new-source" required>
<option value="">Select new source...</option>
<option value="airdate">Air Date</option>
<option value="digital_release">Digital Release</option>
<option value="manual">Manual</option>
<option value="radarr:db.history.import">Radarr Import</option>
<option value="sonarr:history.import">Sonarr Import</option>
</select>
</div>
<button type="submit" class="btn btn-warning">
<i class="fas fa-exchange-alt"></i> Update Sources
</button>
</form>
</div>
<div class="tools-grid">
<!-- Manual Scan Tools -->
<div class="tool-card">
<h3><i class="fas fa-search"></i> Manual Scan</h3>
<p>Run manual scans on your media library</p>
<p>Scan specific folders or perform full library scans</p>
<form id="manual-scan-form">
<div class="form-group">
<label>Scan Type:</label>
<select id="scan-type" required>
<option value="both">Both (Movies & TV)</option>
<option value="both">TV Shows & Movies</option>
<option value="tv">TV Shows Only</option>
<option value="movies">Movies Only</option>
<option value="tv">TV Series Only</option>
</select>
</div>
<div class="form-group">
@@ -347,38 +344,33 @@
<select id="scan-mode" required>
<option value="smart">Smart (Recommended)</option>
<option value="full">Full Scan</option>
<option value="incomplete">Incomplete Items Only</option>
<option value="incomplete">Incomplete Only</option>
</select>
</div>
<button type="submit" class="btn-primary">
<div class="form-group">
<label>Specific Path (Optional):</label>
<input type="text" id="scan-path" placeholder="e.g., /mnt/unionfs/Media/TV/Series Name" title="Leave empty for full library scan">
<small>Leave empty to scan entire library</small>
</div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-play"></i> Start Scan
</button>
</form>
<div id="scan-status" class="scan-status" style="display: none;">
<div class="scan-progress">
<div class="progress-bar">
<div class="progress-fill" id="scan-progress-bar"></div>
</div>
<div class="tool-card">
<h3><i class="fas fa-folder"></i> Custom Directory Scan</h3>
<p>Scan a specific directory or path</p>
<form id="custom-scan-form">
<div class="form-group">
<label>Directory Path:</label>
<input type="text" id="scan-path" placeholder="/media/Movies/specific-folder" />
<small>Enter the full path to scan (will be auto-formatted)</small>
<div class="scan-info">
<span id="scan-current-operation">Initializing...</span>
<span id="scan-progress-text">0%</span>
</div>
<div class="form-group">
<label>Scan Type:</label>
<select id="custom-scan-type" required>
<option value="both">Auto-detect</option>
<option value="movies">Movies</option>
<option value="tv">TV Series</option>
</select>
</div>
<button type="submit" class="btn-primary">
<i class="fas fa-search"></i> Scan Directory
<button class="btn btn-secondary btn-sm" onclick="stopScanPolling()">
<i class="fas fa-times"></i> Hide Status
</button>
</form>
</div>
</div>
<div class="tool-card">
<h3><i class="fas fa-database"></i> Database Statistics</h3>
@@ -390,85 +382,6 @@
<i class="fas fa-sync"></i> Refresh Stats
</button>
</div>
<!-- Database Query Tool -->
<div class="tool-card">
<h3><i class="fas fa-terminal"></i> Database Query Tool</h3>
<p>Execute custom SQL queries on the NFOGuard database</p>
<form id="database-query-form">
<div class="form-group">
<label>Quick Queries:</label>
<select id="quick-query-select" onchange="loadQuickQuery()">
<option value="">Select a predefined query...</option>
</select>
</div>
<div class="form-group">
<label>SQL Query:</label>
<textarea id="sql-query" rows="4" placeholder="SELECT * FROM episodes WHERE dateadded IS NULL LIMIT 10"></textarea>
<small>Query will be automatically limited to 100 results for safety</small>
</div>
<div class="form-group">
<label>Result Limit:</label>
<select id="query-limit">
<option value="10">10 rows</option>
<option value="50">50 rows</option>
<option value="100" selected>100 rows</option>
</select>
</div>
<button type="submit" class="btn btn-info">
<i class="fas fa-play"></i> Execute Query
</button>
</form>
<div id="query-results" class="query-results" style="display: none;">
<h4>Query Results:</h4>
<div id="query-results-content"></div>
</div>
</div>
<!-- NFO File Repair Tool -->
<div class="tool-card">
<h3><i class="fas fa-file-medical"></i> NFO File Repair</h3>
<p>Fix missing &lt;dateadded&gt; elements in NFO files</p>
<div class="form-group">
<button class="btn btn-warning" onclick="checkMissingNFODates()">
<i class="fas fa-search"></i> Scan for Missing dateadded in NFO Files
</button>
</div>
<div id="nfo-scan-results" style="display: none;">
<div id="nfo-scan-content"></div>
<div class="form-group">
<button class="btn btn-success" onclick="fixAllMissingNFODates()">
<i class="fas fa-wrench"></i> Fix All Missing dateadded Elements
</button>
<button class="btn btn-secondary" onclick="fixSpecificSeries()">
<i class="fas fa-cog"></i> Fix Specific Series
</button>
</div>
</div>
</div>
<!-- Database Admin Tools -->
<div class="tool-card">
<h3><i class="fas fa-user-shield"></i> Database Admin</h3>
<p>Advanced database management tools</p>
<div class="admin-tools">
<button class="btn btn-info" onclick="showEpisodesMissingNFODateadded()">
<i class="fas fa-exclamation-triangle"></i> Episodes Missing NFO dateadded
</button>
<button class="btn btn-warning" onclick="showMissingImdbItems()">
<i class="fas fa-search"></i> Items Missing IMDb IDs
</button>
<button class="btn btn-secondary" onclick="exportDatabaseReport()">
<i class="fas fa-download"></i> Export Database Report
</button>
<button class="btn btn-secondary" onclick="showRecentWebhookActivity()">
<i class="fas fa-clock"></i> Recent Webhook Activity
</button>
</div>
<div id="admin-results" class="admin-results" style="display: none;">
<div id="admin-results-content"></div>
</div>
</div>
</div>
</div>
</main>
@@ -536,6 +449,6 @@
<!-- Toast Notifications -->
<div class="toast-container" id="toast-container"></div>
<script src="/static/js/app.js?v=3.0.1-nfo-table-redesign"></script>
<script src="/static/js/app.js?v=manual-scan-ui"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+57
View File
@@ -843,3 +843,60 @@ body {
.d-flex { display: flex; }
.justify-content-between { justify-content: space-between; }
.align-items-center { align-items: center; }
/* Manual Scan Styles */
.scan-status {
margin-top: 1rem;
padding: 1rem;
background-color: var(--light-color);
border: 1px solid var(--border-color);
border-radius: 0.375rem;
}
.scan-progress {
margin-bottom: 1rem;
}
.progress-bar {
width: 100%;
height: 1.5rem;
background-color: #e9ecef;
border-radius: 0.375rem;
overflow: hidden;
margin-bottom: 0.5rem;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--primary-color), var(--success-color));
transition: width 0.3s ease;
width: 0%;
}
.scan-info {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.875rem;
}
.scan-info span:first-child {
color: var(--text-muted);
}
.scan-info span:last-child {
font-weight: 600;
color: var(--primary-color);
}
.form-group small {
display: block;
margin-top: 0.25rem;
color: var(--text-muted);
font-size: 0.875rem;
}
.btn-sm {
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
}
+47 -2
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NFOGuard - Database Management</title>
<link rel="stylesheet" href="/static/css/styles.css">
<link rel="stylesheet" href="/static/css/styles.css?v=manual-scan-ui">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
</head>
<body>
@@ -327,6 +327,51 @@
</form>
</div>
<div class="tool-card">
<h3><i class="fas fa-search"></i> Manual Scan</h3>
<p>Scan specific folders or perform full library scans</p>
<form id="manual-scan-form">
<div class="form-group">
<label>Scan Type:</label>
<select id="scan-type" required>
<option value="both">TV Shows & Movies</option>
<option value="tv">TV Shows Only</option>
<option value="movies">Movies Only</option>
</select>
</div>
<div class="form-group">
<label>Scan Mode:</label>
<select id="scan-mode" required>
<option value="smart">Smart (Recommended)</option>
<option value="full">Full Scan</option>
<option value="incomplete">Incomplete Only</option>
</select>
</div>
<div class="form-group">
<label>Specific Path (Optional):</label>
<input type="text" id="scan-path" placeholder="e.g., /mnt/unionfs/Media/TV/Series Name" title="Leave empty for full library scan">
<small>Leave empty to scan entire library</small>
</div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-play"></i> Start Scan
</button>
</form>
<div id="scan-status" class="scan-status" style="display: none;">
<div class="scan-progress">
<div class="progress-bar">
<div class="progress-fill" id="scan-progress-bar"></div>
</div>
<div class="scan-info">
<span id="scan-current-operation">Initializing...</span>
<span id="scan-progress-text">0%</span>
</div>
</div>
<button class="btn btn-secondary btn-sm" onclick="stopScanPolling()">
<i class="fas fa-times"></i> Hide Status
</button>
</div>
</div>
<div class="tool-card">
<h3><i class="fas fa-database"></i> Database Statistics</h3>
<p>View detailed database information</p>
@@ -404,6 +449,6 @@
<!-- Toast Notifications -->
<div class="toast-container" id="toast-container"></div>
<script src="/static/js/app.js"></script>
<script src="/static/js/app.js?v=manual-scan-ui"></script>
</body>
</html>
+164
View File
@@ -76,6 +76,7 @@ function initializeEventListeners() {
// Forms
document.getElementById('edit-form').addEventListener('submit', handleEditSubmit);
document.getElementById('bulk-update-form').addEventListener('submit', handleBulkUpdate);
document.getElementById('manual-scan-form').addEventListener('submit', handleManualScan);
}
// API calls
@@ -1436,6 +1437,169 @@ async function checkAuthStatus() {
}
}
// Manual Scan Functions
async function handleManualScan(event) {
event.preventDefault();
const scanType = document.getElementById('scan-type').value;
const scanMode = document.getElementById('scan-mode').value;
const scanPath = document.getElementById('scan-path').value.trim();
// Validate inputs
if (!scanType || !scanMode) {
showToast('❌ Please select scan type and mode', 'error');
return;
}
// Build query parameters
const params = new URLSearchParams({
scan_type: scanType,
scan_mode: scanMode
});
if (scanPath) {
params.append('path', scanPath);
}
try {
// Show scan status
showScanStatus();
// Start the scan
showToast('🚀 Starting manual scan...', 'info');
const response = await fetch(`/manual/scan?${params}`, {
method: 'POST',
credentials: 'include'
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
if (result.status === 'started') {
showToast('✅ Scan started successfully', 'success');
// Start polling for status
startScanPolling();
} else {
showToast(`${result.message || 'Scan completed'}`, 'info');
hideScanStatus();
}
} catch (error) {
console.error('Manual scan failed:', error);
showToast(`❌ Scan failed: ${error.message}`, 'error');
hideScanStatus();
}
}
function showScanStatus() {
const scanStatus = document.getElementById('scan-status');
const progressBar = document.getElementById('scan-progress-bar');
const operationText = document.getElementById('scan-current-operation');
const progressText = document.getElementById('scan-progress-text');
// Reset and show
progressBar.style.width = '0%';
operationText.textContent = 'Initializing scan...';
progressText.textContent = '0%';
scanStatus.style.display = 'block';
}
function hideScanStatus() {
document.getElementById('scan-status').style.display = 'none';
if (window.scanPollingInterval) {
clearInterval(window.scanPollingInterval);
window.scanPollingInterval = null;
}
}
function stopScanPolling() {
hideScanStatus();
}
function startScanPolling() {
// Poll every 2 seconds for scan status
window.scanPollingInterval = setInterval(async () => {
try {
const response = await fetch('/api/scan/status');
if (!response.ok) {
throw new Error('Failed to get scan status');
}
const status = await response.json();
updateScanProgress(status);
// Stop polling if scan is complete
if (!status.scanning) {
stopScanPolling();
showToast('✅ Scan completed!', 'success');
}
} catch (error) {
console.error('Failed to poll scan status:', error);
// Don't show error toast repeatedly, just stop polling
stopScanPolling();
}
}, 2000);
}
function updateScanProgress(status) {
const progressBar = document.getElementById('scan-progress-bar');
const operationText = document.getElementById('scan-current-operation');
const progressText = document.getElementById('scan-progress-text');
if (!status.scanning) {
progressBar.style.width = '100%';
operationText.textContent = 'Scan completed';
progressText.textContent = '100%';
return;
}
// Calculate overall progress
let totalProgress = 0;
let progressDetails = '';
if (status.scan_type === 'both' || status.scan_type === 'tv') {
const tvProgress = status.tv_series_total > 0 ?
((status.tv_series_processed + status.tv_series_skipped) / status.tv_series_total) * 50 : 0;
totalProgress += tvProgress;
if (status.tv_series_total > 0) {
progressDetails += `TV: ${status.tv_series_processed + status.tv_series_skipped}/${status.tv_series_total} `;
}
}
if (status.scan_type === 'both' || status.scan_type === 'movies') {
const movieProgress = status.movies_total > 0 ?
((status.movies_processed + status.movies_skipped) / status.movies_total) * 50 : 0;
totalProgress += movieProgress;
if (status.movies_total > 0) {
progressDetails += `Movies: ${status.movies_processed + status.movies_skipped}/${status.movies_total}`;
}
}
// For single type scans, use full 100%
if (status.scan_type !== 'both') {
totalProgress *= 2;
}
// Update progress bar
progressBar.style.width = `${Math.min(totalProgress, 100)}%`;
progressText.textContent = `${Math.round(totalProgress)}%`;
// Update operation text
if (status.current_operation) {
operationText.textContent = status.current_operation;
} else if (status.current_item) {
operationText.textContent = `Processing: ${status.current_item}`;
} else {
operationText.textContent = progressDetails || 'Scanning...';
}
}
async function logout() {
if (!confirm('Are you sure you want to logout?')) {
return;