KaraokeMerge/web/templates/history.html

1047 lines
44 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Karaoke History - Web UI</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
<script>
// Fallback for Sortable.js if CDN fails
if (typeof Sortable === 'undefined') {
console.warn('Primary Sortable.js CDN failed, trying fallback...');
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.15.0/Sortable.min.js';
script.onload = function() {
console.log('Sortable.js loaded from fallback CDN');
};
script.onerror = function() {
console.error('Both Sortable.js CDNs failed to load');
alert('Warning: Sortable.js library failed to load. Drag and drop functionality will not work.');
};
document.head.appendChild(script);
}
</script>
<style>
.history-card {
border-left: 4px solid #17a2b8;
margin-bottom: 1rem;
}
.current-version {
background-color: #d4edda;
border-left: 4px solid #28a745;
}
.alternative-version {
background-color: #f8f9fa;
border-left: 4px solid #6c757d;
}
.file-type-badge {
font-size: 0.75rem;
}
.channel-badge {
font-size: 0.8rem;
}
.stats-card {
background: linear-gradient(135deg, #17a2b8 0%, #138496 100%);
color: white;
}
.file-type-card {
transition: transform 0.2s;
}
.file-type-card:hover {
transform: translateY(-2px);
}
.metric-highlight {
font-weight: bold;
color: #28a745;
}
.metric-warning {
font-weight: bold;
color: #dc3545;
}
.filter-section {
background-color: #f8f9fa;
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
}
.loading {
text-align: center;
padding: 2rem;
}
.pagination-info {
font-size: 0.9rem;
color: #6c757d;
}
.path-text {
font-family: 'Courier New', monospace;
font-size: 0.85rem;
word-break: break-all;
}
/* Drag and Drop Styles */
.sortable-list {
min-height: 50px;
}
.sortable-item {
cursor: grab;
transition: all 0.2s ease;
}
.sortable-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.sortable-item:active {
cursor: grabbing;
}
.sortable-ghost {
opacity: 0.5;
background-color: #e9ecef;
}
.sortable-chosen {
background-color: #e3f2fd;
}
/* Navigation */
.nav-link {
color: #6c757d;
}
.nav-link.active {
color: #17a2b8;
font-weight: bold;
}
/* Video Modal */
.video-modal .modal-dialog {
max-width: 90vw;
}
.video-modal .modal-body {
padding: 0;
}
.video-container {
position: relative;
width: 100%;
height: 0;
padding-bottom: 56.25%; /* 16:9 aspect ratio */
}
.video-container video {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
/* Inline Editing Styles */
.editable-field {
cursor: pointer;
border: 1px solid transparent;
padding: 2px 4px;
border-radius: 3px;
transition: all 0.2s ease;
}
.editable-field:hover {
border-color: #007bff;
background-color: #f8f9fa;
}
.editable-field.editing {
border-color: #007bff;
background-color: #fff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
.edit-input {
width: 100%;
border: 1px solid #007bff;
border-radius: 3px;
padding: 2px 4px;
font-size: inherit;
font-family: inherit;
}
.edit-buttons {
display: flex;
gap: 5px;
margin-top: 5px;
}
.edit-btn {
padding: 2px 8px;
font-size: 0.8rem;
border-radius: 3px;
}
.property-label {
font-weight: bold;
color: #495057;
font-size: 0.9rem;
}
.property-value {
font-size: 0.9rem;
}
.boolean-toggle {
cursor: pointer;
padding: 2px 8px;
border-radius: 3px;
font-size: 0.8rem;
transition: all 0.2s ease;
}
.boolean-toggle.true {
background-color: #28a745;
color: white;
}
.boolean-toggle.false {
background-color: #6c757d;
color: white;
}
.boolean-toggle:hover {
opacity: 0.8;
}
</style>
</head>
<body>
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/">
<i class="fas fa-music"></i> Karaoke Manager
</a>
<div class="navbar-nav">
<a class="nav-link" href="/">
<i class="fas fa-copy"></i> Duplicates
</a>
<a class="nav-link" href="/favorites">
<i class="fas fa-heart"></i> Favorites
</a>
<a class="nav-link active" href="/history">
<i class="fas fa-history"></i> History
</a>
<a class="nav-link" href="/remaining-songs">
<i class="fas fa-list"></i> Remaining Songs
</a>
</div>
</div>
</nav>
<div class="container-fluid mt-3">
<!-- Header -->
<div class="row mb-3">
<div class="col">
<h1><i class="fas fa-history text-info"></i> History Management</h1>
<p class="text-muted">Review and manage your song history with alternative versions</p>
</div>
</div>
<!-- Statistics Cards -->
<div class="row mb-3" id="statsRow">
<div class="col-md-3">
<div class="card stats-card">
<div class="card-body text-center">
<h5 class="card-title">Total History</h5>
<h3 id="totalHistory">-</h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stats-card">
<div class="card-body text-center">
<h5 class="card-title">With Alternatives</h5>
<h3 id="withAlternatives">-</h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stats-card">
<div class="card-body text-center">
<h5 class="card-title">MP4 Versions</h5>
<h3 id="mp4Versions">-</h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stats-card">
<div class="card-body text-center">
<h5 class="card-title">MP3 Versions</h5>
<h3 id="mp3Versions">-</h3>
</div>
</div>
</div>
</div>
<!-- Filters -->
<div class="filter-section">
<div class="row">
<div class="col-md-3">
<label for="artistFilter" class="form-label">Artist Filter</label>
<input type="text" class="form-control" id="artistFilter" placeholder="Filter by artist...">
</div>
<div class="col-md-3">
<label for="titleFilter" class="form-label">Title Filter</label>
<input type="text" class="form-control" id="titleFilter" placeholder="Filter by title...">
</div>
<div class="col-md-2">
<label for="fileTypeFilter" class="form-label">File Type</label>
<select class="form-select" id="fileTypeFilter">
<option value="">All Types</option>
<option value="MP4">MP4</option>
<option value="MP3">MP3</option>
<option value="MP3-only">MP3 Only (No MP4 Alternative)</option>
</select>
</div>
<div class="col-md-2">
<label for="channelFilter" class="form-label">Channel</label>
<input type="text" class="form-control" id="channelFilter" placeholder="Filter by channel...">
</div>
<div class="col-md-2">
<label for="minAlternatives" class="form-label">Min Alternatives</label>
<input type="number" class="form-control" id="minAlternatives" value="0" min="0">
</div>
</div>
<div class="row mt-2">
<div class="col">
<button class="btn btn-primary" onclick="applyFilters()">
<i class="fas fa-filter"></i> Apply Filters
</button>
<button class="btn btn-warning me-2" onclick="mergeHistory()" title="Merge duplicate history entries">
<i class="fas fa-compress-alt"></i> Merge Duplicates
</button>
<button class="btn btn-secondary" onclick="clearFilters()">
<i class="fas fa-times"></i> Clear Filters
</button>
</div>
</div>
</div>
<!-- Loading -->
<div id="loading" class="loading">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2">Loading history data...</p>
</div>
<!-- History List -->
<div id="historyList" style="display: none;">
<!-- History will be loaded here -->
</div>
<!-- Pagination -->
<div class="row mt-3">
<div class="col-md-6">
<div class="pagination-info" id="paginationInfo">
Showing 0 of 0 history items
</div>
</div>
<div class="col-md-6">
<nav aria-label="History pagination">
<ul class="pagination justify-content-end" id="pagination">
<!-- Pagination will be generated here -->
</ul>
</nav>
</div>
</div>
</div>
<!-- Video Modal -->
<div class="modal fade video-modal" id="videoModal" tabindex="-1">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="videoModalTitle">Video Preview</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="video-container">
<video id="videoPlayer" controls>
Your browser does not support the video tag.
</video>
</div>
</div>
</div>
</div>
</div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
let allHistory = [];
let filteredHistory = [];
let currentPage = 1;
const perPage = 20;
let pathChanges = {};
let editingField = null;
// Inline editing functions
function startEdit(index, property) {
// Cancel any existing edit
if (editingField) {
cancelEdit();
}
const field = event.target;
const currentValue = field.textContent;
const isNumber = property === 'count';
// Create input field
const input = document.createElement('input');
input.type = isNumber ? 'number' : 'text';
input.className = 'edit-input';
input.value = currentValue;
// Create buttons
const buttonContainer = document.createElement('div');
buttonContainer.className = 'edit-buttons';
buttonContainer.innerHTML = `
<button class="btn btn-sm btn-success edit-btn" onclick="saveEdit(${index}, '${property}')">
<i class="fas fa-check"></i> Save
</button>
<button class="btn btn-sm btn-secondary edit-btn" onclick="cancelEdit()">
<i class="fas fa-times"></i> Cancel
</button>
`;
// Replace field content
field.innerHTML = '';
field.appendChild(input);
field.appendChild(buttonContainer);
field.classList.add('editing');
// Focus input
input.focus();
input.select();
// Store reference
editingField = {
field: field,
input: input,
originalValue: currentValue,
index: index,
property: property
};
// Handle Enter and Escape keys
input.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
saveEdit(index, property);
} else if (e.key === 'Escape') {
cancelEdit();
}
});
}
function saveEdit(index, property) {
if (!editingField) return;
const newValue = editingField.input.value;
const originalValue = editingField.originalValue;
if (newValue === originalValue) {
cancelEdit();
return;
}
// Update via API
updateHistoryProperty(index, property, newValue);
}
function cancelEdit() {
if (!editingField) return;
const field = editingField.field;
const originalValue = editingField.originalValue;
field.innerHTML = originalValue;
field.classList.remove('editing');
editingField = null;
}
async function updateHistoryProperty(index, property, value) {
try {
const response = await fetch('/api/update-history-property', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ index, property, value })
});
const data = await response.json();
if (data.success) {
// Update local data
const historyItem = allHistory.find(h => h.index === index);
if (historyItem) {
historyItem[property] = value;
}
// Update display
if (editingField) {
editingField.field.innerHTML = escapeHtml(value);
editingField.field.classList.remove('editing');
editingField = null;
}
showSuccess(`Updated ${property} successfully`);
} else {
throw new Error(data.error || 'Failed to update property');
}
} catch (error) {
console.error('Error updating property:', error);
showError('Failed to update property: ' + error.message);
cancelEdit();
}
}
function toggleBoolean(index, property) {
const historyItem = allHistory.find(h => h.index === index);
if (!historyItem) return;
const newValue = !historyItem[property];
updateHistoryProperty(index, property, newValue);
}
async function deleteHistoryItem(index) {
const historyItem = allHistory.find(h => h.index === index);
if (!historyItem) return;
const songName = `${historyItem.artist} - ${historyItem.title}`;
if (!confirm(`Are you sure you want to delete "${songName}" from history?`)) {
return;
}
try {
const response = await fetch('/api/delete-history', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ index })
});
const data = await response.json();
if (data.success) {
// Remove from local arrays
allHistory = allHistory.filter(h => h.index !== index);
filteredHistory = filteredHistory.filter(h => h.index !== index);
// Update display
updateStats();
displayHistory();
showSuccess(data.message || 'History item deleted successfully');
} else {
throw new Error(data.error || 'Failed to delete history item');
}
} catch (error) {
console.error('Error deleting history item:', error);
showError('Failed to delete history item: ' + error.message);
}
}
// Helper functions
function getFileType(path) {
const lower = path.toLowerCase();
if (lower.endsWith('.mp4')) return 'MP4';
if (lower.endsWith('.mp3')) return 'MP3';
if (lower.endsWith('.cdg')) return 'MP3'; // Treat CDG as MP3 since they're paired
return 'Unknown';
}
function extractChannel(path) {
const lower = path.toLowerCase();
const parts = path.split('\\');
// Look for specific known channels first
const knownChannels = ['Sing King Karaoke', 'KaraFun Karaoke', 'Stingray Karaoke'];
for (const channel of knownChannels) {
if (lower.includes(channel.toLowerCase())) {
return channel;
}
}
// Look for MP4 folder structure: MP4/ChannelName/song.mp4
for (let i = 0; i < parts.length; i++) {
if (parts[i].toLowerCase() === 'mp4' && i < parts.length - 1) {
// If MP4 is found, return the next folder (the actual channel)
if (i + 1 < parts.length) {
const nextPart = parts[i + 1];
// Skip if the next part is the filename (no extension means it's a folder)
if (nextPart.indexOf('.') === -1) {
return nextPart;
} else {
return 'MP4 Root'; // File is directly in MP4 folder
}
} else {
return 'MP4 Root';
}
}
}
// Look for any folder that contains 'karaoke' (fallback)
for (const part of parts) {
if (part.toLowerCase().includes('karaoke')) {
return part;
}
}
// If no specific channel found, return the folder containing the file
if (parts.length >= 2) {
const parentFolder = parts[parts.length - 2]; // Second to last part (folder containing the file)
// If parent folder is MP4, then file is in root
if (parentFolder.toLowerCase() === 'mp4') {
return 'MP4 Root';
}
return parentFolder;
}
return 'Unknown';
}
// Load history data
async function loadHistory() {
try {
const response = await fetch('/api/history');
const data = await response.json();
if (data.success) {
allHistory = data.history;
filteredHistory = [...allHistory];
updateStats();
displayHistory();
hideLoading();
} else {
throw new Error(data.error || 'Failed to load history');
}
} catch (error) {
console.error('Error loading history:', error);
hideLoading();
showError('Failed to load history: ' + error.message);
}
}
// Update statistics
function updateStats() {
const totalHistory = allHistory.length;
const withAlternatives = allHistory.filter(h => h.matching_songs.length > 1).length;
const mp4Versions = allHistory.reduce((count, h) =>
count + h.matching_songs.filter(s => s.file_type === 'MP4').length, 0);
const mp3Versions = allHistory.reduce((count, h) =>
count + h.matching_songs.filter(s => s.file_type === 'MP3').length, 0);
document.getElementById('totalHistory').textContent = totalHistory;
document.getElementById('withAlternatives').textContent = withAlternatives;
document.getElementById('mp4Versions').textContent = mp4Versions;
document.getElementById('mp3Versions').textContent = mp3Versions;
}
// Display history
function displayHistory() {
const startIndex = (currentPage - 1) * perPage;
const endIndex = startIndex + perPage;
const pageHistory = filteredHistory.slice(startIndex, endIndex);
const container = document.getElementById('historyList');
container.innerHTML = '';
pageHistory.forEach(historyItem => {
const card = createHistoryCard(historyItem);
container.appendChild(card);
});
updatePagination();
initializeSortable();
}
// Create history card
function createHistoryCard(historyItem) {
const card = document.createElement('div');
card.className = 'card history-card';
card.innerHTML = `
<div class="card-header">
<div class="row align-items-center">
<div class="col-md-6">
<h5 class="mb-0">
<i class="fas fa-history text-info"></i>
<span class="editable-field" onclick="startEdit(${historyItem.index}, 'artist')">${escapeHtml(historyItem.artist)}</span> -
<span class="editable-field" onclick="startEdit(${historyItem.index}, 'title')">${escapeHtml(historyItem.title)}</span>
</h5>
</div>
<div class="col-md-6 text-end">
<button class="btn btn-sm btn-danger me-2" onclick="deleteHistoryItem(${historyItem.index})" title="Delete this history item">
<i class="fas fa-trash"></i> Delete
</button>
<span class="badge bg-primary">${historyItem.matching_songs.length} versions</span>
</div>
</div>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6>All Properties (click to edit):</h6>
<div class="row">
<div class="col-md-6">
<div class="mb-2">
<div class="property-label">Artist:</div>
<div class="editable-field" onclick="startEdit(${historyItem.index}, 'artist')">${escapeHtml(historyItem.artist)}</div>
</div>
<div class="mb-2">
<div class="property-label">Title:</div>
<div class="editable-field" onclick="startEdit(${historyItem.index}, 'title')">${escapeHtml(historyItem.title)}</div>
</div>
<div class="mb-2">
<div class="property-label">Count:</div>
<div class="editable-field" onclick="startEdit(${historyItem.index}, 'count')">${historyItem.count}</div>
</div>
<div class="mb-2">
<div class="property-label">Path:</div>
<div class="editable-field path-text" onclick="startEdit(${historyItem.index}, 'path')">${escapeHtml(historyItem.path)}</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-2">
<div class="property-label">Favorite:</div>
<span class="boolean-toggle ${historyItem.favorite ? 'true' : 'false'}" onclick="toggleBoolean(${historyItem.index}, 'favorite')">
${historyItem.favorite ? 'Yes' : 'No'}
</span>
</div>
<div class="mb-2">
<div class="property-label">Disabled:</div>
<span class="boolean-toggle ${historyItem.disabled ? 'true' : 'false'}" onclick="toggleBoolean(${historyItem.index}, 'disabled')">
${historyItem.disabled ? 'Yes' : 'No'}
</span>
</div>
${historyItem.key ? `
<div class="mb-2">
<div class="property-label">Key:</div>
<div class="editable-field" onclick="startEdit(${historyItem.index}, 'key')">${escapeHtml(historyItem.key)}</div>
</div>
` : `
<div class="mb-2">
<div class="property-label">Key:</div>
<div class="editable-field text-muted" onclick="startEdit(${historyItem.index}, 'key')">(click to add)</div>
</div>
`}
${historyItem.original_path ? `
<div class="mb-2">
<div class="property-label">Original Path:</div>
<div class="editable-field path-text" onclick="startEdit(${historyItem.index}, 'original_path')">${escapeHtml(historyItem.original_path)}</div>
</div>
` : `
<div class="mb-2">
<div class="property-label">Original Path:</div>
<div class="editable-field text-muted" onclick="startEdit(${historyItem.index}, 'original_path')">(click to add)</div>
</div>
`}
</div>
</div>
</div>
<div class="col-md-6">
<h6>Available Versions (drag to reorder):</h6>
<div class="sortable-list" data-history-index="${historyItem.index}">
${historyItem.matching_songs.map((song, songIndex) => `
<div class="card sortable-item ${song.is_current ? 'current-version' : 'alternative-version'} mb-2"
data-song-index="${songIndex}">
<div class="card-body py-2">
<div class="row align-items-center">
<div class="col-md-1">
<i class="fas fa-grip-vertical text-muted"></i>
</div>
<div class="col-md-2">
<span class="badge bg-${song.file_type === 'MP4' ? 'danger' : 'success'} file-type-badge">
${song.file_type}
</span>
${song.channel ? `<span class="badge bg-info channel-badge ms-1">${song.channel}</span>` : ''}
</div>
<div class="col-md-7">
<div class="path-text">${escapeHtml(song.path)}</div>
</div>
<div class="col-md-2 text-end">
${song.is_current ? '<span class="badge bg-success">Current</span>' : ''}
${song.file_type === 'MP4' ?
`<button class="btn btn-sm btn-outline-primary" onclick="playVideo('${song.path.replace(/\\/g, '\\\\')}')">
<i class="fas fa-play"></i> Play
</button>` : ''}
</div>
</div>
</div>
</div>
`).join('')}
</div>
</div>
</div>
</div>
`;
return card;
}
// Initialize sortable for drag and drop
function initializeSortable() {
document.querySelectorAll('.sortable-list').forEach(list => {
new Sortable(list, {
animation: 150,
ghostClass: 'sortable-ghost',
chosenClass: 'sortable-chosen',
onEnd: function(evt) {
const historyIndex = parseInt(evt.target.dataset.historyIndex);
const newOrder = Array.from(evt.target.children).map((item, index) => ({
songIndex: parseInt(item.dataset.songIndex),
newIndex: index
}));
// Update the history item's path to the first song in the list
const firstSong = newOrder[0];
const historyItem = allHistory.find(h => h.index === historyIndex);
if (historyItem && historyItem.matching_songs[firstSong.songIndex]) {
const newPath = historyItem.matching_songs[firstSong.songIndex].path;
updateHistoryPath(historyIndex, newPath);
}
}
});
});
}
// Update history path
async function updateHistoryPath(index, newPath) {
try {
const response = await fetch('/api/update-history-path', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ index, path: newPath })
});
const data = await response.json();
if (data.success) {
// Update local data
const historyItem = allHistory.find(h => h.index === index);
if (historyItem) {
historyItem.current_path = newPath;
// Update is_current flags
historyItem.matching_songs.forEach(song => {
song.is_current = song.path === newPath;
});
}
showSuccess('History path updated successfully');
} else {
throw new Error(data.error || 'Failed to update path');
}
} catch (error) {
console.error('Error updating history path:', error);
showError('Failed to update history path: ' + error.message);
}
}
// Apply filters
function applyFilters() {
const artistFilter = document.getElementById('artistFilter').value.toLowerCase();
const titleFilter = document.getElementById('titleFilter').value.toLowerCase();
const fileTypeFilter = document.getElementById('fileTypeFilter').value.toLowerCase();
const channelFilter = document.getElementById('channelFilter').value.toLowerCase();
const minAlternatives = parseInt(document.getElementById('minAlternatives').value) || 0;
filteredHistory = allHistory.filter(historyItem => {
// Artist filter
if (artistFilter && !historyItem.artist.toLowerCase().includes(artistFilter)) {
return false;
}
// Title filter
if (titleFilter && !historyItem.title.toLowerCase().includes(titleFilter)) {
return false;
}
// Min alternatives filter
if (historyItem.matching_songs.length < minAlternatives) {
return false;
}
// File type and channel filters
if (fileTypeFilter || channelFilter) {
let hasMatchingSong = false;
// Special handling for MP3-only filter
if (fileTypeFilter === 'mp3-only') {
// Check if current history item is MP3 and has no MP4 alternatives
const currentFileType = getFileType(historyItem.current_path);
const hasMp4Alternative = historyItem.matching_songs.some(song =>
song.file_type.toUpperCase() === 'MP4'
);
if (currentFileType.toUpperCase() === 'MP3' && !hasMp4Alternative) {
// Apply channel filter if specified
if (channelFilter) {
const currentChannel = extractChannel(historyItem.current_path);
hasMatchingSong = currentChannel.toLowerCase().includes(channelFilter);
} else {
hasMatchingSong = true;
}
}
} else if (fileTypeFilter === 'mp3') {
// Special handling for MP3 filter - show songs where primary is MP3 and has alternatives
const currentFileType = getFileType(historyItem.current_path);
// Only show if current history item is MP3 and has alternatives
if (currentFileType.toUpperCase() === 'MP3' && historyItem.matching_songs.length > 0) {
// Apply channel filter if specified
if (channelFilter) {
const currentChannel = extractChannel(historyItem.current_path);
hasMatchingSong = currentChannel.toLowerCase().includes(channelFilter);
} else {
hasMatchingSong = true;
}
}
} else {
// Regular file type and channel filtering
hasMatchingSong = historyItem.matching_songs.some(song => {
const matchesFileType = !fileTypeFilter || song.file_type.toLowerCase().includes(fileTypeFilter);
const matchesChannel = !channelFilter || (song.channel && song.channel.toLowerCase().includes(channelFilter));
return matchesFileType && matchesChannel;
});
}
if (!hasMatchingSong) {
return false;
}
}
return true;
});
currentPage = 1;
displayHistory();
}
// Clear filters
function clearFilters() {
document.getElementById('artistFilter').value = '';
document.getElementById('titleFilter').value = '';
document.getElementById('fileTypeFilter').value = '';
document.getElementById('channelFilter').value = '';
document.getElementById('minAlternatives').value = '0';
filteredHistory = [...allHistory];
currentPage = 1;
displayHistory();
}
// Update pagination
function updatePagination() {
const totalPages = Math.ceil(filteredHistory.length / perPage);
const startItem = (currentPage - 1) * perPage + 1;
const endItem = Math.min(currentPage * perPage, filteredHistory.length);
document.getElementById('paginationInfo').textContent =
`Showing ${startItem}-${endItem} of ${filteredHistory.length} history items`;
const pagination = document.getElementById('pagination');
pagination.innerHTML = '';
// Previous button
const prevLi = document.createElement('li');
prevLi.className = `page-item ${currentPage === 1 ? 'disabled' : ''}`;
prevLi.innerHTML = `<a class="page-link" href="#" onclick="changePage(${currentPage - 1})">Previous</a>`;
pagination.appendChild(prevLi);
// Page numbers
for (let i = 1; i <= totalPages; i++) {
if (i === 1 || i === totalPages || (i >= currentPage - 2 && i <= currentPage + 2)) {
const li = document.createElement('li');
li.className = `page-item ${i === currentPage ? 'active' : ''}`;
li.innerHTML = `<a class="page-link" href="#" onclick="changePage(${i})">${i}</a>`;
pagination.appendChild(li);
} else if (i === currentPage - 3 || i === currentPage + 3) {
const li = document.createElement('li');
li.className = 'page-item disabled';
li.innerHTML = '<span class="page-link">...</span>';
pagination.appendChild(li);
}
}
// Next button
const nextLi = document.createElement('li');
nextLi.className = `page-item ${currentPage === totalPages ? 'disabled' : ''}`;
nextLi.innerHTML = `<a class="page-link" href="#" onclick="changePage(${currentPage + 1})">Next</a>`;
pagination.appendChild(nextLi);
}
// Change page
function changePage(page) {
const totalPages = Math.ceil(filteredHistory.length / perPage);
if (page >= 1 && page <= totalPages) {
currentPage = page;
displayHistory();
}
}
// Play video
function playVideo(path) {
const videoPlayer = document.getElementById('videoPlayer');
const modal = new bootstrap.Modal(document.getElementById('videoModal'));
videoPlayer.src = `/api/video/${encodeURIComponent(path)}`;
document.getElementById('videoModalTitle').textContent = `Video Preview: ${path.split('\\').pop()}`;
modal.show();
}
// Merge history duplicates
async function mergeHistory() {
if (!confirm('Are you sure you want to merge duplicate history entries? This will combine entries with the same artist, title, and path, summing their count values.')) {
return;
}
try {
const response = await fetch('/api/merge-history', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (data.success) {
showSuccess(data.message);
// Reload the history data to reflect the changes
loadHistory();
} else {
throw new Error(data.error || 'Failed to merge history');
}
} catch (error) {
console.error('Error merging history:', error);
showError('Failed to merge history: ' + error.message);
}
}
// Utility functions
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function showLoading() {
document.getElementById('loading').style.display = 'block';
document.getElementById('historyList').style.display = 'none';
}
function hideLoading() {
document.getElementById('loading').style.display = 'none';
document.getElementById('historyList').style.display = 'block';
}
function showError(message) {
alert('Error: ' + message);
}
function showSuccess(message) {
// You could implement a toast notification here
console.log('Success: ' + message);
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
showLoading();
loadHistory();
});
</script>
</body>
</html>