Compare commits

...

3 Commits

7 changed files with 789 additions and 7 deletions

17
PRD.md
View File

@ -48,16 +48,26 @@ These principles are fundamental to the project's long-term success and must be
- **PRD.md Updates:** Any changes to project requirements, architecture, or functionality must be reflected in this document
- **README.md Updates:** User-facing features, installation instructions, or usage changes must be documented
- **CLI Commands Documentation:** All CLI functionality, options, and usage examples must be documented in `cli/commands.txt`
- **Code Comments:** Significant logic changes should include inline documentation
- **API Documentation:** New endpoints, functions, or interfaces must be documented
**Documentation Update Checklist:**
- [ ] Update PRD.md with any architectural or requirement changes
- [ ] Update README.md with new features, installation steps, or usage instructions
- [ ] Update `cli/commands.txt` with any new CLI options, examples, or functionality changes
- [ ] Add inline comments for complex logic or business rules
- [ ] Update any configuration examples or file structure documentation
- [ ] Review and update implementation status sections
**CLI Commands Documentation Requirements:**
- **Comprehensive Coverage:** All CLI arguments, options, and flags must be documented with examples
- **Usage Examples:** Provide practical examples for common use cases and combinations
- **Configuration Details:** Document all configuration options and their effects
- **Error Handling:** Include troubleshooting information and common issues
- **Integration Notes:** Document how CLI integrates with web UI and other components
- **Version Tracking:** Keep version information and feature status up to date
This documentation requirement is mandatory and ensures the project remains maintainable and accessible to future developers and users.
### 2.3 Code Quality & Development Standards
@ -230,7 +240,8 @@ KaraokeMerge/
│ ├── matching.py # Song matching logic
│ ├── report.py # Report generation
│ ├── preferences.py # Priority preferences management
│ └── utils.py # Utility functions
│ ├── utils.py # Utility functions
│ └── commands.txt # Comprehensive CLI commands reference
├── web/ # Web UI for manual review
│ ├── app.py # Flask web application
│ └── templates/
@ -255,6 +266,7 @@ KaraokeMerge/
- **Search & Filter**: Real-time search across artists, titles, and paths
- **Responsive Design**: Mobile-friendly interface
- **Easy Startup**: Automated dependency checking and browser launch
- **Remaining Songs View**: Separate page to browse all songs that remain after cleanup
#### **Media Preview & Playback**
- **Video Playback**: Direct MP4 video playback in modal popup for previewing karaoke videos
@ -308,6 +320,7 @@ KaraokeMerge/
#### **Key Components**
- **`web/app.py`**: Flask application with API endpoints
- **`web/templates/index.html`**: Main web interface template
- **`web/templates/remaining_songs.html`**: Remaining songs browsing interface
- **`start_web_ui.py`**: Startup script with dependency management
### 7.3 API Endpoints
@ -317,6 +330,7 @@ KaraokeMerge/
- **`/api/stats`**: Get statistical analysis of the song collection
- **`/api/artists`**: Get list of artists for filtering
- **`/api/mp3-songs`**: Get MP3 songs that remain after cleanup
- **`/api/remaining-songs`**: Get all remaining songs with pagination and filtering
- **`/api/config`**: Get current configuration settings
#### **Priority Management Endpoints**
@ -412,6 +426,7 @@ data/preferences/
- [x] Create responsive design with Bootstrap 5
- [x] Add file path normalization for corrupted paths
- [x] Implement change tracking and save button management
- [x] Create remaining songs browsing page with filtering and video preview
#### **Advanced Features**
- [x] Multi-format support (MP3, CDG, MP4)

261
cli/commands.txt Normal file
View File

@ -0,0 +1,261 @@
# Karaoke Song Library Cleanup Tool - CLI Commands Reference
## Overview
The CLI tool analyzes karaoke song collections, identifies duplicates, and generates skip lists for future imports. It supports multiple file formats (MP3, CDG, MP4) with configurable priority systems.
## Basic Usage
### Standard Analysis
```bash
python cli/main.py
```
Runs the tool with default settings:
- Input: `data/allSongs.json`
- Config: `config/config.json`
- Output: `data/skipSongs.json`
- Verbose: Disabled
- Reports: **Automatically generated** (including web UI data)
### Verbose Output
```bash
python cli/main.py --verbose
# or
python cli/main.py -v
```
Enables detailed output showing:
- Individual song processing
- Duplicate detection details
- File type analysis
- Channel priority decisions
### Dry Run Mode
```bash
python cli/main.py --dry-run
```
Analyzes songs without generating the skip list file. Useful for:
- Testing configuration changes
- Previewing results before committing
- Validating input data
## Configuration Options
### Custom Configuration File
```bash
python cli/main.py --config path/to/custom_config.json
```
Uses a custom configuration file instead of the default `config/config.json`.
### Show Current Configuration
```bash
python cli/main.py --show-config
```
Displays the current configuration settings and exits. Useful for:
- Verifying configuration values
- Debugging configuration issues
- Understanding current settings
## Input/Output Options
### Custom Input File
```bash
python cli/main.py --input path/to/songs.json
```
Specifies a custom input file instead of the default `data/allSongs.json`.
### Custom Output Directory
```bash
python cli/main.py --output-dir ./custom_output
```
Saves output files to a custom directory instead of the default `data/` folder.
## Report Generation
### Detailed Reports (Always Generated)
Reports are now **automatically generated** every time you run the CLI tool. The `--save-reports` flag is kept for backward compatibility but is no longer required.
Generated reports include:
- `enhanced_summary_report.txt` - Comprehensive analysis
- `channel_optimization_report.txt` - Priority optimization suggestions
- `duplicate_pattern_report.txt` - Duplicate pattern analysis
- `actionable_insights_report.txt` - Recommendations and insights
- `detailed_duplicate_analysis.txt` - Specific songs and their duplicates
- `analysis_data.json` - Raw analysis data for further processing
- `skip_songs_detailed.json` - **Web UI data (always generated)**
## Combined Examples
### Full Analysis with Reports
```bash
python cli/main.py --verbose
```
Runs complete analysis with:
- Verbose output for detailed processing information
- **Automatic comprehensive report generation**
- Skip list creation
### Custom Configuration with Dry Run
```bash
python cli/main.py --config custom_config.json --dry-run --verbose
```
Tests a custom configuration without generating files:
- Uses custom configuration
- Shows detailed processing
- No output files created
### Custom Input/Output with Reports
```bash
python cli/main.py --input /path/to/songs.json --output-dir ./reports
```
Processes custom input and saves all outputs to reports directory:
- Custom input file
- Custom output location
- **All report files automatically generated**
### Minimal Output
```bash
python cli/main.py --output-dir ./minimal
```
Runs with minimal output:
- No verbose logging
- No detailed reports
- Only generates skip list
## Configuration File Structure
The default configuration file (`config/config.json`) contains:
```json
{
"channel_priorities": [
"Sing King Karaoke",
"KaraFun Karaoke",
"Stingray Karaoke"
],
"matching": {
"fuzzy_matching": false,
"fuzzy_threshold": 0.85,
"case_sensitive": false
},
"output": {
"verbose": false,
"include_reasons": true,
"max_duplicates_per_song": 10
},
"file_types": {
"supported_extensions": [".mp3", ".cdg", ".mp4"],
"mp4_extensions": [".mp4"]
}
}
```
### Configuration Options Explained
#### Channel Priorities
- **channel_priorities**: Array of folder names for MP4 files
- Order determines priority (first = highest priority)
- Files without matching folders are marked for manual review
#### Matching Settings
- **fuzzy_matching**: Enable/disable fuzzy string matching
- **fuzzy_threshold**: Similarity threshold (0.0-1.0) for fuzzy matching
- **case_sensitive**: Case-sensitive artist/title comparison
#### Output Settings
- **verbose**: Enable detailed output
- **include_reasons**: Include reason field in skip list
- **max_duplicates_per_song**: Maximum duplicates to process per song
#### File Type Settings
- **supported_extensions**: All supported file extensions
- **mp4_extensions**: Extensions treated as MP4 files
## Input File Format
The tool expects a JSON array of song objects:
```json
[
{
"artist": "Artist Name",
"title": "Song Title",
"path": "path/to/file.mp3"
}
]
```
Optional fields for MP4 files:
- `channel`: Channel/folder information
- ID3 tag information (artist, title, etc.)
## Output Files
### Primary Output
- **skipSongs.json**: List of file paths to skip in future imports
- Format: `[{"path": "file/path.mp3", "reason": "duplicate"}]`
### Report Files (with --save-reports)
- **enhanced_summary_report.txt**: Overall analysis and statistics
- **channel_optimization_report.txt**: Channel priority suggestions
- **duplicate_pattern_report.txt**: Duplicate detection patterns
- **actionable_insights_report.txt**: Recommendations for collection management
- **detailed_duplicate_analysis.txt**: Specific duplicate groups
- **analysis_data.json**: Raw data for further processing
- **skip_songs_detailed.json**: Complete skip list with metadata
## File Type Priority System
The tool processes files in this priority order:
1. **MP4 files** (with channel priority sorting)
2. **CDG/MP3 pairs** (treated as single units)
3. **Standalone MP3** files
4. **Standalone CDG** files
## Error Handling
The tool provides clear error messages for:
- Missing input files
- Invalid JSON format
- Configuration errors
- File permission issues
- Processing errors
## Performance Notes
- Successfully tested with 37,000+ songs
- Processes large datasets efficiently
- Shows progress indicators for long operations
- Memory-efficient processing
## Integration with Web UI
The CLI tool integrates with the web UI:
- Web UI can load CLI-generated data
- Priority preferences from web UI are used by CLI
- Shared configuration and data files
- Consistent processing logic
## Troubleshooting
### Common Issues
1. **File not found**: Check input file path and permissions
2. **JSON errors**: Validate input file format
3. **Configuration errors**: Use --show-config to verify settings
4. **Permission errors**: Check output directory permissions
### Debug Mode
```bash
python cli/main.py --verbose --dry-run --show-config
```
Complete debugging setup:
- Shows configuration
- Verbose processing
- No file changes
## Version Information
This commands reference is for Karaoke Song Library Cleanup Tool v2.0
- CLI: Fully functional with comprehensive options
- Web UI: Interactive priority management
- Priority System: Drag-and-drop with persistence
- Reports: Enhanced analysis with actionable insights

View File

@ -22,11 +22,11 @@ def parse_arguments():
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python main.py # Run with default settings
python main.py # Run with default settings (generates reports automatically)
python main.py --verbose # Enable verbose output
python main.py --config custom_config.json # Use custom config
python main.py --output-dir ./reports # Save reports to custom directory
python main.py --dry-run # Analyze without generating skip list
python main.py --dry-run # Analyze without generating files
"""
)
@ -63,7 +63,7 @@ Examples:
parser.add_argument(
'--save-reports',
action='store_true',
help='Save detailed reports to files'
help='Save detailed reports to files (now always enabled by default)'
)
parser.add_argument(
@ -123,6 +123,7 @@ def main():
songs = load_songs(args.input)
# Initialize components
data_dir = args.output_dir
matcher = SongMatcher(config, data_dir)
reporter = ReportGenerator(config)
@ -176,8 +177,8 @@ def main():
elif args.dry_run:
print("\nDRY RUN MODE: No skip list generated")
# Save detailed reports if requested
if args.save_reports:
# Always generate detailed reports (not just when --save-reports is used)
if not args.dry_run:
reports_dir = os.path.join(args.output_dir, 'reports')
os.makedirs(reports_dir, exist_ok=True)
@ -228,7 +229,7 @@ def main():
}
save_json_file(analysis_data, os.path.join(reports_dir, 'analysis_data.json'))
# Save full skip list data
# Save full skip list data (this is what the web UI needs)
save_json_file(skip_songs, os.path.join(reports_dir, 'skip_songs_detailed.json'))
print(f"✅ Enhanced reports saved to: {reports_dir}")
@ -239,6 +240,9 @@ def main():
print(f" • actionable_insights_report.txt - Recommendations and insights")
print(f" • detailed_duplicate_analysis.txt - Specific songs and their duplicates")
print(f" • analysis_data.json - Raw analysis data for further processing")
print(f" • skip_songs_detailed.json - Web UI data (always generated)")
elif args.dry_run:
print("\nDRY RUN MODE: No reports generated")
print("\n" + "=" * 60)
print("Analysis complete!")

View File

@ -191,6 +191,11 @@ def index():
"""Main dashboard page."""
return render_template('index.html')
@app.route('/remaining-songs')
def remaining_songs():
"""Page showing remaining songs after cleanup."""
return render_template('remaining_songs.html')
@app.route('/api/duplicates')
def get_duplicates():
"""API endpoint to get duplicate data."""
@ -435,6 +440,87 @@ def get_mp3_songs():
return jsonify(mp3_song_list)
@app.route('/api/remaining-songs')
def get_remaining_songs():
"""Get all remaining songs (MP4 and MP3) after cleanup with pagination."""
try:
all_songs = load_json_file(os.path.join(DATA_DIR, 'allSongs.json'))
skip_songs = load_json_file(os.path.join(DATA_DIR, 'reports', 'skip_songs_detailed.json'))
if not all_songs:
return jsonify({'error': 'No all songs data found'}), 404
if not skip_songs:
skip_songs = []
# Get pagination parameters
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 50, type=int)
search = request.args.get('search', '').lower()
file_type_filter = request.args.get('file_type', 'all')
artist_filter = request.args.get('artist', '')
# Create a set of paths that are being skipped
skip_paths = {song['path'] for song in skip_songs}
# Filter for songs that are NOT being skipped
remaining_songs = []
for song in all_songs:
path = song.get('path', '')
if path not in skip_paths:
# Apply file type filter
if file_type_filter != 'all':
if file_type_filter == 'mp4' and not path.lower().endswith('.mp4'):
continue
elif file_type_filter == 'mp3' and not path.lower().endswith(('.mp3', '.cdg')):
continue
# Apply search filter
if search:
title = song.get('title', '').lower()
artist = song.get('artist', '').lower()
if search not in title and search not in artist:
continue
# Apply artist filter
if artist_filter:
artist = song.get('artist', '').lower()
if artist_filter.lower() not in artist:
continue
remaining_songs.append({
'title': song.get('title', 'Unknown'),
'artist': song.get('artist', 'Unknown'),
'path': song.get('path', ''),
'file_type': get_file_type(song.get('path', '')),
'channel': extract_channel(song.get('path', ''))
})
# Sort by artist, then by title
remaining_songs.sort(key=lambda x: (x['artist'].lower(), x['title'].lower()))
# Calculate pagination
total_songs = len(remaining_songs)
total_pages = (total_songs + per_page - 1) // per_page
start_idx = (page - 1) * per_page
end_idx = start_idx + per_page
# Get songs for current page
page_songs = remaining_songs[start_idx:end_idx]
return jsonify({
'songs': page_songs,
'pagination': {
'current_page': page,
'per_page': per_page,
'total_songs': total_songs,
'total_pages': total_pages
}
})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/download/mp3-songs')
def download_mp3_songs():

View File

@ -478,6 +478,20 @@
</small>
</div>
</div>
<div class="row mt-3">
<div class="col-md-3">
<a href="/remaining-songs" class="btn btn-outline-success w-100">
<i class="fas fa-eye"></i> View Remaining Songs
</a>
</div>
<div class="col-md-9">
<small class="text-muted">
<i class="fas fa-info-circle"></i>
Browse all remaining songs (MP4 and MP3) after cleanup with filtering,
search, and video preview capabilities.
</small>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,402 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Remaining Songs After Cleanup - Karaoke Library</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">
<style>
.song-card {
border-left: 4px solid #28a745;
margin-bottom: 1rem;
transition: all 0.2s ease;
}
.song-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.file-type-badge {
font-size: 0.75rem;
}
.channel-badge {
font-size: 0.8rem;
}
.stats-card {
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
color: white;
}
.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;
}
.back-button {
margin-bottom: 1rem;
}
</style>
</head>
<body>
<div class="container-fluid">
<!-- Header -->
<div class="row mt-3">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<div>
<a href="/" class="btn btn-outline-secondary back-button">
<i class="fas fa-arrow-left"></i> Back to Duplicates
</a>
<h1 class="d-inline-block ms-3">Remaining Songs After Cleanup</h1>
</div>
<div class="text-end">
<small class="text-muted">All songs that remain after duplicate removal</small>
</div>
</div>
</div>
</div>
<!-- Stats Cards -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card stats-card">
<div class="card-body text-center">
<h3 id="totalSongs">-</h3>
<p class="mb-0">Total Songs</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stats-card">
<div class="card-body text-center">
<h3 id="mp4Count">-</h3>
<p class="mb-0">MP4 Videos</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stats-card">
<div class="card-body text-center">
<h3 id="mp3Count">-</h3>
<p class="mb-0">MP3/CDG Files</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stats-card">
<div class="card-body text-center">
<h3 id="uniqueArtists">-</h3>
<p class="mb-0">Unique Artists</p>
</div>
</div>
</div>
</div>
<!-- Filters -->
<div class="filter-section">
<div class="row">
<div class="col-md-4">
<label for="searchInput" class="form-label">Search</label>
<input type="text" class="form-control" id="searchInput" placeholder="Search by artist or title...">
</div>
<div class="col-md-3">
<label for="fileTypeFilter" class="form-label">File Type</label>
<select class="form-select" id="fileTypeFilter">
<option value="all">All Types</option>
<option value="mp4">MP4 Videos</option>
<option value="mp3">MP3/CDG Files</option>
</select>
</div>
<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-2">
<label for="perPageSelect" class="form-label">Per Page</label>
<select class="form-select" id="perPageSelect">
<option value="25">25</option>
<option value="50" selected>50</option>
<option value="100">100</option>
<option value="200">200</option>
</select>
</div>
</div>
</div>
<!-- Songs List -->
<div id="songsContainer">
<div class="loading">
<i class="fas fa-spinner fa-spin fa-2x"></i>
<p class="mt-2">Loading remaining songs...</p>
</div>
</div>
<!-- Pagination -->
<div class="row mt-4">
<div class="col-12">
<nav aria-label="Songs pagination">
<div class="d-flex justify-content-between align-items-center">
<div class="pagination-info">
Showing <span id="showingInfo">-</span> of <span id="totalInfo">-</span> songs
</div>
<ul class="pagination mb-0" id="pagination">
<!-- Pagination will be generated here -->
</ul>
</div>
</nav>
</div>
</div>
</div>
<!-- Video Modal -->
<div class="modal fade" 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">
<video id="videoPlayer" class="w-100" controls>
Your browser does not support the video tag.
</video>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
let currentPage = 1;
let currentFilters = {
search: '',
file_type: 'all',
artist: '',
per_page: 50
};
// Initialize the page
document.addEventListener('DOMContentLoaded', function() {
loadSongs();
setupEventListeners();
});
function setupEventListeners() {
// Search input
const searchInput = document.getElementById('searchInput');
let searchTimeout;
searchInput.addEventListener('input', function() {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
currentFilters.search = this.value;
currentPage = 1;
loadSongs();
}, 300);
});
// File type filter
document.getElementById('fileTypeFilter').addEventListener('change', function() {
currentFilters.file_type = this.value;
currentPage = 1;
loadSongs();
});
// Artist filter
const artistFilter = document.getElementById('artistFilter');
let artistTimeout;
artistFilter.addEventListener('input', function() {
clearTimeout(artistTimeout);
artistTimeout = setTimeout(() => {
currentFilters.artist = this.value;
currentPage = 1;
loadSongs();
}, 300);
});
// Per page selector
document.getElementById('perPageSelect').addEventListener('change', function() {
currentFilters.per_page = parseInt(this.value);
currentPage = 1;
loadSongs();
});
}
async function loadSongs() {
const container = document.getElementById('songsContainer');
container.innerHTML = '<div class="loading"><i class="fas fa-spinner fa-spin fa-2x"></i><p class="mt-2">Loading songs...</p></div>';
try {
const params = new URLSearchParams({
page: currentPage,
per_page: currentFilters.per_page,
search: currentFilters.search,
file_type: currentFilters.file_type,
artist: currentFilters.artist
});
const response = await fetch(`/api/remaining-songs?${params}`);
const data = await response.json();
if (response.ok) {
displaySongs(data.songs, data.pagination);
updateStats(data.songs, data.pagination);
} else {
container.innerHTML = `<div class="alert alert-danger">Error: ${data.error}</div>`;
}
} catch (error) {
console.error('Error loading songs:', error);
container.innerHTML = '<div class="alert alert-danger">Error loading songs. Please try again.</div>';
}
}
function displaySongs(songs, pagination) {
const container = document.getElementById('songsContainer');
if (songs.length === 0) {
container.innerHTML = '<div class="alert alert-info">No songs found matching your filters.</div>';
return;
}
const songsHtml = songs.map(song => `
<div class="card song-card">
<div class="card-body">
<div class="row align-items-center">
<div class="col-md-8">
<h5 class="card-title mb-1">${escapeHtml(song.title)}</h5>
<p class="card-text text-muted mb-2">${escapeHtml(song.artist)}</p>
<div class="path-text">${escapeHtml(song.path)}</div>
</div>
<div class="col-md-4 text-end">
<span class="badge bg-primary file-type-badge me-2">${song.file_type}</span>
${song.channel ? `<span class="badge bg-info channel-badge me-2">${escapeHtml(song.channel)}</span>` : ''}
${song.file_type === 'MP4' ?
`<button class="btn btn-sm btn-outline-primary" onclick="openVideoPlayer('${escapeHtml(song.path)}', '${escapeHtml(song.artist)}', '${escapeHtml(song.title)}')">
<i class="fas fa-play"></i> Play
</button>` :
''
}
</div>
</div>
</div>
</div>
`).join('');
container.innerHTML = songsHtml;
updatePagination(pagination);
}
function updateStats(songs, pagination) {
// Update total songs
document.getElementById('totalSongs').textContent = pagination.total_songs;
// Count file types
const mp4Count = songs.filter(song => song.file_type === 'MP4').length;
const mp3Count = songs.filter(song => song.file_type === 'MP3').length;
// Get unique artists from current page (this is a simplified count)
const uniqueArtists = new Set(songs.map(song => song.artist)).size;
document.getElementById('mp4Count').textContent = mp4Count;
document.getElementById('mp3Count').textContent = mp3Count;
document.getElementById('uniqueArtists').textContent = uniqueArtists;
}
function updatePagination(pagination) {
const paginationContainer = document.getElementById('pagination');
const showingInfo = document.getElementById('showingInfo');
const totalInfo = document.getElementById('totalInfo');
// Update info text
const start = (pagination.current_page - 1) * pagination.per_page + 1;
const end = Math.min(start + pagination.per_page - 1, pagination.total_songs);
showingInfo.textContent = `${start}-${end}`;
totalInfo.textContent = pagination.total_songs;
// Generate pagination buttons
let paginationHtml = '';
// Previous button
paginationHtml += `
<li class="page-item ${pagination.current_page === 1 ? 'disabled' : ''}">
<a class="page-link" href="#" onclick="changePage(${pagination.current_page - 1})">Previous</a>
</li>
`;
// Page numbers
const startPage = Math.max(1, pagination.current_page - 2);
const endPage = Math.min(pagination.total_pages, pagination.current_page + 2);
for (let i = startPage; i <= endPage; i++) {
paginationHtml += `
<li class="page-item ${i === pagination.current_page ? 'active' : ''}">
<a class="page-link" href="#" onclick="changePage(${i})">${i}</a>
</li>
`;
}
// Next button
paginationHtml += `
<li class="page-item ${pagination.current_page === pagination.total_pages ? 'disabled' : ''}">
<a class="page-link" href="#" onclick="changePage(${pagination.current_page + 1})">Next</a>
</li>
`;
paginationContainer.innerHTML = paginationHtml;
}
function changePage(page) {
currentPage = page;
loadSongs();
}
function openVideoPlayer(filePath, artist, title) {
const modal = new bootstrap.Modal(document.getElementById('videoModal'));
const videoPlayer = document.getElementById('videoPlayer');
const modalTitle = document.getElementById('videoModalTitle');
modalTitle.textContent = `${artist} - ${title}`;
// Normalize the path for video playback
const normalizedPath = normalizePath(filePath);
const encodedPath = encodeURIComponent(normalizedPath);
const videoUrl = `/api/video/${encodedPath}`;
videoPlayer.src = videoUrl;
modal.show();
// Handle video errors
videoPlayer.onerror = function() {
console.error('Error loading video:', videoUrl);
alert(`Error loading video: ${filePath}`);
};
}
function normalizePath(filePath) {
// Simple path normalization - just handle :// corruption
if (filePath.includes('://')) {
return filePath.replace('://', ':\\');
}
return filePath;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
</script>
</body>
</html>