Prefer album versions over singles when matching tracks to metadata sources
Add album_type and total_tracks fields to Track dataclass, populate from Spotify/iTunes/Deezer API responses, and apply a small tiebreaker bonus (+0.02 for albums, +0.01 for EPs) in all matching loops so album versions win when confidence scores are otherwise equal.
This commit is contained in:
parent
171a64005d
commit
6bf337423d
4 changed files with 60 additions and 6 deletions
|
|
@ -83,6 +83,17 @@ class Track:
|
||||||
if track_data.get('link'):
|
if track_data.get('link'):
|
||||||
external_urls['deezer'] = track_data['link']
|
external_urls['deezer'] = track_data['link']
|
||||||
|
|
||||||
|
# Deezer search doesn't return album_type directly; infer if nb_tracks available
|
||||||
|
nb_tracks = album_data.get('nb_tracks') if isinstance(album_data, dict) else None
|
||||||
|
album_type = track_data.get('type') # Deezer sometimes returns 'album'/'single'
|
||||||
|
if not album_type and nb_tracks:
|
||||||
|
if nb_tracks <= 3:
|
||||||
|
album_type = 'single'
|
||||||
|
elif nb_tracks <= 6:
|
||||||
|
album_type = 'ep'
|
||||||
|
else:
|
||||||
|
album_type = 'album'
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
id=str(track_data.get('id', '')),
|
id=str(track_data.get('id', '')),
|
||||||
name=track_data.get('title', ''),
|
name=track_data.get('title', ''),
|
||||||
|
|
@ -96,6 +107,8 @@ class Track:
|
||||||
release_date=track_data.get('release_date') or (album_data.get('release_date') if isinstance(album_data, dict) else None),
|
release_date=track_data.get('release_date') or (album_data.get('release_date') if isinstance(album_data, dict) else None),
|
||||||
track_number=track_data.get('track_position'),
|
track_number=track_data.get('track_position'),
|
||||||
disc_number=track_data.get('disk_number', 1),
|
disc_number=track_data.get('disk_number', 1),
|
||||||
|
album_type=album_type,
|
||||||
|
total_tracks=nb_tracks,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,8 @@ class Track:
|
||||||
release_date: Optional[str] = None
|
release_date: Optional[str] = None
|
||||||
track_number: Optional[int] = None
|
track_number: Optional[int] = None
|
||||||
disc_number: Optional[int] = None
|
disc_number: Optional[int] = None
|
||||||
|
album_type: Optional[str] = None
|
||||||
|
total_tracks: Optional[int] = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_itunes_track(cls, track_data: Dict[str, Any], clean_artist_name: Optional[str] = None) -> 'Track':
|
def from_itunes_track(cls, track_data: Dict[str, Any], clean_artist_name: Optional[str] = None) -> 'Track':
|
||||||
|
|
@ -92,6 +94,15 @@ class Track:
|
||||||
if 'trackViewUrl' in track_data:
|
if 'trackViewUrl' in track_data:
|
||||||
external_urls['itunes'] = track_data['trackViewUrl']
|
external_urls['itunes'] = track_data['trackViewUrl']
|
||||||
|
|
||||||
|
# Infer album type from track count
|
||||||
|
track_count = track_data.get('trackCount', 0)
|
||||||
|
if track_count <= 3:
|
||||||
|
album_type = 'single'
|
||||||
|
elif track_count <= 6:
|
||||||
|
album_type = 'ep'
|
||||||
|
else:
|
||||||
|
album_type = 'album'
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
id=str(track_data.get('trackId', '')),
|
id=str(track_data.get('trackId', '')),
|
||||||
name=track_data.get('trackName', ''),
|
name=track_data.get('trackName', ''),
|
||||||
|
|
@ -102,7 +113,9 @@ class Track:
|
||||||
preview_url=track_data.get('previewUrl'),
|
preview_url=track_data.get('previewUrl'),
|
||||||
external_urls=external_urls if external_urls else None,
|
external_urls=external_urls if external_urls else None,
|
||||||
image_url=album_image_url,
|
image_url=album_image_url,
|
||||||
release_date=track_data.get('releaseDate', '').split('T')[0] if track_data.get('releaseDate') else None
|
release_date=track_data.get('releaseDate', '').split('T')[0] if track_data.get('releaseDate') else None,
|
||||||
|
album_type=album_type,
|
||||||
|
total_tracks=track_count or None
|
||||||
)
|
)
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
|
||||||
|
|
@ -317,7 +317,9 @@ class Track:
|
||||||
preview_url=track_data.get('preview_url'),
|
preview_url=track_data.get('preview_url'),
|
||||||
external_urls=track_data.get('external_urls'),
|
external_urls=track_data.get('external_urls'),
|
||||||
image_url=album_image_url,
|
image_url=album_image_url,
|
||||||
release_date=track_data.get('album', {}).get('release_date')
|
release_date=track_data.get('album', {}).get('release_date'),
|
||||||
|
album_type=track_data.get('album', {}).get('album_type'),
|
||||||
|
total_tracks=track_data.get('album', {}).get('total_tracks')
|
||||||
)
|
)
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
|
||||||
|
|
@ -9761,6 +9761,12 @@ def enhance_artist_quality(artist_id):
|
||||||
matching_engine.normalize_string(sp_track.name)
|
matching_engine.normalize_string(sp_track.name)
|
||||||
)
|
)
|
||||||
combined = artist_conf * 0.5 + title_conf * 0.5
|
combined = artist_conf * 0.5 + title_conf * 0.5
|
||||||
|
# Small bonus for album tracks over singles
|
||||||
|
_at = getattr(sp_track, 'album_type', None) or ''
|
||||||
|
if _at == 'album':
|
||||||
|
combined += 0.02
|
||||||
|
elif _at == 'ep':
|
||||||
|
combined += 0.01
|
||||||
if combined > best_confidence and combined >= 0.7:
|
if combined > best_confidence and combined >= 0.7:
|
||||||
best_confidence = combined
|
best_confidence = combined
|
||||||
best_match = sp_track
|
best_match = sp_track
|
||||||
|
|
@ -9831,6 +9837,12 @@ def enhance_artist_quality(artist_id):
|
||||||
matching_engine.normalize_string(it_track.name)
|
matching_engine.normalize_string(it_track.name)
|
||||||
)
|
)
|
||||||
combined = artist_conf * 0.5 + title_conf * 0.5
|
combined = artist_conf * 0.5 + title_conf * 0.5
|
||||||
|
# Small bonus for album tracks over singles
|
||||||
|
_at = getattr(it_track, 'album_type', None) or ''
|
||||||
|
if _at == 'album':
|
||||||
|
combined += 0.02
|
||||||
|
elif _at == 'ep':
|
||||||
|
combined += 0.01
|
||||||
if combined > itunes_best_conf and combined >= 0.7:
|
if combined > itunes_best_conf and combined >= 0.7:
|
||||||
itunes_best_conf = combined
|
itunes_best_conf = combined
|
||||||
itunes_best = it_track
|
itunes_best = it_track
|
||||||
|
|
@ -13012,10 +13024,10 @@ def _detect_album_info_web(context: dict, artist: dict) -> dict:
|
||||||
query = f"artist:{artist_name} track:{clean_title}"
|
query = f"artist:{artist_name} track:{clean_title}"
|
||||||
tracks = spotify_client.search_tracks(query, limit=5)
|
tracks = spotify_client.search_tracks(query, limit=5)
|
||||||
|
|
||||||
# Find the best matching track
|
# Find the best matching track (prefer album versions over singles)
|
||||||
best_match = None
|
best_match = None
|
||||||
best_confidence = 0
|
best_confidence = 0
|
||||||
|
|
||||||
if tracks:
|
if tracks:
|
||||||
from core.matching_engine import MusicMatchingEngine
|
from core.matching_engine import MusicMatchingEngine
|
||||||
matching_engine = MusicMatchingEngine()
|
matching_engine = MusicMatchingEngine()
|
||||||
|
|
@ -13029,9 +13041,16 @@ def _detect_album_info_web(context: dict, artist: dict) -> dict:
|
||||||
matching_engine.normalize_string(clean_title),
|
matching_engine.normalize_string(clean_title),
|
||||||
matching_engine.normalize_string(track.name)
|
matching_engine.normalize_string(track.name)
|
||||||
)
|
)
|
||||||
|
|
||||||
combined_confidence = (artist_confidence * 0.6 + title_confidence * 0.4)
|
combined_confidence = (artist_confidence * 0.6 + title_confidence * 0.4)
|
||||||
|
|
||||||
|
# Small bonus for album tracks so they win ties over singles/EPs
|
||||||
|
album_type = getattr(track, 'album_type', None) or ''
|
||||||
|
if album_type == 'album':
|
||||||
|
combined_confidence += 0.02
|
||||||
|
elif album_type == 'ep':
|
||||||
|
combined_confidence += 0.01
|
||||||
|
|
||||||
if combined_confidence > best_confidence and combined_confidence > 0.75: # Higher threshold to avoid bad matches
|
if combined_confidence > best_confidence and combined_confidence > 0.75: # Higher threshold to avoid bad matches
|
||||||
best_match = track
|
best_match = track
|
||||||
best_confidence = combined_confidence
|
best_confidence = combined_confidence
|
||||||
|
|
@ -19877,6 +19896,13 @@ def _run_quality_scanner(scope='watchlist', profile_id=1):
|
||||||
# Combined confidence (50% artist + 50% title)
|
# Combined confidence (50% artist + 50% title)
|
||||||
combined_confidence = (artist_confidence * 0.5 + title_confidence * 0.5)
|
combined_confidence = (artist_confidence * 0.5 + title_confidence * 0.5)
|
||||||
|
|
||||||
|
# Small bonus for album tracks over singles
|
||||||
|
_at = getattr(spotify_track, 'album_type', None) or ''
|
||||||
|
if _at == 'album':
|
||||||
|
combined_confidence += 0.02
|
||||||
|
elif _at == 'ep':
|
||||||
|
combined_confidence += 0.01
|
||||||
|
|
||||||
print(f"🔍 [Quality Scanner] Candidate: '{spotify_track.artists[0]}' - '{spotify_track.name}' (confidence: {combined_confidence:.3f})")
|
print(f"🔍 [Quality Scanner] Candidate: '{spotify_track.artists[0]}' - '{spotify_track.name}' (confidence: {combined_confidence:.3f})")
|
||||||
|
|
||||||
# Update best match if this is better
|
# Update best match if this is better
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue