This commit is contained in:
@@ -45,15 +45,23 @@ def map_source_to_description(source: str) -> str:
|
||||
elif "omdb:" in source_lower:
|
||||
return "OMDb Release"
|
||||
|
||||
# Sonarr sources
|
||||
elif "sonarr:" in source_lower:
|
||||
return "Sonarr API"
|
||||
|
||||
# Manual and other sources
|
||||
elif "manual" in source_lower:
|
||||
return "Manual Entry"
|
||||
elif "digital_release" in source_lower:
|
||||
return "Digital Release"
|
||||
elif "nfo_file_existing" in source_lower:
|
||||
return "NFO File (Legacy)"
|
||||
elif "nfo:" in source_lower:
|
||||
return "NFO File"
|
||||
elif "webhook:" in source_lower:
|
||||
return "Webhook/API"
|
||||
elif "database" in source_lower:
|
||||
return "Database"
|
||||
|
||||
# Fallback for unknown patterns
|
||||
return source.title()
|
||||
|
||||
@@ -48,15 +48,23 @@ def map_source_to_description(source: str) -> str:
|
||||
elif "omdb:" in source_lower:
|
||||
return "OMDb Release"
|
||||
|
||||
# Sonarr sources
|
||||
elif "sonarr:" in source_lower:
|
||||
return "Sonarr API"
|
||||
|
||||
# Manual and other sources
|
||||
elif "manual" in source_lower:
|
||||
return "Manual Entry"
|
||||
elif "digital_release" in source_lower:
|
||||
return "Digital Release"
|
||||
elif "nfo_file_existing" in source_lower:
|
||||
return "NFO File (Legacy)"
|
||||
elif "nfo:" in source_lower:
|
||||
return "NFO File"
|
||||
elif "webhook:" in source_lower:
|
||||
return "Webhook/API"
|
||||
elif "database" in source_lower:
|
||||
return "Database"
|
||||
|
||||
# Fallback for unknown patterns
|
||||
return source.title()
|
||||
@@ -469,10 +477,17 @@ async def get_dashboard_stats(dependencies: dict):
|
||||
cursor.execute("SELECT COUNT(*) FROM episodes WHERE source = 'no_valid_date_source'")
|
||||
episodes_no_valid_source = db._get_first_value(cursor.fetchone())
|
||||
|
||||
# Recent activity (last 7 days)
|
||||
# Recent activity (last 7 days) - count items processed, not history events
|
||||
cursor.execute("""
|
||||
SELECT COUNT(*) FROM processing_history
|
||||
WHERE processed_at > NOW() - INTERVAL '7 days'
|
||||
SELECT COUNT(*) FROM (
|
||||
SELECT imdb_id FROM movies
|
||||
WHERE created_at > NOW() - INTERVAL '7 days'
|
||||
OR updated_at > NOW() - INTERVAL '7 days'
|
||||
UNION
|
||||
SELECT DISTINCT imdb_id FROM episodes
|
||||
WHERE created_at > NOW() - INTERVAL '7 days'
|
||||
OR updated_at > NOW() - INTERVAL '7 days'
|
||||
) AS recent_items
|
||||
""")
|
||||
recent_activity = db._get_first_value(cursor.fetchone())
|
||||
|
||||
|
||||
@@ -139,16 +139,30 @@
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<table class="data-table" id="movies-table">
|
||||
<table class="data-table sortable-table" id="movies-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>IMDb ID</th>
|
||||
<th>Movie Released</th>
|
||||
<th>Date Added to Library</th>
|
||||
<th>Source</th>
|
||||
<th>Date Type</th>
|
||||
<th>Video File</th>
|
||||
<th class="sortable" onclick="sortTable('movies-tbody', 0, 'text')" style="cursor: pointer;">
|
||||
Title <i class="fas fa-sort"></i>
|
||||
</th>
|
||||
<th class="sortable" onclick="sortTable('movies-tbody', 1, 'text')" style="cursor: pointer;">
|
||||
IMDb ID <i class="fas fa-sort"></i>
|
||||
</th>
|
||||
<th class="sortable" onclick="sortTable('movies-tbody', 2, 'date')" style="cursor: pointer;">
|
||||
Movie Released <i class="fas fa-sort"></i>
|
||||
</th>
|
||||
<th class="sortable" onclick="sortTable('movies-tbody', 3, 'date')" style="cursor: pointer;">
|
||||
Date Added to Library <i class="fas fa-sort"></i>
|
||||
</th>
|
||||
<th class="sortable" onclick="sortTable('movies-tbody', 4, 'text')" style="cursor: pointer;">
|
||||
Source <i class="fas fa-sort"></i>
|
||||
</th>
|
||||
<th class="sortable" onclick="sortTable('movies-tbody', 5, 'text')" style="cursor: pointer;">
|
||||
Date Type <i class="fas fa-sort"></i>
|
||||
</th>
|
||||
<th class="sortable" onclick="sortTable('movies-tbody', 6, 'text')" style="cursor: pointer;">
|
||||
Video File <i class="fas fa-sort"></i>
|
||||
</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -196,14 +210,24 @@
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<table class="data-table" id="series-table">
|
||||
<table class="data-table sortable-table" id="series-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Series Title</th>
|
||||
<th>IMDb ID</th>
|
||||
<th>Episodes</th>
|
||||
<th>With Dates</th>
|
||||
<th>With Video</th>
|
||||
<th class="sortable" onclick="sortTable('series-tbody', 0, 'text')" style="cursor: pointer;">
|
||||
Series Title <i class="fas fa-sort"></i>
|
||||
</th>
|
||||
<th class="sortable" onclick="sortTable('series-tbody', 1, 'text')" style="cursor: pointer;">
|
||||
IMDb ID <i class="fas fa-sort"></i>
|
||||
</th>
|
||||
<th class="sortable" onclick="sortTable('series-tbody', 2, 'number')" style="cursor: pointer;">
|
||||
Episodes <i class="fas fa-sort"></i>
|
||||
</th>
|
||||
<th class="sortable" onclick="sortTable('series-tbody', 3, 'number')" style="cursor: pointer;">
|
||||
With Dates <i class="fas fa-sort"></i>
|
||||
</th>
|
||||
<th class="sortable" onclick="sortTable('series-tbody', 4, 'number')" style="cursor: pointer;">
|
||||
With Video <i class="fas fa-sort"></i>
|
||||
</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -514,17 +514,27 @@ function showEpisodesModal(data) {
|
||||
</button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table class="data-table">
|
||||
<table class="data-table sortable-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="40px">
|
||||
<input type="checkbox" id="select-all-checkbox" onchange="toggleSelectAll()">
|
||||
</th>
|
||||
<th>Episode</th>
|
||||
<th>Aired</th>
|
||||
<th>Date Added</th>
|
||||
<th>Source</th>
|
||||
<th>Video</th>
|
||||
<th class="sortable" onclick="sortTable('episodes-table-body', 1, 'text')" style="cursor: pointer;">
|
||||
Episode <i class="fas fa-sort"></i>
|
||||
</th>
|
||||
<th class="sortable" onclick="sortTable('episodes-table-body', 2, 'date')" style="cursor: pointer;">
|
||||
Aired <i class="fas fa-sort"></i>
|
||||
</th>
|
||||
<th class="sortable" onclick="sortTable('episodes-table-body', 3, 'date')" style="cursor: pointer;">
|
||||
Date Added <i class="fas fa-sort"></i>
|
||||
</th>
|
||||
<th class="sortable" onclick="sortTable('episodes-table-body', 4, 'text')" style="cursor: pointer;">
|
||||
Source <i class="fas fa-sort"></i>
|
||||
</th>
|
||||
<th class="sortable" onclick="sortTable('episodes-table-body', 5, 'text')" style="cursor: pointer;">
|
||||
Video <i class="fas fa-sort"></i>
|
||||
</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -1952,3 +1962,101 @@ function updatePopulateProgress(status) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Table Sorting Functions ====================
|
||||
|
||||
let sortDirections = {}; // Track sort direction for each table column
|
||||
|
||||
function sortTable(tableBodyId, columnIndex, dataType) {
|
||||
const tbody = document.getElementById(tableBodyId);
|
||||
if (!tbody) return;
|
||||
|
||||
const sortKey = `${tableBodyId}-${columnIndex}`;
|
||||
|
||||
// Toggle sort direction
|
||||
if (!sortDirections[sortKey]) {
|
||||
sortDirections[sortKey] = 'asc';
|
||||
} else {
|
||||
sortDirections[sortKey] = sortDirections[sortKey] === 'asc' ? 'desc' : 'asc';
|
||||
}
|
||||
|
||||
const rows = Array.from(tbody.querySelectorAll('tr'));
|
||||
|
||||
rows.sort((a, b) => {
|
||||
const aCell = a.cells[columnIndex];
|
||||
const bCell = b.cells[columnIndex];
|
||||
|
||||
if (!aCell || !bCell) return 0;
|
||||
|
||||
let aValue = aCell.textContent.trim();
|
||||
let bValue = bCell.textContent.trim();
|
||||
|
||||
// Handle MISSING values - always sort to bottom
|
||||
if (aValue === 'MISSING') return 1;
|
||||
if (bValue === 'MISSING') return -1;
|
||||
|
||||
// Handle different data types
|
||||
if (dataType === 'date') {
|
||||
// Parse dates for comparison
|
||||
aValue = aValue === '-' ? '' : aValue;
|
||||
bValue = bValue === '-' ? '' : bValue;
|
||||
|
||||
if (!aValue && !bValue) return 0;
|
||||
if (!aValue) return 1;
|
||||
if (!bValue) return -1;
|
||||
|
||||
const aDate = new Date(aValue);
|
||||
const bDate = new Date(bValue);
|
||||
|
||||
if (sortDirections[sortKey] === 'asc') {
|
||||
return aDate - bDate;
|
||||
} else {
|
||||
return bDate - aDate;
|
||||
}
|
||||
} else if (dataType === 'number') {
|
||||
const aNum = parseFloat(aValue) || 0;
|
||||
const bNum = parseFloat(bValue) || 0;
|
||||
|
||||
if (sortDirections[sortKey] === 'asc') {
|
||||
return aNum - bNum;
|
||||
} else {
|
||||
return bNum - aNum;
|
||||
}
|
||||
} else {
|
||||
// Text comparison
|
||||
if (sortDirections[sortKey] === 'asc') {
|
||||
return aValue.localeCompare(bValue);
|
||||
} else {
|
||||
return bValue.localeCompare(aValue);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Re-append sorted rows
|
||||
rows.forEach(row => tbody.appendChild(row));
|
||||
|
||||
// Update sort icons
|
||||
updateSortIcons(tableBodyId, columnIndex, sortDirections[sortKey]);
|
||||
}
|
||||
|
||||
function updateSortIcons(tableBodyId, activeColumn, direction) {
|
||||
// Find the table and update icons
|
||||
const tbody = document.getElementById(tableBodyId);
|
||||
if (!tbody) return;
|
||||
|
||||
const table = tbody.closest('table');
|
||||
if (!table) return;
|
||||
|
||||
const headers = table.querySelectorAll('th.sortable');
|
||||
headers.forEach((header, index) => {
|
||||
const icon = header.querySelector('i');
|
||||
if (!icon) return;
|
||||
|
||||
// +1 to account for checkbox column
|
||||
if (index + 1 === activeColumn) {
|
||||
icon.className = direction === 'asc' ? 'fas fa-sort-up' : 'fas fa-sort-down';
|
||||
} else {
|
||||
icon.className = 'fas fa-sort';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user