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 - **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 - **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 - **Code Comments:** Significant logic changes should include inline documentation
- **API Documentation:** New endpoints, functions, or interfaces must be documented - **API Documentation:** New endpoints, functions, or interfaces must be documented
**Documentation Update Checklist:** **Documentation Update Checklist:**
- [ ] Update PRD.md with any architectural or requirement changes - [ ] Update PRD.md with any architectural or requirement changes
- [ ] Update README.md with new features, installation steps, or usage instructions - [ ] 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 - [ ] Add inline comments for complex logic or business rules
- [ ] Update any configuration examples or file structure documentation - [ ] Update any configuration examples or file structure documentation
- [ ] Review and update implementation status sections - [ ] 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. This documentation requirement is mandatory and ensures the project remains maintainable and accessible to future developers and users.
### 2.3 Code Quality & Development Standards ### 2.3 Code Quality & Development Standards
@ -230,7 +240,8 @@ KaraokeMerge/
│ ├── matching.py # Song matching logic │ ├── matching.py # Song matching logic
│ ├── report.py # Report generation │ ├── report.py # Report generation
│ ├── preferences.py # Priority preferences management │ ├── 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 ├── web/ # Web UI for manual review
│ ├── app.py # Flask web application │ ├── app.py # Flask web application
│ └── templates/ │ └── templates/
@ -255,6 +266,7 @@ KaraokeMerge/
- **Search & Filter**: Real-time search across artists, titles, and paths - **Search & Filter**: Real-time search across artists, titles, and paths
- **Responsive Design**: Mobile-friendly interface - **Responsive Design**: Mobile-friendly interface
- **Easy Startup**: Automated dependency checking and browser launch - **Easy Startup**: Automated dependency checking and browser launch
- **Remaining Songs View**: Separate page to browse all songs that remain after cleanup
#### **Media Preview & Playback** #### **Media Preview & Playback**
- **Video Playback**: Direct MP4 video playback in modal popup for previewing karaoke videos - **Video Playback**: Direct MP4 video playback in modal popup for previewing karaoke videos
@ -308,6 +320,7 @@ KaraokeMerge/
#### **Key Components** #### **Key Components**
- **`web/app.py`**: Flask application with API endpoints - **`web/app.py`**: Flask application with API endpoints
- **`web/templates/index.html`**: Main web interface template - **`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 - **`start_web_ui.py`**: Startup script with dependency management
### 7.3 API Endpoints ### 7.3 API Endpoints
@ -317,6 +330,7 @@ KaraokeMerge/
- **`/api/stats`**: Get statistical analysis of the song collection - **`/api/stats`**: Get statistical analysis of the song collection
- **`/api/artists`**: Get list of artists for filtering - **`/api/artists`**: Get list of artists for filtering
- **`/api/mp3-songs`**: Get MP3 songs that remain after cleanup - **`/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 - **`/api/config`**: Get current configuration settings
#### **Priority Management Endpoints** #### **Priority Management Endpoints**
@ -412,6 +426,7 @@ data/preferences/
- [x] Create responsive design with Bootstrap 5 - [x] Create responsive design with Bootstrap 5
- [x] Add file path normalization for corrupted paths - [x] Add file path normalization for corrupted paths
- [x] Implement change tracking and save button management - [x] Implement change tracking and save button management
- [x] Create remaining songs browsing page with filtering and video preview
#### **Advanced Features** #### **Advanced Features**
- [x] Multi-format support (MP3, CDG, MP4) - [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, formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=""" epilog="""
Examples: 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 --verbose # Enable verbose output
python main.py --config custom_config.json # Use custom config python main.py --config custom_config.json # Use custom config
python main.py --output-dir ./reports # Save reports to custom directory 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( parser.add_argument(
'--save-reports', '--save-reports',
action='store_true', action='store_true',
help='Save detailed reports to files' help='Save detailed reports to files (now always enabled by default)'
) )
parser.add_argument( parser.add_argument(
@ -123,6 +123,7 @@ def main():
songs = load_songs(args.input) songs = load_songs(args.input)
# Initialize components # Initialize components
data_dir = args.output_dir
matcher = SongMatcher(config, data_dir) matcher = SongMatcher(config, data_dir)
reporter = ReportGenerator(config) reporter = ReportGenerator(config)
@ -176,8 +177,8 @@ def main():
elif args.dry_run: elif args.dry_run:
print("\nDRY RUN MODE: No skip list generated") print("\nDRY RUN MODE: No skip list generated")
# Save detailed reports if requested # Always generate detailed reports (not just when --save-reports is used)
if args.save_reports: if not args.dry_run:
reports_dir = os.path.join(args.output_dir, 'reports') reports_dir = os.path.join(args.output_dir, 'reports')
os.makedirs(reports_dir, exist_ok=True) 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_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')) save_json_file(skip_songs, os.path.join(reports_dir, 'skip_songs_detailed.json'))
print(f"✅ Enhanced reports saved to: {reports_dir}") 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" • actionable_insights_report.txt - Recommendations and insights")
print(f" • detailed_duplicate_analysis.txt - Specific songs and their duplicates") print(f" • detailed_duplicate_analysis.txt - Specific songs and their duplicates")
print(f" • analysis_data.json - Raw analysis data for further processing") 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("\n" + "=" * 60)
print("Analysis complete!") print("Analysis complete!")

View File

@ -191,6 +191,11 @@ def index():
"""Main dashboard page.""" """Main dashboard page."""
return render_template('index.html') 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') @app.route('/api/duplicates')
def get_duplicates(): def get_duplicates():
"""API endpoint to get duplicate data.""" """API endpoint to get duplicate data."""
@ -435,6 +440,87 @@ def get_mp3_songs():
return jsonify(mp3_song_list) 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') @app.route('/api/download/mp3-songs')
def download_mp3_songs(): def download_mp3_songs():

View File

@ -478,6 +478,20 @@
</small> </small>
</div> </div>
</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> </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>