better
This commit is contained in:
parent
f50f32dfc5
commit
b7ef260609
4 changed files with 210 additions and 3 deletions
|
|
@ -1 +1 @@
|
|||
{"access_token": "BQBSqtKDkqV6UKUvbjiQwVB9gKAkFH77HtkUMg1ZL6xiqnBvWilNsahaVug2bEetLc6yJ-kQYSlihYdduklgYnWk4Yy-S985W8l6-4YeiBUTBUauA33_mEUHZm7Fsl_cU-3OMiWexQSyuEN-Mzi_dKqSQyISZSItvgMnuBpM8lMuqeANGmX_mQeYFVztUaSFTmuXOE5j1-UIiz18vHJzpCUc34cZcIJ1SUzxbLY9JJLxrX81MTMCfoynkGepJWne", "token_type": "Bearer", "expires_in": 3600, "scope": "user-library-read user-read-private playlist-read-private playlist-read-collaborative user-read-email", "expires_at": 1752516257, "refresh_token": "AQDmfQkPCGObfJeTUIbW1hAAwhSqkuHRA3Qh2dqVYMRh0eCkFMQgPNJDDzF8y-BiaVbj80zePkK_XSfYH1aJutMtNbnsqRKWuxP31BTrMc7pdUdbE7Fma4oH8wpDUKdG3MM"}
|
||||
{"access_token": "BQDXSOsKBrR2sR0Bf23nD1sV3aZh52IJ9vp7SV1X7ETZHjQFSDN3adTAlMKi8XKtTnpgx402dvCMt-IFyhFD5aX6rs6GmEAFlw49sFxlPZbaoHPoXMXsXZDoxLWDJE8rGGQmOce86VDqJXBqRWzlqIdhxYkHvRjNk1ouYZvwEj1Bq8k15zBxBHR89dIsyI_kSBeXWjt_yHzD8VWX7VLUeFILVn3TTeMtYRho7XB8D4hibhBnjWD2YrQPXIU10HPk", "token_type": "Bearer", "expires_in": 3600, "scope": "user-library-read user-read-private playlist-read-private playlist-read-collaborative user-read-email", "expires_at": 1752520100, "refresh_token": "AQDmfQkPCGObfJeTUIbW1hAAwhSqkuHRA3Qh2dqVYMRh0eCkFMQgPNJDDzF8y-BiaVbj80zePkK_XSfYH1aJutMtNbnsqRKWuxP31BTrMc7pdUdbE7Fma4oH8wpDUKdG3MM"}
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -3856,6 +3856,11 @@ class DownloadsPage(QWidget):
|
|||
self.spotify_client = SpotifyClient()
|
||||
self.matching_engine = MusicMatchingEngine()
|
||||
|
||||
# Album grouping system to ensure tracks from same album go to same folder
|
||||
self.album_groups = {} # Maps original album name -> resolved clean album name
|
||||
self.album_artists = {} # Maps original album name -> artist for consistency
|
||||
self.album_editions = {} # Maps original album name -> edition level ("standard", "deluxe")
|
||||
|
||||
# Initialize audio player for streaming
|
||||
self.audio_player = AudioPlayer(self)
|
||||
self.audio_player.playback_finished.connect(self.on_audio_playback_finished)
|
||||
|
|
@ -6058,6 +6063,18 @@ class DownloadsPage(QWidget):
|
|||
# Determine if this is a single or album track
|
||||
album_info = self._detect_album_info(download_item, artist)
|
||||
|
||||
# Resolve consistent album name for grouping tracks from same album
|
||||
if album_info and album_info['is_album']:
|
||||
print(f"\n🎯 SMART ALBUM GROUPING for track: '{download_item.title}'")
|
||||
print(f" Original album: '{getattr(download_item, 'album', 'None')}'")
|
||||
print(f" Detected album: '{album_info.get('album_name', 'None')}'")
|
||||
|
||||
consistent_album_name = self._resolve_album_group(download_item, artist, album_info)
|
||||
album_info['album_name'] = consistent_album_name
|
||||
|
||||
print(f" Final album name: '{consistent_album_name}'")
|
||||
print(f"🔗 ✅ Album grouping complete!\n")
|
||||
|
||||
if album_info and album_info['is_album']:
|
||||
# Album track structure: Transfer/ARTIST/ARTIST - ALBUM/TRACK# TRACK.ext
|
||||
print(f"🔍 Creating album folder:")
|
||||
|
|
@ -6189,7 +6206,7 @@ class DownloadsPage(QWidget):
|
|||
for pattern in quality_patterns:
|
||||
cleaned = re.sub(pattern, ' ', cleaned, flags=re.IGNORECASE)
|
||||
|
||||
# Remove source/torrent indicators
|
||||
# Remove source/torrent indicators
|
||||
source_patterns = [
|
||||
r'\s*[\[\(]album[\]\)]\s*',
|
||||
r'\s*[\[\(]itunes[\]\)]\s*',
|
||||
|
|
@ -6200,7 +6217,10 @@ class DownloadsPage(QWidget):
|
|||
r'\s*[\[\(]bonus\s*tracks?[\]\)]\s*',
|
||||
r'\s*[\[\(]remaster(ed)?[\]\)]\s*',
|
||||
r'\s*[\[\(]clean[\]\)]\s*',
|
||||
r'\s*[\[\(]explicit[\]\)]\s*'
|
||||
r'\s*[\[\(]explicit[\]\)]\s*',
|
||||
r'\s*[\[\(]album\+itunes\+bonus\s*tracks?[\]\)]\s*', # [Album+iTunes+Bonus Tracks]
|
||||
r'\s*[\[\(]itunes\+bonus[\]\)]\s*',
|
||||
r'\s*[\[\(]bonus[\]\)]\s*'
|
||||
]
|
||||
for pattern in source_patterns:
|
||||
cleaned = re.sub(pattern, ' ', cleaned, flags=re.IGNORECASE)
|
||||
|
|
@ -6276,6 +6296,193 @@ class DownloadsPage(QWidget):
|
|||
print(f"🧹 Track Title Result: '{original}' -> '{cleaned}'")
|
||||
return cleaned
|
||||
|
||||
def _resolve_album_group(self, download_item, artist: Artist, album_info: dict) -> str:
|
||||
"""
|
||||
Smart album grouping: Start with standard, upgrade to deluxe if ANY track is deluxe.
|
||||
This ensures all tracks from the same album get the same folder name.
|
||||
"""
|
||||
try:
|
||||
# Get the original album name from the download item (if it has one)
|
||||
original_album = getattr(download_item, 'album', None)
|
||||
detected_album = album_info.get('album_name', '')
|
||||
|
||||
# Extract base album name (without edition indicators)
|
||||
if album_info.get('spotify_track'):
|
||||
# Use Spotify album name for base
|
||||
base_album = self._get_base_album_name(detected_album)
|
||||
elif original_album:
|
||||
# Clean the original Soulseek album name
|
||||
cleaned_original = self._clean_album_title(original_album, artist.name)
|
||||
base_album = self._get_base_album_name(cleaned_original)
|
||||
else:
|
||||
base_album = self._get_base_album_name(detected_album)
|
||||
|
||||
# Normalize the base name (handle case variations, etc.)
|
||||
base_album = self._normalize_base_album_name(base_album, artist.name)
|
||||
|
||||
# Create a key for this album group (artist + base album)
|
||||
album_key = f"{artist.name}::{base_album}"
|
||||
|
||||
print(f"🔍 Album grouping - Key: '{album_key}', Detected: '{detected_album}'")
|
||||
|
||||
# Check if this track indicates a deluxe edition
|
||||
is_deluxe_track = False
|
||||
if album_info.get('spotify_track'):
|
||||
is_deluxe_track = self._detect_deluxe_edition(detected_album)
|
||||
elif original_album:
|
||||
is_deluxe_track = self._detect_deluxe_edition(original_album)
|
||||
|
||||
# Get current edition level for this album group (default to standard)
|
||||
current_edition = self.album_editions.get(album_key, "standard")
|
||||
|
||||
# SMART ALGORITHM: Upgrade to deluxe if this track is deluxe
|
||||
if is_deluxe_track and current_edition == "standard":
|
||||
print(f"🎯 UPGRADE: Album '{base_album}' upgraded from standard to deluxe!")
|
||||
self.album_editions[album_key] = "deluxe"
|
||||
current_edition = "deluxe"
|
||||
|
||||
# Build final album name based on edition level
|
||||
if current_edition == "deluxe":
|
||||
final_album_name = f"{base_album} (Deluxe Edition)"
|
||||
else:
|
||||
final_album_name = base_album
|
||||
|
||||
# Store the resolution
|
||||
self.album_groups[album_key] = final_album_name
|
||||
self.album_artists[album_key] = artist.name
|
||||
|
||||
print(f"🔗 Album resolution: '{detected_album}' -> '{final_album_name}' (edition: {current_edition})")
|
||||
|
||||
return final_album_name
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error resolving album group: {e}")
|
||||
return album_info.get('album_name', download_item.title)
|
||||
|
||||
def _normalize_base_album_name(self, base_album: str, artist_name: str) -> str:
|
||||
"""
|
||||
Normalize the base album name to handle case variations and known corrections.
|
||||
"""
|
||||
import re
|
||||
|
||||
# Apply known album corrections for consistent naming
|
||||
normalized_lower = base_album.lower().strip()
|
||||
|
||||
known_corrections = {
|
||||
'good kid maad city': 'good kid, m.A.A.d city',
|
||||
'good kid m.a.a.d city': 'good kid, m.A.A.d city',
|
||||
'good kid m.a.a.d. city': 'good kid, m.A.A.d city',
|
||||
'good kid m a a d city': 'good kid, m.A.A.d city',
|
||||
'good kid m.a.a.d city': 'good kid, m.A.A.d city'
|
||||
}
|
||||
|
||||
for key, correction in known_corrections.items():
|
||||
if key == normalized_lower:
|
||||
print(f"📝 Base album correction: '{base_album}' -> '{correction}'")
|
||||
return correction
|
||||
|
||||
# If no specific correction, return cleaned version
|
||||
return base_album.strip()
|
||||
|
||||
def _normalize_spotify_album_variants(self, album_name: str, artist_name: str) -> str:
|
||||
"""
|
||||
Normalize different Spotify album variants to a consistent name.
|
||||
E.g., 'good kid, m.A.A.d city' and 'good kid, m.A.A.d city (Deluxe)'
|
||||
should both resolve to 'good kid, m.A.A.d city (Deluxe Edition)'
|
||||
"""
|
||||
import re
|
||||
|
||||
normalized = album_name.strip()
|
||||
|
||||
# Convert various deluxe indicators to standard format
|
||||
deluxe_patterns = [
|
||||
(r'\s*\(deluxe\)\s*$', ' (Deluxe Edition)'),
|
||||
(r'\s*\[deluxe\]\s*$', ' (Deluxe Edition)'),
|
||||
(r'\s*deluxe\s*$', ' (Deluxe Edition)'),
|
||||
(r'\s*\(deluxe\s+edition\)\s*$', ' (Deluxe Edition)'),
|
||||
(r'\s*\[deluxe\s+edition\]\s*$', ' (Deluxe Edition)')
|
||||
]
|
||||
|
||||
for pattern, replacement in deluxe_patterns:
|
||||
if re.search(pattern, normalized, re.IGNORECASE):
|
||||
normalized = re.sub(pattern, replacement, normalized, flags=re.IGNORECASE)
|
||||
break
|
||||
|
||||
# Normalize case inconsistencies for known albums
|
||||
# This handles cases like "good kid, m.A.A.d city" vs "Good Kid M.A.A.D City"
|
||||
known_album_corrections = {
|
||||
'good kid maad city': 'good kid, m.A.A.d city',
|
||||
'good kid m.a.a.d city': 'good kid, m.A.A.d city',
|
||||
'good kid m.a.a.d. city': 'good kid, m.A.A.d city',
|
||||
'good kid m.a.a.d city': 'good kid, m.A.A.d city',
|
||||
'good kid m a a d city': 'good kid, m.A.A.d city'
|
||||
}
|
||||
|
||||
normalized_lower = normalized.lower()
|
||||
for key, correction in known_album_corrections.items():
|
||||
if key in normalized_lower:
|
||||
# Preserve any edition suffix
|
||||
suffix = ''
|
||||
if '(deluxe edition)' in normalized_lower:
|
||||
suffix = ' (Deluxe Edition)'
|
||||
elif '(deluxe)' in normalized_lower:
|
||||
suffix = ' (Deluxe Edition)'
|
||||
|
||||
normalized = correction + suffix
|
||||
break
|
||||
|
||||
print(f"📀 Album variant normalization: '{album_name}' -> '{normalized}'")
|
||||
return normalized
|
||||
|
||||
def _detect_deluxe_edition(self, album_name: str) -> bool:
|
||||
"""
|
||||
Detect if an album name indicates a deluxe/special edition.
|
||||
Returns True if it's a deluxe variant, False for standard.
|
||||
"""
|
||||
if not album_name:
|
||||
return False
|
||||
|
||||
album_lower = album_name.lower()
|
||||
|
||||
# Check for deluxe indicators
|
||||
deluxe_indicators = [
|
||||
'deluxe',
|
||||
'deluxe edition',
|
||||
'special edition',
|
||||
'expanded edition',
|
||||
'extended edition',
|
||||
'bonus',
|
||||
'remastered',
|
||||
'anniversary',
|
||||
'collectors edition',
|
||||
'limited edition'
|
||||
]
|
||||
|
||||
for indicator in deluxe_indicators:
|
||||
if indicator in album_lower:
|
||||
print(f"🎯 Detected deluxe edition: '{album_name}' contains '{indicator}'")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _get_base_album_name(self, album_name: str) -> str:
|
||||
"""
|
||||
Extract the base album name without edition indicators.
|
||||
E.g., 'good kid, m.A.A.d city (Deluxe Edition)' -> 'good kid, m.A.A.d city'
|
||||
"""
|
||||
import re
|
||||
|
||||
# Remove common edition suffixes
|
||||
base_name = album_name
|
||||
|
||||
# Remove edition indicators in parentheses or brackets
|
||||
base_name = re.sub(r'\s*[\[\(](deluxe|special|expanded|extended|bonus|remastered|anniversary|collectors?|limited).*?[\]\)]\s*$', '', base_name, flags=re.IGNORECASE)
|
||||
|
||||
# Remove standalone edition words at the end
|
||||
base_name = re.sub(r'\s+(deluxe|special|expanded|extended|bonus|remastered|anniversary|collectors?|limited)\s*(edition)?\s*$', '', base_name, flags=re.IGNORECASE)
|
||||
|
||||
return base_name.strip()
|
||||
|
||||
def _detect_album_info(self, download_item, artist: Artist) -> Optional[dict]:
|
||||
"""Detect if track is part of an album using Spotify API as primary source"""
|
||||
try:
|
||||
|
|
|
|||
Loading…
Reference in a new issue