This commit is contained in:
@@ -45,15 +45,23 @@ def map_source_to_description(source: str) -> str:
|
|||||||
elif "omdb:" in source_lower:
|
elif "omdb:" in source_lower:
|
||||||
return "OMDb Release"
|
return "OMDb Release"
|
||||||
|
|
||||||
|
# Sonarr sources
|
||||||
|
elif "sonarr:" in source_lower:
|
||||||
|
return "Sonarr API"
|
||||||
|
|
||||||
# Manual and other sources
|
# Manual and other sources
|
||||||
elif "manual" in source_lower:
|
elif "manual" in source_lower:
|
||||||
return "Manual Entry"
|
return "Manual Entry"
|
||||||
elif "digital_release" in source_lower:
|
elif "digital_release" in source_lower:
|
||||||
return "Digital Release"
|
return "Digital Release"
|
||||||
|
elif "nfo_file_existing" in source_lower:
|
||||||
|
return "NFO File (Legacy)"
|
||||||
elif "nfo:" in source_lower:
|
elif "nfo:" in source_lower:
|
||||||
return "NFO File"
|
return "NFO File"
|
||||||
elif "webhook:" in source_lower:
|
elif "webhook:" in source_lower:
|
||||||
return "Webhook/API"
|
return "Webhook/API"
|
||||||
|
elif "database" in source_lower:
|
||||||
|
return "Database"
|
||||||
|
|
||||||
# Fallback for unknown patterns
|
# Fallback for unknown patterns
|
||||||
return source.title()
|
return source.title()
|
||||||
|
|||||||
@@ -48,15 +48,23 @@ def map_source_to_description(source: str) -> str:
|
|||||||
elif "omdb:" in source_lower:
|
elif "omdb:" in source_lower:
|
||||||
return "OMDb Release"
|
return "OMDb Release"
|
||||||
|
|
||||||
|
# Sonarr sources
|
||||||
|
elif "sonarr:" in source_lower:
|
||||||
|
return "Sonarr API"
|
||||||
|
|
||||||
# Manual and other sources
|
# Manual and other sources
|
||||||
elif "manual" in source_lower:
|
elif "manual" in source_lower:
|
||||||
return "Manual Entry"
|
return "Manual Entry"
|
||||||
elif "digital_release" in source_lower:
|
elif "digital_release" in source_lower:
|
||||||
return "Digital Release"
|
return "Digital Release"
|
||||||
|
elif "nfo_file_existing" in source_lower:
|
||||||
|
return "NFO File (Legacy)"
|
||||||
elif "nfo:" in source_lower:
|
elif "nfo:" in source_lower:
|
||||||
return "NFO File"
|
return "NFO File"
|
||||||
elif "webhook:" in source_lower:
|
elif "webhook:" in source_lower:
|
||||||
return "Webhook/API"
|
return "Webhook/API"
|
||||||
|
elif "database" in source_lower:
|
||||||
|
return "Database"
|
||||||
|
|
||||||
# Fallback for unknown patterns
|
# Fallback for unknown patterns
|
||||||
return source.title()
|
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'")
|
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())
|
||||||
|
|
||||||
# Recent activity (last 7 days)
|
# Recent activity (last 7 days) - count items processed, not history events
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
SELECT COUNT(*) FROM processing_history
|
SELECT COUNT(*) FROM (
|
||||||
WHERE processed_at > NOW() - INTERVAL '7 days'
|
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())
|
recent_activity = db._get_first_value(cursor.fetchone())
|
||||||
|
|
||||||
|
|||||||
@@ -139,16 +139,30 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<table class="data-table" id="movies-table">
|
<table class="data-table sortable-table" id="movies-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Title</th>
|
<th class="sortable" onclick="sortTable('movies-tbody', 0, 'text')" style="cursor: pointer;">
|
||||||
<th>IMDb ID</th>
|
Title <i class="fas fa-sort"></i>
|
||||||
<th>Movie Released</th>
|
</th>
|
||||||
<th>Date Added to Library</th>
|
<th class="sortable" onclick="sortTable('movies-tbody', 1, 'text')" style="cursor: pointer;">
|
||||||
<th>Source</th>
|
IMDb ID <i class="fas fa-sort"></i>
|
||||||
<th>Date Type</th>
|
</th>
|
||||||
<th>Video File</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>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -196,14 +210,24 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<table class="data-table" id="series-table">
|
<table class="data-table sortable-table" id="series-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Series Title</th>
|
<th class="sortable" onclick="sortTable('series-tbody', 0, 'text')" style="cursor: pointer;">
|
||||||
<th>IMDb ID</th>
|
Series Title <i class="fas fa-sort"></i>
|
||||||
<th>Episodes</th>
|
</th>
|
||||||
<th>With Dates</th>
|
<th class="sortable" onclick="sortTable('series-tbody', 1, 'text')" style="cursor: pointer;">
|
||||||
<th>With Video</th>
|
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>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|||||||
@@ -514,17 +514,27 @@ function showEpisodesModal(data) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<table class="data-table">
|
<table class="data-table sortable-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th width="40px">
|
<th width="40px">
|
||||||
<input type="checkbox" id="select-all-checkbox" onchange="toggleSelectAll()">
|
<input type="checkbox" id="select-all-checkbox" onchange="toggleSelectAll()">
|
||||||
</th>
|
</th>
|
||||||
<th>Episode</th>
|
<th class="sortable" onclick="sortTable('episodes-table-body', 1, 'text')" style="cursor: pointer;">
|
||||||
<th>Aired</th>
|
Episode <i class="fas fa-sort"></i>
|
||||||
<th>Date Added</th>
|
</th>
|
||||||
<th>Source</th>
|
<th class="sortable" onclick="sortTable('episodes-table-body', 2, 'date')" style="cursor: pointer;">
|
||||||
<th>Video</th>
|
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>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</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