mirror of https://github.com/Nezreka/SoulSync.git
parent
941e5e7625
commit
c5477f6a87
@ -0,0 +1,504 @@
|
||||
# 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.
|
||||
Loading…
Reference in new issue