current job

This commit is contained in:
Broque Thomas 2025-07-13 13:00:59 -07:00
parent bed02b2faf
commit b9b5bdb65d
2 changed files with 1099 additions and 3 deletions

195
CLAUDE.md
View file

@ -66,10 +66,199 @@ The application will use a central `config.json` file to store:
- **COMPLETED**: Clean, functional button design with immediate accessibility
**Active Work**:
- ⏳ Additional UI polish and user experience improvements
- ⏳ Matching engine development for cross-service track matching
- 🎯 **PRIORITY FEATURE**: Spotify Matched Download System - Advanced music organization with intelligent matching
- ⏳ Additional UI polish and user experience improvements
- ⏳ Enhanced matching engine development for cross-service track matching
**Current System Status**: All major download management functionality is working correctly.
**Current System Status**: All major download management functionality is working correctly. Ready for advanced Spotify integration features.
---
# 🎯 MAJOR FEATURE: Spotify Matched Download & Organization System
## 🎯 Feature Overview
**Ultimate Goal**: Transform downloaded music files into a perfectly organized library with Spotify-accurate metadata and professional folder structure.
**Core Concept**: Add "Matched Download" buttons (🎯) alongside regular download buttons that automatically:
1. Download the track using existing Soulseek integration
2. Intelligently match the track with Spotify's database for accurate metadata
3. Transfer and organize the file into a professional folder structure
4. Handle complex edge cases like remixes, compilations, and mixed-artist albums
**Target Folder Structure**:
```
Transfer/
├── Taylor Swift/
│ ├── Taylor Swift - 1989 (Taylor's Version)/
│ │ ├── Taylor Swift - Shake It Off (Taylor's Version).flac
│ │ ├── Taylor Swift - Blank Space (Taylor's Version).flac
│ │ └── ...
│ └── Taylor Swift - Folklore/
│ ├── Taylor Swift - Cardigan.flac
│ └── ...
├── Daft Punk/
│ └── Daft Punk - Random Access Memories/
│ ├── Daft Punk - Get Lucky (feat. Pharrell Williams).flac
│ └── ...
└── Various Artists/
└── Various Artists - Now That's What I Call Music 50/
├── Britney Spears - Toxic.flac
└── ...
```
## 🚀 Implementation Phases
### Phase 1: Foundation & Architecture ⏳
**Status**: In Planning
**Deliverables**:
- ✅ Comprehensive specification document (SPOTIFY_MATCHING_SPEC.md)
- ⏳ Advanced metadata extraction system leveraging existing TrackResult/AlbumResult data
- ⏳ Sophisticated matching algorithms with multi-stage fallback logic
- ⏳ Professional UI architecture with responsive modal design
### Phase 2: Core Services Development 📋
**Status**: Pending Phase 1
**Deliverables**:
- SpotifyMatchingService with intelligent search query generation
- FileOrganizationService with atomic file operations and conflict resolution
- MatchingEngine with confidence scoring and remix detection
- Enhanced error handling and logging systems
### Phase 3: Professional UI Implementation 🎨
**Status**: Pending Phase 2
**Deliverables**:
- Responsive MatchingModal with proper spacing and layouts
- Real-time search interface with debouncing and progress indicators
- Confidence visualization and user feedback systems
- Accessibility features and keyboard navigation
### Phase 4: Integration & Polish ⚡
**Status**: Pending Phase 3
**Deliverables**:
- 🎯 Matched download buttons integrated into existing UI components
- Download completion detection and automatic processing
- Album-level matching and batch processing capabilities
- Comprehensive testing and edge case handling
## 🎵 Supported Download Types
### 1. **Singles** (Primary Focus)
- Individual tracks from search results
- Most straightforward matching scenario
- Foundation for more complex matching logic
### 2. **Albums** (Future Enhancement)
- Complete album downloads with track-by-track matching
- Handle mixed-artist compilations intelligently
- Detect and separate "fake albums" (user playlists disguised as albums)
### 3. **Individual Album Tracks** (Future Enhancement)
- Tracks downloaded individually from within album results
- Inherit album context for better matching accuracy
- Maintain consistency with full album downloads
## 🧠 Intelligent Matching System
### Advanced Metadata Extraction
**Challenge**: Soulseek filenames are inconsistent and unreliable
**Solution**: Multi-source metadata aggregation
- **Primary**: Leverage existing `TrackResult.artist`, `TrackResult.title`, `TrackResult.album` fields
- **Secondary**: Enhanced filename parsing with regex patterns
- **Tertiary**: Directory path analysis for album context
- **Fallback**: Manual user input through search interface
### Sophisticated Search Strategies
1. **Exact Match**: Artist + Title + Album (highest confidence)
2. **Partial Match**: Artist + Title (good confidence)
3. **Fuzzy Match**: Normalized strings with similarity scoring
4. **Remix Detection**: Extract remix artist from title patterns
5. **Manual Search**: User-driven fallback with suggestions
### Confidence Scoring System
- **90-100%**: Exact metadata match, auto-proceed
- **75-89%**: High confidence, show for user confirmation
- **60-74%**: Medium confidence, require user review
- **Below 60%**: Low confidence, manual search required
## 🎛️ User Experience Flow
### Seamless Workflow
1. **User clicks 🎯** on any track (single or within album)
2. **Download starts immediately** using existing proven download system
3. **Matching modal appears** with elegant, responsive design
4. **Automatic matching runs** in background with progress indication
5. **Results displayed** with confidence scores and preview information
6. **User confirms or refines** the match through intuitive interface
7. **File transferred atomically** to organized structure
8. **Success feedback** with option to open destination folder
### Error Handling & Fallbacks
- **No automatic match**: Manual search interface with intelligent suggestions
- **Multiple high-confidence matches**: User selection with detailed comparison
- **No suitable matches found**: Option to proceed with regular download
- **File transfer errors**: Rollback mechanisms and detailed error reporting
- **Spotify API failures**: Graceful degradation with retry logic
## 🔧 Technical Challenges & Solutions
### Challenge 1: Inconsistent Soulseek Metadata
**Problem**: Filenames like "Track 01.mp3" or "asdjkfh - some song.flac"
**Solution**: Multi-stage extraction using existing TrackResult fields + enhanced parsing
### Challenge 2: Remix Track Attribution
**Problem**: "Song Title (Artist Remix)" should match to remix artist, not original
**Solution**: Regex-based remix detection with artist extraction patterns
### Challenge 3: Album vs Playlist Distinction
**Problem**: User playlists disguised as "albums" with mixed artists
**Solution**: Artist consistency analysis and intelligent categorization
### Challenge 4: File Organization Conflicts
**Problem**: Duplicate files, naming conflicts, atomic operations
**Solution**: Professional file management with backup, rollback, and deduplication
### Challenge 5: Spotify API Rate Limits
**Problem**: Search throttling and request failures
**Solution**: Intelligent caching, request batching, and exponential backoff
## ⚙️ Configuration & Settings
### User Preferences
- **Auto-match threshold**: Minimum confidence for automatic processing
- **Folder naming patterns**: Customizable organization schemes
- **Transfer location**: Default destination directory
- **Conflict resolution**: Overwrite, rename, or skip duplicate files
- **Remix handling**: Original artist vs remix artist preference
### Advanced Options
- **Search aggressiveness**: Number of search strategies to attempt
- **Metadata sources**: Priority order for information extraction
- **Quality preferences**: File format and bitrate handling
- **Cover art download**: Album artwork integration
## 🎯 Success Criteria
### Functional Requirements
- [ ] 95%+ success rate for popular tracks with clear metadata
- [ ] Graceful fallback handling for edge cases
- [ ] Sub-3-second matching time for typical searches
- [ ] Professional folder organization matching industry standards
- [ ] Zero data loss during file operations
### User Experience Requirements
- [ ] Intuitive interface requiring minimal user training
- [ ] Clear progress indication and feedback
- [ ] Responsive design that adapts to different screen sizes
- [ ] Accessibility compliance for keyboard navigation
- [ ] Professional visual design matching existing application theme
### Technical Requirements
- [ ] Robust error handling with detailed logging
- [ ] Atomic file operations with rollback capability
- [ ] Efficient memory usage during batch operations
- [ ] Integration with existing download queue system
- [ ] Maintainable code architecture for future enhancements
## Key Components Status

907
SPOTIFY_MATCHING_SPEC.md Normal file
View file

@ -0,0 +1,907 @@
# Spotify Matched Download System - Technical Specification
## 📋 Document Purpose
This document provides comprehensive technical specifications for implementing the Spotify Matched Download System. It addresses the complexity of music metadata matching, file organization, and user interface design based on real-world challenges and requirements.
---
## 🏗️ System Architecture Overview
### Core Components
```
┌─────────────────────────────────────────────────────────────────┐
│ Spotify Matched Download System │
├─────────────────────────────────────────────────────────────────┤
│ UI Layer │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Matched Download│ │ Matching Modal │ │ Progress Tracking│ │
│ │ Buttons │ │ │ │ │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ Service Layer │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Metadata │ │ Spotify Matching│ │ File Organization│ │
│ │ Extraction │ │ Service │ │ Service │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ Integration Layer │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Soulseek │ │ Spotify │ │ File System │ │
│ │ Client │ │ Client │ │ Manager │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
---
## 🔍 Advanced Metadata Extraction System
### Problem Statement
Soulseek metadata is notoriously inconsistent:
- Filenames: `"Track 01.mp3"`, `"asdjklh.flac"`, `"Artist - Song (Remix).mp3"`
- Directory paths: `"/User/Music/Random Folder/"`
- Missing or incorrect artist/title/album information
### Solution: Multi-Tier Extraction Strategy
#### Tier 1: Leverage Existing TrackResult Fields (PRIMARY)
```python
class EnhancedMetadataExtractor:
def extract_from_track_result(self, track: TrackResult) -> TrackMetadata:
"""
Primary extraction using existing TrackResult fields.
These are already parsed by soulseek_client.py
"""
return TrackMetadata(
artist=track.artist, # Already extracted!
title=track.title, # Already extracted!
album=track.album, # Already extracted!
track_number=track.track_number, # Already extracted!
filename=track.filename,
confidence=self.calculate_field_confidence(track)
)
```
#### Tier 2: Enhanced Filename Parsing (SECONDARY)
```python
class AdvancedFilenameParser:
PATTERNS = [
# Pattern: "Artist - Title"
r'^(?P<artist>.+?)\s*[-–—]\s*(?P<title>.+?)(?:\s*\[(?P<extra>.*?)\])?(?:\s*\((?P<remix>.*?[Rr]emix.*?)\))?$',
# Pattern: "01 - Artist - Title"
r'^(?P<track>\d+)\s*[-\.]\s*(?P<artist>.+?)\s*[-–—]\s*(?P<title>.+?)(?:\s*\((?P<remix>.*?)\))?$',
# Pattern: "Artist - Album - Title"
r'^(?P<artist>.+?)\s*[-–—]\s*(?P<album>.+?)\s*[-–—]\s*(?P<title>.+?)$',
# Pattern: "Title (Artist Remix)"
r'^(?P<title>.+?)\s*\((?P<remix_artist>.+?)\s+[Rr]emix\)$',
# Pattern: "Album - Track - Title"
r'^(?P<album>.+?)\s*[-–—]\s*(?P<track>\d+)\s*[-–—]\s*(?P<title>.+?)$'
]
def parse_filename(self, filename: str) -> Optional[TrackMetadata]:
"""Enhanced filename parsing with remix detection"""
base_name = self.clean_filename(filename)
for pattern in self.PATTERNS:
match = re.match(pattern, base_name, re.IGNORECASE)
if match:
return self.create_metadata_from_match(match)
return None
```
#### Tier 3: Directory Context Analysis (TERTIARY)
```python
class DirectoryContextAnalyzer:
def analyze_path_context(self, filepath: str) -> Optional[AlbumContext]:
"""
Extract album context from directory structure
Example: "/Music/Artist/Album (Year)/Track.flac"
"""
path_parts = Path(filepath).parts
# Common patterns for album directories
album_patterns = [
r'(?P<artist>.+?)\s*[-–—]\s*(?P<album>.+?)(?:\s*\((?P<year>\d{4})\))?',
r'(?P<album>.+?)(?:\s*\((?P<year>\d{4})\))?',
r'\[(?P<year>\d{4})\]\s*(?P<album>.+?)'
]
# Analyze parent directories for album info
for part in reversed(path_parts):
for pattern in album_patterns:
match = re.match(pattern, part, re.IGNORECASE)
if match:
return AlbumContext(**match.groupdict())
return None
```
---
## 🎵 Sophisticated Matching Algorithms
### Multi-Stage Matching Pipeline
#### Stage 1: Exact Match Strategy
```python
class ExactMatcher:
def find_exact_match(self, metadata: TrackMetadata) -> List[SpotifyMatch]:
"""
Highest confidence matching with exact metadata
"""
if not (metadata.artist and metadata.title):
return []
# Build exact search query
query_parts = []
if metadata.artist:
query_parts.append(f'artist:"{metadata.artist}"')
if metadata.title:
query_parts.append(f'track:"{metadata.title}"')
if metadata.album:
query_parts.append(f'album:"{metadata.album}"')
query = ' '.join(query_parts)
results = self.spotify_client.search_tracks(query, limit=5)
return [SpotifyMatch(track, confidence=0.95) for track in results[:3]]
```
#### Stage 2: Fuzzy Match Strategy
```python
class FuzzyMatcher:
def find_fuzzy_matches(self, metadata: TrackMetadata) -> List[SpotifyMatch]:
"""
Similarity-based matching with confidence scoring
"""
# Normalize strings for comparison
normalized_artist = self.normalize_string(metadata.artist)
normalized_title = self.normalize_string(metadata.title)
# Generate search variations
search_queries = [
f"{normalized_artist} {normalized_title}",
f"{metadata.artist} {metadata.title}", # Original strings
f'"{normalized_artist}" "{normalized_title}"', # Quoted search
]
all_matches = []
for query in search_queries:
results = self.spotify_client.search_tracks(query, limit=10)
for track in results:
confidence = self.calculate_similarity_confidence(metadata, track)
if confidence >= 0.6: # Minimum threshold
all_matches.append(SpotifyMatch(track, confidence))
# Deduplicate and sort by confidence
return self.deduplicate_matches(all_matches)
def calculate_similarity_confidence(self, metadata: TrackMetadata, spotify_track: SpotifyTrack) -> float:
"""
Advanced confidence calculation with multiple factors
"""
# Artist similarity (weight: 40%)
artist_sim = self.string_similarity(
self.normalize_string(metadata.artist),
self.normalize_string(spotify_track.artists[0])
)
# Title similarity (weight: 50%)
title_sim = self.string_similarity(
self.normalize_string(metadata.title),
self.normalize_string(spotify_track.name)
)
# Album similarity (weight: 10%)
album_sim = 0.0
if metadata.album and spotify_track.album:
album_sim = self.string_similarity(
self.normalize_string(metadata.album),
self.normalize_string(spotify_track.album)
)
# Duration similarity bonus (weight: bonus +5%)
duration_bonus = 0.0
if metadata.duration and spotify_track.duration_ms:
duration_diff = abs(metadata.duration - (spotify_track.duration_ms / 1000))
if duration_diff <= 5: # Within 5 seconds
duration_bonus = 0.05
confidence = (artist_sim * 0.4) + (title_sim * 0.5) + (album_sim * 0.1) + duration_bonus
return min(confidence, 1.0)
```
#### Stage 3: Remix Detection & Handling
```python
class RemixMatcher:
REMIX_PATTERNS = [
r'(?P<title>.+?)\s*\((?P<remix_artist>.+?)\s+[Rr]emix\)',
r'(?P<title>.+?)\s*\[(?P<remix_artist>.+?)\s+[Rr]emix\]',
r'(?P<title>.+?)\s*-\s*(?P<remix_artist>.+?)\s+[Rr]emix',
r'(?P<title>.+?)\s+\((?P<remix_artist>.+?)\s+[Vv]ersion\)',
]
def detect_remix(self, title: str) -> Optional[RemixInfo]:
"""
Extract remix information from track title
"""
for pattern in self.REMIX_PATTERNS:
match = re.search(pattern, title, re.IGNORECASE)
if match:
return RemixInfo(
original_title=match.group('title').strip(),
remix_artist=match.group('remix_artist').strip(),
is_remix=True
)
return None
def match_remix_track(self, metadata: TrackMetadata, remix_info: RemixInfo) -> List[SpotifyMatch]:
"""
Search for remix tracks with proper artist attribution
"""
search_queries = [
f'artist:"{remix_info.remix_artist}" track:"{remix_info.original_title}"',
f'"{remix_info.remix_artist}" "{remix_info.original_title}" remix',
f'"{remix_info.original_title}" "{remix_info.remix_artist}"'
]
matches = []
for query in search_queries:
results = self.spotify_client.search_tracks(query, limit=5)
for track in results:
# Prioritize tracks where remix artist is primary artist
if remix_info.remix_artist.lower() in [a.lower() for a in track.artists]:
confidence = 0.85 # High confidence for proper remix attribution
matches.append(SpotifyMatch(track, confidence, match_type="remix"))
return matches
```
---
## 🎨 Professional UI Architecture
### Responsive Modal Design
#### Problem with Previous Implementation
- "Squished" content with poor spacing
- Inflexible layouts that didn't adapt to content
- Poor user experience with cramped interface
#### Solution: Professional Modal Architecture
```python
class ResponsiveMatchingModal(QDialog):
"""
Professional modal with responsive design and proper spacing
"""
def __init__(self, parent=None):
super().__init__(parent)
self.setup_responsive_ui()
def setup_responsive_ui(self):
"""
Create responsive layout with proper spacing and sizing
"""
# Modal sizing - responsive to screen size
screen = QApplication.primaryScreen().geometry()
modal_width = min(900, int(screen.width() * 0.7)) # 70% of screen width, max 900px
modal_height = min(700, int(screen.height() * 0.8)) # 80% of screen height, max 700px
self.resize(modal_width, modal_height)
self.setMinimumSize(600, 500) # Minimum usable size
# Center on parent/screen
self.center_on_parent()
# Main layout with proper margins
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(24, 24, 24, 24) # Generous margins
main_layout.setSpacing(20) # Proper spacing between sections
# Create sections
self.create_header_section(main_layout)
self.create_progress_section(main_layout)
self.create_results_section(main_layout)
self.create_manual_search_section(main_layout)
self.create_action_buttons_section(main_layout)
def create_header_section(self, parent_layout):
"""
Track information header with proper typography
"""
header_frame = QFrame()
header_frame.setStyleSheet("""
QFrame {
background: rgba(30, 30, 30, 0.9);
border-radius: 12px;
padding: 20px;
}
""")
header_layout = QVBoxLayout(header_frame)
header_layout.setSpacing(12)
# Title with proper typography
title = QLabel("🎯 Spotify Track Matching")
title.setStyleSheet("""
QLabel {
font-size: 22px;
font-weight: bold;
color: #1db954;
margin-bottom: 8px;
}
""")
# Track info with readable formatting
track_info_layout = QGridLayout()
track_info_layout.setColumnStretch(1, 1) # Second column expands
# Add track details with proper alignment
self.add_info_row(track_info_layout, 0, "Track:", self.track_metadata.title)
self.add_info_row(track_info_layout, 1, "Artist:", self.track_metadata.artist)
self.add_info_row(track_info_layout, 2, "Album:", self.track_metadata.album or "Unknown")
header_layout.addWidget(title)
header_layout.addLayout(track_info_layout)
parent_layout.addWidget(header_frame)
def create_results_section(self, parent_layout):
"""
Results section with proper scrolling and spacing
"""
results_frame = QFrame()
results_frame.setStyleSheet("""
QFrame {
background: rgba(40, 40, 40, 0.9);
border-radius: 12px;
padding: 20px;
}
""")
results_layout = QVBoxLayout(results_frame)
results_layout.setSpacing(16)
# Section title
results_title = QLabel("🎵 Automatic Match Results")
results_title.setStyleSheet("""
QLabel {
font-size: 18px;
font-weight: bold;
color: white;
margin-bottom: 10px;
}
""")
# Scrollable results area
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarNever)
scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
scroll_area.setMinimumHeight(200) # Ensure minimum visible area
scroll_area.setStyleSheet("""
QScrollArea {
border: none;
background: transparent;
}
QScrollBar:vertical {
background: rgba(60, 60, 60, 0.5);
width: 12px;
border-radius: 6px;
}
QScrollBar::handle:vertical {
background: rgba(29, 185, 84, 0.8);
border-radius: 6px;
min-height: 20px;
}
""")
# Results container
self.results_container = QWidget()
self.results_layout = QVBoxLayout(self.results_container)
self.results_layout.setSpacing(12) # Proper spacing between result items
self.results_layout.setContentsMargins(0, 0, 0, 0)
scroll_area.setWidget(self.results_container)
results_layout.addWidget(results_title)
results_layout.addWidget(scroll_area, 1) # Expand to fill space
parent_layout.addWidget(results_frame, 1) # Allow results section to expand
```
#### Individual Result Item Design
```python
class SpotifyMatchResultItem(QFrame):
"""
Individual Spotify match result with professional styling
"""
def __init__(self, spotify_track: SpotifyTrack, confidence: float, parent=None):
super().__init__(parent)
self.spotify_track = spotify_track
self.confidence = confidence
self.setup_professional_ui()
def setup_professional_ui(self):
"""
Create professional result item with proper spacing
"""
self.setFixedHeight(100) # Consistent height for all items
self.setStyleSheet("""
QFrame {
background: rgba(50, 50, 50, 0.8);
border: 1px solid rgba(80, 80, 80, 0.6);
border-radius: 10px;
margin: 4px 0px;
}
QFrame:hover {
background: rgba(60, 60, 60, 0.9);
border-color: rgba(29, 185, 84, 0.8);
}
""")
layout = QHBoxLayout(self)
layout.setContentsMargins(16, 12, 16, 12) # Proper margins
layout.setSpacing(16) # Good spacing between elements
# Left section: Track info
info_layout = QVBoxLayout()
info_layout.setSpacing(4)
# Track title
title_label = QLabel(spotify_track.name)
title_label.setStyleSheet("""
QLabel {
font-size: 16px;
font-weight: bold;
color: white;
}
""")
title_label.setWordWrap(True)
# Artist and album
artist_text = ", ".join(spotify_track.artists)
details_label = QLabel(f"by {artist_text}")
details_label.setStyleSheet("""
QLabel {
font-size: 13px;
color: rgba(255, 255, 255, 0.8);
}
""")
album_label = QLabel(f"from {spotify_track.album}")
album_label.setStyleSheet("""
QLabel {
font-size: 12px;
color: rgba(255, 255, 255, 0.6);
}
""")
info_layout.addWidget(title_label)
info_layout.addWidget(details_label)
info_layout.addWidget(album_label)
# Right section: Confidence and select button
right_layout = QVBoxLayout()
right_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
# Confidence indicator
confidence_widget = self.create_confidence_widget()
# Select button
select_button = QPushButton("Select This Track")
select_button.setStyleSheet("""
QPushButton {
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
stop:0 rgba(29, 185, 84, 0.9),
stop:1 rgba(25, 156, 71, 0.9));
color: white;
border: none;
border-radius: 6px;
padding: 8px 16px;
font-weight: bold;
font-size: 13px;
}
QPushButton:hover {
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
stop:0 rgba(32, 200, 90, 1.0),
stop:1 rgba(28, 170, 76, 1.0));
}
""")
select_button.clicked.connect(self.on_select_clicked)
right_layout.addWidget(confidence_widget)
right_layout.addStretch()
right_layout.addWidget(select_button)
# Assembly
layout.addLayout(info_layout, 1) # Expand info section
layout.addLayout(right_layout)
def create_confidence_widget(self) -> QWidget:
"""
Create professional confidence indicator
"""
confidence_widget = QFrame()
confidence_widget.setFixedSize(60, 60)
# Color based on confidence level
if self.confidence >= 0.9:
color = "#28a745" # Green
text_color = "white"
elif self.confidence >= 0.75:
color = "#ffc107" # Yellow
text_color = "black"
elif self.confidence >= 0.6:
color = "#fd7e14" # Orange
text_color = "white"
else:
color = "#dc3545" # Red
text_color = "white"
confidence_widget.setStyleSheet(f"""
QFrame {{
background: {color};
border-radius: 30px;
border: 2px solid rgba(255, 255, 255, 0.2);
}}
""")
layout = QVBoxLayout(confidence_widget)
layout.setContentsMargins(0, 0, 0, 0)
percentage_label = QLabel(f"{int(self.confidence * 100)}%")
percentage_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
percentage_label.setStyleSheet(f"""
QLabel {{
color: {text_color};
font-size: 14px;
font-weight: bold;
}}
""")
layout.addWidget(percentage_label)
return confidence_widget
```
---
## 📁 Professional File Organization System
### Atomic File Operations
```python
class AtomicFileOrganizer:
"""
Professional file organization with rollback capability
"""
def __init__(self, transfer_base_path: str = "Transfer"):
self.transfer_base_path = Path(transfer_base_path)
self.operation_log = [] # Track operations for rollback
def organize_file(self, source_path: str, spotify_track: SpotifyTrack) -> FileOrganizationResult:
"""
Atomically organize file with full rollback capability
"""
try:
# Phase 1: Validation
source_file = Path(source_path)
if not source_file.exists():
return FileOrganizationResult(
success=False,
error="Source file does not exist",
source_path=source_path
)
# Phase 2: Destination planning
destination_path = self.calculate_destination_path(spotify_track, source_file.suffix)
# Phase 3: Conflict resolution
final_destination = self.resolve_conflicts(destination_path)
# Phase 4: Atomic operation
return self.perform_atomic_move(source_file, final_destination)
except Exception as e:
return FileOrganizationResult(
success=False,
error=f"Organization failed: {str(e)}",
source_path=source_path
)
def calculate_destination_path(self, spotify_track: SpotifyTrack, file_extension: str) -> Path:
"""
Calculate organized file path following professional naming conventions
"""
# Sanitize names for filesystem compatibility
artist_name = self.sanitize_filename(spotify_track.artists[0])
album_name = self.sanitize_filename(spotify_track.album)
track_name = self.sanitize_filename(spotify_track.name)
# Handle multi-artist tracks
if len(spotify_track.artists) > 1:
primary_artist = spotify_track.artists[0]
# Keep featured artists in track name
if "feat." in track_name.lower() or "featuring" in track_name.lower():
final_track_name = f"{primary_artist} - {track_name}"
else:
featured_artists = ", ".join(spotify_track.artists[1:])
final_track_name = f"{primary_artist} - {track_name} (feat. {featured_artists})"
else:
final_track_name = f"{artist_name} - {track_name}"
# Create path structure
artist_folder = artist_name
album_folder = f"{artist_name} - {album_name}"
filename = f"{final_track_name}{file_extension}"
return self.transfer_base_path / artist_folder / album_folder / filename
def resolve_conflicts(self, destination_path: Path) -> Path:
"""
Handle file naming conflicts professionally
"""
if not destination_path.exists():
return destination_path
# Generate unique filename
base_path = destination_path.parent / destination_path.stem
extension = destination_path.suffix
counter = 1
while True:
new_path = Path(f"{base_path} ({counter}){extension}")
if not new_path.exists():
return new_path
counter += 1
# Safety limit
if counter > 100:
raise Exception("Too many file conflicts")
def perform_atomic_move(self, source: Path, destination: Path) -> FileOrganizationResult:
"""
Perform atomic file move with backup and rollback
"""
operation_id = str(uuid.uuid4())
try:
# Ensure destination directory exists
destination.parent.mkdir(parents=True, exist_ok=True)
# Create backup if destination exists
backup_path = None
if destination.exists():
backup_path = destination.with_suffix(f".backup_{operation_id}")
shutil.copy2(destination, backup_path)
self.operation_log.append({
'operation_id': operation_id,
'type': 'backup',
'path': backup_path
})
# Perform the move
shutil.move(str(source), str(destination))
self.operation_log.append({
'operation_id': operation_id,
'type': 'move',
'source': str(source),
'destination': str(destination)
})
# Cleanup backup on success
if backup_path and backup_path.exists():
backup_path.unlink()
return FileOrganizationResult(
success=True,
source_path=str(source),
destination_path=str(destination),
operation_id=operation_id
)
except Exception as e:
# Rollback on failure
self.rollback_operation(operation_id)
return FileOrganizationResult(
success=False,
error=f"File move failed: {str(e)}",
source_path=str(source)
)
def sanitize_filename(self, name: str) -> str:
"""
Sanitize filename for cross-platform compatibility
"""
# Remove/replace invalid characters
invalid_chars = r'<>:"/\|?*'
for char in invalid_chars:
name = name.replace(char, '')
# Handle special cases
name = name.replace('..', '.') # Double dots
name = re.sub(r'\s+', ' ', name) # Multiple spaces
name = name.strip(' .') # Leading/trailing spaces and dots
# Length limit
if len(name) > 200:
name = name[:200].rsplit(' ', 1)[0] # Break at word boundary
return name or "Unknown" # Fallback for empty names
```
---
## 🔄 Integration with Existing Download System
### Download Completion Detection
```python
class DownloadCompletionMonitor:
"""
Monitor download completions and trigger matching process
"""
def __init__(self, download_manager, matching_service):
self.download_manager = download_manager
self.matching_service = matching_service
self.pending_matches = {} # Track matched downloads
def register_matched_download(self, search_result, track_metadata):
"""
Register a download for post-completion matching
"""
download_id = self.generate_download_id(search_result)
self.pending_matches[download_id] = {
'search_result': search_result,
'track_metadata': track_metadata,
'timestamp': time.time()
}
def on_download_completed(self, download_item):
"""
Handle download completion and trigger matching if needed
"""
download_id = self.generate_download_id(download_item.search_result)
if download_id in self.pending_matches:
# This was a matched download - trigger matching process
match_info = self.pending_matches[download_id]
self.trigger_post_download_matching(download_item, match_info)
del self.pending_matches[download_id]
def trigger_post_download_matching(self, download_item, match_info):
"""
Start matching process after download completion
"""
# Update track metadata with actual download path
track_metadata = match_info['track_metadata']
track_metadata.file_path = download_item.local_path
# Show matching modal
modal = MatchingModal(
matching_service=self.matching_service,
track_metadata=track_metadata,
download_path=download_item.local_path
)
modal.show()
```
---
## 🧪 Testing Strategy
### Unit Tests
```python
class TestMetadataExtraction:
"""Test metadata extraction with real-world examples"""
def test_common_filename_patterns(self):
test_cases = [
("Artist - Song.mp3", {"artist": "Artist", "title": "Song"}),
("01 - Artist - Song.flac", {"track": 1, "artist": "Artist", "title": "Song"}),
("Song (Artist Remix).mp3", {"title": "Song", "remix_artist": "Artist"}),
("Artist - Album - Song.mp3", {"artist": "Artist", "album": "Album", "title": "Song"}),
]
extractor = AdvancedFilenameParser()
for filename, expected in test_cases:
result = extractor.parse_filename(filename)
assert result.artist == expected.get("artist")
assert result.title == expected.get("title")
class TestMatchingAlgorithms:
"""Test matching accuracy with known examples"""
def test_exact_matches(self):
"""Test exact matching with perfect metadata"""
pass
def test_fuzzy_matches(self):
"""Test fuzzy matching with slight variations"""
pass
def test_remix_detection(self):
"""Test remix detection and proper artist attribution"""
pass
class TestFileOrganization:
"""Test file organization and conflict resolution"""
def test_atomic_operations(self):
"""Test atomic file moves with rollback"""
pass
def test_conflict_resolution(self):
"""Test handling of duplicate files"""
pass
def test_cross_platform_compatibility(self):
"""Test filename sanitization across platforms"""
pass
```
### Integration Tests
```python
class TestEndToEndWorkflow:
"""Test complete matched download workflow"""
def test_single_track_workflow(self):
"""Test complete single track matched download"""
pass
def test_error_handling_workflow(self):
"""Test error scenarios and fallbacks"""
pass
def test_ui_responsiveness(self):
"""Test UI behavior under various conditions"""
pass
```
---
## 📊 Performance Considerations
### Optimization Strategies
1. **Caching**: Cache Spotify search results to avoid duplicate API calls
2. **Batch Processing**: Group multiple searches for efficiency
3. **Lazy Loading**: Load UI elements as needed
4. **Background Processing**: Perform heavy operations in separate threads
5. **Memory Management**: Proper cleanup of modal dialogs and threads
### Monitoring & Metrics
- Track matching success rates
- Monitor API response times
- Log file organization errors
- Measure user interaction patterns
---
## 🎯 Implementation Priorities
### Phase 1: Core Foundation
1. ✅ Enhanced metadata extraction system
2. ✅ Basic matching algorithms
3. ✅ File organization framework
4. ✅ Professional UI architecture
### Phase 2: Advanced Features
1. Remix detection and handling
2. Confidence scoring system
3. Error handling and rollback
4. Performance optimizations
### Phase 3: Integration & Polish
1. Download system integration
2. Comprehensive testing
3. User experience refinements
4. Documentation and deployment
This specification provides a comprehensive foundation for implementing a professional-grade Spotify matching system that addresses real-world complexity and user experience requirements.