Compare commits
No commits in common. "391408e4d4c504bb9309e2740b80cd1614c090b2" and "63df6cebafb3a8641bd7210cfbceebc0284a9439" have entirely different histories.
391408e4d4
...
63df6cebaf
17
PRD.md
17
PRD.md
@ -48,26 +48,16 @@ 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
|
||||||
@ -240,8 +230,7 @@ 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/
|
||||||
@ -266,7 +255,6 @@ 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
|
||||||
@ -320,7 +308,6 @@ 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
|
||||||
@ -330,7 +317,6 @@ 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**
|
||||||
@ -426,7 +412,6 @@ 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)
|
||||||
|
|||||||
Binary file not shown.
261
cli/commands.txt
261
cli/commands.txt
@ -1,261 +0,0 @@
|
|||||||
# 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
|
|
||||||
16
cli/main.py
16
cli/main.py
@ -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 (generates reports automatically)
|
python main.py # Run with default settings
|
||||||
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 files
|
python main.py --dry-run # Analyze without generating skip list
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -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 (now always enabled by default)'
|
help='Save detailed reports to files'
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@ -123,7 +123,6 @@ 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)
|
||||||
|
|
||||||
@ -177,8 +176,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")
|
||||||
|
|
||||||
# Always generate detailed reports (not just when --save-reports is used)
|
# Save detailed reports if requested
|
||||||
if not args.dry_run:
|
if args.save_reports:
|
||||||
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)
|
||||||
|
|
||||||
@ -229,7 +228,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 (this is what the web UI needs)
|
# Save full skip list data
|
||||||
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}")
|
||||||
@ -240,9 +239,6 @@ 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!")
|
||||||
|
|||||||
86
web/app.py
86
web/app.py
@ -191,11 +191,6 @@ 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."""
|
||||||
@ -440,87 +435,6 @@ 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():
|
||||||
|
|||||||
@ -478,20 +478,6 @@
|
|||||||
</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>
|
||||||
|
|||||||
@ -1,402 +0,0 @@
|
|||||||
<!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>
|
|
||||||
Loading…
Reference in New Issue
Block a user