more
This commit is contained in:
parent
b912ae352c
commit
712d492ade
6 changed files with 2783 additions and 83 deletions
|
|
@ -1 +1 @@
|
|||
{"access_token": "BQCAapY2h9Qx4vb4MFeuaJdmgDbZvFn99VDepd7mYu-rccmTk0CSwQY3ONrb3FL3_BJ8jawbiwVjURbr7ARJ3CInbYoXquK94ywQMXiCftLB49DMAmJpGeT-8R1OGtXcvn2WBx-sJriuznVSHjTD_l6odjiSxiIoPpPuopqB6xU8lACeoxwW4bCGkxdRAMyHzRPG9DnbJdyNdSTNp3IHqBR6zMxXMtXIUjWsKPos-6LaEfcC96E47j5h5ucB8VAT", "token_type": "Bearer", "expires_in": 3600, "scope": "user-library-read user-read-private playlist-read-private playlist-read-collaborative user-read-email", "expires_at": 1752197783, "refresh_token": "AQDmfQkPCGObfJeTUIbW1hAAwhSqkuHRA3Qh2dqVYMRh0eCkFMQgPNJDDzF8y-BiaVbj80zePkK_XSfYH1aJutMtNbnsqRKWuxP31BTrMc7pdUdbE7Fma4oH8wpDUKdG3MM"}
|
||||
{"access_token": "BQA45hmTHB-qxEFzKWp_uj7aSKM-RT8L1E88T8GauYSMqzRJnx_cdm-fvYsHoeO5nJ62_sRg67Q8wX5MZeABH0mySVhP2f_V_NT6KHPt2nW32j3rcQAmaTxW7kUQwlKNJdbiXSP-p4inH13WunPa2tNSZf0Ym1YfxZgVUYWzqEDYxjMm327TQVM0kIqSoZM7NYCx2_FQnslF2EebhZqA0KzfMpwzSTgYCUVT9-wtD73r1z3WMqgDGrWgF4FBA6no", "token_type": "Bearer", "expires_in": 3600, "scope": "user-library-read user-read-private playlist-read-private playlist-read-collaborative user-read-email", "expires_at": 1752205183, "refresh_token": "AQDmfQkPCGObfJeTUIbW1hAAwhSqkuHRA3Qh2dqVYMRh0eCkFMQgPNJDDzF8y-BiaVbj80zePkK_XSfYH1aJutMtNbnsqRKWuxP31BTrMc7pdUdbE7Fma4oH8wpDUKdG3MM"}
|
||||
Binary file not shown.
|
|
@ -12,6 +12,7 @@ logger = get_logger("soulseek_client")
|
|||
|
||||
@dataclass
|
||||
class SearchResult:
|
||||
"""Base class for search results"""
|
||||
username: str
|
||||
filename: str
|
||||
size: int
|
||||
|
|
@ -21,6 +22,7 @@ class SearchResult:
|
|||
free_upload_slots: int
|
||||
upload_speed: int
|
||||
queue_length: int
|
||||
result_type: str = "track" # "track" or "album"
|
||||
|
||||
@property
|
||||
def quality_score(self) -> float:
|
||||
|
|
@ -53,6 +55,137 @@ class SearchResult:
|
|||
|
||||
return min(base_score, 1.0)
|
||||
|
||||
@dataclass
|
||||
class TrackResult(SearchResult):
|
||||
"""Individual track search result"""
|
||||
artist: Optional[str] = None
|
||||
title: Optional[str] = None
|
||||
album: Optional[str] = None
|
||||
track_number: Optional[int] = None
|
||||
|
||||
def __post_init__(self):
|
||||
self.result_type = "track"
|
||||
# Try to extract metadata from filename if not provided
|
||||
if not self.title or not self.artist:
|
||||
self._parse_filename_metadata()
|
||||
|
||||
def _parse_filename_metadata(self):
|
||||
"""Extract artist, title, album from filename patterns"""
|
||||
import re
|
||||
import os
|
||||
|
||||
# Get just the filename without extension and path
|
||||
base_name = os.path.splitext(os.path.basename(self.filename))[0]
|
||||
|
||||
# Common patterns for track naming
|
||||
patterns = [
|
||||
r'^(\d+)\s*[-\.]\s*(.+?)\s*[-–]\s*(.+)$', # "01 - Artist - Title" or "01. Artist - Title"
|
||||
r'^(.+?)\s*[-–]\s*(.+)$', # "Artist - Title"
|
||||
r'^(\d+)\s*[-\.]\s*(.+)$', # "01 - Title" or "01. Title"
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
match = re.match(pattern, base_name)
|
||||
if match:
|
||||
groups = match.groups()
|
||||
if len(groups) == 3: # Track number, artist, title
|
||||
try:
|
||||
self.track_number = int(groups[0])
|
||||
self.artist = self.artist or groups[1].strip()
|
||||
self.title = self.title or groups[2].strip()
|
||||
except ValueError:
|
||||
# First group might not be a number
|
||||
self.artist = self.artist or groups[0].strip()
|
||||
self.title = self.title or f"{groups[1]} - {groups[2]}".strip()
|
||||
elif len(groups) == 2:
|
||||
if groups[0].isdigit(): # Track number and title
|
||||
try:
|
||||
self.track_number = int(groups[0])
|
||||
self.title = self.title or groups[1].strip()
|
||||
except ValueError:
|
||||
pass
|
||||
else: # Artist and title
|
||||
self.artist = self.artist or groups[0].strip()
|
||||
self.title = self.title or groups[1].strip()
|
||||
break
|
||||
|
||||
# Fallback: use filename as title if nothing was extracted
|
||||
if not self.title:
|
||||
self.title = base_name
|
||||
|
||||
# Try to extract album from directory path
|
||||
if not self.album and '/' in self.filename:
|
||||
path_parts = self.filename.split('/')
|
||||
if len(path_parts) >= 2:
|
||||
# Look for album-like directory names
|
||||
for part in reversed(path_parts[:-1]): # Exclude filename
|
||||
if part and not part.startswith('@'): # Skip system directories
|
||||
# Clean up common patterns
|
||||
cleaned = re.sub(r'^\d+\s*[-\.]\s*', '', part) # Remove leading numbers
|
||||
if len(cleaned) > 3: # Must be substantial
|
||||
self.album = cleaned
|
||||
break
|
||||
|
||||
@dataclass
|
||||
class AlbumResult:
|
||||
"""Album/folder search result containing multiple tracks"""
|
||||
username: str
|
||||
album_path: str # Directory path
|
||||
album_title: str
|
||||
artist: Optional[str]
|
||||
track_count: int
|
||||
total_size: int
|
||||
tracks: List[TrackResult]
|
||||
dominant_quality: str # Most common quality in album
|
||||
year: Optional[str] = None
|
||||
free_upload_slots: int = 0
|
||||
upload_speed: int = 0
|
||||
queue_length: int = 0
|
||||
result_type: str = "album"
|
||||
|
||||
@property
|
||||
def quality_score(self) -> float:
|
||||
"""Calculate album quality score based on dominant quality and track count"""
|
||||
quality_weights = {
|
||||
'flac': 1.0,
|
||||
'mp3': 0.8,
|
||||
'ogg': 0.7,
|
||||
'aac': 0.6,
|
||||
'wma': 0.5
|
||||
}
|
||||
|
||||
base_score = quality_weights.get(self.dominant_quality.lower(), 0.3)
|
||||
|
||||
# Bonus for complete albums (typically 8-15 tracks)
|
||||
if 8 <= self.track_count <= 20:
|
||||
base_score += 0.1
|
||||
elif self.track_count > 20:
|
||||
base_score += 0.05
|
||||
|
||||
# User metrics (same as individual tracks)
|
||||
if self.free_upload_slots > 0:
|
||||
base_score += 0.1
|
||||
|
||||
if self.upload_speed > 100:
|
||||
base_score += 0.05
|
||||
|
||||
if self.queue_length > 10:
|
||||
base_score -= 0.1
|
||||
|
||||
return min(base_score, 1.0)
|
||||
|
||||
@property
|
||||
def size_mb(self) -> int:
|
||||
"""Album size in MB"""
|
||||
return self.total_size // (1024 * 1024)
|
||||
|
||||
@property
|
||||
def average_track_size_mb(self) -> float:
|
||||
"""Average track size in MB"""
|
||||
if self.track_count > 0:
|
||||
return self.size_mb / self.track_count
|
||||
return 0
|
||||
|
||||
@dataclass
|
||||
class DownloadStatus:
|
||||
id: str
|
||||
|
|
@ -201,12 +334,19 @@ class SoulseekClient:
|
|||
except:
|
||||
pass
|
||||
|
||||
def _process_search_responses(self, responses_data: List[Dict[str, Any]]) -> List[SearchResult]:
|
||||
"""Process search response data into SearchResult objects"""
|
||||
search_results = []
|
||||
def _process_search_responses(self, responses_data: List[Dict[str, Any]]) -> tuple[List[TrackResult], List[AlbumResult]]:
|
||||
"""Process search response data into TrackResult and AlbumResult objects"""
|
||||
from collections import defaultdict
|
||||
import re
|
||||
|
||||
all_tracks = []
|
||||
albums_by_path = defaultdict(list)
|
||||
|
||||
logger.debug(f"Processing {len(responses_data)} user responses")
|
||||
|
||||
# Audio file extensions to filter for
|
||||
audio_extensions = {'.mp3', '.flac', '.ogg', '.aac', '.wma', '.wav', '.m4a'}
|
||||
|
||||
for response_data in responses_data:
|
||||
username = response_data.get('username', '')
|
||||
files = response_data.get('files', [])
|
||||
|
|
@ -217,9 +357,15 @@ class SoulseekClient:
|
|||
size = file_data.get('size', 0)
|
||||
|
||||
file_ext = Path(filename).suffix.lower().lstrip('.')
|
||||
|
||||
# Only process audio files
|
||||
if f'.{file_ext}' not in audio_extensions:
|
||||
continue
|
||||
|
||||
quality = file_ext if file_ext in ['flac', 'mp3', 'ogg', 'aac', 'wma'] else 'unknown'
|
||||
|
||||
result = SearchResult(
|
||||
# Create TrackResult
|
||||
track = TrackResult(
|
||||
username=username,
|
||||
filename=filename,
|
||||
size=size,
|
||||
|
|
@ -230,14 +376,176 @@ class SoulseekClient:
|
|||
upload_speed=response_data.get('uploadSpeed', 0),
|
||||
queue_length=response_data.get('queueLength', 0)
|
||||
)
|
||||
search_results.append(result)
|
||||
|
||||
all_tracks.append(track)
|
||||
|
||||
# Group tracks by album path for album detection
|
||||
album_path = self._extract_album_path(filename)
|
||||
if album_path:
|
||||
albums_by_path[(username, album_path)].append(track)
|
||||
|
||||
return search_results
|
||||
# Create AlbumResults from grouped tracks
|
||||
album_results = self._create_album_results(albums_by_path)
|
||||
|
||||
# Keep individual tracks that weren't grouped into albums
|
||||
album_track_filenames = set()
|
||||
for album in album_results:
|
||||
for track in album.tracks:
|
||||
album_track_filenames.add(track.filename)
|
||||
|
||||
# Individual tracks are those not part of any album
|
||||
individual_tracks = [track for track in all_tracks if track.filename not in album_track_filenames]
|
||||
|
||||
logger.info(f"Found {len(individual_tracks)} individual tracks and {len(album_results)} albums")
|
||||
logger.debug(f"Album detection details: {len(albums_by_path)} potential albums processed")
|
||||
for (username, album_path), tracks in list(albums_by_path.items())[:3]: # Log first 3 for debugging
|
||||
logger.debug(f"Album: {username}/{album_path} -> {len(tracks)} tracks")
|
||||
|
||||
return individual_tracks, album_results
|
||||
|
||||
async def search(self, query: str, timeout: int = 30, progress_callback=None) -> List[SearchResult]:
|
||||
def _extract_album_path(self, filename: str) -> Optional[str]:
|
||||
"""Extract potential album directory path from filename"""
|
||||
# Handle both Windows (\) and Unix (/) path separators
|
||||
if '/' not in filename and '\\' not in filename:
|
||||
return None
|
||||
|
||||
# Normalize path separators to forward slashes for consistent processing
|
||||
normalized_path = filename.replace('\\', '/')
|
||||
path_parts = normalized_path.split('/')
|
||||
|
||||
if len(path_parts) < 2:
|
||||
return None
|
||||
|
||||
# Take the directory containing the file as potential album path
|
||||
album_dir = path_parts[-2] # Directory containing the file
|
||||
|
||||
# Skip system directories that start with @ or are too short
|
||||
if album_dir.startswith('@') or len(album_dir) < 2:
|
||||
return None
|
||||
|
||||
# Return the full path up to the album directory (keeping forward slashes)
|
||||
return '/'.join(path_parts[:-1])
|
||||
|
||||
|
||||
def _create_album_results(self, albums_by_path: dict) -> List[AlbumResult]:
|
||||
"""Create AlbumResult objects from grouped tracks"""
|
||||
import re
|
||||
from collections import Counter
|
||||
|
||||
album_results = []
|
||||
|
||||
for (username, album_path), tracks in albums_by_path.items():
|
||||
# Only create albums for paths with multiple tracks (2+ tracks)
|
||||
if len(tracks) < 2:
|
||||
continue
|
||||
|
||||
# Calculate album metadata
|
||||
total_size = sum(track.size for track in tracks)
|
||||
quality_counts = Counter(track.quality for track in tracks)
|
||||
dominant_quality = quality_counts.most_common(1)[0][0]
|
||||
|
||||
# Extract album title from path
|
||||
album_title = self._extract_album_title(album_path)
|
||||
|
||||
# Try to determine artist from tracks or path
|
||||
artist = self._determine_album_artist(tracks, album_path)
|
||||
|
||||
# Extract year if present
|
||||
year = self._extract_year(album_path, album_title)
|
||||
|
||||
# Use user metrics from first track (they should be the same for all tracks from same user)
|
||||
first_track = tracks[0]
|
||||
|
||||
album = AlbumResult(
|
||||
username=username,
|
||||
album_path=album_path,
|
||||
album_title=album_title,
|
||||
artist=artist,
|
||||
track_count=len(tracks),
|
||||
total_size=total_size,
|
||||
tracks=sorted(tracks, key=lambda t: t.track_number or 0), # Sort by track number
|
||||
dominant_quality=dominant_quality,
|
||||
year=year,
|
||||
free_upload_slots=first_track.free_upload_slots,
|
||||
upload_speed=first_track.upload_speed,
|
||||
queue_length=first_track.queue_length
|
||||
)
|
||||
|
||||
album_results.append(album)
|
||||
|
||||
return album_results
|
||||
|
||||
def _extract_album_title(self, album_path: str) -> str:
|
||||
"""Extract album title from directory path"""
|
||||
import re
|
||||
|
||||
# Get the last directory name as album title
|
||||
album_dir = album_path.split('/')[-1]
|
||||
|
||||
# Clean up common patterns
|
||||
# Remove leading numbers and separators
|
||||
cleaned = re.sub(r'^\d+\s*[-\.\s]+', '', album_dir)
|
||||
|
||||
# Remove year patterns at the end: (2023), [2023], - 2023
|
||||
cleaned = re.sub(r'\s*[-\(\[]?\d{4}[-\)\]]?\s*$', '', cleaned)
|
||||
|
||||
# Remove common separators and extra spaces
|
||||
cleaned = re.sub(r'\s+', ' ', cleaned).strip()
|
||||
|
||||
return cleaned if cleaned else album_dir
|
||||
|
||||
def _determine_album_artist(self, tracks: List[TrackResult], album_path: str) -> Optional[str]:
|
||||
"""Determine album artist from track artists or path"""
|
||||
from collections import Counter
|
||||
|
||||
# Get artist from tracks
|
||||
track_artists = [track.artist for track in tracks if track.artist]
|
||||
if track_artists:
|
||||
# Use most common artist
|
||||
artist_counts = Counter(track_artists)
|
||||
return artist_counts.most_common(1)[0][0]
|
||||
|
||||
# Try to extract from path
|
||||
import re
|
||||
album_dir = album_path.split('/')[-1]
|
||||
|
||||
# Look for "Artist - Album" pattern
|
||||
artist_match = re.match(r'^(.+?)\s*[-–]\s*(.+)$', album_dir)
|
||||
if artist_match:
|
||||
potential_artist = artist_match.group(1).strip()
|
||||
if len(potential_artist) > 1:
|
||||
return potential_artist
|
||||
|
||||
return None
|
||||
|
||||
def _extract_year(self, album_path: str, album_title: str) -> Optional[str]:
|
||||
"""Extract year from album path or title"""
|
||||
import re
|
||||
|
||||
# Look for 4-digit year in parentheses, brackets, or after dash
|
||||
text_to_search = f"{album_path} {album_title}"
|
||||
year_patterns = [
|
||||
r'\((\d{4})\)', # (2023)
|
||||
r'\[(\d{4})\]', # [2023]
|
||||
r'\s-(\d{4})$', # - 2023 at end
|
||||
r'\s(\d{4})\s', # 2023 with spaces
|
||||
r'\s(\d{4})$' # 2023 at end
|
||||
]
|
||||
|
||||
for pattern in year_patterns:
|
||||
match = re.search(pattern, text_to_search)
|
||||
if match:
|
||||
year = match.group(1)
|
||||
# Validate year range (1900-2030)
|
||||
if 1900 <= int(year) <= 2030:
|
||||
return year
|
||||
|
||||
return None
|
||||
|
||||
async def search(self, query: str, timeout: int = 30, progress_callback=None) -> tuple[List[TrackResult], List[AlbumResult]]:
|
||||
if not self.base_url:
|
||||
logger.error("Soulseek client not configured")
|
||||
return []
|
||||
return [], []
|
||||
|
||||
try:
|
||||
logger.info(f"Starting search for: '{query}'")
|
||||
|
|
@ -256,18 +564,18 @@ class SoulseekClient:
|
|||
response = await self._make_request('POST', 'searches', json=search_data)
|
||||
if not response:
|
||||
logger.error("No response from search POST request")
|
||||
return []
|
||||
return [], []
|
||||
|
||||
search_id = response.get('id')
|
||||
if not search_id:
|
||||
logger.error("No search ID returned from POST request")
|
||||
logger.debug(f"Full response: {response}")
|
||||
return []
|
||||
return [], []
|
||||
|
||||
logger.info(f"Search initiated with ID: {search_id}")
|
||||
|
||||
# Poll for results instead of blocking sleep - like web interface does
|
||||
all_results = []
|
||||
# Poll for results - collect all responses first, then process at the end
|
||||
all_responses = []
|
||||
poll_interval = 1.5 # Check every 1.5 seconds for more responsive updates
|
||||
max_polls = int(timeout / poll_interval) # 20 attempts over 30 seconds
|
||||
|
||||
|
|
@ -277,44 +585,49 @@ class SoulseekClient:
|
|||
# Get current search responses
|
||||
responses_data = await self._make_request('GET', f'searches/{search_id}/responses')
|
||||
if responses_data and isinstance(responses_data, list):
|
||||
current_results = self._process_search_responses(responses_data)
|
||||
|
||||
# Add new unique results
|
||||
existing_filenames = {r.filename for r in all_results}
|
||||
new_results = [r for r in current_results if r.filename not in existing_filenames]
|
||||
all_results.extend(new_results)
|
||||
|
||||
if new_results:
|
||||
logger.info(f"Found {len(new_results)} new results (total: {len(all_results)}) at {poll_count * poll_interval:.1f}s")
|
||||
# Call progress callback with new results
|
||||
# Check if we got new responses
|
||||
new_response_count = len(responses_data) - len(all_responses)
|
||||
if new_response_count > 0:
|
||||
all_responses = responses_data
|
||||
logger.info(f"Found {len(all_responses)} total responses at {poll_count * poll_interval:.1f}s")
|
||||
|
||||
# Call progress callback with current count
|
||||
if progress_callback:
|
||||
try:
|
||||
progress_callback(new_results, len(all_results))
|
||||
progress_callback([], len(all_responses))
|
||||
except Exception as e:
|
||||
logger.error(f"Error in progress callback: {e}")
|
||||
|
||||
# Early termination if we have enough results
|
||||
if len(all_results) >= 45: # Stop after 45 results for better performance (3 pages of 15)
|
||||
logger.info(f"Early termination: Found {len(all_results)} results, stopping search")
|
||||
# Early termination if we have enough responses
|
||||
if len(all_responses) >= 30: # Stop after 30 responses for better performance
|
||||
logger.info(f"Early termination: Found {len(all_responses)} responses, stopping search")
|
||||
break
|
||||
elif len(all_results) > 0:
|
||||
logger.debug(f"No new results, total still: {len(all_results)}")
|
||||
elif len(all_responses) > 0:
|
||||
logger.debug(f"No new responses, total still: {len(all_responses)}")
|
||||
else:
|
||||
logger.debug(f"Still waiting for results... ({poll_count * poll_interval:.1f}s elapsed)")
|
||||
logger.debug(f"Still waiting for responses... ({poll_count * poll_interval:.1f}s elapsed)")
|
||||
|
||||
# Wait before next poll (unless this is the last attempt)
|
||||
if poll_count < max_polls - 1:
|
||||
await asyncio.sleep(poll_interval)
|
||||
|
||||
logger.info(f"Search completed. Found {len(all_results)} total results for query: {query}")
|
||||
|
||||
# Sort by quality score and return
|
||||
all_results.sort(key=lambda x: x.quality_score, reverse=True)
|
||||
return all_results
|
||||
# Process all collected responses at the end
|
||||
if all_responses:
|
||||
tracks, albums = self._process_search_responses(all_responses)
|
||||
|
||||
# Sort tracks by quality score
|
||||
tracks.sort(key=lambda x: x.quality_score, reverse=True)
|
||||
albums.sort(key=lambda x: x.quality_score, reverse=True)
|
||||
|
||||
logger.info(f"Search completed. Found {len(tracks)} tracks and {len(albums)} albums for query: {query}")
|
||||
return tracks, albums
|
||||
else:
|
||||
logger.info(f"Search completed with no results for query: {query}")
|
||||
return [], []
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error searching: {e}")
|
||||
return []
|
||||
return [], []
|
||||
|
||||
async def download(self, username: str, filename: str, file_size: int = 0) -> Optional[str]:
|
||||
if not self.base_url:
|
||||
|
|
|
|||
1968
logs/app.log
1968
logs/app.log
File diff suppressed because it is too large
Load diff
Binary file not shown.
|
|
@ -8,6 +8,9 @@ from PyQt6.QtMultimedia import QMediaPlayer, QAudioOutput
|
|||
import functools # For fixing lambda memory leaks
|
||||
import os
|
||||
|
||||
# Import the new search result classes
|
||||
from core.soulseek_client import TrackResult, AlbumResult
|
||||
|
||||
class AudioPlayer(QMediaPlayer):
|
||||
"""Simple audio player for streaming music files"""
|
||||
playback_finished = pyqtSignal()
|
||||
|
|
@ -273,7 +276,7 @@ class ExploreApiThread(QThread):
|
|||
self._stop_requested = True
|
||||
|
||||
class SearchThread(QThread):
|
||||
search_completed = pyqtSignal(list) # List of search results
|
||||
search_completed = pyqtSignal(object) # Tuple of (tracks, albums) or list for backward compatibility
|
||||
search_failed = pyqtSignal(str) # Error message
|
||||
search_progress = pyqtSignal(str) # Progress message
|
||||
search_results_partial = pyqtSignal(list, int) # Partial results, total count
|
||||
|
|
@ -305,8 +308,9 @@ class SearchThread(QThread):
|
|||
results = loop.run_until_complete(self._do_search())
|
||||
|
||||
if not self._stop_requested:
|
||||
# Emit final completion with all results
|
||||
self.search_completed.emit(self.all_results if self.all_results else results)
|
||||
# Emit final completion with proper tuple format
|
||||
# results should be a tuple (tracks, albums) from the search client
|
||||
self.search_completed.emit(results)
|
||||
|
||||
except Exception as e:
|
||||
if not self._stop_requested:
|
||||
|
|
@ -456,10 +460,9 @@ class StreamingThread(QThread):
|
|||
if found_file:
|
||||
print(f"✓ Found downloaded file: {found_file}")
|
||||
|
||||
# Move the file to Stream folder
|
||||
file_extension = os.path.splitext(found_file)[1]
|
||||
stream_filename = f"current_stream{file_extension}"
|
||||
stream_path = os.path.join(stream_folder, stream_filename)
|
||||
# Move the file to Stream folder with original filename
|
||||
original_filename = os.path.basename(found_file)
|
||||
stream_path = os.path.join(stream_folder, original_filename)
|
||||
|
||||
try:
|
||||
# Move file to Stream folder
|
||||
|
|
@ -592,6 +595,338 @@ class StreamingThread(QThread):
|
|||
"""Stop the streaming gracefully"""
|
||||
self._stop_requested = True
|
||||
|
||||
class TrackItem(QFrame):
|
||||
"""Individual track item within an album"""
|
||||
track_download_requested = pyqtSignal(object) # TrackResult object
|
||||
track_stream_requested = pyqtSignal(object) # TrackResult object
|
||||
|
||||
def __init__(self, track_result, parent=None):
|
||||
super().__init__(parent)
|
||||
self.track_result = track_result
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
self.setFixedHeight(50)
|
||||
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||||
|
||||
self.setStyleSheet("""
|
||||
TrackItem {
|
||||
background: rgba(40, 40, 40, 0.5);
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(60, 60, 60, 0.3);
|
||||
margin: 2px 8px;
|
||||
}
|
||||
TrackItem:hover {
|
||||
background: rgba(50, 50, 50, 0.7);
|
||||
border: 1px solid rgba(29, 185, 84, 0.5);
|
||||
}
|
||||
""")
|
||||
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(12, 8, 12, 8)
|
||||
layout.setSpacing(12)
|
||||
|
||||
# Track info
|
||||
track_info = QVBoxLayout()
|
||||
track_info.setSpacing(2)
|
||||
|
||||
# Track title
|
||||
title = QLabel(self.track_result.title or "Unknown Title")
|
||||
title.setFont(QFont("Arial", 11, QFont.Weight.Bold))
|
||||
title.setStyleSheet("color: #ffffff;")
|
||||
|
||||
# Track details
|
||||
details = []
|
||||
if self.track_result.track_number:
|
||||
details.append(f"#{self.track_result.track_number:02d}")
|
||||
details.append(self.track_result.quality.upper())
|
||||
if self.track_result.bitrate:
|
||||
details.append(f"{self.track_result.bitrate}kbps")
|
||||
details.append(f"{self.track_result.size // (1024*1024)}MB")
|
||||
|
||||
details_text = " • ".join(details)
|
||||
track_details = QLabel(details_text)
|
||||
track_details.setFont(QFont("Arial", 9))
|
||||
track_details.setStyleSheet("color: rgba(179, 179, 179, 0.8);")
|
||||
|
||||
track_info.addWidget(title)
|
||||
track_info.addWidget(track_details)
|
||||
|
||||
# Control buttons
|
||||
button_layout = QHBoxLayout()
|
||||
button_layout.setSpacing(8)
|
||||
|
||||
# Play button
|
||||
play_btn = QPushButton("▶️")
|
||||
play_btn.setFixedSize(32, 32)
|
||||
play_btn.clicked.connect(self.request_stream)
|
||||
play_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background: rgba(29, 185, 84, 0.8);
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
color: #000000;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background: rgba(30, 215, 96, 1.0);
|
||||
}
|
||||
""")
|
||||
|
||||
# Download button
|
||||
download_btn = QPushButton("⬇️")
|
||||
download_btn.setFixedSize(32, 32)
|
||||
download_btn.clicked.connect(self.request_download)
|
||||
download_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background: rgba(64, 64, 64, 0.8);
|
||||
border: 1px solid rgba(29, 185, 84, 0.6);
|
||||
border-radius: 16px;
|
||||
color: #1db954;
|
||||
font-size: 10px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background: rgba(29, 185, 84, 0.2);
|
||||
}
|
||||
""")
|
||||
|
||||
button_layout.addWidget(play_btn)
|
||||
button_layout.addWidget(download_btn)
|
||||
|
||||
# Store button references for state management
|
||||
self.play_btn = play_btn
|
||||
self.download_btn = download_btn
|
||||
|
||||
# Assembly
|
||||
layout.addLayout(track_info, 1)
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
def request_stream(self):
|
||||
"""Request streaming of this track"""
|
||||
self.track_stream_requested.emit(self.track_result)
|
||||
|
||||
def request_download(self):
|
||||
"""Request download of this track"""
|
||||
self.track_download_requested.emit(self.track_result)
|
||||
|
||||
def set_loading_state(self):
|
||||
"""Set play button to loading state"""
|
||||
self.play_btn.setText("⏳")
|
||||
self.play_btn.setEnabled(False)
|
||||
|
||||
def set_playing_state(self):
|
||||
"""Set play button to playing/pause state"""
|
||||
self.play_btn.setText("⏸️")
|
||||
self.play_btn.setEnabled(True)
|
||||
|
||||
def reset_play_state(self):
|
||||
"""Reset play button to default state"""
|
||||
self.play_btn.setText("▶️")
|
||||
self.play_btn.setEnabled(True)
|
||||
|
||||
class AlbumResultItem(QFrame):
|
||||
"""Expandable UI component for displaying album search results"""
|
||||
album_download_requested = pyqtSignal(object) # AlbumResult object
|
||||
track_download_requested = pyqtSignal(object) # TrackResult object
|
||||
track_stream_requested = pyqtSignal(object, object) # TrackResult object, TrackItem object
|
||||
|
||||
def __init__(self, album_result, parent=None):
|
||||
super().__init__(parent)
|
||||
self.album_result = album_result
|
||||
self.is_expanded = False
|
||||
self.track_items = []
|
||||
self.tracks_container = None
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
# Dynamic height based on expansion state
|
||||
self.collapsed_height = 80
|
||||
self.setFixedHeight(self.collapsed_height)
|
||||
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||||
|
||||
# Enable mouse tracking for click detection
|
||||
self.setMouseTracking(True)
|
||||
|
||||
self.setStyleSheet("""
|
||||
AlbumResultItem {
|
||||
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
||||
stop:0 rgba(45, 45, 45, 0.9),
|
||||
stop:1 rgba(35, 35, 35, 0.95));
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(80, 80, 80, 0.4);
|
||||
margin: 6px 4px;
|
||||
}
|
||||
AlbumResultItem:hover {
|
||||
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
||||
stop:0 rgba(55, 55, 55, 0.95),
|
||||
stop:1 rgba(45, 45, 45, 0.98));
|
||||
border: 1px solid rgba(29, 185, 84, 0.7);
|
||||
}
|
||||
""")
|
||||
|
||||
# Main vertical layout for album header + tracks
|
||||
main_layout = QVBoxLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.setSpacing(0)
|
||||
|
||||
# Album header (always visible, clickable)
|
||||
self.header_widget = QWidget()
|
||||
self.header_widget.setFixedHeight(80)
|
||||
self.header_widget.setStyleSheet("QWidget { background: transparent; }")
|
||||
header_layout = QHBoxLayout(self.header_widget)
|
||||
header_layout.setContentsMargins(16, 12, 16, 12)
|
||||
header_layout.setSpacing(16)
|
||||
|
||||
# Album icon with expand indicator
|
||||
icon_container = QVBoxLayout()
|
||||
album_icon = QLabel("💿")
|
||||
album_icon.setFixedSize(32, 32)
|
||||
album_icon.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
album_icon.setStyleSheet("""
|
||||
QLabel {
|
||||
font-size: 20px;
|
||||
background: rgba(29, 185, 84, 0.1);
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(29, 185, 84, 0.3);
|
||||
}
|
||||
""")
|
||||
|
||||
# Expand indicator
|
||||
self.expand_indicator = QLabel("▶")
|
||||
self.expand_indicator.setFixedSize(16, 16)
|
||||
self.expand_indicator.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.expand_indicator.setStyleSheet("""
|
||||
QLabel {
|
||||
color: rgba(29, 185, 84, 0.8);
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
""")
|
||||
|
||||
icon_container.addWidget(album_icon)
|
||||
icon_container.addWidget(self.expand_indicator)
|
||||
|
||||
# Album info section
|
||||
info_section = QVBoxLayout()
|
||||
info_section.setSpacing(2)
|
||||
info_section.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
# Album title
|
||||
album_title = QLabel(self.album_result.album_title)
|
||||
album_title.setFont(QFont("Arial", 12, QFont.Weight.Bold))
|
||||
album_title.setStyleSheet("color: #ffffff;")
|
||||
|
||||
# Artist and details
|
||||
details = []
|
||||
if self.album_result.artist:
|
||||
details.append(self.album_result.artist)
|
||||
details.append(f"{self.album_result.track_count} tracks")
|
||||
details.append(f"{self.album_result.size_mb}MB")
|
||||
details.append(self.album_result.dominant_quality.upper())
|
||||
if self.album_result.year:
|
||||
details.append(f"({self.album_result.year})")
|
||||
|
||||
details_text = " • ".join(details)
|
||||
album_details = QLabel(details_text)
|
||||
album_details.setFont(QFont("Arial", 10))
|
||||
album_details.setStyleSheet("color: rgba(179, 179, 179, 0.9);")
|
||||
|
||||
# User info
|
||||
user_info = QLabel(f"👤 {self.album_result.username}")
|
||||
user_info.setFont(QFont("Arial", 9))
|
||||
user_info.setStyleSheet("color: rgba(29, 185, 84, 0.8);")
|
||||
|
||||
info_section.addWidget(album_title)
|
||||
info_section.addWidget(album_details)
|
||||
info_section.addWidget(user_info)
|
||||
|
||||
# Download button
|
||||
self.download_btn = QPushButton("⬇️ Download Album")
|
||||
self.download_btn.setFixedSize(120, 36)
|
||||
self.download_btn.clicked.connect(self.request_album_download)
|
||||
self.download_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
||||
stop:0 rgba(29, 185, 84, 0.9),
|
||||
stop:1 rgba(24, 156, 71, 0.9));
|
||||
border: none;
|
||||
border-radius: 18px;
|
||||
color: #000000;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
||||
stop:0 rgba(30, 215, 96, 1.0),
|
||||
stop:1 rgba(25, 180, 80, 1.0));
|
||||
}
|
||||
""")
|
||||
|
||||
# Assembly header
|
||||
header_layout.addLayout(icon_container)
|
||||
header_layout.addLayout(info_section, 1)
|
||||
header_layout.addWidget(self.download_btn)
|
||||
|
||||
# Tracks container (hidden by default)
|
||||
self.tracks_container = QWidget()
|
||||
self.tracks_container.setVisible(False)
|
||||
tracks_layout = QVBoxLayout(self.tracks_container)
|
||||
tracks_layout.setContentsMargins(16, 8, 16, 16)
|
||||
tracks_layout.setSpacing(4)
|
||||
|
||||
# Create track items
|
||||
for track in self.album_result.tracks:
|
||||
track_item = TrackItem(track)
|
||||
track_item.track_download_requested.connect(self.track_download_requested.emit)
|
||||
# Use lambda to pass both track result and track item reference
|
||||
track_item.track_stream_requested.connect(
|
||||
lambda track_result, item=track_item: self.handle_track_stream_request(track_result, item)
|
||||
)
|
||||
tracks_layout.addWidget(track_item)
|
||||
self.track_items.append(track_item)
|
||||
|
||||
# Assembly main layout
|
||||
main_layout.addWidget(self.header_widget)
|
||||
main_layout.addWidget(self.tracks_container)
|
||||
|
||||
# Make header clickable
|
||||
self.header_widget.mousePressEvent = self.toggle_expansion
|
||||
|
||||
def request_album_download(self):
|
||||
"""Request download of the entire album"""
|
||||
self.download_btn.setText("⏳")
|
||||
self.download_btn.setEnabled(False)
|
||||
self.album_download_requested.emit(self.album_result)
|
||||
|
||||
def toggle_expansion(self, event):
|
||||
"""Toggle album expansion to show/hide tracks"""
|
||||
self.is_expanded = not self.is_expanded
|
||||
|
||||
if self.is_expanded:
|
||||
# Expand to show tracks
|
||||
self.tracks_container.setVisible(True)
|
||||
self.expand_indicator.setText("▼")
|
||||
# Calculate height: header + (tracks * track_height) + padding
|
||||
track_height = 54 # 50px + margin
|
||||
total_height = self.collapsed_height + (len(self.track_items) * track_height) + 24
|
||||
self.setFixedHeight(total_height)
|
||||
else:
|
||||
# Collapse to hide tracks
|
||||
self.tracks_container.setVisible(False)
|
||||
self.expand_indicator.setText("▶")
|
||||
self.setFixedHeight(self.collapsed_height)
|
||||
|
||||
# Force layout update
|
||||
self.updateGeometry()
|
||||
if self.parent():
|
||||
self.parent().updateGeometry()
|
||||
|
||||
def handle_track_stream_request(self, track_result, track_item):
|
||||
"""Handle stream request from a track item, passing the correct button reference"""
|
||||
# Emit the stream request with the track item that contains the button
|
||||
self.track_stream_requested.emit(track_result, track_item)
|
||||
|
||||
class SearchResultItem(QFrame):
|
||||
download_requested = pyqtSignal(object) # SearchResult object
|
||||
stream_requested = pyqtSignal(object) # SearchResult object for streaming
|
||||
|
|
@ -943,33 +1278,63 @@ class SearchResultItem(QFrame):
|
|||
return '/'.join(truncated_parts)
|
||||
|
||||
def _extract_song_info(self):
|
||||
"""Extract song title and artist from filename"""
|
||||
filename = self.search_result.filename
|
||||
"""Extract song title and artist from TrackResult"""
|
||||
# Handle case where search_result is a list (shouldn't happen but be defensive)
|
||||
if isinstance(self.search_result, list):
|
||||
if len(self.search_result) > 0:
|
||||
# Take the first item if it's a list
|
||||
actual_result = self.search_result[0]
|
||||
else:
|
||||
# Empty list, return defaults
|
||||
return {'title': 'Unknown Title', 'artist': 'Unknown Artist'}
|
||||
else:
|
||||
actual_result = self.search_result
|
||||
|
||||
# Remove file extension
|
||||
name_without_ext = filename.rsplit('.', 1)[0]
|
||||
# TrackResult objects have parsed metadata available
|
||||
if hasattr(actual_result, 'title') and hasattr(actual_result, 'artist'):
|
||||
# Use parsed metadata from TrackResult
|
||||
return {
|
||||
'title': actual_result.title or 'Unknown Title',
|
||||
'artist': actual_result.artist or 'Unknown Artist'
|
||||
}
|
||||
|
||||
# Common patterns for artist - title separation
|
||||
separators = [' - ', ' – ', ' — ', '_-_', ' | ']
|
||||
|
||||
for sep in separators:
|
||||
if sep in name_without_ext:
|
||||
parts = name_without_ext.split(sep, 1)
|
||||
return {
|
||||
'title': parts[1].strip(),
|
||||
'artist': parts[0].strip()
|
||||
}
|
||||
|
||||
# If no separator found, use filename as title
|
||||
return {
|
||||
'title': name_without_ext,
|
||||
'artist': 'Unknown Artist'
|
||||
}
|
||||
# Fallback: parse from filename if metadata not available
|
||||
if hasattr(actual_result, 'filename'):
|
||||
filename = actual_result.filename
|
||||
|
||||
# Remove file extension
|
||||
name_without_ext = filename.rsplit('.', 1)[0]
|
||||
|
||||
# Common patterns for artist - title separation
|
||||
separators = [' - ', ' – ', ' — ', '_-_', ' | ']
|
||||
|
||||
for sep in separators:
|
||||
if sep in name_without_ext:
|
||||
parts = name_without_ext.split(sep, 1)
|
||||
return {
|
||||
'title': parts[1].strip(),
|
||||
'artist': parts[0].strip()
|
||||
}
|
||||
|
||||
# If no separator found, use filename as title
|
||||
return {
|
||||
'title': name_without_ext,
|
||||
'artist': 'Unknown Artist'
|
||||
}
|
||||
else:
|
||||
# No filename attribute, return defaults
|
||||
return {
|
||||
'title': 'Unknown Title',
|
||||
'artist': 'Unknown Artist'
|
||||
}
|
||||
|
||||
def _create_compact_quality_badge(self):
|
||||
"""Create a compact quality indicator badge"""
|
||||
quality = self.search_result.quality.upper()
|
||||
bitrate = self.search_result.bitrate
|
||||
# Handle list case defensively
|
||||
result = self.search_result[0] if isinstance(self.search_result, list) else self.search_result
|
||||
|
||||
quality = result.quality.upper()
|
||||
bitrate = result.bitrate
|
||||
|
||||
if quality == 'FLAC':
|
||||
badge_text = "FLAC"
|
||||
|
|
@ -1004,8 +1369,11 @@ class SearchResultItem(QFrame):
|
|||
|
||||
def _create_compact_speed_indicator(self):
|
||||
"""Create compact upload speed indicator"""
|
||||
speed = self.search_result.upload_speed
|
||||
slots = self.search_result.free_upload_slots
|
||||
# Handle list case defensively
|
||||
result = self.search_result[0] if isinstance(self.search_result, list) else self.search_result
|
||||
|
||||
speed = result.upload_speed
|
||||
slots = result.free_upload_slots
|
||||
|
||||
if slots > 0 and speed > 100:
|
||||
indicator_color = "#1db954"
|
||||
|
|
@ -2430,12 +2798,20 @@ class DownloadsPage(QWidget):
|
|||
self.search_btn.setText("🔍 Search")
|
||||
self.search_btn.setEnabled(True)
|
||||
|
||||
# Use temp results from progressive loading if available, otherwise use results
|
||||
if hasattr(self, '_temp_search_results') and self._temp_search_results:
|
||||
self.search_results = self._temp_search_results
|
||||
del self._temp_search_results # Clean up temp storage
|
||||
# Handle tuple format (tracks, albums) from enhanced search
|
||||
if isinstance(results, tuple) and len(results) == 2:
|
||||
tracks, albums = results
|
||||
|
||||
# Combine into single list for display - albums first, then tracks
|
||||
combined_results = albums + tracks
|
||||
self.search_results = combined_results
|
||||
self.track_results = tracks # Store separately for future use
|
||||
self.album_results = albums # Store separately for future use
|
||||
else:
|
||||
# Fallback for old list format or empty results
|
||||
self.search_results = results or []
|
||||
self.track_results = results or [] # Assume all are tracks in old format
|
||||
self.album_results = []
|
||||
|
||||
total_results = len(self.search_results)
|
||||
|
||||
|
|
@ -2446,12 +2822,24 @@ class DownloadsPage(QWidget):
|
|||
self.update_search_status(f"✨ Search completed • Found {self.displayed_results} total results", "#1db954")
|
||||
return
|
||||
|
||||
# Update status with album/track breakdown
|
||||
album_count = len(self.album_results) if hasattr(self, 'album_results') else 0
|
||||
track_count = len(self.track_results) if hasattr(self, 'track_results') else total_results
|
||||
|
||||
status_parts = []
|
||||
if album_count > 0:
|
||||
status_parts.append(f"{album_count} album{'s' if album_count != 1 else ''}")
|
||||
if track_count > 0:
|
||||
status_parts.append(f"{track_count} track{'s' if track_count != 1 else ''}")
|
||||
|
||||
result_summary = " • ".join(status_parts) if status_parts else f"{total_results} results"
|
||||
|
||||
# Update status based on whether there are more results to load
|
||||
if self.displayed_results < total_results:
|
||||
remaining = total_results - self.displayed_results
|
||||
self.update_search_status(f"✨ Found {total_results} results • Showing first {self.displayed_results} (scroll down for {remaining} more)", "#1db954")
|
||||
self.update_search_status(f"✨ Found {result_summary} • Showing first {self.displayed_results} (scroll down for {remaining} more)", "#1db954")
|
||||
else:
|
||||
self.update_search_status(f"✨ Search completed • Showing all {total_results} results", "#1db954")
|
||||
self.update_search_status(f"✨ Search completed • Found {result_summary}", "#1db954")
|
||||
|
||||
# If we have no displayed results yet, show the first batch
|
||||
if self.displayed_results == 0 and total_results > 0:
|
||||
|
|
@ -2459,12 +2847,14 @@ class DownloadsPage(QWidget):
|
|||
|
||||
def clear_search_results(self):
|
||||
"""Clear all search result items from the layout"""
|
||||
# Remove all SearchResultItem widgets (but keep stretch)
|
||||
# Remove all SearchResultItem and AlbumResultItem widgets (but keep stretch)
|
||||
items_to_remove = []
|
||||
for i in range(self.search_results_layout.count()):
|
||||
item = self.search_results_layout.itemAt(i)
|
||||
if item and item.widget() and isinstance(item.widget(), SearchResultItem):
|
||||
items_to_remove.append(item.widget())
|
||||
if item and item.widget():
|
||||
widget = item.widget()
|
||||
if isinstance(widget, (SearchResultItem, AlbumResultItem)):
|
||||
items_to_remove.append(widget)
|
||||
|
||||
for widget in items_to_remove:
|
||||
self.search_results_layout.removeWidget(widget)
|
||||
|
|
@ -2501,10 +2891,21 @@ class DownloadsPage(QWidget):
|
|||
# Add result items to UI
|
||||
for i in range(start_index, end_index):
|
||||
result = self.search_results[i]
|
||||
result_item = SearchResultItem(result)
|
||||
result_item.download_requested.connect(self.start_download)
|
||||
result_item.stream_requested.connect(lambda search_result, item=result_item: self.start_stream(search_result, item))
|
||||
result_item.expansion_requested.connect(self.handle_expansion_request)
|
||||
|
||||
# Create appropriate UI component based on result type
|
||||
if isinstance(result, AlbumResult):
|
||||
# Create expandable album result item
|
||||
result_item = AlbumResultItem(result)
|
||||
result_item.album_download_requested.connect(self.start_album_download)
|
||||
result_item.track_download_requested.connect(self.start_download) # Individual track downloads
|
||||
result_item.track_stream_requested.connect(lambda search_result, track_item: self.start_stream(search_result, track_item)) # Individual track streaming
|
||||
else:
|
||||
# Create track result item (play + download)
|
||||
result_item = SearchResultItem(result)
|
||||
result_item.download_requested.connect(self.start_download)
|
||||
result_item.stream_requested.connect(lambda search_result, item=result_item: self.start_stream(search_result, item))
|
||||
result_item.expansion_requested.connect(self.handle_expansion_request)
|
||||
|
||||
# Insert before the stretch (which is always last)
|
||||
insert_position = self.search_results_layout.count() - 1
|
||||
self.search_results_layout.insertWidget(insert_position, result_item)
|
||||
|
|
@ -2606,6 +3007,20 @@ class DownloadsPage(QWidget):
|
|||
except Exception as e:
|
||||
print(f"Failed to start download: {str(e)}")
|
||||
|
||||
def start_album_download(self, album_result):
|
||||
"""Start downloading all tracks in an album"""
|
||||
try:
|
||||
print(f"🎵 Starting album download: {album_result.album_title} by {album_result.artist}")
|
||||
|
||||
# Download each track in the album
|
||||
for track in album_result.tracks:
|
||||
self.start_download(track)
|
||||
|
||||
print(f"✓ Queued {len(album_result.tracks)} tracks for download from album: {album_result.album_title}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed to start album download: {str(e)}")
|
||||
|
||||
def start_stream(self, search_result, result_item=None):
|
||||
"""Start streaming a search result using StreamingThread"""
|
||||
try:
|
||||
|
|
@ -2671,11 +3086,15 @@ class DownloadsPage(QWidget):
|
|||
project_root = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) # Go up from ui/pages/
|
||||
stream_folder = os.path.join(project_root, 'Stream')
|
||||
|
||||
# Find the current stream file
|
||||
# Find any audio file in the stream folder (should only be one)
|
||||
stream_file = None
|
||||
audio_extensions = {'.mp3', '.flac', '.ogg', '.aac', '.wma', '.wav', '.m4a'}
|
||||
|
||||
for filename in os.listdir(stream_folder):
|
||||
if filename.startswith('current_stream') and os.path.isfile(os.path.join(stream_folder, filename)):
|
||||
stream_file = os.path.join(stream_folder, filename)
|
||||
file_path = os.path.join(stream_folder, filename)
|
||||
if (os.path.isfile(file_path) and
|
||||
os.path.splitext(filename)[1].lower() in audio_extensions):
|
||||
stream_file = file_path
|
||||
break
|
||||
|
||||
if stream_file and os.path.exists(stream_file):
|
||||
|
|
|
|||
Loading…
Reference in a new issue