dont need

This commit is contained in:
Broque Thomas 2025-07-23 10:53:55 -07:00
parent fede561d50
commit 299e968522
9 changed files with 0 additions and 3550 deletions

View file

@ -1,368 +0,0 @@
# Playlist Download Implementation Plan
## Overview
This document outlines the implementation plan for the "Download Missing Tracks" feature that enables intelligent playlist downloads with Plex integration and Soulseek source matching.
## Feature Requirements
### Core Functionality
- **Plex Integration**: Check track existence before downloading (if Plex connected)
- **Fallback Mode**: Download all tracks if no Plex connection
- **Intelligent Matching**: Use Spotify metadata to match Soulseek results
- **File Organization**: Create playlist-based folder structure in Transfer directory
- **Metadata Enhancement**: Apply Spotify metadata to downloaded tracks
- **Progress Tracking**: Real-time download progress and status updates
### User Experience Flow
1. User clicks "Download Missing Tracks" in playlist details modal
2. System checks Plex connection status
3. If Plex connected: analyze playlist and identify missing tracks
4. If no Plex: queue all tracks for download
5. Use existing Spotify matching for artist/track identification
6. Download tracks to Transfer/[PLAYLIST_NAME]/ folder structure
7. Apply Spotify metadata to downloaded files
## Current Architecture Analysis
### Downloads.py Strengths
- **Robust download workflow** with `start_download()`, `start_matched_download()` patterns
- **Sophisticated track matching** via `SpotifyMatchingModal` and `MusicMatchingEngine`
- **File organization system** with Transfer folder structure (Artist/Album/Track)
- **Queue management** with progress tracking and status monitoring
- **Metadata enhancement** from Spotify API integration
- **Thread-safe operations** with background workers
### Plex Integration Capabilities
- **Track existence checking** via `PlexClient._find_track()` method
- **Bulk track retrieval** with `search_tracks()` (up to 10K tracks)
- **Matching engine integration** with confidence scoring (0.7+ threshold)
- **Lazy connection pattern** prevents UI blocking
### Extension Points Identified
- `start_matched_download()` pattern can be extended for playlist context
- Existing `SpotifyMatchingModal` can handle batch artist selection
- Current Transfer folder organization supports playlist grouping
- Thread management patterns support playlist-scale operations
## Implementation Plan
### Phase 1: Core Infrastructure
#### 1. Create Playlist Download Service
**File**: `/services/playlist_download_service.py` (NEW)
**Purpose**: Orchestrate playlist-wide download operations
**Key Components**:
```python
class PlaylistDownloadService:
def __init__(self, spotify_client, plex_client, soulseek_client, downloads_page):
self.spotify_client = spotify_client
self.plex_client = plex_client
self.soulseek_client = soulseek_client
self.downloads_page = downloads_page
async def download_missing_tracks(self, playlist: PlaylistInfo):
"""Main entry point for playlist downloads"""
def _check_plex_existence(self, tracks: List[SpotifyTrack]) -> List[SpotifyTrack]:
"""Check which tracks are missing from Plex"""
def _queue_playlist_downloads(self, tracks: List[SpotifyTrack], playlist_name: str):
"""Add tracks to download queue with playlist organization"""
def _create_playlist_folder_structure(self, playlist_name: str) -> str:
"""Create Transfer/[PLAYLIST_NAME]/ directory structure"""
```
**Estimated Lines**: 300-400
#### 2. Extend Downloads Page Integration
**File**: `/ui/pages/downloads.py` (MODIFY)
**Changes Required**:
- Add `start_playlist_download()` method following existing patterns
- Extend download queue to support playlist containers
- Add playlist-level progress tracking
- Integrate with existing `SpotifyMatchingModal` for batch operations
**New Methods**:
```python
def start_playlist_download(self, playlist: PlaylistInfo):
"""Initiate playlist-wide download operation"""
def _handle_playlist_download_progress(self, playlist_id: str, progress: dict):
"""Track progress across multiple playlist downloads"""
def _organize_playlist_download(self, track_result: TrackResult, playlist_name: str):
"""Apply playlist-specific folder organization"""
```
**Estimated Lines Added**: 100-150
#### 3. Enhance Sync Page Modal
**File**: `/ui/pages/sync.py` (MODIFY)
**Changes Required**:
- Connect "Download Missing Tracks" button to playlist download service
- Add playlist download progress indicators
- Handle user feedback for Plex vs. non-Plex scenarios
**New Methods**:
```python
def on_download_missing_tracks_clicked(self):
"""Handle Download Missing Tracks button click"""
def _show_playlist_download_progress(self, playlist_id: str):
"""Display download progress in modal"""
def _handle_download_completion(self, playlist_id: str, results: dict):
"""Process playlist download completion"""
```
**Button Integration**:
```python
# In create_buttons() method
download_btn.clicked.connect(self.on_download_missing_tracks_clicked)
```
**Estimated Lines Added**: 50-75
### Phase 2: Plex Integration Optimization
#### 1. Enhance Track Existence Checking
**File**: `/core/plex_client.py` (MODIFY)
**New Methods**:
```python
def check_tracks_existence_batch(self, spotify_tracks: List[SpotifyTrack]) -> Dict[str, PlexTrackInfo]:
"""Efficiently check existence of multiple tracks"""
def _build_track_cache(self) -> Dict[str, PlexTrackInfo]:
"""Create in-memory cache of all Plex tracks for fast lookup"""
def _normalize_track_key(self, title: str, artist: str) -> str:
"""Create normalized key for track matching"""
```
**Optimization Strategy**:
- Cache all Plex tracks in memory for O(1) lookup
- Use normalized title+artist keys for matching
- Leverage existing `MusicMatchingEngine` for confidence scoring
- Implement batch processing for large playlists
**Estimated Lines Added**: 75-100
#### 2. Smart Download Logic Implementation
**Core Algorithm**:
```python
def determine_tracks_to_download(self, playlist_tracks: List[SpotifyTrack]) -> Tuple[List[SpotifyTrack], Dict[str, str]]:
"""
Returns:
- List of tracks to download
- Dictionary of skipped tracks with reasons
"""
if not self.plex_client.is_connected():
return playlist_tracks, {}
tracks_to_download = []
skipped_tracks = {}
plex_cache = self.plex_client._build_track_cache()
for track in playlist_tracks:
existing_track = self._find_in_cache(track, plex_cache)
if existing_track and self._calculate_confidence(track, existing_track) >= 0.8:
skipped_tracks[track.id] = f"Already exists in Plex: {existing_track.title}"
else:
tracks_to_download.append(track)
return tracks_to_download, skipped_tracks
```
### Phase 3: Enhanced Matching & Metadata
#### 1. Leverage Existing Spotify Matching
**Files**: `/ui/pages/downloads.py`, `/core/matching_engine.py` (MODIFY)
**Extensions Required**:
- Extend `SpotifyMatchingModal` for playlist context
- Add batch artist suggestion for album-heavy playlists
- Use existing confidence scoring algorithms
- Apply Spotify metadata to matched downloads
**Playlist Context Enhancement**:
```python
class PlaylistSpotifyMatchingModal(SpotifyMatchingModal):
def __init__(self, track_results: List[TrackResult], playlist_context: PlaylistInfo):
super().__init__(track_results)
self.playlist_context = playlist_context
def _generate_playlist_aware_suggestions(self):
"""Generate suggestions considering playlist context (album groupings, etc.)"""
```
#### 2. Soulseek Result Matching Integration
**Leverage Existing Systems**:
- Use existing search and matching algorithms from downloads.py
- Apply track info from Spotify API (title, artist, album, duration)
- Leverage existing `TrackResult` parsing and scoring
- Maintain current quality filtering and selection logic
### Phase 4: File Organization & Metadata
#### 1. Transfer Folder Structure
**Pattern Implementation**:
```
Transfer/
[PLAYLIST_NAME]/
ARTIST_NAME/
ARTIST_NAME - ALBUM_NAME/
01 - TRACK_NAME.flac
cover.jpg
ARTIST_NAME/
ARTIST_NAME - SINGLE_NAME/
SINGLE_NAME.flac
cover.jpg
```
**Organization Logic**:
```python
def _create_playlist_folder_structure(self, playlist_name: str, track: SpotifyTrack) -> str:
"""Create appropriate folder structure within playlist directory"""
base_path = os.path.join(config_manager.get('soulseek.transfer_path'), playlist_name)
if self._is_part_of_album(track):
return os.path.join(base_path, track.artist, f"{track.artist} - {track.album}")
else:
return os.path.join(base_path, track.artist, f"{track.artist} - {track.title}")
```
#### 2. Metadata Enhancement
**Integration with Existing Systems**:
- Apply Spotify track metadata to downloaded files
- Use existing metadata writing functionality from downloads.py
- Preserve track numbers, album info, cover art
- Maintain existing file naming conventions
## Technical Implementation Details
### Performance Optimizations
- **Plex Track Caching**: Load all Plex tracks once, cache in memory for O(1) lookups
- **Batch Processing**: Process large playlists (>100 tracks) in chunks
- **Progressive Downloads**: Use existing thread pool patterns for concurrent downloads
- **Background Processing**: Leverage existing worker thread architecture
### Error Handling Strategies
- **Plex Connection Failures**: Graceful fallback to "download all tracks" mode
- **Soulseek Search Failures**: Use existing retry mechanisms and error handling
- **Download Failures**: Leverage existing queue management and retry logic
- **Metadata Failures**: Graceful degradation with basic file naming
### User Experience Enhancements
- **Progress Indicators**: Real-time progress for playlist analysis and downloads
- **Skip Existing Option**: Clear feedback when tracks exist in Plex
- **Batch Operations**: Efficient handling of large playlists
- **Status Feedback**: Clear messaging for Plex vs. non-Plex scenarios
### Thread Safety Considerations
- **Queue Thread Safety**: Use existing `ThreadSafeQueueManager` patterns
- **UI Updates**: Leverage existing signal/slot patterns for thread-safe UI updates
- **Resource Management**: Follow existing thread cleanup and management patterns
## Integration Points
### Existing Service Integration
- **SyncService**: Extend existing playlist sync workflows
- **Downloads Page**: Integrate with current download management
- **Spotify Client**: Use existing OAuth and API integration
- **Plex Client**: Extend current track lookup and library access
### Signal/Slot Connections
```python
# In sync.py - PlaylistDetailsModal
self.download_btn.clicked.connect(self.on_download_missing_tracks_clicked)
# Progress tracking
self.playlist_download_service.progress_updated.connect(self.update_download_progress)
self.playlist_download_service.download_completed.connect(self.on_playlist_download_complete)
```
## Testing Strategy
### Unit Testing
- **Track Existence Checking**: Test Plex integration with various track matching scenarios
- **Download Logic**: Test playlist download workflow with and without Plex
- **File Organization**: Test folder structure creation and metadata application
### Integration Testing
- **End-to-End Workflow**: Test complete playlist download from button click to completion
- **Error Scenarios**: Test Plex disconnection, Soulseek failures, network issues
- **Large Playlist Handling**: Test performance with 100+ track playlists
### User Acceptance Testing
- **Plex Integration**: Verify correct skip behavior for existing tracks
- **Progress Tracking**: Ensure clear feedback during long download operations
- **File Organization**: Verify correct Transfer folder structure and metadata
## Future Enhancements (Post-MVP)
### Database Integration
- **Download History**: Track download history and statistics
- **Playlist Sync Status**: Store playlist sync timestamps and status
- **Quality Tracking**: Record download quality and source information
- **User Preferences**: Store user settings and preferences
### Advanced Features
- **Selective Download**: Allow users to manually select tracks from playlist
- **Quality Preferences**: Automatic quality selection based on user preferences
- **Duplicate Detection**: Advanced duplicate handling across playlists
- **Sync Scheduling**: Automatic periodic playlist synchronization
## Estimated Implementation Timeline
### Phase 1: Core Infrastructure (2-3 days)
- Create `PlaylistDownloadService`
- Basic downloads.py integration
- Sync page button connection
### Phase 2: Plex Integration (1-2 days)
- Optimize track existence checking
- Implement smart download logic
- Add playlist analysis
### Phase 3: Enhanced Matching (1-2 days)
- Extend Spotify matching for playlists
- Integrate with existing Soulseek matching
- Add metadata enhancement
### Phase 4: File Organization & Polish (1-2 days)
- Implement Transfer folder structure
- Add progress tracking and error handling
- User experience refinements
**Total Estimated Effort**: 5-9 days
## Success Criteria
### Functional Requirements
- ✅ Playlist tracks download to Transfer/[PLAYLIST_NAME]/ structure
- ✅ Plex integration skips existing tracks (confidence ≥ 0.8)
- ✅ Fallback to download all tracks when Plex not connected
- ✅ Spotify metadata applied to downloaded files
- ✅ Progress tracking and error handling
### Performance Requirements
- ✅ Playlist analysis completes within 10 seconds for 100-track playlists
- ✅ Download initiation within 5 seconds of button click
- ✅ UI remains responsive during playlist processing
### User Experience Requirements
- ✅ Clear feedback for Plex vs. non-Plex scenarios
- ✅ Progress indicators for long-running operations
- ✅ Intuitive folder organization in Transfer directory
- ✅ Error handling with clear user messaging
This implementation plan leverages the existing robust architecture while adding intelligent Plex checking and playlist-based download functionality in a maintainable and extensible way.

View file

@ -1,442 +0,0 @@
# Spotify Matched Download System - Technical Specification
## Expected Use Case for a single(mostly the same for album?)
User clicks 'matched download' button on a 'single' and an elegant modal expands into view that offers two options: the top half (spotify auto matching with a list or slideshow or top 5 likely artists), the bottom half(manual use search on spotify to match the track to an artist). the app will use spotify metadata to update the track name and create the folder structure I detailed. so lets talk about the top half of the modal first. It will automatically populate the top 5 most likely artists to match the track with. each likely artist will display, if possible, the artist image, artist name, and percentage likelihood of match. clicking the artist will select that artist as the matched artist and the download will begin. now the bottom half: it will be a simple but elegant search bar for the user to search for an artist and it will display a list of 5 results similar to the top half but these results are user searched. it will display the same content, artist picture, artist name, percentage liklihood of match. clicking the artist will select that artist as the matched artist and the download will begin. So now that the user has decided which artist the track belongs to the track has begun downloading as normal to the download folder. the track and its parent folder will then appear in the downloads folder once complete. but while the track is downloading the app should attempt to gather additional information about the artist / album / track. specifically we will need to see if the track we downloaded was part of an album and if it is, make sure we create the correct folder structure. if a track is a single. it is layed out like this:
```
Transfer/
├── EXAMPLE ARTIST/
│ ├── EXAMPLE ARTIST - EXAMPLE SINGLE/
├── EXAMPLE SINGLE.flac
├── cover.png/jpg
```
if we determine a track we downloaded is part of an album by the matched artist it would be setup like this:
```
Transfer/
├── EXAMPLE ARTIST/
│ ├── EXAMPLE ARTIST - EXAMPLE ALBUM/
├── TRACK# EXAMPLE SINGLE.flac
├── cover.png/jpg
```
If we happen to download multiple tracks from the same album they should all end up with the same folder structure and in the same location.
```
Transfer/
├── EXAMPLE ARTIST/
│ ├── EXAMPLE ARTIST - EXAMPLE ALBUM/
├── TRACK# EXAMPLE SINGLE.flac
├── TRACK# EXAMPLE SINGLE.flac
├── TRACK# EXAMPLE SINGLE.flac
├── cover.png/jpg
├── ...
```
All accurate title information and cover art for albums, tracks, artists can be found with the matched artist via spotify api. this information is used to for renaming tracks and folders. That way we know tracks and albums will end up together with albums and artists having the exact same name. After we determine if the track is part of an album or not we can begin copying the download to the 'transfer' folder and creating the appropriate folder structure from above and rename the track as needed. After the folder structure is setup correctly we will begin updating the metadata within the actual track file based on the data pulled from spotify. Things like title, track number, genres, album, contributing artists and anything else spotify api provides. once folder structure is done and metadata data for all tracks is done, then delete the original download in the downloads folder and run 'clear completed' buttons function. now with everything cleaned up we can move on to the next matched download.
Now we need to incorporate this functionality into full album downloads by adding a 'matched album download' button beside the 'download album' button. this will essentially do the exact same process as singles but its a big batch added to the queue. we can't assume what we are downloading is an actual 'album' by an artist but could instead be a folder of a users favorite songs. but our app would download those songs and put them in the correct artist folder with correct metadata. if you think im missing intuitive or critical please add it in.
If we fail to match an artist in the modal, treat the download as a normal downoad without any matching and keep it in the downloads folder. Also any matched downloads need to update the 'download queue' the same way a normal download would. The cancel button should remain functional on a matched download in the queue and clicking it should behave exaclty the same. a finished matched download should transfer to finished downloads as expected.
Remix should be handled elegantly. If artist A does a remix of Artist B song. The song artist will be Artist A with a contributting artist of Artist B.
the matching system should be super extensive and robust and professional. at the level of Spotify, Google, Facebook and Apple. So logical, practical and sophisticated it would make them proud. I provided how i want this to play out. I expect a 'Matched Download' button do appear on all singles and all tracks inside albums beside the 'download' button. and albums should have a matched download button as well that match downloads all tracks in the album. The modal should be beautiful, elegant, and provide space for content to fit. We should be very smart with our api calls to spotify so we don't reach limits. If it doesn't need to be in another file, then don't put it in one. you can do this. Give your best work. I'ts also very important that you come to this document and update the TODO list with what you are doing, what you are going to do next. And udpating the TODO list at each step.
---
## DOWNLOAD MISSING TRACKS - NEW FEATURE SPECIFICATION
### Feature Overview
A new "Download Missing Tracks" button on playlist sync modals that intelligently downloads only tracks that don't exist in Plex, with proper folder organization mimicking the matched download system.
### Core Workflow
1. **Button Click**: User clicks "Download Missing Tracks" on a playlist in sync modal
2. **Plex Check**: If Plex is connected, analyze each track for existence with high confidence matching (≥0.8)
3. **Track Processing**: For each missing track:
- Search Soulseek using: `{TrackName}` first, then `{ArtistName} {TrackName}` if needed
- Apply intelligent filtering to find best quality match
- Queue download with custom folder structure
4. **Folder Organization**: Use same structure as matched downloads:
- **Album tracks**: `ArtistName/ArtistName - AlbumName/Track.ext`
- **Singles**: `ArtistName/ArtistName - TrackName/Track.ext`
5. **Failed Matches**: Save tracks that can't be matched with high certainty for manual review
### Technical Requirements
#### Plex Integration
- Use intelligent track matching with confidence scoring
- If Plex unavailable/unreachable: download ALL tracks in playlist
- Per-track analysis with real-time progress feedback
#### Search Strategy
- Primary: Search by track name only
- Fallback: Search by artist + track name
- Use existing search filtering for quality/format preferences
- Modern, performant async operations
#### Folder Structure
- **NOT** `Transfer/[PLAYLIST_NAME]/` as originally specified
- **INSTEAD**: Use matched download structure:
- Singles: `ArtistName/ArtistName - TrackName/Track.ext`
- Albums: `ArtistName/ArtistName - AlbumName/Track.ext`
- Leverage existing downloads.py folder organization logic
- Automatic album vs single detection using Spotify metadata
#### Performance & API Efficiency
- Smart Spotify API usage to avoid rate limits
- Batch operations where possible
- Background processing with progress tracking
- Minimal changes to existing downloads.py infrastructure
### Implementation Strategy
#### Extend Existing Downloads.py
- Add custom path support (minimal changes required)
- Leverage existing search, filtering, and download queue logic
- Use existing folder organization patterns from matched downloads
- Integrate with current download progress tracking
#### Data Flow
```
Playlist Track → Plex Check → (Missing) → Soulseek Search → Quality Filter → Queue Download → Folder Organization
```
#### Error Handling
- Network failures: Continue with remaining tracks
- Search failures: Log for manual review
- Plex unavailable: Download all tracks
- API limits: Implement backoff/retry logic
### User Experience
- Real-time progress indication during Plex analysis
- Clear feedback on skipped vs queued tracks
- Integration with existing download queue UI
- Optional: Progress tracking for playlist download completion
### Success Criteria
- Seamless integration with existing download infrastructure
- No modifications needed to core downloads.py functionality
- Intelligent track matching preventing duplicates
- Proper folder organization matching app standards
- Robust error handling with graceful degradation
---
## VERY IMPORTANT! DO NOT BREAK ANYTHING
## TODO LIST:
### ✅ COMPLETED IMPLEMENTATION:
1. **✅ SpotifyMatchingModal Creation** - Created elegant QDialog with:
- Auto-matching section showing top 5 artist suggestions with confidence scores
- Manual search section with real-time artist search
- Beautiful UI with proper styling matching the app's theme
- Background threads for search operations to keep UI responsive
2. **✅ Spotify Client Enhancement** - Added to `core/spotify_client.py`:
- `Artist` dataclass with full metadata (name, image, popularity, genres, etc.)
- `search_artists()` method for artist-specific searches
- Full integration with existing SpotifyClient architecture
3. **✅ Artist Suggestion Engine** - Implemented in modal classes:
- `ArtistSuggestionThread` for generating auto-suggestions
- `ArtistSearchThread` for manual search results
- Multiple matching strategies: direct artist search + track combination search
- Confidence scoring using existing MusicMatchingEngine
4. **✅ UI Integration** - Added to `ui/pages/downloads.py`:
- **📱 Matched Download buttons** next to all existing download buttons
- Purple theme for matched download buttons to distinguish from regular downloads
- Individual track matched downloads (SearchResultItem)
- Album track matched downloads (TrackItem)
- Full album matched downloads (AlbumResultItem)
- Proper signal connections and error handling
5. **✅ Download Flow Integration** - Enhanced DownloadsPage:
- `start_matched_download()` method triggers Spotify modal
- `start_matched_album_download()` processes entire albums
- `_handle_matched_download()` manages artist selection results
- Fallback to normal downloads if Spotify auth fails or user cancels
6. **✅ Transfer Folder Organization** - Implemented complete folder structure:
- **Singles**: `Transfer/ARTIST_NAME/ARTIST_NAME - SINGLE_NAME/SINGLE_NAME.flac`
- **Albums**: `Transfer/ARTIST_NAME/ARTIST_NAME - ALBUM_NAME/01 TRACK_NAME.flac`
- Automatic detection of single vs album tracks using Spotify API
- File sanitization for cross-platform compatibility
- Conflict resolution with numbered duplicates
7. **✅ Album vs Single Detection** - Smart logic implementation:
- Spotify API track lookup by artist and title
- Confidence-based matching to ensure accuracy
- Album detection by comparing album name vs track name
- Track numbering for album tracks (extensible for full album metadata)
8. **✅ Cover Art Integration** - Basic implementation:
- Downloads artist images as cover.jpg for albums
- Proper error handling and duplicate prevention
- Extensible for full album artwork via additional Spotify API calls
9. **✅ Post-Download Processing** - Integrated with existing download completion:
- Hooks into download status monitoring at completion detection
- Automatically organizes matched downloads to Transfer folder
- Preserves original files in downloads folder for now (can be enhanced)
- Proper error handling with fallback to normal download flow
10. **✅ Error Handling & Fallbacks** - Comprehensive safety measures:
- Spotify authentication checks before showing modal
- Graceful fallback to normal downloads on any errors
- User cancellation handling (proceeds with normal download)
- File operation error handling
- API rate limiting considerations
### 🆕 NEW FEATURE: ENHANCED DOWNLOAD MISSING TRACKS MODAL
#### 📋 CURRENT IMPLEMENTATION STEPS:
1. **✅ COMPLETED - Basic Infrastructure**
- ✅ Hook "Download Missing Tracks" button to workflow
- ✅ Implement basic playlist track retrieval from Spotify
- ✅ Create PlaylistTrackAnalysisWorker for Plex analysis
- ✅ Background worker with progress tracking and confidence scoring
- ✅ String normalization, similarity scoring, and duration matching
2. **✅ COMPLETED - Enhanced Modal Interface**
- ✅ Replace simple QMessageBox with sophisticated modal
- ✅ Modal closes sync window and opens new interface
- ✅ Dashboard with live counters: Total Tracks, Matched Tracks, To Download
- ✅ Enhanced track table with Matched and Downloaded status columns
- ✅ Dual progress bar system (Plex analysis + Download progress)
- ✅ Three-button system: Begin Search, Cancel, Close
3. **✅ COMPLETED - Modal State Persistence**
- ✅ Playlist status indicator system when modal is closed during operations
- ✅ Real-time status updates on playlist buttons (🔍 Analyzing, ⏬ Downloading)
- ✅ Maintain operation state across modal open/close cycles
4. **✅ COMPLETED - Soulseek Search Integration**
- ✅ **CRITICAL**: Using existing downloads.py infrastructure for search/download
- ✅ **CRITICAL**: Implemented smart search strategy for artist name issues
- ✅ **CRITICAL**: Using existing quality filtering and result matching logic
- ✅ **CRITICAL**: Integrated with existing download queue system
5. **✅ COMPLETED - Smart Search Strategy**
- ✅ **Single-word tracks**: Track + full artist first (e.g., "Aether Virtual Mage")
- ✅ **Multi-word tracks**: Track name first (e.g., "Astral Chill")
- ✅ **Fallback strategies**: Shortened artist, first word, full artist combinations
- ✅ **Strict matching**: Exact track name containment required in results
6. **✅ COMPLETED - Downloads.py Integration**
- ✅ Using existing `SoulseekClient.search()` and filtering infrastructure
- ✅ Integrated with existing download queue management
- ✅ Applied matched download folder structure automatically
- ✅ Using existing file organization and metadata handling
7. **⚠️ NEEDS IMPROVEMENT - Advanced Matching & Quality Selection**
- ⚠️ **HIGH PRIORITY**: FLAC preference when multiple valid matches exist
- ⚠️ **HIGH PRIORITY**: More intelligent track title parsing (handle '-', '_', bitrate, etc.)
- ⚠️ **HIGH PRIORITY**: Spotify matching for proper folder naming structure
- ⚠️ **HIGH PRIORITY**: Confidence-based auto-matching with failed matches tracking
### ✅ COMPLETE WORKFLOW IMPLEMENTED:
**User Experience Flow:**
1. **Click "Download Missing Tracks"** → Sync modal closes, Download modal opens
2. **Click "Begin Search"** → Playlist button shows "🔍 Analyzing..." status
3. **Plex Analysis Phase** → Real-time track table updates with ✅/❌ status
4. **Auto-Download Phase** → Playlist button shows "⏬ Downloading X/Y" status
5. **Modal Interaction Options:**
- **Cancel Button**: Stops all operations, closes modal, restores playlist button
- **Close Button**: Closes modal, continues operations with status updates
- **Re-open Modal**: Click playlist status indicator to view detailed progress
6. **Completion** → Playlist button returns to normal "Sync / Download"
**Playlist Status Indicators:**
- `🔍 Analyzing X/Y` - During Plex analysis phase
- `⏬ Downloading X/Y` - During Soulseek download phase
- `✅ Complete` - When all operations finished
- Clickable to reopen detailed progress modal
**Track Table Status Updates:**
- **Matched Column**: ✅ Found (confidence), ❌ Missing, ⏳ Pending
- **Downloaded Column**: ✅ Downloaded, ⏬ Downloading, ❌ Failed, ⏳ Pending
### 🔧 TECHNICAL ARCHITECTURE:
**Core Integration Points:**
- `SyncPage` - Enhanced with soulseek_client parameter and playlist status indicators
- `PlaylistItem` - Added show/hide/update operation status methods
- `DownloadMissingTracksModal` - Complete workflow with real-time UI updates
- `TrackDownloadWorker` - Background Soulseek download integration
- Existing `plex_client.py` and `soulseek_client.py` - Leveraged without modification
**Data Flow:**
```
Playlist → Spotify Tracks → Plex Analysis → Track Table Updates → Missing Tracks → Soulseek Downloads → Status Updates
```
### 🎯 SUCCESS METRICS:
- No breaking changes to existing download functionality
- Seamless integration with current UI and workflow
- Intelligent Plex deduplication preventing unnecessary downloads
- Proper folder organization matching app standards
- Robust error handling with graceful degradation to download all tracks
---
## 🚀 CURRENT STATE & NEXT PHASE IMPROVEMENTS
### ✅ CURRENT WORKING STATE (What's Working Now):
#### **Core Functionality Complete:**
1. **Modal System**: Sophisticated UI with live counters, dual progress bars, track table
2. **Plex Analysis**: Background thread analyzes tracks against Plex library
3. **Smart Search**: Single-word tracks prioritize artist inclusion, multi-word tracks work well
4. **Download Integration**: Uses existing downloads.py infrastructure properly
5. **Progress Tracking**: Real-time updates, modal can be closed/reopened
6. **Folder Structure**: Basic folder creation for downloaded tracks
#### **Search Strategy Working:**
- ✅ "Aether Virtual Mage" → finds correct Virtual Mage track
- ✅ "Astral Chill" → finds correct track
- ✅ "Orbit Love" → finds correct track
- ✅ Downloads integrate with existing queue system
- ✅ Sequential searching prevents overwhelming slskd
### ⚠️ CRITICAL IMPROVEMENTS NEEDED (Next Phase):
#### **1. INTELLIGENT MATCHING SYSTEM**
**Current Issue**: System is finding tracks but not always selecting the best quality/match
**Requirements**:
- **FLAC Priority**: When multiple valid matches exist, always choose FLAC over MP3/other formats
- **Advanced Title Parsing**: Handle track names with extra characters like:
- `Artist - Track Name [320kbps]`
- `01. Track_Name - Artist_Name.flac`
- `Track Name (feat. Other Artist) - 2023 Remaster`
- **Bitrate Recognition**: Parse and prefer higher quality files
- **Version Filtering**: Avoid unwanted remixes, live versions, instrumentals unless specified
#### **2. SPOTIFY INTEGRATION FOR FOLDER STRUCTURE**
**Current Issue**: Downloads go to basic folders without proper Spotify metadata integration
**Requirements**:
- **Must work exactly like "matched downloads"** from the main downloads.py functionality
- **Spotify API Lookup**: For each track, find exact Spotify match for metadata
- **Album Detection**: Determine if track is part of album or is a single
- **Proper Folder Structure**:
- **Singles**: `Transfer/ARTIST_NAME/ARTIST_NAME - SINGLE_NAME/SINGLE_NAME.flac`
- **Albums**: `Transfer/ARTIST_NAME/ARTIST_NAME - ALBUM_NAME/01 TRACK_NAME.flac`
- **Cover Art**: Download album/artist artwork automatically
- **Metadata Enhancement**: Update file tags with Spotify metadata
#### **3. CONFIDENCE-BASED AUTO-MATCHING**
**Current Issue**: No systematic tracking of failed matches or confidence thresholds
**Requirements**:
- **High Confidence Auto-Download**: Tracks with >80% confidence match automatically
- **Medium Confidence Review**: 60-80% confidence tracks flagged for manual review
- **Failed Matches List**: Maintain list of tracks that couldn't be matched reliably
- **Manual Search Integration**: Allow manual search for failed tracks
- **Success Rate Tracking**: Show user statistics on match success rates
#### **4. ENHANCED QUALITY SELECTION ALGORITHM**
**Current Scoring System Improvements Needed**:
```python
# Current basic scoring needs enhancement:
# - Track name containment: 120-150 points
# - Artist containment: 40-80 points
# - Duration matching: Up to 100 points
# NEEDED: Advanced quality scoring:
# - FLAC/Lossless: +50 points (higher than current +15)
# - High bitrate: +30 points (320kbps vs 128kbps)
# - Clean filename: +20 points (avoid [tags], underscores)
# - Proper metadata: +15 points (correct artist/title fields)
# - Album context: +10 points (part of complete album)
```
### 🔧 TECHNICAL IMPLEMENTATION ROADMAP:
#### **Phase 1: FLAC Priority & Quality Enhancement** (Immediate)
1. Update `select_best_match()` scoring in `sync.py:2955`
2. Add FLAC detection and boost scoring significantly
3. Implement bitrate parsing and quality preference
4. Add file format detection improvements
#### **Phase 2: Spotify Matching Integration** (High Priority)
1. Add Spotify API lookup for each downloaded track
2. Implement album vs single detection using existing matched download logic
3. Create proper Transfer folder structure with Spotify metadata
4. Integration with existing downloads.py matched download functions
#### **Phase 3: Advanced Matching Intelligence** (Critical)
1. Enhanced track title parsing with regex patterns
2. Improved artist name normalization and matching
3. Context-aware matching (album context, release year, etc.)
4. Machine learning-style confidence scoring improvements
#### **Phase 4: Failed Matches & Manual Review** (Important)
1. Failed matches tracking and storage
2. Manual search interface for problem tracks
3. Success rate analytics and reporting
4. User feedback integration for match quality
### 📊 EXPECTED OUTCOMES:
- **90%+ automatic match rate** for popular tracks
- **FLAC preference** ensuring highest quality downloads
- **Perfect folder organization** matching existing matched download standards
- **Zero manual intervention** for high-confidence matches
- **Clear manual review workflow** for edge cases
### 🎯 CURRENT NEXT STEPS:
1. **Update FLAC priority** in matching algorithm
2. **Add Spotify metadata lookup** for proper folder structure
3. **Enhance track title parsing** for better matching accuracy
4. **Implement confidence thresholds** for auto vs manual matching
### 🚨 CRITICAL ISSUE: INCORRECT FILE NAMING AND FOLDER STRUCTURE
#### **Current Problem - DOWNLOAD MISSING TRACKS**
The current "Download Missing Tracks" implementation has a **CRITICAL FLAW** in how it handles file naming and folder organization:
**❌ CURRENT WRONG BEHAVIOR:**
1. System searches slskd for tracks based on Spotify playlist track names
2. When match is found, uses **PLAYLIST TRACK METADATA** for folder/file naming
3. **RESULT:** Can download wrong song but rename it correctly, making it appear successful
4. **EXAMPLE:** Downloads "Random Song.flac" → renames to "Orbit Love by Virtual Mage" in correct folder
**✅ REQUIRED CORRECT BEHAVIOR (Like Matched Downloads):**
1. Find slskd track result using playlist search
2. Extract **ACTUAL TRACK TITLE** from the slskd result filename/metadata
3. Search **SPOTIFY API** with the slskd track title to find correct artist
4. Use **SPOTIFY API RESPONSE** metadata for folder/file naming (not playlist metadata)
5. **RESULT:** File naming reflects what was actually downloaded
#### **Implementation Flow - Download Missing Tracks Should Work Like Matched Downloads:**
```
Playlist Track → slskd Search → slskd Result Found →
Extract slskd Track Title → Spotify API Search(slskd_title) →
Spotify Match → Use Spotify Metadata for Naming → Download + Organize
```
**This matches exactly how the main downloads page "matched downloads" work:**
- User clicks "Matched Download" on slskd result
- System extracts track title from slskd result
- Searches Spotify API with extracted title
- Uses Spotify API response for folder structure and metadata
- Downloads and organizes with correct naming
#### **Why This Is Critical:**
- **Data Integrity:** File names must reflect actual downloaded content
- **Consistency:** Must match behavior of existing matched download system
- **User Trust:** Prevents false positive downloads that appear successful but are wrong
- **Library Organization:** Ensures Transfer folder contains accurately named content
### 🤔 FUTURE CONSIDERATIONS (May Be Overkill):
#### **Advanced Spotify API Validation**
**Concept:** After finding slskd match, extract artist/title from result and re-query Spotify API to double-validate
**Flow:** slskd result → extract metadata → Spotify API lookup → compare to original → approve/reject
**Pros:** Ultimate validation accuracy, consistent with matched download system
**Cons:** Extra API calls, rate limiting concerns, added complexity, slower performance
**Decision:** ~~Current strict title+artist matching may be sufficient~~ **REQUIRED** - This is exactly what matched downloads do and what Download Missing Tracks should do

View file

@ -1,278 +0,0 @@
# Album Download Lag Fixes - Implementation Guide
## Problem Summary
The newMusic application experiences significant UI lag in two scenarios:
1. **Album Download Button Lag**: Clicking album download buttons causes 1+ second UI freeze
2. **Instant Error Cascade**: Albums that fail immediately (network errors, unavailable tracks) cause 1+ second lag due to 15+ errors processing individually
## Root Causes Identified
### 1. Album Download Processing
- **Location**: `_handle_matched_album_download()` (line ~7200)
- **Issue**: Sequential processing with `time.sleep(0.1)` per track = 1.5s+ for 15-track album
- **Problem**: All processing happens on main UI thread
### 2. UI Queue Updates
- **Location**: `DownloadQueue.add_download_item()` (line ~4436)
- **Issue**: Each track creates individual UI widget with immediate layout updates
- **Problem**: 15 tracks = 15 individual UI operations
### 3. Completion Cascade
- **Location**: Status update functions (line ~9880, ~9925, ~9979)
- **Issue**: When tracks complete simultaneously, each calls `move_to_finished()` individually
- **Problem**: 15 simultaneous completions = 15 individual UI transitions
### 4. Instant Error Cascade
- **Location**: `on_download_failed()` (line ~9096)
- **Issue**: Album failures trigger immediate individual error processing
- **Problem**: 15 instant errors = 15 individual `move_to_finished()` calls
## Solution Architecture
### 1. UI Batch Mode System
**Purpose**: Defer UI updates until all items are ready, then apply in single operation
**Implementation**:
```python
class DownloadQueue:
def start_batch_mode(self):
"""Start collecting items without UI updates"""
self._batch_mode = True
self._batch_items = []
def _add_download_item_batch(self, ...):
"""Add item to batch without immediate UI rendering"""
item = CompactDownloadItem(...)
self.download_items.append(item)
self._batch_items.append(item)
return item
def end_batch_mode(self):
"""Apply all UI updates at once"""
for item in self._batch_items:
self.queue_layout.insertWidget(insert_index, item)
self.update_queue_count() # Single count update
self._batch_items = []
```
**Integration**: Modify `add_download_item()` to check batch mode and route accordingly.
### 2. Enhanced Album Download Processing
**Purpose**: Move album processing to background with batch UI updates
**Implementation**:
```python
def _handle_matched_album_download_v2(self, album_result, artist):
"""Optimized non-blocking album download"""
# Prepare tracks (no blocking operations)
prepared_tracks = []
for track in album_result.tracks:
# Set metadata without sleep delays
track.matched_artist = artist
track.album = clean_album_title
prepared_tracks.append(track)
def batch_download_tracks():
# Start UI batch mode
self.download_queue.start_batch_mode()
try:
# Process in smaller batches (3 tracks at a time)
for i in range(0, len(prepared_tracks), 3):
batch = prepared_tracks[i:i + 3]
for track in batch:
self._start_download_with_artist(track, artist)
# Reduced sleep: 0.05s between batches
if i + 3 < len(prepared_tracks):
time.sleep(0.05)
finally:
# Apply all UI updates at once
self.download_queue.end_batch_mode()
# Submit to background thread pool
self._optimized_api_pool.submit(batch_download_tracks)
# Return immediately (no blocking)
print(f"🚀 Album download queued in background")
```
### 3. Completion Batching System
**Purpose**: Collect completions and process them in batches rather than individually
**Implementation**:
```python
class DownloadsPage:
def __init__(self):
# Completion batching state
self._completion_batch_mode = False
self._completion_batch_items = []
self._completion_batch_timer = QTimer()
self._completion_batch_timer.setSingleShot(True)
self._completion_batch_timer.timeout.connect(self._process_completion_batch)
def _start_completion_batch_mode(self):
"""Activate completion batching for album downloads"""
self._completion_batch_mode = True
self._completion_batch_items = []
self._completion_batch_timer.start(2000) # 2-second collection window
def _add_to_completion_batch(self, download_item):
"""Add item to completion batch instead of immediate processing"""
if self._completion_batch_mode:
self._completion_batch_items.append(download_item)
return True # Item batched
return False # Process immediately
def _process_completion_batch(self):
"""Process all batched completions at once"""
self.download_queue.start_batch_mode() # Enable UI batching
try:
for download_item in self._completion_batch_items:
self.download_queue.move_to_finished(download_item)
finally:
self.download_queue.end_batch_mode() # Apply UI updates
self._completion_batch_mode = False
self._completion_batch_items = []
```
**Integration**: Modify status update completion handling:
```python
# In status update functions (lines ~9880, ~9925, ~9979)
# Replace direct move_to_finished calls with:
if not self._add_to_completion_batch(download_item):
self.download_queue.move_to_finished(download_item)
```
### 4. Error Batching System
**Purpose**: Handle instant failure cascades by batching error processing
**Implementation**:
```python
class DownloadsPage:
def __init__(self):
# Error batching state
self._error_batch_mode = False
self._error_batch_items = []
self._error_batch_timer = QTimer()
self._error_batch_timer.setSingleShot(True)
self._error_batch_timer.timeout.connect(self._process_error_batch)
def _start_completion_batch_mode(self):
"""Modified to also enable error batching"""
# ... existing completion batching code ...
# Also enable error batching for instant failures
self._error_batch_mode = True
self._error_batch_items = []
self._error_batch_timer.start(500) # 500ms window for errors
def _add_to_error_batch(self, download_item):
"""Add item to error batch instead of immediate processing"""
if self._error_batch_mode:
self._error_batch_items.append(download_item)
return True
return False
def _process_error_batch(self):
"""Process all batched errors at once"""
self.download_queue.start_batch_mode()
try:
for download_item in self._error_batch_items:
self.download_queue.move_to_finished(download_item)
finally:
self.download_queue.end_batch_mode()
self._error_batch_mode = False
self._error_batch_items = []
```
**Integration**: Modify `on_download_failed()`:
```python
def on_download_failed(self, error_msg, download_item):
download_item.status = "failed"
download_item.progress = 0
# Use error batch if active to prevent instant cascade
if not self._add_to_error_batch(download_item):
self.download_queue.move_to_finished(download_item)
```
## Implementation Plan
### Step 1: UI Batch Mode System
1. Add batch mode state to `DownloadQueue.__init__()`
2. Create `start_batch_mode()`, `_add_download_item_batch()`, `end_batch_mode()` methods
3. Modify `add_download_item()` to check batch mode
### Step 2: Enhanced Album Processing
1. Create `_handle_matched_album_download_v2()` method
2. Modify existing album download to route to v2 when optimizations enabled
3. Add background thread processing with batch mode integration
### Step 3: Completion Batching
1. Add completion batch state to `DownloadsPage.__init__()`
2. Create completion batching methods
3. Add `_start_completion_batch_mode()` call to album downloads
4. Modify status update functions to use completion batching
### Step 4: Error Batching
1. Add error batch state to `DownloadsPage.__init__()`
2. Create error batching methods
3. Integrate error batching with completion batch activation
4. Modify `on_download_failed()` to use error batching
5. Update status update failure handling
### Step 5: Integration Points
1. **Album Download Start**: Call `_start_completion_batch_mode()`
2. **Status Updates**: Use batching for completed/cancelled/failed states
3. **Cleanup**: Process pending batches in `cleanup_resources()`
## Key Files and Locations
### Primary File: `/ui/pages/downloads.py`
**DownloadQueue Class** (~line 4326):
- Add batch mode methods
- Modify `add_download_item()`
**DownloadsPage Class** (~line 4802):
- Add batch state variables to `__init__()` (~line 4880)
- Create completion batching methods (~line 10800+)
- Create error batching methods (~line 10850+)
- Modify `_handle_matched_album_download_v2()` (~line 7224)
- Modify `on_download_failed()` (~line 9096)
- Modify status update completion handling (~lines 9880, 9925, 9979)
## Expected Results
### Performance Improvements:
1. **Album Download**: Near-instant button response (background processing)
2. **Completion Processing**: Batch transitions instead of individual UI updates
3. **Error Handling**: 500ms batched processing instead of instant cascade
4. **UI Responsiveness**: Smooth interaction during all album operations
### Functional Preservation:
- All existing download functionality maintained
- Complete API compatibility preserved
- Error handling and cleanup operations intact
- Progress tracking and status updates continue working
## Testing Scenarios
1. **Large Album Download**: 15+ track album with successful downloads
2. **Network Failure Album**: Album that fails immediately due to network issues
3. **Mixed Results**: Album with some successes and some failures
4. **Single Track**: Ensure individual downloads still work without batching
5. **Rapid Multiple Albums**: Multiple album downloads in quick succession
## Rollback Strategy
If issues occur:
1. Set feature flags to disable optimizations
2. Comment out batch mode routing in `add_download_item()`
3. Remove batch mode checks in status update functions
4. Restore original album download method routing
The implementation preserves all original functionality while adding performance optimizations that can be easily disabled if needed.

View file

@ -1,504 +0,0 @@
# newMusic Download System - Complete Technical Details
## Overview
The newMusic application implements a sophisticated download management system that handles music downloads from the Soulseek P2P network. This document provides comprehensive technical details on how downloads work, from button clicks to queue management and cleanup operations.
## Download Button Behaviors
### Single Track Downloads
**Button Location:** Individual track results in search interface
**Implementation:** `/ui/pages/downloads.py:1081, 1557`
#### Click Flow
1. **Button Click**: Download button connects to `request_download()` method
```python
download_btn.clicked.connect(self.request_download) # Line 1081
```
2. **Signal Emission**: `request_download()` emits `track_download_requested` signal
```python
def request_download(self): # Line 1110
self.track_download_requested.emit(self.search_result)
```
3. **Signal Connection**: Connected to `start_download()` method in DownloadsPage
```python
# Connections at lines 4350, 4833, 4985
result_item.track_download_requested.connect(self.start_download)
```
4. **Download Initiation**: `start_download()` method processes the request
```python
def start_download(self, search_result): # Line 5065
```
#### Processing Steps in `start_download()`
1. **Track Information Extraction** (Lines 5069-5107):
```python
# Parse filename for metadata
filename = search_result.filename
title = search_result.title or "Unknown Track"
artist = search_result.artist or "Unknown Artist"
```
2. **Unique ID Generation** (Lines 5109-5111):
```python
download_id = f"{search_result.user}_{filename}"
```
3. **Queue Addition** (Lines 5114-5124):
```python
self.download_queue.add_download_item(
title=title,
artist=artist,
status="downloading",
progress=0,
file_size=search_result.size,
download_id=download_id,
username=search_result.user
)
```
4. **Download Thread Creation** (Lines 5127-5140):
```python
download_thread = DownloadThread(
search_result=search_result,
soulseek_client=self.soulseek_client
)
download_thread.start()
```
### Album Downloads
**Button Location:** Album result headers
**Implementation:** `/ui/pages/downloads.py:1329`
#### Click Flow
1. **Button Click**: Album download button connects to `request_album_download()`
```python
self.download_btn.clicked.connect(self.request_album_download) # Line 1329
```
2. **Signal Emission**: Emits `album_download_requested` signal
```python
def request_album_download(self): # Line 1382
self.album_download_requested.emit(self.album_result)
```
3. **Album Processing**: `start_album_download()` handles batch operations
```python
def start_album_download(self, album_result): # Line 5147
# Disable all track buttons for this album
for track_item in self.search_results_items:
if hasattr(track_item, 'search_result') and track_item.search_result.album == album_result.title:
track_item.set_download_downloading_state() # Line 5153
# Download each track individually
for track in album_result.tracks:
self.start_download(track) # Line 5157
```
### Individual Track Downloads from Albums
**Behavior:** Identical to single track downloads
**Implementation:** Same signal/slot mechanism as standalone tracks
- Album track items emit `track_download_requested` signal (Line 1367)
- Connected to same `start_download()` method
- No functional difference from standalone single track downloads
## Queue Management System
### Data Structures
#### Primary Queue Components
- **TabbedDownloadManager**: Container for both active and finished queues
- **DownloadQueue**: Individual queue implementation for items
- **CompactDownloadItem**: UI widget representing each download
#### Queue Storage
```python
class DownloadQueue(QScrollArea):
def __init__(self):
self.download_items = [] # List of CompactDownloadItem objects
self.layout = QVBoxLayout()
```
#### Download Item Structure
```python
class CompactDownloadItem(QFrame):
def __init__(self, title: str, artist: str, status: str = "queued",
progress: int = 0, file_size: int = 0, download_speed: int = 0,
file_path: str = "", download_id: str = "", username: str = "",
soulseek_client=None, queue_type: str = "active", parent=None):
```
### Queue States and Transitions
#### Download States
- **"queued"**: Initial state when added to queue
- **"downloading"**: Active download in progress
- **"completed"**: Successfully finished download
- **"failed"**: Download encountered an error
- **"cancelled"/"canceled"**: User cancelled the download
- **"finished"**: Generic completion state
- **Compound states**: "completed, succeeded", "completed, cancelled"
#### State Transition Flow
1. **Initial Addition**: Items added as "downloading" status
```python
self.download_queue.add_download_item(status="downloading") # Line 5117
```
2. **Progress Updates**: Via `TransferStatusThread` monitoring
```python
def on_download_progress(self, download_id, status_data): # Line 5891
# Update progress bar and status
```
3. **Completion Handling**:
```python
def on_download_completed(self, download_id, status_data): # Line 5860
# Move to finished queue
```
4. **Queue Movement**: Automatic transition to finished queue
```python
def move_to_finished(self, item): # Line 3175
self.finished_queue.add_download_item(...)
self.active_queue.remove_download_item(item)
```
### Threading and Concurrency
#### Thread Types
1. **DownloadThread**: Individual download initiation
2. **TransferStatusThread**: Real-time progress monitoring
3. **TrackedStatusUpdateThread**: Periodic status updates
#### Thread Management
```python
# Thread tracking collections
self.download_threads = [] # Active download threads
self.status_update_threads = [] # Status monitoring threads
# Thread cleanup
def cleanup_all_threads(self):
for thread in self.download_threads:
thread.stop()
if not thread.wait(2000): # 2 second timeout
thread.terminate()
```
#### Concurrency Safety
- Qt's `QueuedConnection` for cross-thread signals
- Individual asyncio event loops per thread
- Proper signal disconnection during cleanup
- Thread-safe status updates via signal/slot mechanism
## File Operations
### Download Location Management
#### Initial Storage
- **Primary Path**: Configured via `config_manager.get('soulseek.download_path')`
- **Default Structure**: Downloads maintain uploader's directory structure
- **File Verification**: Minimum 1KB size and audio format validation
#### File Movement Operations
```python
def _find_downloaded_file(self, download_path): # Line 903
"""Locate downloaded file in directory tree"""
audio_extensions = {'.mp3', '.flac', '.ogg', '.aac', '.wma', '.wav', '.m4a'}
for root, dirs, files in os.walk(download_path):
# Match filename and verify audio format
```
#### Storage Locations
- **Downloads**: `./downloads/` (primary download location)
- **Streaming**: `./Stream/` (temporary playback files)
- **Transfer**: `./Transfer/` (intended for matched downloads - future feature)
### Progress Tracking Mechanism
#### Real-time Monitoring
```python
class TransferStatusThread(QThread): # Line 411
def run(self):
while not self._stop_requested:
downloads = await self.soulseek_client.get_all_downloads()
self.status_updated.emit(downloads or [])
await asyncio.sleep(2) # Poll every 2 seconds
```
#### Progress Data Flow
1. **API Polling**: Queries `/api/v0/transfers/downloads` endpoint
2. **Data Processing**: Parses nested JSON structure from slskd
3. **Progress Extraction**: Matches downloads by filename/username
4. **UI Updates**: Emits signals to update progress bars
## Clear Completed Button Functionality
### Button Implementation
**Location:** Both in queue controls and main download area
**Connections:**
- Line 4457: `clear_btn.clicked.connect(self.clear_completed_downloads)`
- Line 6486: `clear_btn.clicked.connect(self.clear_completed_downloads)`
### Complete Clear Operation Flow
#### Main Method: `clear_completed_downloads()`
**Location:** `/ui/pages/downloads.py:5976`
#### Step-by-Step Process
1. **Initial Validation** (Lines 5983-5990):
```python
if not self.soulseek_client:
print("[ERROR] No soulseek_client available!")
return
```
2. **UI Callback Setup** (Lines 5992-6003):
```python
def update_ui_callback():
"""UI updates after backend clearing"""
self.download_queue.clear_local_queues_only()
self.update_download_manager_stats()
```
3. **Background Thread Operation** (Lines 6004-6054):
```python
def run_clear_operation():
"""Separate thread for backend operations"""
# Create new event loop for this thread
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
# Clear from slskd backend
success = await self.soulseek_client.clear_all_completed_downloads()
finally:
loop.close()
# Signal completion back to main thread
self.clear_completed_finished.emit(success, update_ui_callback)
```
#### Backend API Call
**Method:** `SoulseekClient.clear_all_completed_downloads()`
**Location:** `/core/soulseek_client.py:875`
**Endpoint:** `DELETE /api/v0/transfers/downloads/all/completed`
```python
async def clear_all_completed_downloads(self):
"""Clear all completed downloads from slskd backend"""
try:
response = await self._api_delete('/api/v0/transfers/downloads/all/completed')
success = response is not None
return success
except Exception as e:
logger.error(f"Error clearing completed downloads: {e}")
return False
```
#### Queue-Level Clearing
**Method:** `DownloadQueue.clear_completed_downloads()`
**Location:** `/ui/pages/downloads.py:3071`
```python
def clear_completed_downloads(self):
"""Remove all completed and cancelled download items"""
items_to_remove = []
for item in self.download_items:
status_lower = item.status.lower()
# Check for completion states
if (status_lower in ["completed", "finished", "cancelled", "canceled", "failed"] or
"completed" in status_lower):
items_to_remove.append(item)
# Remove items from queue
for item in items_to_remove:
self.remove_download_item(item)
```
#### Status Identification Logic
**Completed States Detection:**
```python
# Primary states
["completed", "finished", "cancelled", "canceled", "failed"]
# Partial matching for compound states
"completed" in status_lower # Catches "completed, succeeded", etc.
```
### Thread-Safe Communication
#### Signal-Based Completion Handling
```python
# Signal definition
clear_completed_finished = pyqtSignal(bool, object) # Line 3278
# Signal connection
self.clear_completed_finished.connect(self._handle_clear_completion) # Line 3308
# Completion handler
def _handle_clear_completion(self, backend_success, ui_callback): # Line 6061
"""Handle completion on main thread"""
if ui_callback:
ui_callback() # Execute UI updates safely
```
## Download States and Lifecycle
### Complete State Machine
#### State Definitions
```python
DOWNLOAD_STATES = {
"queued": "Initial state, waiting to start",
"downloading": "Active download in progress",
"completed": "Successfully finished",
"failed": "Download encountered error",
"cancelled": "User cancelled download",
"finished": "Generic completion state"
}
```
#### Button State Management
##### State Methods
```python
def set_download_queued_state(self): # Line 1133
def set_download_downloading_state(self): # Line 1147
def set_download_completed_state(self): # Line 1161
def reset_download_state(self): # Line 1175
```
##### Button Visual States
- **Available**: Green download button, clickable
- **Queued**: Orange "Queued" button, disabled
- **Downloading**: Blue "Downloading..." with progress
- **Completed**: Green "Downloaded" button, disabled
### Error Handling and Recovery
#### Download Cancellation
```python
def cancel_download(self): # Line 2439
"""Cancel active download"""
if self.download_thread and self.download_thread.isRunning():
self.download_thread.stop()
# Update backend via API
self.soulseek_client.cancel_download(self.download_id, self.username, remove=True)
```
#### Retry Mechanism
```python
def retry_download(self): # Line 2480
"""Retry failed download"""
self.reset_download_state()
self.request_download() # Re-emit download request
```
#### Thread Cleanup
```python
def on_download_thread_finished(self, download_id): # Line 5898
"""Clean up finished download threads"""
self.download_threads = [t for t in self.download_threads if t.download_id != download_id]
```
## Memory Management and Performance
### Queue Optimization
#### Efficient Item Display
- **Fixed Height Items**: 70-120px per item for memory efficiency
- **Scroll Optimization**: Only visible items fully rendered
- **Compact Design**: Minimal widget overhead per download
#### Thread Management
```python
def cleanup_all_threads(self):
"""Comprehensive thread cleanup"""
# Disconnect all signals first
for thread in self.download_threads:
thread.disconnect()
# Stop threads gracefully
for thread in self.download_threads:
thread.stop()
if not thread.wait(2000): # 2 second timeout
thread.terminate()
# Clear collections
self.download_threads.clear()
self.status_update_threads.clear()
```
### Memory Leak Prevention
#### Signal Disconnection
```python
# Proper cleanup pattern
thread.disconnect() # Disconnect all signals
thread.stop() # Request stop
thread.wait(2000) # Wait for graceful exit
thread.terminate() # Force stop if needed
thread.deleteLater() # Qt cleanup
```
#### Event Loop Management
```python
# Per-thread event loop creation
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
# Async operations
finally:
loop.close() # Always close loops
```
## Integration Points for Spotify Matching
### Current System Extension Points
Based on the download system analysis, the Spotify matching functionality should integrate at these key points:
#### 1. Download Initiation
**Extension Point:** `start_download()` method (Line 5065)
**Integration:** Add matched download variant that calls Spotify matching modal before download
#### 2. Queue Integration
**Extension Point:** `add_download_item()` method (Line 3006)
**Integration:** Support "matched" download type with enhanced metadata
#### 3. File Organization
**Extension Point:** Download completion handling (Line 5860)
**Integration:** Apply Spotify metadata and organize into Transfer folder structure
#### 4. Clear Operations
**Extension Point:** `clear_completed_downloads()` (Line 5976)
**Integration:** Matched downloads participate in same cleanup process
### Recommended Implementation Approach
1. **Create Matched Download Variants**:
- `start_matched_download()` - Show Spotify matching modal first
- `SpotifyMatchingModal` - New UI component for artist selection
- Enhanced queue items with Spotify metadata
2. **Extend File Operations**:
- Post-download metadata application
- Transfer folder organization
- Cover art downloading
3. **Maintain Compatibility**:
- Use same thread management patterns
- Follow existing signal/slot architecture
- Preserve queue state management
The current download system provides a robust foundation for the Spotify matching enhancement, with clear extension points and well-defined interfaces for adding the matching functionality while maintaining system stability and performance.

View file

@ -1,732 +0,0 @@
# newMusic Downloads System - Comprehensive Analysis
## Executive Summary
This document provides a comprehensive analysis of the downloads.py file (10,668 lines) in the newMusic application. The analysis focuses on the download queue system, finished downloads management, transfer mechanisms, and performance bottlenecks that cause UI blocking and the reported 1-second lag spikes.
## Table of Contents
1. [Architecture Overview](#architecture-overview)
2. [Key Classes Analysis](#key-classes-analysis)
3. [Performance Bottlenecks](#performance-bottlenecks)
4. [Data Flow Mapping](#data-flow-mapping)
5. [Threading Analysis](#threading-analysis)
6. [Optimization Opportunities](#optimization-opportunities)
---
## Architecture Overview
The downloads.py file implements a complex music download management system with the following architectural layers:
### System Components
- **UI Layer**: PyQt6-based user interface components
- **Download Management**: Queue-based download tracking and status management
- **API Integration**: Soulseek P2P network integration via slskd API
- **Spotify Integration**: Artist matching and metadata enhancement
- **Threading Layer**: Background processing for non-blocking operations
### Key Architectural Patterns
- **Tabbed Interface**: Separates active and finished downloads
- **Queue Management**: FIFO download processing with status tracking
- **Thread Pool**: Background workers for API calls and processing
- **Signal-Slot Communication**: PyQt6 event-driven architecture
- **State Machine**: Download state transitions (queued → in_progress → completed/failed)
---
## Key Classes Analysis
### Data Classes
#### ArtistMatch (Line 23)
```python
@dataclass
class ArtistMatch:
artist: Artist
confidence: float
match_reason: str = ""
```
**Purpose**: Represents Spotify artist matching results with confidence scoring
**Performance Impact**: Minimal - simple data container
#### AlbumMatch (Line 30)
```python
@dataclass
class AlbumMatch:
album: Album
confidence: float
match_reason: str = ""
```
**Purpose**: Represents Spotify album matching results
**Performance Impact**: Minimal - simple data container
### Worker Classes
#### DownloadCompletionWorker (Line 41)
**Purpose**: Background processing for download completion
**Key Methods**:
- `run()`: Processes download completion with file organization
**Performance Impact**:
- **POSITIVE**: Moves file organization off main thread
- **ISSUE**: Uses `time.sleep(1)` which blocks worker thread
#### OptimizedDownloadCompletionWorker (Line 73)
**Purpose**: Optimized version of download completion processing
**Key Methods**:
- `run()`: Improved file stability checking
**Performance Impact**:
- **POSITIVE**: Reduces sleep time to 0.1s
- **ISSUE**: Still uses blocking sleep calls
#### ThreadSafeQueueManager (Line 115)
**Purpose**: Thread-safe operations for download queue management
**Key Methods**:
- `add_download_item_safe()`: Thread-safe item addition
- `remove_download_item_safe()`: Thread-safe item removal
- `atomic_state_transition()`: Atomic state changes
**Performance Impact**:
- **POSITIVE**: Prevents race conditions
- **ISSUE**: Multiple lock acquisitions could cause contention
### UI Classes
#### SpotifyMatchingModal (Line 165)
**Purpose**: Artist/album matching interface before download
**Key Methods**:
- `setup_ui()`: Creates modal interface
- `generate_auto_suggestions()`: Auto-matching algorithm
- `perform_manual_search()`: Manual search functionality
**Performance Impact**:
- **ISSUE**: Modal blocks UI until selection
- **ISSUE**: API calls on main thread during search
#### DownloadQueue (Line 4326)
**Purpose**: Visual container for download items
**Key Methods**:
- `add_download_item()`: Adds new download to queue
- `remove_download_item()`: Removes download from queue
- `clear_completed_downloads()`: Bulk removal of completed items
**Performance Impact**:
- **POSITIVE**: Efficient widget management
- **ISSUE**: Linear search through items for removal
#### TabbedDownloadManager (Line 4554)
**Purpose**: Manages active and finished download tabs
**Key Methods**:
- `move_to_finished()`: Transfers items between queues
- `clear_completed_downloads()`: Clears completed items
- `update_tab_counts()`: Updates tab labels
**Performance Impact**:
- **CRITICAL**: `move_to_finished()` creates new widgets instead of moving existing ones
- **ISSUE**: Tab count updates trigger on every change
### Thread Classes
#### DownloadThread (Line 1456)
**Purpose**: Background download initiation
**Key Methods**:
- `run()`: Initiates download via slskd API
**Performance Impact**:
- **POSITIVE**: Non-blocking download initiation
- **ISSUE**: Creates new thread for each download
#### TransferStatusThread (Line 1637)
**Purpose**: Background transfer status polling
**Key Methods**:
- `run()`: Polls slskd transfers API
**Performance Impact**:
- **CRITICAL**: New thread created every 1 second
- **ISSUE**: Thread creation overhead accumulates
#### ApiCleanupThread (Line 1700)
**Purpose**: Background API cleanup operations
**Key Methods**:
- `run()`: Removes completed downloads from slskd
**Performance Impact**:
- **POSITIVE**: Non-blocking cleanup
- **ISSUE**: Thread lifecycle management problems
### Main Classes
#### DownloadsPage (Line 4802)
**Purpose**: Main downloads management interface
**Key Methods**:
- `update_download_status()`: **CRITICAL PERFORMANCE BOTTLENECK**
- `clear_completed_downloads()`: Bulk cleanup operations
- `perform_search()`: Search functionality
**Performance Impact**:
- **CRITICAL**: Main source of 1-second lag spikes
- **ISSUE**: Complex nested loops in status updates
---
## Performance Bottlenecks
### Critical Issue: update_download_status() Method (Lines 9547-9900+)
#### The Problem
This method executes every 1000ms via QTimer and performs extremely expensive operations on the main UI thread:
```python
def update_download_status(self):
# Called every 1000ms by QTimer
for download_item in self.download_queue.download_items.copy():
for transfer in all_transfers: # Nested loop!
# 5 different matching strategies
# Import statements inside loops
# Complex string operations
# Regex compilation
```
#### Performance Analysis
- **Complexity**: O(n*m*k) where n=downloads, m=transfers, k=matching strategies
- **Execution Frequency**: Every 1000ms
- **Thread**: Main UI thread (blocks interface)
- **Duration**: Can take 200-500ms with moderate queues
#### Specific Bottlenecks
##### 1. Nested Loop Structure (Lines 9578-9704)
```python
for download_item in self.download_queue.download_items.copy():
for transfer in all_transfers:
# 5 matching strategies executed for each combination
```
- **Impact**: O(n*m) complexity
- **Real-world**: 10 downloads × 100 transfers = 1000 iterations
##### 2. Repeated Imports (Lines 9608, 9655, 9684)
```python
# Inside nested loops:
import os # Line 9608
import re # Line 9655
import re # Line 9684
```
- **Impact**: Import overhead multiplied by loop iterations
- **Solution**: Move imports to module level
##### 3. Complex String Operations (Lines 9609-9646)
```python
# Repeated for every transfer/download combination:
basename = os.path.basename(full_filename).lower()
download_title_lower = download_item.title.lower()
```
- **Impact**: String processing overhead in nested loops
- **Solution**: Pre-compute and cache results
##### 4. Regex Compilation (Line 9655)
```python
# Inside loop:
import re
core_title = re.sub(r'\([^)]*\)', '', download_item.title)
```
- **Impact**: Regex compilation on every iteration
- **Solution**: Pre-compile patterns
##### 5. Thread Creation (Lines 9967-9982)
```python
# Creates new thread every 1000ms:
status_thread = TransferStatusThread(self.soulseek_client)
status_thread.start()
```
- **Impact**: Thread creation overhead every second
- **Solution**: Thread pooling
### Secondary Performance Issues
#### 1. Widget Creation in move_to_finished() (Line 4649)
```python
finished_item = self.finished_queue.add_download_item(
# Creates entirely new widget instead of moving existing
)
```
- **Impact**: Unnecessary widget creation/destruction
- **Solution**: Move existing widgets between containers
#### 2. Synchronous API Calls
Multiple methods perform synchronous API calls on main thread:
- `clear_completed_downloads()` (Line 9141)
- Various status update calls
- **Impact**: UI freezing during API calls
- **Solution**: Async API calls with proper threading
#### 3. Linear Search Operations
- `remove_download_item()` uses linear search
- `find_item_by_id()` iterates through entire list
- **Impact**: O(n) operations on every removal
- **Solution**: Use dictionaries for O(1) lookups
---
## Data Flow Mapping
### Download Lifecycle
1. **Search & Selection**: User searches → selects track → Spotify matching modal
2. **Download Initiation**: Modal selection → DownloadThread → slskd API call
3. **Active Queue**: Download added to active queue → progress tracking begins
4. **Status Monitoring**: QTimer triggers `update_download_status()` every 1000ms
5. **Completion**: Download completes → move_to_finished() → finished queue
6. **Cleanup**: Background cleanup removes from slskd API
### Queue Management Flow
```
┌─────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Search UI │ -> │ Active Queue │ -> │ Finished Queue │
│ (SpotifyModal) │ │ (DownloadQueue) │ │ (DownloadQueue) │
└─────────────────┘ └──────────────────┘ └──────────────────┘
│ │ │
│ │ │
v v v
┌─────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ DownloadThread │ │ Status Monitor │ │ Cleanup Thread │
│ (Background) │ │ (QTimer) │ │ (Background) │
└─────────────────┘ └──────────────────┘ └──────────────────┘
```
### Data Structure Relationships
- **TabbedDownloadManager**: Contains active_queue and finished_queue
- **DownloadQueue**: Contains list of CompactDownloadItem widgets
- **CompactDownloadItem**: Individual download with status and progress
- **Threading**: Multiple background threads for different operations
---
## Threading Analysis
### Current Threading Model
#### Thread Types
1. **Main Thread**: UI updates, QTimer callbacks, most processing
2. **DownloadThread**: Download initiation (short-lived)
3. **TransferStatusThread**: Status polling (created every 1000ms)
4. **ApiCleanupThread**: Background cleanup (intermittent)
5. **Various Worker Threads**: Search, suggestions, etc.
#### Threading Issues
##### 1. Thread Proliferation
```python
# Creates new thread every second:
status_thread = TransferStatusThread(self.soulseek_client)
status_thread.start()
```
- **Problem**: Accumulating thread overhead
- **Impact**: Memory leaks, resource exhaustion
- **Solution**: Thread pooling with reusable workers
##### 2. Main Thread Blocking
```python
def update_download_status(self):
# This runs on main thread and blocks UI
for download_item in downloads:
for transfer in transfers:
# Complex processing
```
- **Problem**: Heavy processing on main thread
- **Impact**: UI freezing, lag spikes
- **Solution**: Background processing with signals
##### 3. Thread Lifecycle Management
- Threads created but not properly cleaned up
- Memory leaks from abandoned threads
- No thread limits or pooling
##### 4. Race Conditions
- Multiple threads accessing download_items list
- Inconsistent state during transfers
- ThreadSafeQueueManager helps but not used everywhere
### Recommended Threading Architecture
#### Thread Pool Pattern
```python
class OptimizedDownloadsPage:
def __init__(self):
self.thread_pool = QThreadPool()
self.thread_pool.setMaxThreadCount(4)
def update_download_status(self):
# Don't create new threads - use pool
worker = StatusUpdateWorker(self.get_status_data())
self.thread_pool.start(worker)
```
#### Background Processing
- Move all expensive operations to background threads
- Use signals for thread-to-main communication
- Implement proper thread cleanup
---
## Optimization Opportunities
### Immediate Fixes (High Impact)
#### 1. Move Imports to Module Level
**Current**: Imports inside nested loops
**Fix**: Move all imports to top of file
**Impact**: Eliminates import overhead
#### 2. Pre-compile Regex Patterns
**Current**: Regex compilation in loops
**Fix**: Pre-compile at class initialization
**Impact**: Significant performance improvement
#### 3. Cache Expensive Operations
**Current**: Repeated string processing
**Fix**: Cache normalized strings and filenames
**Impact**: Reduces computational overhead
#### 4. Implement Thread Pooling
**Current**: New thread every 1000ms
**Fix**: Reusable worker threads
**Impact**: Eliminates thread creation overhead
### Medium-term Improvements
#### 1. Optimize Data Structures
- Replace lists with dictionaries for O(1) lookups
- Index downloads by ID for faster matching
- Use sets for duplicate tracking
#### 2. Batch UI Updates
- Collect all status changes
- Apply updates in single batch
- Reduce widget redraw overhead
#### 3. Async API Integration
- Convert synchronous API calls to async
- Use proper async/await patterns
- Implement request batching
### Long-term Architectural Changes
#### 1. Reactive Architecture
- Event-driven state management
- Unidirectional data flow
- Proper separation of concerns
#### 2. Background Processing Pipeline
- Queue-based processing
- Worker thread pool
- Proper error handling
#### 3. Memory Management
- Widget recycling
- Proper cleanup procedures
- Memory profiling integration
---
## Specific Recommendations
### 1. Critical Fix: update_download_status() Optimization
```python
class OptimizedDownloadsPage:
def __init__(self):
# Pre-compile regex patterns
self.track_number_pattern = re.compile(r'^(\d+)\.\s*(.+)')
self.parenthetical_pattern = re.compile(r'\([^)]*\)')
# Cache for expensive operations
self.filename_cache = {}
self.transfer_index = {}
# Thread pool for background processing
self.thread_pool = QThreadPool()
self.thread_pool.setMaxThreadCount(2)
def update_download_status(self):
# Move to background thread
worker = StatusUpdateWorker(self.get_current_state())
worker.signals.completed.connect(self.handle_status_update)
self.thread_pool.start(worker)
```
### 2. Efficient Queue Management
```python
class OptimizedDownloadQueue:
def __init__(self):
self.download_items = []
self.items_by_id = {} # O(1) lookup
def add_download_item(self, item):
self.download_items.append(item)
self.items_by_id[item.download_id] = item
def remove_download_item(self, item):
if item.download_id in self.items_by_id:
del self.items_by_id[item.download_id]
self.download_items.remove(item)
```
### 3. Background Processing Pattern
```python
class BackgroundWorker(QRunnable):
def __init__(self, data):
super().__init__()
self.data = data
self.signals = WorkerSignals()
def run(self):
# Process data in background
results = self.process_data()
# Emit results to main thread
self.signals.completed.emit(results)
```
---
## Detailed Class Analysis
### Widget Classes
#### DownloadItem (Line 3461)
**Purpose**: Full-featured download widget (legacy)
**Key Methods**:
- `setup_ui()`: Creates detailed download interface
- `mark_completion_processed()`: Thread-safe completion tracking
- `update_status()`: Status and progress updates
**Performance Impact**:
- **POSITIVE**: Thread-safe completion tracking
- **ISSUE**: Heavy widget with complex styling
#### CompactDownloadItem (Line 3882)
**Purpose**: Optimized download widget for queue display
**Key Methods**:
- `setup_ui()`: Creates compact interface
- `get_display_filename()`: Filename processing
- `update_status()`: Efficient status updates
**Performance Impact**:
- **POSITIVE**: Compact design reduces memory usage
- **ISSUE**: Still creates new widgets in move_to_finished()
### Search and UI Components
#### TrackItem (Line 2238)
**Purpose**: Individual track display in search results
**Key Methods**:
- `setup_ui()`: Track display interface
- `download_track()`: Download initiation
**Performance Impact**: Minimal - search result display only
#### AlbumResultItem (Line 2489)
**Purpose**: Album display in search results
**Key Methods**:
- `setup_ui()`: Album display interface
- `download_album()`: Bulk album download
**Performance Impact**:
- **ISSUE**: Bulk downloads create multiple threads
#### SearchResultItem (Line 2767)
**Purpose**: Generic search result container
**Key Methods**:
- `setup_ui()`: Result display interface
**Performance Impact**: Minimal - display only
### Threading and Worker Classes
#### SearchThread (Line 1751)
**Purpose**: Background search operations
**Key Methods**:
- `run()`: Executes search queries
**Performance Impact**:
- **POSITIVE**: Non-blocking search
- **ISSUE**: Creates new thread for each search
#### StreamingThread (Line 1866)
**Purpose**: Audio streaming functionality
**Key Methods**:
- `run()`: Handles audio streaming
- `_cleanup_completed_streaming_downloads()`: Cleanup operations
**Performance Impact**:
- **POSITIVE**: Background streaming
- **ISSUE**: Complex cleanup logic
#### TrackedStatusUpdateThread (Line 1816)
**Purpose**: Managed status update threading
**Key Methods**:
- `run()`: Status update processing
**Performance Impact**:
- **POSITIVE**: Better thread management than TransferStatusThread
- **ISSUE**: Still creates new threads
### Utility and Helper Classes
#### BouncingDotsWidget (Line 1191)
**Purpose**: Loading animation widget
**Key Methods**:
- `paintEvent()`: Custom painting for animation
**Performance Impact**:
- **ISSUE**: Frequent repaints during animation
#### SpinningCircleWidget (Line 1259)
**Purpose**: Alternative loading animation
**Key Methods**:
- `paintEvent()`: Spinning circle animation
**Performance Impact**:
- **ISSUE**: Continuous repaints
#### AudioPlayer (Line 1335)
**Purpose**: Media player functionality
**Key Methods**:
- `play()`: Audio playback
- `pause()`: Playback control
**Performance Impact**: Minimal - standard media player
---
## Critical Performance Analysis
### Primary Bottleneck: update_download_status() Deep Dive
#### Method Structure Analysis (Lines 9547-10000+)
```python
def update_download_status(self):
# Phase 1: Data Collection (Lines 9549-9567)
if not self.soulseek_client or not self.download_queue.download_items:
return
# Phase 2: Threading Decision (Lines 9552-9554)
if hasattr(self, '_use_optimized_systems') and self._use_optimized_systems:
return self.update_download_status_v2()
# Phase 3: Adaptive Polling (Line 9557)
self._update_adaptive_polling()
# Phase 4: Main Processing Loop (Lines 9578-9950)
# THIS IS WHERE THE PERFORMANCE PROBLEMS OCCUR
```
#### Detailed Loop Analysis
The core bottleneck occurs in the nested matching loops:
```python
# Outer loop: For each download item (Lines 9578-9579)
for download_item in self.download_queue.download_items.copy():
if download_item.status.lower() in ['completed', 'finished', 'cancelled', 'failed']:
continue # Skip completed items
# Inner loop: For each transfer (Lines 9587-9704)
for transfer in all_transfers:
# EXPENSIVE OPERATIONS HAPPEN HERE:
# 1. Import statements (Lines 9608, 9655, 9684)
import os # Repeated import
import re # Repeated import
# 2. String processing (Lines 9609-9614)
basename = os.path.basename(full_filename).lower()
download_title_lower = download_item.title.lower()
basename_lower = basename.lower()
# 3. Multiple matching strategies (Lines 9626-9704)
# Strategy 1: Direct filename match
# Strategy 2: Track title substring matching
# Strategy 3: Album track parsing
# Strategy 3.5: Core track name matching
# Strategy 4: Word matching
# Strategy 5: File path matching
```
#### Performance Calculations
For a typical scenario:
- **Downloads**: 10 active items
- **Transfers**: 100 total transfers
- **Matching strategies**: 5 per combination
- **Total iterations**: 10 × 100 × 5 = 5,000 operations
- **Execution time**: 200-500ms on main thread
- **Frequency**: Every 1000ms
- **Result**: 20-50% UI blocking every second
### Secondary Performance Issues
#### 1. Widget Lifecycle Management
```python
# In move_to_finished() - INEFFICIENT PATTERN:
finished_item = self.finished_queue.add_download_item(
title=download_item.title,
artist=download_item.artist,
# ... recreate entire widget
)
```
**Problem**: Creates new widget instead of moving existing one
**Impact**: Unnecessary memory allocation and UI updates
#### 2. Thread Management Problems
```python
# In update_download_status() - CREATES THREAD EVERY SECOND:
status_thread = TransferStatusThread(self.soulseek_client)
status_thread.transfer_status_completed.connect(handle_status_update)
status_thread.start()
```
**Problem**: No thread reuse or pooling
**Impact**: Thread creation overhead accumulates
#### 3. API Call Patterns
Multiple synchronous API calls block the main thread:
- `clear_all_completed_downloads()` in cleanup methods
- Status polling in various update methods
- Transfer data retrieval
---
## Memory Management Analysis
### Memory Leaks and Issues
#### 1. Thread Accumulation
- New threads created every second
- Threads not properly cleaned up
- Memory usage grows over time
#### 2. Widget Accumulation
- Widgets recreated instead of moved
- Old widgets not properly deleted
- UI memory usage increases
#### 3. Cache Management
- No cleanup of filename_cache
- Transfer data accumulates
- Memory usage grows with usage time
### Resource Usage Patterns
#### CPU Usage
- Periodic spikes every 1000ms
- High CPU during status updates
- Inefficient string processing
#### Memory Usage
- Gradual increase over time
- Widget and thread accumulation
- Cache growth without limits
---
## Conclusion
The downloads.py file contains a sophisticated but performance-problematic download management system. The primary issue is the `update_download_status()` method which performs expensive operations on the main UI thread every 1000ms, causing the reported lag spikes.
**Critical Issues Identified**:
1. **O(n*m*k) complexity** in status update loops
2. **Thread proliferation** with new threads every second
3. **Main thread blocking** for 200-500ms every second
4. **Memory leaks** from thread and widget accumulation
5. **Inefficient widget management** recreating instead of moving
**Immediate Actions Required**:
1. Move expensive operations to background threads
2. Pre-compile regex patterns and cache results
3. Implement proper thread pooling
4. Optimize data structures for O(1) lookups
5. Fix widget movement instead of recreation
**Success Metrics**:
- Eliminate 1-second lag spikes
- Reduce main thread blocking time by 80%
- Maintain full functional compatibility
- Improve scalability for large download queues
- Fix memory leaks and resource accumulation
The system is well-structured but needs performance optimization to handle the real-world usage patterns that cause UI blocking.

546
helper.md
View file

@ -1,546 +0,0 @@
# Downloads.py Deep Dive - Queue System and API Cleanup Documentation
## Overview
This document provides a comprehensive analysis of the download queue system in `/ui/pages/downloads.py` (10,668 lines). The system manages music downloads from the Soulseek network with sophisticated queue management, status tracking, and API synchronization.
## Architecture Overview
### Core Components
The download system consists of several key classes working together:
1. **DownloadQueue** - Individual queue management (active/finished)
2. **TabbedDownloadManager** - Tabbed interface coordinator
3. **CompactDownloadItem** - Individual download item UI
4. **ThreadSafeQueueManager** - Thread-safe operations
5. **ApiCleanupThread** - Background API cleanup
### Data Flow
```
User Action → Download Queue → Status Updates → API Cleanup → UI Updates
```
## Download Queue System
### DownloadQueue Class (Lines 4326-4553)
The `DownloadQueue` class manages individual queues with two modes:
```python
class DownloadQueue(QFrame):
def __init__(self, title="Download Queue", queue_type="active", parent=None):
self.queue_type = queue_type # "active" or "finished"
self.download_items = []
self.setup_ui()
```
#### Key Properties:
- **queue_type**: `"active"` or `"finished"`
- **download_items**: List of `CompactDownloadItem` objects
- **queue_count_label**: Shows item count in UI
- **empty_message**: Placeholder when queue is empty
#### Critical Methods:
**add_download_item()** (Line 4436):
```python
def add_download_item(self, title: str, artist: str, status: str = "queued",
progress: int = 0, file_size: int = 0, download_speed: int = 0,
file_path: str = "", download_id: str = "", username: str = "",
soulseek_client=None, album: str = None, track_number: int = None):
# Hide empty message if first item
if len(self.download_items) == 0:
self.empty_message.hide()
# Create new CompactDownloadItem
item = CompactDownloadItem(title, artist, status, progress, file_size, download_speed,
file_path, download_id, username, soulseek_client, self.queue_type,
album, track_number)
self.download_items.append(item)
# Insert before stretch
insert_index = self.queue_layout.count() - 1
self.queue_layout.insertWidget(insert_index, item)
self.update_queue_count()
return item
```
**remove_download_item()** (Line 4470):
```python
def remove_download_item(self, item):
if item in self.download_items:
self.download_items.remove(item)
self.queue_layout.removeWidget(item)
self._schedule_widget_deletion(item) # Batched deletion for performance
self.update_queue_count()
# Notify parent to update tab counts
parent_widget = self.parent()
while parent_widget and not hasattr(parent_widget, 'update_tab_counts'):
parent_widget = parent_widget.parent()
if parent_widget:
parent_widget.update_tab_counts()
```
**clear_completed_downloads()** (Line 4519):
```python
def clear_completed_downloads(self):
items_to_remove = []
for item in self.download_items:
status_lower = item.status.lower()
should_remove = False
# Check for exact matches
if status_lower in ["completed", "finished", "cancelled", "canceled", "failed"]:
should_remove = True
# Check for partial matches (handles compound statuses)
elif any(keyword in status_lower for keyword in
["completed", "finished", "cancelled", "canceled", "failed", "succeeded"]):
should_remove = True
if should_remove:
items_to_remove.append(item)
for item in items_to_remove:
self.remove_download_item(item)
```
### TabbedDownloadManager Class (Lines 4554-4801)
Manages both active and finished queues in a tabbed interface:
```python
class TabbedDownloadManager(QTabWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.active_queue = DownloadQueue("Active Downloads", "active")
self.finished_queue = DownloadQueue("Finished Downloads", "finished")
self.addTab(self.active_queue, "Download Queue")
self.addTab(self.finished_queue, "Finished Downloads")
```
#### Key Methods:
**move_to_finished()** (Line 4631) - **CRITICAL TRANSITION METHOD**:
```python
def move_to_finished(self, download_item):
if download_item in self.active_queue.download_items:
# Remove from active queue
self.active_queue.remove_download_item(download_item)
# Ensure completed downloads have 100% progress
final_progress = download_item.progress
if download_item.status == 'completed':
final_progress = 100
# Add to finished queue
finished_item = self.finished_queue.add_download_item(
title=download_item.title,
artist=download_item.artist,
status=download_item.status,
progress=final_progress,
file_size=download_item.file_size,
download_speed=download_item.download_speed,
file_path=download_item.file_path,
download_id=download_item.download_id,
username=download_item.username,
soulseek_client=download_item.soulseek_client
)
# API Cleanup for completed downloads only
if (download_item.status == 'completed' and
download_item.download_id and download_item.username and download_item.soulseek_client):
# Create API cleanup thread
cleanup_thread = ApiCleanupThread(
download_item.soulseek_client,
download_item.download_id,
download_item.username
)
cleanup_thread.cleanup_completed.connect(parent_page.api_cleanup_finished)
cleanup_thread.start()
self.update_tab_counts()
return finished_item
```
## CompactDownloadItem Class (Lines 3882-4323)
Individual download items with queue-specific behavior:
```python
class CompactDownloadItem(QFrame):
def __init__(self, title: str, artist: str, status: str = "queued",
progress: int = 0, file_size: int = 0, download_speed: int = 0,
file_path: str = "", download_id: str = "", username: str = "",
soulseek_client=None, queue_type: str = "active",
album: str = None, track_number: int = None):
self.queue_type = queue_type # "active" or "finished"
# ... other properties
```
### Status Handling and UI Representation
**update_status()** (Line 3708) - **CRITICAL STATUS UPDATE METHOD**:
```python
def update_status(self, status: str, progress: int = None, download_speed: int = None, file_path: str = None):
self.status = status
if progress is not None:
self.progress = progress
if download_speed is not None:
self.download_speed = download_speed
if file_path:
self.file_path = file_path
# Status mapping for clean display
status_mapping = {
"completed, succeeded": "Finished",
"completed, cancelled": "Cancelled",
"completed": "Finished",
"cancelled": "Cancelled",
"downloading": "Downloading",
"failed": "Failed",
"queued": "Queued"
}
clean_status = status_mapping.get(self.status.lower(), self.status.title())
status_text = clean_status
if self.status.lower() in ["downloading", "queued"]:
status_text += f" - {self.progress}%"
self.status_label.setText(status_text)
# Update action button based on status
if self.status == "downloading":
self.action_btn.setText("Cancel")
self.action_btn.clicked.connect(self.cancel_download)
# Red cancel button styling
elif self.status == "failed":
self.action_btn.setText("Retry")
self.action_btn.clicked.connect(self.retry_download)
# Yellow retry button styling
else:
self.action_btn.setText("📂 Open")
self.action_btn.clicked.connect(self.open_download_location)
# Green open button styling
```
### Visual Differences by Status
#### Completed Downloads:
- **Status Text**: "Finished"
- **Progress**: 100%
- **Button**: "📂 Open" (Green)
- **Action**: Opens download location
#### Cancelled Downloads:
- **Status Text**: "Cancelled"
- **Progress**: Maintains last known progress
- **Button**: "📂 Open" (Green)
- **Action**: Opens download location (if file exists)
#### Failed Downloads:
- **Status Text**: "Failed"
- **Progress**: Maintains last known progress
- **Button**: "Retry" (Yellow)
- **Action**: Retries download
## API Cleanup System
### Three Cleanup Methods
#### 1. signal_download_completion() - For Completed Downloads
**Location**: `/core/soulseek_client.py:875`
**Usage**: Called when download completes successfully
```python
async def signal_download_completion(self, download_id: str, username: str, remove: bool = True) -> bool:
"""Signal the Soulseek API that a download has completed"""
# Signals completion to slskd backend
# Used by ApiCleanupThread for completed downloads
```
#### 2. cancel_download() - For Cancelled Downloads
**Location**: `/core/soulseek_client.py:839`
**Usage**: Called when user cancels download or download fails
```python
async def cancel_download(self, download_id: str, username: str = None, remove: bool = False) -> bool:
"""Cancel a download and optionally remove it from the queue"""
# Used for both user cancellations and automatic cleanup of failed downloads
```
#### 3. clear_all_completed_downloads() - Bulk Cleanup
**Location**: `/core/soulseek_client.py:910`
**Usage**: Called by "Clear Completed Downloads" button
```python
async def clear_all_completed_downloads(self) -> bool:
"""Clear all completed/finished downloads from slskd backend"""
# Uses endpoint: DELETE /api/v0/transfers/downloads/all/completed
# Removes all downloads with completed, cancelled, or failed status
```
### ApiCleanupThread Class (Lines 1700-1750)
Background thread for API cleanup without blocking UI:
```python
class ApiCleanupThread(QThread):
cleanup_completed = pyqtSignal(bool, str, str) # success, download_id, username
def __init__(self, soulseek_client, download_id, username):
super().__init__()
self.soulseek_client = soulseek_client
self.download_id = download_id
self.username = username
def run(self):
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
# Signal download completion
success = loop.run_until_complete(
self.soulseek_client.signal_download_completion(
self.download_id,
self.username,
remove=True
)
)
self.cleanup_completed.emit(success, self.download_id, self.username)
except Exception as e:
self.cleanup_completed.emit(False, self.download_id, self.username)
finally:
loop.close()
```
## Status Polling and Transitions
### update_download_status() (Line 9547)
Main status polling method that keeps UI synchronized with backend:
```python
def update_download_status(self):
"""Poll slskd API for download status updates"""
if not self.soulseek_client or not self.download_queue.download_items:
return
def handle_status_update(transfers_data):
# Flatten transfers data structure
all_transfers = []
for user_data in transfers_data:
if 'directories' in user_data:
for directory in user_data['directories']:
if 'files' in directory:
all_transfers.extend(directory['files'])
# Update each download item
for download_item in self.download_queue.download_items.copy():
matching_transfer = None
# Find matching transfer by various criteria
for transfer in all_transfers:
if self._is_matching_transfer(download_item, transfer):
matching_transfer = transfer
break
if matching_transfer:
state = matching_transfer.get('state', '')
# Handle different states
if 'Completed' in state:
# Process completed download
download_item.update_status('completed', 100, 0)
self.download_queue.move_to_finished(download_item)
elif 'Cancelled' in state or 'Canceled' in state:
# Process cancelled download
download_item.update_status('cancelled', download_item.progress, 0)
# IMMEDIATE CLEANUP for cancelled downloads
if download_item.download_id and download_item.username:
self._cleanup_cancelled_download(download_item)
self.download_queue.move_to_finished(download_item)
elif 'Failed' in state or 'Errored' in state:
# Process failed download
download_item.update_status('failed', download_item.progress, 0)
# RETRY CLEANUP for failed downloads
if download_item.download_id and download_item.username:
self._cleanup_errored_download_with_retry(download_item)
self.download_queue.move_to_finished(download_item)
elif 'Downloading' in state:
# Update progress for active downloads
progress = int(matching_transfer.get('percentComplete', 0))
speed = int(matching_transfer.get('averageSpeed', 0))
download_item.update_status('downloading', progress, speed)
elif 'Queued' in state:
download_item.update_status('queued', download_item.progress, 0)
```
### Cleanup Strategies by Status
#### Completed Downloads (Lines 4665-4698):
- **When**: After successful download completion
- **Method**: `signal_download_completion()` via `ApiCleanupThread`
- **Purpose**: Notify slskd that download is complete and can be removed
- **Threading**: Background thread to prevent UI blocking
#### Cancelled Downloads (Lines 9794-9823):
- **When**: User clicks cancel or download is cancelled by backend
- **Method**: `cancel_download()` with `remove=True`
- **Purpose**: Remove from slskd queue immediately
- **Threading**: Background thread with immediate execution
```python
def cleanup_cancelled_download():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
success = loop.run_until_complete(
download_item.soulseek_client.cancel_download(
download_item.download_id,
download_item.username,
remove=True
)
)
loop.close()
```
#### Failed Downloads (Lines 9837-9872):
- **When**: Download fails due to network/peer issues
- **Method**: `cancel_download()` with `remove=True` and retry mechanism
- **Purpose**: Clean up failed downloads with multiple retry attempts
- **Threading**: Background thread with progressive retry delays
```python
def cleanup_errored_download_with_retry():
max_retries = 3
for attempt in range(max_retries):
try:
if attempt == 0:
time.sleep(2) # Initial delay
else:
time.sleep(5 * attempt) # Progressive delay
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
success = loop.run_until_complete(
download_item.soulseek_client.cancel_download(
download_item.download_id,
download_item.username,
remove=True
)
)
loop.close()
if success:
return # Success, exit retry loop
except Exception as e:
if attempt == max_retries - 1:
print(f"Failed to clean up after {max_retries} attempts")
```
## Queue Transitions Flow
### 1. New Download Added
```
User clicks download → add_download_item() → Active Queue → UI Update
```
### 2. Download Progresses
```
Status Polling → update_download_status() → UI Update → Progress Bar Update
```
### 3. Download Completes
```
Status Polling → 'Completed' State → update_status() → move_to_finished() → API Cleanup Thread → Finished Queue
```
### 4. Download Cancelled
```
User clicks Cancel → cancel_download() → API Cleanup → 'Cancelled' State → move_to_finished() → Finished Queue
```
### 5. Download Fails
```
Status Polling → 'Failed' State → update_status() → API Cleanup with Retry → move_to_finished() → Finished Queue
```
### 6. Clear Completed Downloads
```
User clicks Clear → clear_completed_downloads() → Backend API Call → clear_local_queues_only() → UI Update
```
## Critical Integration Points
### 1. Status Synchronization (Lines 9547-9979)
- **Polling Frequency**: 1-second intervals with adaptive optimization
- **Thread Safety**: Uses `ThreadSafeQueueManager` for concurrent access
- **Error Handling**: Grace periods for API delays and missing transfers
### 2. UI Thread Safety (Lines 4561-4567)
- **Batched Updates**: Timer-based batching to prevent excessive UI updates
- **Widget Deletion**: Scheduled deletion to prevent memory leaks
- **Signal-based Communication**: Thread-safe communication between components
### 3. Performance Optimizations
- **Adaptive Polling**: Adjusts polling frequency based on active downloads
- **Thread Pooling**: Reuses threads for API operations
- **Memory Management**: Proper cleanup of Qt objects and threads
## Error Handling and Edge Cases
### API Missing Downloads (Lines 9916-9944)
When downloads disappear from API (backend cleanup/restart):
- **Grace Period**: 2-3 polling cycles before marking as failed
- **State Transition**: Moves to finished queue with 'failed' status
- **User Feedback**: Visible in finished downloads for user awareness
### Network Issues
- **Retry Mechanisms**: Progressive delays for API calls
- **Fallback Cleanup**: Secondary cleanup attempts for persistent errors
- **UI Resilience**: Continues to function even with API failures
### Concurrent Operations
- **Thread Safety**: All queue operations are thread-safe
- **Lock Management**: Prevents race conditions in status updates
- **Atomic Transitions**: Ensures consistent state during transitions
## Key File Locations for Modifications
### For Visual Changes to Finished Downloads:
- **CompactDownloadItem.setup_ui()** (Lines 3927-4110): UI styling and layout
- **CompactDownloadItem.update_status()** (Lines 3708-3891): Status-specific styling
- **Status mapping** (Lines 3728-3738): Status text display
### For Queue Behavior Changes:
- **TabbedDownloadManager.move_to_finished()** (Lines 4631-4712): Transition logic
- **DownloadQueue.clear_completed_downloads()** (Lines 4519-4552): Clearing logic
- **update_download_status()** (Lines 9547-9979): Status polling and transitions
### For API Cleanup Changes:
- **ApiCleanupThread** (Lines 1700-1750): Background cleanup
- **Cleanup strategies** (Lines 9794-9872): Status-specific cleanup
- **SoulseekClient methods** in `/core/soulseek_client.py`: API endpoints
## Conclusion
The download queue system is a sophisticated, thread-safe implementation that manages the complete lifecycle of music downloads. Understanding these components and their interactions is crucial for safely modifying the appearance and behavior of finished downloads without breaking the underlying synchronization with the Soulseek backend.
The system's strength lies in its separation of concerns: UI presentation, queue management, status tracking, and API synchronization are all handled by separate components that communicate through well-defined interfaces. This architecture enables safe modifications to the visual representation while maintaining the integrity of the download management system.

View file

@ -1,104 +0,0 @@
Matched Downloads: A Deep Dive
This document provides a detailed, step-by-step explanation of the "Matched Downloads" feature in the downloads.py module. This feature intelligently uses the Spotify API to correctly identify, tag, and organize downloaded music into a clean, structured library.
Feature Overview
The core purpose of the Matched Download feature is to solve the problem of inconsistent or messy filenames commonly found on peer-to-peer networks. Instead of relying on the original filename, it uses the track's metadata to find a definitive match on Spotify. This allows the application to retrieve the correct artist, album, and track title, and then use that information to save the downloaded file in a standardized Artist/Album/Track folder structure.
The process can be broken down into four main stages:
Initiation: The user triggers a matched download from a search result.
Artist Matching: A modal dialog appears, suggesting potential artist matches from Spotify.
Download with Enhanced Metadata: The download proceeds, but the chosen Spotify artist information is attached to the download job.
Post-Download Organization: After the file is downloaded, it is automatically moved and renamed into a clean folder structure based on the matched Spotify data.
1. Initiation: Starting the Matched Download
The process begins when the user decides to use the matching feature on a search result.
UI Trigger: In the SearchResultItem (for single tracks) and AlbumResultItem (for albums), there is a dedicated "Matched Download" button, visually represented by a phone icon (📱).
Signal Connection: Clicking this button calls one of two methods in the main DownloadsPage class:
start_matched_download(search_result): For single tracks.
start_matched_album_download(album_result): For full albums.
2. The Matching Process: SpotifyMatchingModal
Once initiated, the DownloadsPage hands control over to the SpotifyMatchingModal. This dialog is the heart of the matching logic.
SpotifyMatchingModal Class
This QDialog is responsible for finding and confirming the correct artist with the user.
__init__(self, track_result, spotify_client, matching_engine, parent):
The modal is initialized with the specific track_result to be matched.
It receives the spotify_client to communicate with the Spotify API and the matching_engine to score the similarity between names.
Automatic Suggestions: Immediately upon opening, the modal kicks off a background thread to find likely matches without freezing the UI.
generate_auto_suggestions(): This method starts the ArtistSuggestionThread.
ArtistSuggestionThread Class
This QThread performs the Spotify search in the background.
run(): The thread's main execution method. It calls generate_artist_suggestions() and emits a suggestions_ready signal with the results.
generate_artist_suggestions(): This is where the core matching intelligence lies. It employs two main strategies to find potential artists:
Direct Artist Search: It takes the artist name from the Soulseek result and searches for it directly on Spotify.
Track Search: It performs a search on Spotify using the "artist - title" combination from the Soulseek result. It then inspects the artists of the resulting Spotify tracks.
For each potential artist found, it uses the MusicMatchingEngine.similarity_score() method to calculate a confidence score, which represents how closely the Spotify artist's name matches the original metadata.
The results are returned as a list of ArtistMatch objects, sorted by confidence.
User Interaction in the Modal
display_auto_suggestions(suggestions): This method populates the modal's UI with the top 5 suggestions from the ArtistSuggestionThread. Each suggestion shows the artist's name, the confidence score, and the reason for the match.
Manual Search: If the automatic suggestions are incorrect, the user can type in a name to perform a manual search, which uses the ArtistSearchThread to get results.
select_artist(artist): When the user is satisfied and clicks the "Select" button for an artist, this method is triggered. It emits the crucial artist_selected signal, passing the selected Artist object, and then closes the dialog.
3. Download with Enhanced Metadata
The DownloadsPage listens for the artist_selected signal from the modal to proceed with the download.
_handle_matched_download(self, search_result, artist): This slot is connected to the modal's signal.
It receives both the original search_result and the artist the user selected on Spotify.
Metadata Enhancement: This is a key step. It attaches the selected Spotify Artist object directly to the search_result object by creating a new attribute: search_result.matched_artist = artist.
For albums, the _handle_matched_album_download method does this for every single track in the album, ensuring each track carries the correct artist and album context.
Finally, it calls the standard start_download(search_result) method. The download proceeds as normal, but the search_result object is now enriched with the definitive Spotify data.
4. Post-Download Organization
The final step occurs after the file has been successfully downloaded from the Soulseek user.
update_download_status(): This method periodically checks the status of all active downloads. When it finds a download that has completed, it performs a critical check:
It inspects the download_item to see if it has the matched_artist attribute that was attached in the previous step.
If the attribute exists, it calls _organize_matched_download().
_organize_matched_download(self, download_item, original_file_path): This function orchestrates the final file organization.
Create Directory Structure: It creates a base Transfer folder, and inside it, a folder for the artist (e.g., Transfer/Daft Punk/). It uses _sanitize_filename() to ensure folder names are valid.
Detect Album vs. Single: It calls _detect_album_info() to determine if the track is part of a larger album or a standalone single.
_detect_album_info(): This helper function first checks if the download_item already has album information (from being part of an AlbumResultItem). If not, it queries the Spotify API with the track title and matched artist to find the official album data.
Move and Rename File:
If Album: It creates a subfolder for the album (e.g., Transfer/Daft Punk/Daft Punk - Discovery/) and renames the file to a clean format: 01 - One More Time.flac.
If Single: It creates a subfolder for the single (e.g., Transfer/Virtual Riot/Virtual Riot - Pray For Riddim/) and renames the file: Pray For Riddim.mp3.
Download Cover Art: If the track was identified as part of an album, the _download_cover_art() function is called. It fetches the album's cover image from the Spotify API and saves it as cover.jpg inside the album's folder.
This completes the matched download process, turning a potentially messy file from random_user/daft-punk-discovery-2001/01_daft_punk_-_one_more_time.flac into a perfectly organized and tagged file at Transfer/Daft Punk/Daft Punk - Discovery/01 - One More Time.flac with accompanying cover art.

View file

@ -1,383 +0,0 @@
# newMusic Performance Analysis: 1-Second Lag Issues
## Executive Summary
After deep analysis of the `downloads.py` file, I've identified **the real root causes** of the 1-second lag spikes affecting the newMusic application. The issue is **NOT** simply the timer interval - it's **massive computational overhead** in the `update_download_status()` method that blocks the main UI thread.
## Critical Performance Bottlenecks Identified
### 1. **CRITICAL: Computational Complexity in Main Thread**
**Location:** `ui/pages/downloads.py:9547-9950` - `update_download_status()` method
**Problem:** The method performs extremely expensive operations on the main UI thread:
#### 1.1 Complex Filename Matching (Lines 9626-9694)
The method implements **5 different matching strategies** for each download item:
- **Strategy 1:** Direct filename match with extension checks
- **Strategy 2:** Track title substring matching
- **Strategy 3:** Album track parsing with split operations
- **Strategy 3.5:** Core track name matching with regex
- **Strategy 4:** Word matching with common term exclusions
- **Strategy 5:** File path matching
**Performance Impact:** For each download item, this creates **nested loops** that iterate through ALL transfers (potentially hundreds) and perform complex string operations.
#### 1.2 Repeated Imports Inside Hot Loops (Lines 9608, 9655, 9684)
```python
# These imports happen INSIDE the nested loops, potentially hundreds of times per second
import os # Line 9608 - inside transfer matching loop
import re # Line 9655 - inside core title matching
import re # Line 9684 - inside word matching
```
**Performance Impact:** Python imports are expensive operations and should never be inside loops.
#### 1.3 Expensive String Processing
```python
# Complex string operations repeated for every transfer/download combination
basename = os.path.basename(full_filename).lower()
download_title_lower = download_item.title.lower()
basename_lower = basename.lower()
```
**Performance Impact:** String operations compound with nested loops, creating O(n²) complexity.
### 2. **Threading Architecture Issues**
**Location:** `ui/pages/downloads.py:9967-9982` - Thread creation pattern
**Problem:** Creates **NEW threads every second** instead of reusing them:
```python
# PROBLEMATIC: Creates new thread every 1000ms
status_thread = TransferStatusThread(self.soulseek_client)
status_thread.transfer_status_completed.connect(handle_status_update)
# ...
status_thread.start()
```
**Performance Impact:**
- **Thread creation overhead** accumulates over time
- **Memory leaks** from abandoned threads
- **Resource exhaustion** with long-running applications
### 3. **UI Update Cascade**
**Location:** `ui/pages/downloads.py:3708-3800` - `update_status()` method
**Problem:** Immediate UI updates for every download item status change:
```python
def update_status(self, status: str, progress: int = None, download_speed: int = None, file_path: str = None):
# Update properties
self.status = status
# ...
# SYNCHRONOUS UI UPDATES ON MAIN THREAD
if hasattr(self, 'progress_bar') and self.progress_bar:
self.progress_bar.setValue(self.progress) # Triggers widget redraw
if hasattr(self, 'status_label') and self.status_label:
self.status_label.setText(status_text) # Triggers widget redraw
```
**Performance Impact:** Each status update triggers immediate widget redraws, compounding the blocking effect.
### 4. **Inefficient Data Structures**
**Location:** Throughout the status update loop
**Problem:** Linear search operations in nested loops:
```python
# O(n) search for each download item
for download_item in self.download_queue.download_items.copy():
# O(m) search for each transfer
for transfer in all_transfers:
# Complex matching logic for each combination
```
**Performance Impact:** O(n*m) complexity where n=download_items and m=transfers.
## Detailed Code Analysis
### Main Performance Hotspot: `update_download_status()` Method
**File:** `ui/pages/downloads.py`
**Lines:** 9547-9950
**Execution Frequency:** Every 1000ms via QTimer
#### Flow Analysis:
1. **Line 9567:** Flatten transfers data structure (acceptable performance)
2. **Line 9579:** Copy download items list (acceptable performance)
3. **Lines 9587-9704:** **CRITICAL BOTTLENECK** - Complex filename matching
4. **Lines 9706-9950:** Status processing and UI updates
#### The Killer Loop (Lines 9587-9704):
```python
# This creates O(n*m*k) complexity where:
# n = number of download items
# m = number of transfers
# k = complexity of each matching strategy
for download_item in self.download_queue.download_items.copy():
for transfer in all_transfers:
# Strategy 1: Direct filename match
if basename_lower == download_title_lower + '.mp3':
# Complex extension checking...
# Strategy 2: Track title matching
elif download_title_lower in basename_lower:
# Complex extension checking...
# Strategy 3: Album track parsing
elif ' - ' in download_item.title:
title_parts = download_item.title.split(' - ')
# Complex parsing logic...
# Strategy 3.5: Core track name matching
elif '(' in download_item.title and ')' in download_item.title:
import re # EXPENSIVE IMPORT IN LOOP!
core_title = re.sub(r'\([^)]*\)', '', download_item.title)
# More complex logic...
# Strategy 4: Word matching
elif any(word.lower() in basename_lower for word in download_item.title.split()):
# Complex word filtering and matching...
# Strategy 5: File path matching
elif download_item.file_path:
# More matching logic...
```
## Root Cause Analysis
### Why the 1-Second Lag Occurs:
1. **QTimer triggers** `update_download_status()` every 1000ms
2. **Method executes** expensive operations on the main UI thread
3. **UI becomes unresponsive** during processing (the "quarter-second lag")
4. **Cycle repeats** every second, creating consistent lag spikes
### Why Previous Solutions Failed:
1. **Timer interval changes** don't address the computational complexity
2. **Optimized polling** still blocks the main thread during processing
3. **Threading issues** persist with new thread creation every cycle
## Optimization Strategy
### Phase 1: Move Heavy Processing Off Main Thread
#### 1.1 Extract Filename Matching to Background Workers
```python
class MatchingWorker(QRunnable):
def __init__(self, download_items, all_transfers):
super().__init__()
self.download_items = download_items
self.all_transfers = all_transfers
self.signals = MatchingWorkerSignals()
def run(self):
# Move expensive matching logic here
matches = self.perform_matching()
self.signals.matches_found.emit(matches)
```
#### 1.2 Pre-compile Regex Patterns
```python
# At class initialization, not in loops
class DownloadsPage(QWidget):
def __init__(self, ...):
# Pre-compile expensive regex patterns
self.track_number_pattern = re.compile(r'^(\d+)\.\s*(.+)')
self.parenthetical_pattern = re.compile(r'\([^)]*\)')
self.uuid_pattern = re.compile(r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$')
```
#### 1.3 Cache Expensive Operations
```python
# Cache parsed results to avoid repeated processing
self.filename_cache = {} # filename -> parsed_data
self.title_cache = {} # title -> normalized_title
```
### Phase 2: Optimize Threading Architecture
#### 2.1 Implement Thread Pooling
```python
# Replace single-use threads with reusable pool
self.status_thread_pool = QThreadPool()
self.status_thread_pool.setMaxThreadCount(2)
# Reuse workers instead of creating new ones
class ReusableStatusWorker(QRunnable):
def __init__(self, soulseek_client):
super().__init__()
self.soulseek_client = soulseek_client
self.signals = StatusWorkerSignals()
def run(self):
# Reusable worker logic
pass
```
#### 2.2 Implement Proper Thread Lifecycle Management
```python
def update_download_status(self):
# Check if worker is already running
if self.status_worker_running:
return
# Reuse existing worker
worker = self.get_or_create_worker()
self.status_thread_pool.start(worker)
```
### Phase 3: Improve Data Structures
#### 3.1 Index Transfers by ID
```python
# O(1) lookup instead of O(n) search
def create_transfer_index(self, all_transfers):
transfer_index = {}
for transfer in all_transfers:
transfer_id = transfer.get('id')
if transfer_id:
transfer_index[transfer_id] = transfer
return transfer_index
```
#### 3.2 Use Efficient Matching Algorithms
```python
# Replace nested loops with efficient algorithms
def match_downloads_efficiently(self, download_items, transfers):
# Use set intersections, hash maps, and other efficient data structures
pass
```
### Phase 4: Optimize UI Updates
#### 4.1 Batch UI Updates
```python
# Instead of immediate updates, batch them
self.pending_ui_updates = []
def schedule_ui_update(self, download_item, status):
self.pending_ui_updates.append((download_item, status))
def process_batched_updates(self):
# Process all updates at once
for download_item, status in self.pending_ui_updates:
download_item.update_status(status)
self.pending_ui_updates.clear()
```
#### 4.2 Implement Dirty Flagging
```python
# Only update items that have actually changed
def update_status(self, status: str, progress: int = None, ...):
if self.status == status and self.progress == progress:
return # No change, skip update
# Mark as dirty and schedule update
self.is_dirty = True
self.schedule_update()
```
## Implementation Plan
### Step 1: Create Optimized Method (Week 1)
1. **Create new method** `update_download_status_optimized()`
2. **Implement background processing** for filename matching
3. **Add proper caching** for repeated operations
4. **Maintain full API compatibility** with existing functions
### Step 2: Optimize Threading (Week 2)
1. **Implement thread pooling** for status updates
2. **Add proper lifecycle management** for worker threads
3. **Implement worker reuse** to eliminate creation overhead
4. **Add performance monitoring** to measure improvements
### Step 3: Improve Data Structures (Week 3)
1. **Create efficient indexing** for transfer lookups
2. **Implement smart matching algorithms** to reduce complexity
3. **Add result caching** for repeated operations
4. **Optimize memory usage** with better data structures
### Step 4: Optimize UI Updates (Week 4)
1. **Implement batched UI updates** to reduce redraws
2. **Add dirty flagging** to skip unnecessary updates
3. **Optimize widget operations** for better performance
4. **Add user feedback** for long-running operations
## Testing Methodology
### Performance Metrics
1. **Main Thread Blocking Time:** Measure time spent in `update_download_status()`
2. **UI Responsiveness:** Track frame rate and input lag
3. **Memory Usage:** Monitor thread count and memory consumption
4. **CPU Usage:** Profile CPU utilization during status updates
### Test Scenarios
1. **Small Queue:** 1-5 downloads (baseline performance)
2. **Medium Queue:** 10-20 downloads (typical usage)
3. **Large Queue:** 50+ downloads (stress test)
4. **Mixed States:** Various download states (downloading, completed, failed)
### Success Criteria
1. **Zero lag spikes** during normal operation
2. **60-80% reduction** in main thread blocking time
3. **Consistent UI responsiveness** regardless of queue size
4. **Full functional compatibility** with existing features
## Rollback Strategy
### Rollback Triggers
1. **Functional regression** in download management
2. **API cleanup failures** breaking slskd integration
3. **UI corruption** or unresponsive interface
4. **Memory leaks** or resource exhaustion
### Rollback Process
1. **Disable optimized method** via feature flag
2. **Revert to original** `update_download_status()` method
3. **Clean up new threads** and workers
4. **Restore original timer** configuration
### Rollback Code
```python
# Feature flag for safe rollback
USE_OPTIMIZED_STATUS_UPDATE = False
def update_download_status(self):
if USE_OPTIMIZED_STATUS_UPDATE:
return self.update_download_status_optimized()
else:
return self.update_download_status_original()
```
## Expected Performance Gains
### Quantitative Improvements
- **60-80% reduction** in main thread blocking time
- **Eliminate 1-second lag spikes** entirely
- **50% reduction** in CPU usage during status updates
- **30% reduction** in memory usage from thread optimization
### Qualitative Improvements
- **Smooth UI interaction** during downloads
- **Responsive interface** regardless of queue size
- **Better scalability** for large download queues
- **Maintained reliability** with all existing features
## Conclusion
The 1-second lag issue in newMusic is caused by **computational complexity** in the `update_download_status()` method, not just the timer interval. The solution requires:
1. **Moving expensive operations** off the main thread
2. **Optimizing data structures** and algorithms
3. **Implementing proper threading** architecture
4. **Batching UI updates** for efficiency
This comprehensive approach will eliminate the lag while preserving all existing functionality including critical API cleanup operations.
---
*This analysis provides a complete roadmap to resolve the performance issues. The next step is to implement the optimized solution following the detailed plan above.*

View file

@ -1,193 +0,0 @@
# Performance Optimizations Applied to downloads.py
## Summary of Changes
The downloads.py file has been optimized to eliminate the 1-second lag spikes caused by expensive operations running on the main UI thread. All functionality has been preserved while implementing significant performance improvements.
## Key Optimizations Implemented
### 1. Module-Level Import Optimization
**Problem**: Import statements inside nested loops executed thousands of times per second
**Solution**: Moved all imports to module level
```python
# BEFORE: Inside loops (lines 9608, 9655, 9684)
import os # Repeated import
import re # Repeated import
# AFTER: Module level (lines 10-14)
import os
import re # OPTIMIZATION: Moved to module level to prevent repeated imports
import time # OPTIMIZATION: Moved to module level
import asyncio # OPTIMIZATION: Moved to module level
from pathlib import Path # OPTIMIZATION: Moved to module level
```
### 2. Pre-compiled Regex Patterns
**Problem**: Regex compilation in nested loops
**Solution**: Pre-compile patterns at class initialization
```python
# Added to DownloadsPage.__init__ (lines 4900-4903)
self.track_number_pattern = re.compile(r'^(\d+)\.\s*(.+)')
self.parenthetical_pattern = re.compile(r'\([^)]*\)')
self.uuid_pattern = re.compile(r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$')
```
### 3. Efficient Caching System
**Problem**: Repeated expensive string operations
**Solution**: Cache normalized strings and filename processing
```python
# Added to DownloadsPage.__init__ (lines 4905-4908)
self.filename_cache = {} # filename -> parsed_data
self.title_cache = {} # title -> normalized_title
self.transfer_index_cache = {} # transfer_id -> transfer_data
```
### 4. Background Processing with OptimizedStatusWorker
**Problem**: O(n*m*k) complexity on main thread
**Solution**: Background worker class for expensive operations
```python
class OptimizedStatusWorker(QRunnable):
"""OPTIMIZATION: Background worker for status updates to prevent main thread blocking"""
```
**Key Features**:
- Asynchronous transfer data retrieval
- O(1) transfer lookups using dictionaries instead of O(n) linear searches
- Reduced matching strategies from 5 to 3 most effective ones
- Cached filename processing to avoid repeated computation
### 5. Thread Pooling Architecture
**Problem**: New thread created every 1000ms
**Solution**: Thread pool with reusable workers
```python
# Added to DownloadsPage.__init__ (lines 4910-4913)
self.status_thread_pool = QThreadPool()
self.status_thread_pool.setMaxThreadCount(2) # Limit to 2 concurrent status workers
self._status_worker_running = False # Prevent multiple concurrent status updates
```
### 6. Optimized Status Update Method
**Problem**: Main thread blocking for 200-500ms every second
**Solution**: Background processing with minimal main thread work
```python
def update_download_status_optimized(self):
"""OPTIMIZATION: Background-processed status updates to eliminate main thread blocking"""
```
**Flow**:
1. Background worker processes transfers data
2. Worker performs efficient matching with O(n+m) complexity
3. Results sent to main thread via signals
4. Main thread applies minimal updates
### 7. Feature Flag System
**Problem**: Need safe rollback capability
**Solution**: Feature flag for easy reversion
```python
# Lines 4915-4916
self._use_optimized_status_update = True # Set to False to revert to original method
```
### 8. Signal-Based Architecture
**Problem**: Direct method calls blocking threads
**Solution**: PyQt signal system for thread-safe communication
```python
# New signal added (line 4826)
optimized_status_update_completed = pyqtSignal(object) # update_results
# Signal connection (line 5101)
self.optimized_status_update_completed.connect(self.handle_optimized_update_complete)
```
## Performance Improvements
### Complexity Reduction
- **Before**: O(n*m*k) - 10 downloads × 100 transfers × 5 strategies = 5,000 operations
- **After**: O(n+m) - 10 downloads + 100 transfers = 110 operations
- **Improvement**: 45x reduction in computational complexity
### Main Thread Blocking
- **Before**: 200-500ms blocking every 1000ms (20-50% UI freeze)
- **After**: <10ms on main thread (>95% reduction)
- **Improvement**: Eliminated lag spikes entirely
### Thread Management
- **Before**: New thread every 1000ms → memory leaks
- **After**: Thread pool with 2 reusable workers
- **Improvement**: Fixed memory leaks, reduced overhead
### Memory Usage
- **Before**: Unlimited cache growth, thread accumulation
- **After**: Controlled caching, proper thread lifecycle
- **Improvement**: Stable memory usage
## Functionality Preservation
### 100% API Compatibility
- All existing method signatures preserved
- All signals and slots maintained
- Complete error handling preserved
### Critical Features Maintained
- **API Cleanup**: All slskd cleanup operations preserved
- **Download States**: All state transitions maintained
- **Queue Management**: Complete active/finished queue system
- **Progress Tracking**: Full progress and speed calculations
- **File Organization**: Complete Spotify matching and folder structure
### Error Handling
- Comprehensive exception handling in background worker
- Automatic fallback to original method on errors
- Thread safety with proper locking mechanisms
## Usage
### Automatic Activation
The optimizations are enabled by default. The timer now calls:
```python
self.download_status_timer.timeout.connect(self.update_download_status_optimized)
```
### Rollback Instructions
To revert to original behavior:
```python
# Set flag to False
self._use_optimized_status_update = False
# Or change timer connection
self.download_status_timer.timeout.connect(self.update_download_status)
```
### Performance Monitoring
The optimized system includes built-in performance monitoring:
```
⚡ Optimized status update completed in 45.2ms (background)
⚡ Processed 8 downloads against 67 transfers (main thread)
```
## Testing Verification
### Functionality Tests
- [x] Download initiation works
- [x] Progress tracking accurate
- [x] State transitions preserved
- [x] Queue management functional
- [x] API cleanup operational
- [x] File organization intact
### Performance Tests
- [x] No syntax errors (py_compile successful)
- [x] Main thread blocking eliminated
- [x] Memory usage stable
- [x] Thread pooling functional
## Expected Results
Users should experience:
1. **Immediate**: No more 1-second lag spikes
2. **Responsive UI**: Smooth interaction during downloads
3. **Better Performance**: Faster overall application response
4. **Stable Memory**: No memory leaks or resource accumulation
5. **Full Functionality**: All existing features work identically
The optimizations maintain complete backwards compatibility while delivering significant performance improvements for the reported lag issues.