From 019253087fa743a170749e2a7ea1b5c0f8f7c8b0 Mon Sep 17 00:00:00 2001 From: Broque Thomas Date: Wed, 6 Aug 2025 11:31:35 -0700 Subject: [PATCH] better matching on albums --- .../music_database.cpython-312.pyc | Bin 37717 -> 50528 bytes database/music_database.py | 352 ++++++++++++++++-- ui/pages/__pycache__/artists.cpython-312.pyc | Bin 215573 -> 216424 bytes ui/pages/__pycache__/sync.cpython-312.pyc | Bin 232218 -> 232213 bytes ui/pages/artists.py | 214 ++++++----- ui/pages/sync.py | 4 +- 6 files changed, 447 insertions(+), 123 deletions(-) diff --git a/database/__pycache__/music_database.cpython-312.pyc b/database/__pycache__/music_database.cpython-312.pyc index a015a52762828526ad8023f4b336be0c3124e0f5..4b823b0909e76f3d3c0fae10069ce82f7ea1b7f7 100644 GIT binary patch delta 13217 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 delta 1801 zcmX|?c}!GC7{F)d?b`#Ey(Y!vH7jPXMX`88 zlo0*Q;0g6w6|1hxo-nUXvFW;MPc`tFU5!;EwL~?FI~mmI&9X9DjoFNpBz6?X5gg7=OJ;&cu|7GH;7qt>`Y8_S&4fnt=_Du5!gFX=)u6>{QKDp-k!O>YL^TV1mK2-?+c;ft#Ny(-YMFtv zAuhy;$3Q{Iw7fJPZO}qBgJvCWn0J)E*`Q>MSTjegM9tt<=h_rk4l4`Yg7IYbr}aI8 zXRzAvIaXZ6x)R$k-pVSIW=ptRwelG` zgE4%}7Kx^ELAMILFASu3fD3&E&1%Vt8tN{sV)p8d_yw{~Mra*BT7+Cuae1D;ZWosC zY8LBSVGA5xH6DKsH&^-Lr%EflxjI>1z@Pa8cz5+4ybl~S27DE~yzyHR)&ZS$Nm3A3z>T`Kct57I@7!1@3N4g-)LxX#I5l*4pZe5}wan%%567 z)VMAX{(RsIVyjO*JfYZL7Pu-9gmdl5(Acmft)GwN-@$DSXeh*H$?kWg|LT}>-Z3R0 z2kQEYmR@u$1M8-D90{`s+MUVKLoBoMq@K{^S-DbAuFQ41orlA|PvY36!{ZHkU3?b| zbC=fC@++fXRzJNZie3`k|$5T_qo8VK=7Y+*q77E-G$Q9sjSB@_s1N@E#QjJ%16Lc4hhT1?L$&lEg zKs9y*5q%_19&)%ay`zg_7bzZAvtUlg7vtK5hgsm5fc~8K3r&C6H-z?wfJxvl1TS_b zn)0~r?;vb>=gSywcNLQR3*c7QHdnNq9Y`BOTy*<&(N|L0SLHijQmyZX5SmY3C0vK* z@8-(fZ77>Xo$z4t7(qk#B;zxl_jQSNbTb_Luc&uI;4w!~uBg$xerPxuL!KhoadM)O z|COK>5}ZAGEdBxaa3#~}!n;&>lWITx2f_YRE2mSNusQ^s0%HY4N-BPFshuNem{(m| zMfHk_!YKNn23XviWDn(4#K+)2#W5sdj2{{i4!@2<;iKMkIMX{fY`GQN?jymWnWMn* H!_5BxGqwa3 diff --git a/database/music_database.py b/database/music_database.py index 9fd2a263..84a98548 100644 --- a/database/music_database.py +++ b/database/music_database.py @@ -2,7 +2,9 @@ import sqlite3 import json +import logging import os +import re import threading from datetime import datetime from typing import List, Optional, Dict, Any, Tuple @@ -11,6 +13,8 @@ from pathlib import Path from utils.logging_config import get_logger logger = get_logger("music_database") +# Temporarily enable debug logging for edition matching +logger.setLevel(logging.DEBUG) @dataclass class DatabaseArtist: @@ -578,38 +582,46 @@ class MusicDatabase: def check_track_exists(self, title: str, artist: str, confidence_threshold: float = 0.8) -> Tuple[Optional[DatabaseTrack], float]: """ - Check if a track exists in the database with fuzzy matching and confidence scoring. + Check if a track exists in the database with enhanced fuzzy matching and confidence scoring. + Now uses the same sophisticated matching approach as album checking for consistency. Returns (track, confidence) tuple where confidence is 0.0-1.0 """ try: - # Search for potential matches - potential_matches = self.search_tracks(title=title, artist=artist, limit=20) + # Generate title variations for better matching (similar to album approach) + title_variations = self._generate_track_title_variations(title) - if not potential_matches: - return None, 0.0 + logger.debug(f"🔍 Enhanced track matching for '{title}' by '{artist}': trying {len(title_variations)} variations") + for i, var in enumerate(title_variations): + logger.debug(f" {i+1}. '{var}'") - # Simple confidence scoring based on string similarity - def calculate_confidence(db_track: DatabaseTrack) -> float: - title_similarity = self._string_similarity(title.lower().strip(), db_track.title.lower().strip()) - artist_similarity = self._string_similarity(artist.lower().strip(), db_track.artist_name.lower().strip()) - - # Weight title slightly more than artist - return (title_similarity * 0.6) + (artist_similarity * 0.4) - - # Find best match best_match = None best_confidence = 0.0 - for track in potential_matches: - confidence = calculate_confidence(track) - if confidence > best_confidence: - best_confidence = confidence - best_match = track + # Try each title variation + for title_variation in title_variations: + # Search for potential matches with this variation + potential_matches = self.search_tracks(title=title_variation, artist=artist, limit=20) + + if not potential_matches: + continue + + logger.debug(f"🎵 Found {len(potential_matches)} tracks for variation '{title_variation}'") + + # Score each potential match + for track in potential_matches: + confidence = self._calculate_track_confidence(title, artist, track) + logger.debug(f" 🎯 '{track.title}' confidence: {confidence:.3f}") + + if confidence > best_confidence: + best_confidence = confidence + best_match = track # Return match only if it meets threshold - if best_confidence >= confidence_threshold: + if best_match and best_confidence >= confidence_threshold: + logger.debug(f"✅ Enhanced track match found: '{title}' -> '{best_match.title}' (confidence: {best_confidence:.3f})") return best_match, best_confidence else: + logger.debug(f"❌ No confident track match for '{title}' (best: {best_confidence:.3f}, threshold: {confidence_threshold})") return None, best_confidence except Exception as e: @@ -736,11 +748,12 @@ class MusicDatabase: def check_album_exists_with_completeness(self, title: str, artist: str, expected_track_count: Optional[int] = None, confidence_threshold: float = 0.8) -> Tuple[Optional[DatabaseAlbum], float, int, int, bool]: """ Check if an album exists in the database with completeness information. + Enhanced to handle edition matching (standard <-> deluxe variants). Returns (album, confidence, owned_tracks, expected_tracks, is_complete) """ try: - # First find the album match - album, confidence = self.check_album_exists(title, artist, confidence_threshold) + # Try enhanced edition-aware matching first with expected track count for Smart Edition Matching + album, confidence = self.check_album_exists_with_editions(title, artist, confidence_threshold, expected_track_count) if not album: return None, 0.0, 0, 0, False @@ -754,6 +767,301 @@ class MusicDatabase: logger.error(f"Error checking album existence with completeness for '{title}' by '{artist}': {e}") return None, 0.0, 0, 0, False + def check_album_exists_with_editions(self, title: str, artist: str, confidence_threshold: float = 0.8, expected_track_count: Optional[int] = None) -> Tuple[Optional[DatabaseAlbum], float]: + """ + Enhanced album existence check that handles edition variants. + Matches standard albums with deluxe/platinum/special editions and vice versa. + """ + try: + # Generate album title variations for edition matching + title_variations = self._generate_album_title_variations(title) + + logger.debug(f"🔍 Edition matching for '{title}' by '{artist}': trying {len(title_variations)} variations") + for i, var in enumerate(title_variations): + logger.debug(f" {i+1}. '{var}'") + + best_match = None + best_confidence = 0.0 + + for variation in title_variations: + # Search for this variation + albums = self.search_albums(title=variation, artist=artist, limit=10) + + if albums: + logger.debug(f"📀 Found {len(albums)} albums for variation '{variation}'") + + if not albums: + continue + + # Score each potential match with Smart Edition Matching + for album in albums: + confidence = self._calculate_album_confidence(title, artist, album, expected_track_count) + logger.debug(f" 🎯 '{album.title}' confidence: {confidence:.3f}") + + if confidence > best_confidence: + best_confidence = confidence + best_match = album + + # Return match only if it meets threshold + if best_match and best_confidence >= confidence_threshold: + logger.debug(f"✅ Edition match found: '{title}' -> '{best_match.title}' (confidence: {best_confidence:.3f})") + return best_match, best_confidence + else: + logger.debug(f"❌ No confident edition match for '{title}' (best: {best_confidence:.3f}, threshold: {confidence_threshold})") + return None, best_confidence + + except Exception as e: + logger.error(f"Error in edition-aware album matching for '{title}' by '{artist}': {e}") + return None, 0.0 + + def _generate_album_title_variations(self, title: str) -> List[str]: + """Generate variations of album title to handle edition matching""" + variations = [title] # Always include original + + # Clean up the title + title_lower = title.lower().strip() + + # Define edition patterns and their variations + edition_patterns = { + r'\s*\(deluxe\s*edition?\)': ['deluxe', 'deluxe edition'], + r'\s*\(expanded\s*edition?\)': ['expanded', 'expanded edition'], + r'\s*\(platinum\s*edition?\)': ['platinum', 'platinum edition'], + r'\s*\(special\s*edition?\)': ['special', 'special edition'], + r'\s*\(remastered?\)': ['remastered', 'remaster'], + r'\s*\(anniversary\s*edition?\)': ['anniversary', 'anniversary edition'], + r'\s*\(.*version\)': ['version'], + r'\s+deluxe\s*edition?$': ['deluxe', 'deluxe edition'], + r'\s+platinum\s*edition?$': ['platinum', 'platinum edition'], + r'\s+special\s*edition?$': ['special', 'special edition'], + r'\s*-\s*deluxe': ['deluxe'], + r'\s*-\s*platinum\s*edition?': ['platinum', 'platinum edition'], + } + + # Check if title contains any edition indicators + base_title = title + found_editions = [] + + for pattern, edition_types in edition_patterns.items(): + if re.search(pattern, title_lower): + # Remove the edition part to get base title + base_title = re.sub(pattern, '', title, flags=re.IGNORECASE).strip() + found_editions.extend(edition_types) + break + + # Add base title (without edition markers) + if base_title != title: + variations.append(base_title) + + # If we found a base title, add common edition variants + if base_title != title: + # Add common deluxe/platinum/special variants + common_editions = [ + 'deluxe edition', + 'deluxe', + 'platinum edition', + 'platinum', + 'special edition', + 'expanded edition', + 'remastered', + 'anniversary edition' + ] + + for edition in common_editions: + variations.extend([ + f"{base_title} ({edition.title()})", + f"{base_title} ({edition})", + f"{base_title} - {edition.title()}", + f"{base_title} {edition.title()}", + ]) + + # If original title is base form, add edition variants + elif not any(re.search(pattern, title_lower) for pattern in edition_patterns.keys()): + # This appears to be a base album, add deluxe variants + common_editions = ['Deluxe Edition', 'Deluxe', 'Platinum Edition', 'Special Edition'] + for edition in common_editions: + variations.extend([ + f"{title} ({edition})", + f"{title} - {edition}", + f"{title} {edition}", + ]) + + # Remove duplicates while preserving order + seen = set() + unique_variations = [] + for var in variations: + var_clean = var.strip() + if var_clean and var_clean.lower() not in seen: + seen.add(var_clean.lower()) + unique_variations.append(var_clean) + + return unique_variations + + def _calculate_album_confidence(self, search_title: str, search_artist: str, db_album: DatabaseAlbum, expected_track_count: Optional[int] = None) -> float: + """Calculate confidence score for album match with Smart Edition Matching""" + try: + # Simple confidence based on string similarity + title_similarity = self._string_similarity(search_title.lower(), db_album.title.lower()) + artist_similarity = self._string_similarity(search_artist.lower(), db_album.artist_name.lower()) + + # Also try with cleaned versions (removing edition markers) + clean_search_title = self._clean_album_title_for_comparison(search_title) + clean_db_title = self._clean_album_title_for_comparison(db_album.title) + clean_title_similarity = self._string_similarity(clean_search_title, clean_db_title) + + # Use the best title similarity + best_title_similarity = max(title_similarity, clean_title_similarity) + + # Weight: 50% title, 50% artist (equal weight to prevent false positives) + # Also require minimum artist similarity to prevent matching wrong artists + confidence = (best_title_similarity * 0.5) + (artist_similarity * 0.5) + + # Apply artist similarity penalty: if artist match is too low, drastically reduce confidence + if artist_similarity < 0.6: # Less than 60% artist match + confidence *= 0.3 # Reduce confidence by 70% + + # Smart Edition Matching: Boost confidence if we found a "better" edition + if expected_track_count and db_album.track_count and clean_title_similarity >= 0.8: + # If the cleaned titles match well, check if this is an edition upgrade + if db_album.track_count >= expected_track_count: + # Found same/better edition (e.g., Deluxe when searching for Standard) + edition_bonus = min(0.15, (db_album.track_count - expected_track_count) / expected_track_count * 0.1) + confidence += edition_bonus + logger.debug(f" 📀 Edition upgrade bonus: +{edition_bonus:.3f} ({db_album.track_count} >= {expected_track_count} tracks)") + elif db_album.track_count < expected_track_count * 0.8: + # Found significantly smaller edition, apply penalty + edition_penalty = 0.1 + confidence -= edition_penalty + logger.debug(f" 📀 Edition downgrade penalty: -{edition_penalty:.3f} ({db_album.track_count} << {expected_track_count} tracks)") + + return min(confidence, 1.0) # Cap at 1.0 + + except Exception as e: + logger.error(f"Error calculating album confidence: {e}") + return 0.0 + + def _generate_track_title_variations(self, title: str) -> List[str]: + """Generate variations of track title for better matching""" + variations = [title] # Always include original + + # Clean up the title + title_lower = title.lower().strip() + + # Common track title variations + track_patterns = [ + # Remove version/remix info + r'\s*\(.*version\)', + r'\s*\(.*remix\)', + r'\s*\(.*mix\)', + r'\s*\(.*edit\)', + r'\s*\(.*radio\)', + # Remove featuring artists + r'\s*\(.*feat\..*\)', + r'\s*\(.*featuring.*\)', + r'\s*\(.*ft\..*\)', + # Remove brackets/parentheses content + r'\s*\[.*\]', + r'\s*\(.*\)', + # Remove everything after dash + r'\s*-\s*.*' + ] + + for pattern in track_patterns: + # Apply pattern to original title + cleaned = re.sub(pattern, '', title, flags=re.IGNORECASE).strip() + if cleaned and cleaned.lower() != title_lower and cleaned not in variations: + variations.append(cleaned) + + # Apply pattern to lowercase version + cleaned_lower = re.sub(pattern, '', title_lower, flags=re.IGNORECASE).strip() + if cleaned_lower and cleaned_lower != title_lower: + # Convert back to proper case + cleaned_proper = cleaned_lower.title() + if cleaned_proper not in variations: + variations.append(cleaned_proper) + + # Remove duplicates while preserving order + seen = set() + unique_variations = [] + for var in variations: + var_key = var.lower().strip() + if var_key not in seen and var.strip(): + seen.add(var_key) + unique_variations.append(var.strip()) + + return unique_variations + + def _calculate_track_confidence(self, search_title: str, search_artist: str, db_track: DatabaseTrack) -> float: + """Calculate confidence score for track match with enhanced cleaning""" + try: + # Direct similarity + title_similarity = self._string_similarity(search_title.lower(), db_track.title.lower()) + artist_similarity = self._string_similarity(search_artist.lower(), db_track.artist_name.lower()) + + # Also try with cleaned versions (removing parentheses, brackets, etc.) + clean_search_title = self._clean_track_title_for_comparison(search_title) + clean_db_title = self._clean_track_title_for_comparison(db_track.title) + clean_title_similarity = self._string_similarity(clean_search_title, clean_db_title) + + # Use the best title similarity (direct or cleaned) + best_title_similarity = max(title_similarity, clean_title_similarity) + + # Weight: 50% title, 50% artist (equal weight to prevent false positives) + # Also require minimum artist similarity to prevent matching wrong artists + confidence = (best_title_similarity * 0.5) + (artist_similarity * 0.5) + + # Apply artist similarity penalty: if artist match is too low, drastically reduce confidence + if artist_similarity < 0.6: # Less than 60% artist match + confidence *= 0.3 # Reduce confidence by 70% + + return confidence + + except Exception as e: + logger.error(f"Error calculating track confidence: {e}") + return 0.0 + + def _clean_track_title_for_comparison(self, title: str) -> str: + """Clean track title for comparison by removing common noise""" + cleaned = title.lower().strip() + + # Remove common patterns that cause mismatches + patterns_to_remove = [ + r'\s*\(.*\)', # Remove anything in parentheses + r'\s*\[.*\]', # Remove anything in brackets + r'\s*-\s*.*', # Remove everything after dash + r'\s*feat\..*', # Remove featuring artists + r'\s*ft\..*', # Remove ft. artists + r'\s*featuring.*', # Remove featuring + ] + + for pattern in patterns_to_remove: + cleaned = re.sub(pattern, '', cleaned, flags=re.IGNORECASE).strip() + + return cleaned + + def _clean_album_title_for_comparison(self, title: str) -> str: + """Clean album title by removing edition markers for comparison""" + cleaned = title.lower() + + # Remove common edition patterns + patterns = [ + r'\s*\(deluxe\s*edition?\)', + r'\s*\(expanded\s*edition?\)', + r'\s*\(platinum\s*edition?\)', + r'\s*\(special\s*edition?\)', + r'\s*\(remastered?\)', + r'\s*\(anniversary\s*edition?\)', + r'\s*\(.*version\)', + r'\s*-\s*deluxe\s*edition?', + r'\s*-\s*platinum\s*edition?', + r'\s+deluxe\s*edition?$', + r'\s+platinum\s*edition?$', + ] + + for pattern in patterns: + cleaned = re.sub(pattern, '', cleaned, flags=re.IGNORECASE) + + return cleaned.strip() + def get_album_completion_stats(self, artist_name: str) -> Dict[str, int]: """ Get completion statistics for all albums by an artist. diff --git a/ui/pages/__pycache__/artists.cpython-312.pyc b/ui/pages/__pycache__/artists.cpython-312.pyc index 62a7ca02202982da26ae6a09522c88f266a74f7a..08a0510c1a19ff16926fdc0908ade98e5468da0b 100644 GIT binary patch delta 8267 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 delta 7472 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 E0CutrlmGw# diff --git a/ui/pages/__pycache__/sync.cpython-312.pyc b/ui/pages/__pycache__/sync.cpython-312.pyc index ec546018129f33f11aca1db030640e0e6d5ba9fa..eac4b51304ac9a7c245c2b114f387f093345c84a 100644 GIT binary patch delta 20746 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

-bBN}Kyp75^3Sx!+-bBH(fG(I zP!p_@yA1k=vp&*GRvIPrzf&uXF0}`M;1Iy4032nu0>d2uasl!H`T~pt=mOkt1K`bq zeJ#WD-3|nyrTKI8pt6eMs#4Do)roXMBO|<;;Max_;z!Cn%@sdJIIMf`HtrYy8{fm4 A<^TWy delta 20369 zcmcJ133ydSvVXcyauc#ZLKd=;gf$^7VF`qUMPwIh>b~5ysq4L)y2kxEE-pHZ{ytk{F59#-E;UztiQU!s_@n@h#z1{Qs3=WmvPZAZ(?`PxKzl z>nkGEy@c8OZT_W3=5ML=U-X|liogwFU&+X%|5$UmSqx}%T z-@RkZ40k)dqrL~2BMR7BLe-xG8#3wJMflB#Z)NoX|0iHG= zEX@;#&DTqNiS6D`N-Lz?Zqw&Di1vf#@^N|Mpn0}D&HTa{uCKpD_B7udx7YCjT0b!F z9zQ^Q0K&aT$G zuA^3CXTst$Gc)xyTPI!>_XVCGC`RDS!;*9rfd909WLVvMLq;?wr@x z!33t6N9Rq8c>w*l0PHe{%%341HLslyf!;sg6n%4vbM^{*qXhC*HcD9lh4>7}ya#G} zn4T-9h#uw(R~!}D=G_bWIQpZyzxm37V%cAqFQ=t?lddEcO*3aM%*o|OX4|g`Reprc zm^2I-1Tffp`@$7QQ9jB{GS{NrmsCMgsc5Wqw$#?RJ@&S7t332%#%G(w6$5A)l@*yn zoHj2Tm*glwV}W_^k~~pB6w>mSY#JMQh)Ei%I> z{4fk+LfHh&GJ7`-cQExu=7|ZJ-sYygLR@B!Svg58@?N*H+7W+ga6~-_8D)OAdZ5TN z`>Yv63ORetjiTIqZ%v6=a_wR!4}VFNazF?vOf zyJl%kZH=deZIZhSA<7B-Db}-<-f=1z$)7`8b&EZQFiaiZb&T zPo~8)UszhR(fMWb<|ngbnK$9?WeqFUi{{f$E)|#S>m-?^Ax~|M6TdVM?kbGDk0@}i zh|r%Llf~YPUYHOeZZfameT9fHkL)h&_Y81dPvA@*^~*0+ksugEv=@#!bkBrRCV&+O zR?QdVc6nwutenV)0vo%57|QznGp0SYCr#`&_wD(W*z2AD%B5m-GtH2n$jkI~p}p*J z*3`R{(wO*O0zO00njYhc_~@>vbuM+)s@>-Cz1iX!^Iv=Wiq&SHSFaW=X8e!o-tDip zMTuv;`TO4q7cV{i)(2u3YZMC-;#O`TjjXJ#sa#Ixs1)tdjVbq;1iRn*hH8hwJ<-n5#H4w89;K~ zne-7sPN+_pwgE}!p;&2a4iobJnIY{PGEaV+WS%;kF7EaI zbhaqGZ30MUMY;{b3HG!-rkha5ie8O=H{9(NcsDiK{wGt#6C5Ud z%ASQrlA&iaJ)g>I4Vr>P(oR+$@}JZd)aHgw1%v3%o@$=8FhZn7v>n1A7-;ntN}xt9 z0RUg~v+Rw>0^y$TqEiyukD+uNAQhi~N9hE>Nq|!T?3IP|ju){Jo%7JjdNH^XMfN*g zD20@Ol?p_tsQ|VL=-e2QmPUfVpxhV04qJ6G;!EjdYG+Z$GCHk8G+ahCzNi}aw3@0a zSG}!i&}>_W(O+FbAh-)qfp)h+bO)ISqRow;rdz(QVw-f>Jk;wFME;zPQ>jYO{{P0l zfbI>_;oU`V2iRj(bX#NF21(KSEg;3z@(evaQ@ku@=q_2})%Zcc!`tLuG(Ke=%M#h5tz8YOp<=$68h1tTSk1+_*=Tze z;Ew=*0+>s{*R`V2RbR#7Gkg!`L%?uc0JSOtgGUnZTMd1Ci-@<@XNy`Pc3J<-5$6rZ za?I$le(ED4V@0MFF;sjldcd>&p<87`ZG$Sw>0Ql#rRqizX9Ch|ZRW5T?E??TvtfDa`8;u5(*l}lfo!8LG z-}@CX+Drrs5;#br!^G3%_PcS-=y4~&CIEP7zG!Dqr^nVHnisI z;$o2_=IRB-Vpxu*DqmN)jEHF{PNkSY-Hm?t0DP~XC>EJwzV%wMC~_p<0&3R-a8iM( zt=HYhi?QM!t9-mT72d0sTBqa-UnDuS92W+?hqbn~&eU@!iTT;+rx5)3qUMm-Q;ukg zr3IBxJM`<5#PA_3nwzPoFLr4y<(=l#TvhL^b(6c~@=OFX$q&0GM2;?)Ear-OYwcw5 zl+50Q1~%v#rMpA@XjWCPXHFC8iHm7Azd`iV_qf$EO^h_=Y^MHmrk0s8tUEJPZKp0v7mDOU@LZMu{!@i@D;DDfiRVXISbFV979iXS#&x1M@^? z;UlQY0CNuyqTyEJ?&%{&-LKm@j($EV|Ou=Zo?ltUpkZe1DYm6fkJLW4V0HCQXzw3qerEP&32Zv}+1Dh9s^G?48@wq0QU0R!`64k+CbNz)Djvalx6Qh^LM#*s+{QS#dauw2M$07Yl_la4 z;b8NzSJy2S12Q-l!|D{&LH{(j>Zg~AuVX^9?9w|c#r`;q4*1`Ft@TwxixkxPy41Mm zBPynRC=;|>7gy81xJ}Qi78}Jf{ZX|jT(k$vcf4Z0b$XfjFuZpr1l2@CT4S-R zaxQIz4REWKz;ZJho2^gkM4c4-tt%VF>!O%Xy9;FwE@XvZ#?)k>c~~!0VpNZgmM4%t z(t5iRP^gpx42!7MpDD79pv$fL>$T0IP|VUho5e`6RsW+|j1p6HPK)S4vlq9Bq4Awj zSuHK%M#{Na=Uc@$vfB>Ans4~z;6k5hRD-m!My$_b`-Yr|nnu7EQ|qK09?UP9opt(& zH6pX^S|TaGn=icH)$CFIL0>P>XVg=?9UKnzX{c^cbxx0Z7kz55LJn8qPx+$AwXOEl zP{MW{T3PQQm;9)9u@ig4Oh;@W7_DgNvh-^88OH3hE^QN*=;@=6fKNV%P;~Ig_vk*? zi8o@CXfHcSn*$Mdckt=HTvDu=Lx0RC(bXP4Y(xNE9xKNJqT zKZ01hHelZ4*NK<9vqQmy3_A_iSMRw+q=-yS&BOJ8Tf~js+5PbMk)WVspO{<4631!K zI!s@4t612Eu8N6gFLXFJ0d$fruVxewvWH%T5+Al70Rd+6>yNY2Q?mkTF18dbE}oAa zNVv*$&`>g{!qY&4S9->J5tAKq<5_-e)p+wd$gNgeb<=v0H-y|J#1`(-foF43ng9IX)q zD4zPiNi?-dt^i~ID|_lx>uCI={N2k1CT zKK<$aqCq5Eiysh0;pyCJHw61(m@h>i_=89(Xcxg@F~0E1hQ=0k41LqF8s7aGC`Ic} z9}*XfeOBSaVp4c7o}2ptyU%EBXtX(zfX=xDpe4!rFHeXl?cFANi(Kp7ZDNzmXKL6* z;o(<~8FEaF% zPm4a?5U2eRnN#hosU-_}THpS(_~l@psMB6S5+jr%QW=C-J%UAL=?S|;Vq0ZHU1O~a zN0<5vjA1Jo>u#cap4(laoF13GgI!gDD;0b90S{{{Ye-Nze36$=m|Zr#Y^pDE&b)~e zC(oIqm?hxZg>38qLNk)ud`6x7Dm56y^7`ReWAD=6MfU!U)5NM=ePXAGvU>ks)I<j{oM1SA-NL=Y_{q#yF|}=-a`MG_K3-|XJ0zoCOF6_7Wa1lO%&|B8V^3P zrh{HLd_yMGrn4KZrZrdZc!KQA`@6(ise2s}Nh1eh5Z^$sXR(zA`Ok1)*We+8e5hMJ z3f>Mz|AE?hL?nuA>y^Yozu&M4cx(yA?$ZyxL2lI;O`J^854=X2|IQo2 z710Z9YNG$bPHnkOjf1AtfqPRi$j_w32Sg9Y7*sfP>j9C~)`=G(g&B)Z|BEG$L%-l= zw$~Y4of=QAowou!lhzbF=Co1{ikD?N&Wp1AXkWMTmdiXN2Vds7yrzmS8{KNA{`_r` zUc~7QW;mZLd_?WT0!s)4oL*)j>)lMyWbYEkBr3P|(a*m_8_#`6yesCk)pSJE%bppSOxl;|>`KyiSjb}$$3<1aVX9Lc7_koL;xV3eKkdd)oB*A4N(!&WX+<@e5+eIWf2&BtyF6@>>wj$N9O6 zj2OimJJy^y)+Hsp#6sb>sDH5lqVPJ^gOYq5QG3cIh;*-^lok319 z+1@AE$YE1&bZOq?kbNB3IYj>ehny^`twx8O>nM(5exm`)ur;rQc-$JtDTs3a`P@DS zff4sx=VIj`5wkzI8n-o~tNeo)Q;ud}ttmc#{yb?B47xAI(?Ei*sh^8T`vvF~d)2YT z>`?t-Yf!v=RkkrhlI*P6W0-(->wlZ(h#~C0*nneEvDB#)_~MHTR(U`K)GKt9s_U3d z)GyyL#*W)PbSJ|Bir611|V@TyQ!Y9xY-OX z!mxI>1)r?57m_NknKd%DrrzyR0i|rfs8E;@Ixc&FQ*7ySMHUqyeP4G9kAiPB3BzET-xynX;c)Y}IATg%K(L1+h+}A!TY6 ziXQ`fqL1gwQ8OT~*7yl@kyRDEYA?xYl{tBKu$gM-O`<>`AHNe&>IFH)RjUSiQy0ju ze4&^0lIh9p&$9o(UgI9jRjTjkB{TYgEx2%Rsddd+=AuV)Y)*d-rVKzg&f;v<2MA3T zcoT>aaJbRlCYx7LQ%~8Pr*vd*IaUnT54(8oJ{I&aE^wAWXixW?|;o!rWw)j*{8J!4qd#ON!+(5m`sW z%G6Fxad@J2YP5V+4D3Nwtz8Gw!xMV)S?{TF)|TYZk+K$Kv8%&AG`qk@^joF!3i31y z#>fFOJrgu=gBK<4$0MEr{v;7?-7rpmB-&cBIHR@Oq#Ac6vLZQ`yIjj_TNHZ}*s9bR zGjUl>ZEeo{nx!uH8BQhmN5x#;pqA66QO>NI<})0b_(yb^O-3PSuBvG~GZD1jLo@)< zl!nSCch0OPcTJ^g0bS+5R7Oh*`dGdMWgpl{8p@WW3z*Ff%8eoSBtmOiqgRPPY z@?$|_?>A8{PG+nu#4MUeu@AS{X22`fQxl~j2EgO?g}a(vm4T}o*3Q1vzoT|;v3!%{ z527m+(-);NX~O6MLC$qv@6ZThDvvUh4oJ7|GnWZZ3$FVe+R<>@vK;tIi&_dyXy zcY8qjclbm~P;pACau}`w%<^HW)~ZWnOoT|#>n@c&&s=feAB@D;S6hEx%@>p_NY{oE?6RuIoN3mx4Jr| zS0>}6@x_rh*i`Fr+i{+{U%yi+r-%rhRVB@~>v>`VW%2SOB5SJXB z$&S~%;2V51TzEQF91it?;1y9=)Yn()F!%s#@-IBnATv zik|JaW^d{j>tuG!7oePt>kInBI@yczr(Nphba7apT`g0rntJIm#vdZ$&ya_A-hSIb z#UT@&)Wg zGkR7~1?)vkXvh1knw7FZW zdx_7jp4ZA{ayi=6OwjxTjSI0Kgfa6l6Xh?R&FTx(@d7?WiJkkiR0`0JGu;c3nsbHw-h)9Ync@1P42I7m-YA2%(Wm~30scdbFclofIa zqu~rB&&&6;JTFtw`3m?S)(bbveAyeLe6jQbrP4|FdbLPg^YYrQM@O!c{ZbzWZQC$8 zjR;A&`(u;WNjZimPXP})kB0t&8v*)p3DtrAM19?@@=meMN?9+zG#nWiAa&O}i5i1uzn?Fy0(`xri*yNPi2w5D#7S+a=r4jzEd;h~?$Oz_wg zZKLr4wr|AdY_B$5tb6X3_lTh^FW%7c5Z`-PKi@zv0#APaK4g)uyjOl7!>ZZ=Y_63tg_*$BkaRRnpH>taV%E<0AOmodhhQNY8$V5&|gaXPIYH)7sP;j30utb1FVD{nZm0~L3PpFv$ z+(Q4wBcdJ5pwnDF8ts12an6rn0K%&4w=(+;V>h8 z1fLMTc)n$JRa7;stVf*ewjZsvzp89Ug4Obb92*gX6U=9Lmbuh@de06yr(i26Vnh1^ zN-qWpyapxg96EuAqx`fUzmuGqlh#c;?v=_#S8?_gTm|oFesvgd&e?mQv`r(u z?z1yrtM$3(R$ty1I%tIi?Y0xeq7xWBOM~}@;%{A+! z*QIm7PK;p-7r~Db>Dt(-s`wWkbavxcBYuzbEjt~tRL?5+Q>cl;C_Q$MjJ3|bE+Znv z{rbBDGAEm<-A@Fy2H!bW@?R+@@~n+-$sWR*^_ILv^a(A4Iex(v=pWvenK7I|d>_kf z*13lW!zMlYkjxNT&pRX+JD@nN!}QJ@j9!-Ski0cYG-=l{nJZT6n~%x7?#M8cpY%Eq zyaDhRz2}%rb})YrJ&zeD1Hra7T{X|tlO7F7xPWEoi_#_nzpqMoPK^?)0rp9 z-C1ajJt;krZC?U68!0Tg38~0acX)Hk?Pst2;<<^IA9iZ(ooWgCf@5X*DfX(~1r=<# zSnb(*@tfz9Xh=eT-U_=>I)b{R1bp#y)FZDtp|&?&MAYHdqf>@B58INje~$@$K2@Y8 zKME9&0Xz-x48S>k-5EJb+_Y(pkz*Y`BS*@?@fdhLH3r;GOsJBnwxb^fVp<1i5ocsr zS9~JJI~@1ZGF6kcE{>k=Ni&T=**@>J}nM^VLmXD$6cd$Nu_M|S2e zeJ+-mJ^!3sZT$cF2}?F;eH+4PAMtEJZKwPl{ZLSCA=+C9Lyo!6tTLvH%p1X}>EQ1r z5IX%A^xUhhlfu|6+lB)9EOde5+d9AHHp(dRFQr-M@eY^SvKhQ-!Q`s|CgAfLfMNJ# zhwNf}B0jhGL{Rku2|Re9_Di-{dXZPTEEUg4Nc%gb60Z{ax~q+BYj}ilP>M@+M3gZ; zeG<@41z;N37r^OD;pi`-jI4_%qiGWW%am^_+2u+=Cq$Zoeef_U$_R8+on_V^qKq@* z;)P(xBJ_{69b-m-JY+lctURnTi>aXlZ)=1ecT#5SV^Kz|l^A0@DAHR%7Mte#P+~K| z3)rmp#~OphLv$l!bdionskL=Di_jX_%~)!1_{#4C%jh;{8wPI=VbD)>H~QN@9671q z>u%JgvsD0>JKw!OWmO~^*UI?aSo<8HfHm^v>8DbSyiozafrDuz2k4&!Ai(D3?^NQe zk{tfCBflLWbSwzXv|K61Z6clr3;<#t|DhJC#^9KXpn?`^kLnVt=nHH%@?hVeA%DiY zJ=M6(=+B5QrNM!J7Qqf$*w)N^`EKO;4tr5*+O^DccHbF%_JfzkvtaKxFUkM}c* z16rKf-PCZnbf;= zD)44-IHSi8HAWUs2O&%-GnIE1lg#s-0ltnpt3E_sseWpx(JuzM4>b)art0HEjh;!Y zar7OAU2Sjb85a50!=uufb|zJTc3yyi(h0q|#3&fXtHcWg}tsCSea(*j!buTrCb4=^kcnmvr+C$apaddwIjJ%;l-ywF7L9AhM= zgHSjA)fw=^{ql*vV z2*6Q*xApi*M)EAq5mZw};8$uNf+l9@33TI)#N$ul^Eg^z#xC?P0laUKahMq%&#Tyl zp1cB(#zo7_=macpha?zEttotUSn z6mUtHd(3AnrIRCa+M-x)s+31Kb>R~y7 zjRliyAL|*quH5KuIp!FD64QXyC+SBNrBuK*92;pS{?|0ZUK;xt+lcppbQ(g@4*tex zd`MU$<{9fnJdfs745x!i^_%mJp5jmXv-w6}hY32^er8xBE;sgyM9^Fo2ogSm==bU9 z1x7z{Mh{(JY{~o-Rg5zcp5kZimitfrUki*p$Je2?S;Mb1=8D`02&bLR;2>xReFm7# zhv1`n+d`w4_?=Dm#|w?alAbaD@fXI^Guek@mAjB5$8@liLk?6W2xK1ZM(H&G+u_rD zD~x`Qogi-n+fplf3HewtA;<75eO;xIrf;~KoR~RFjU$qP>Bjc<#p9n; zQT789dc;Ag{Uz96A*&Ix<-9E~LhIJRWNcG>j;ko+ODSa76FV)&Ie32iR*wG50WO(~ zr69u^Foq%f@-|3`@HpMe-HI*I37}%X3Fm{VBXB{Zc-~rjwJ|t6``ZD%ZXrzfz)+3F^UBdB4kohNlkuowxH@#RHgZjo(eu>_2 z@u#p}^ zpFi_Vx6`=5Mt)Nn$yU!5Mlt<(m52)dJ4deqPqe;vrO`*cYCX8pI2SIS)LT{=%KkTV zSO<24fJT680A2)Od%G2-RRlV?XuNMu>ax|wV)2Q7ay!PxGV?%fw2DL^EuBN+& zc7{F&G|*NEQ@K>~g>y6V8>9kk+u2*<3Hfy3{^+W9V-UMWJ5lnH=mPeQPpN~{$u@3$ z7h-k$C5bEUIiRNJq`+`c7vEqExHyY=a+G!=|5IrHydmsc85uXie$|}=c3qDl{v3P| zvqFvjW1IMIbL}mn diff --git a/ui/pages/artists.py b/ui/pages/artists.py index d968c229..b03dbea6 100644 --- a/ui/pages/artists.py +++ b/ui/pages/artists.py @@ -1128,30 +1128,17 @@ class AlbumCard(QFrame): self.overlay.setFixedSize(164, 164) self.overlay.setAlignment(Qt.AlignmentFlag.AlignCenter) - if self.is_owned: - self.overlay.setStyleSheet(""" - QLabel { - background: rgba(29, 185, 84, 0.8); - border-radius: 6px; - color: white; - font-size: 24px; - font-weight: bold; - } - """) - self.overlay.setText("✓") - else: - self.overlay.setStyleSheet(""" - QLabel { - background: rgba(0, 0, 0, 0.7); - border-radius: 6px; - color: white; - font-size: 16px; - font-weight: bold; - } - """) - self.overlay.setText("📥\nDownload") - self.overlay.setCursor(Qt.CursorShape.PointingHandCursor) - + # Set up initial overlay appearance (will be updated by update_ownership) + self.overlay.setStyleSheet(""" + QLabel { + background: rgba(0, 0, 0, 0.7); + border-radius: 6px; + color: white; + font-size: 16px; + font-weight: bold; + } + """) + self.overlay.setText("Loading...") self.overlay.hide() # Initially hidden, shown on hover # Download progress overlay (shown during downloads) @@ -1195,6 +1182,9 @@ class AlbumCard(QFrame): layout.addWidget(album_label) layout.addWidget(year_label) layout.addStretch() + + # Initialize overlay text based on current ownership status + self._refresh_overlay_text() def load_album_image(self): """Load album image in background""" @@ -1216,14 +1206,98 @@ class AlbumCard(QFrame): def enterEvent(self, event): """Show overlay on hover""" - self.overlay.show() + try: + if hasattr(self, 'overlay') and self.overlay: + self.overlay.show() + self.overlay.raise_() # Bring to front + except (RuntimeError, AttributeError): + # Object has been deleted or is invalid, skip + pass super().enterEvent(event) def leaveEvent(self, event): """Hide overlay when not hovering""" - self.overlay.hide() + try: + if hasattr(self, 'overlay') and self.overlay: + self.overlay.hide() + except (RuntimeError, AttributeError): + # Object has been deleted or is invalid, skip + pass super().leaveEvent(event) + def _refresh_overlay_text(self): + """Refresh overlay text based on current ownership status""" + if self.is_owned: + if self.ownership_status and self.ownership_status.is_complete: + # Complete album (90%+) - green checkmark overlay + self.overlay.setStyleSheet(""" + QLabel { + background: rgba(29, 185, 84, 0.8); + border-radius: 6px; + color: white; + font-size: 16px; + font-weight: bold; + } + """) + self.overlay.setText("✓ Complete\nVerify tracks") + self.overlay.setCursor(Qt.CursorShape.PointingHandCursor) + elif self.ownership_status and self.ownership_status.is_nearly_complete: + # Nearly complete album (80-89%) - blue overlay + self.overlay.setStyleSheet(""" + QLabel { + background: rgba(13, 110, 253, 0.8); + border-radius: 6px; + color: white; + font-size: 14px; + font-weight: bold; + } + """) + percentage = int(self.ownership_status.completion_ratio * 100) + missing_tracks = self.ownership_status.expected_tracks - self.ownership_status.owned_tracks + self.overlay.setText(f"◐ Nearly Complete\n({percentage}%)\nGet {missing_tracks} missing") + self.overlay.setCursor(Qt.CursorShape.PointingHandCursor) + elif self.ownership_status: + # Partial album (<80%) - yellow warning overlay + self.overlay.setStyleSheet(""" + QLabel { + background: rgba(255, 193, 7, 0.8); + border-radius: 6px; + color: #212529; + font-size: 14px; + font-weight: bold; + } + """) + percentage = int(self.ownership_status.completion_ratio * 100) + missing_tracks = self.ownership_status.expected_tracks - self.ownership_status.owned_tracks + self.overlay.setText(f"⚠ Partial\n({percentage}%)\nGet {missing_tracks} missing") + self.overlay.setCursor(Qt.CursorShape.PointingHandCursor) + else: + # Legacy complete album - green checkmark overlay + self.overlay.setStyleSheet(""" + QLabel { + background: rgba(29, 185, 84, 0.8); + border-radius: 6px; + color: white; + font-size: 16px; + font-weight: bold; + } + """) + self.overlay.setText("✓ Complete\nVerify tracks") + self.overlay.setCursor(Qt.CursorShape.PointingHandCursor) + else: + # Missing album - download overlay + self.overlay.setStyleSheet(""" + QLabel { + background: rgba(0, 0, 0, 0.7); + border-radius: 6px; + color: white; + font-size: 16px; + font-weight: bold; + } + """) + self.overlay.setText("📥 Missing\n(0%)\nDownload") + self.overlay.setCursor(Qt.CursorShape.PointingHandCursor) + def update_status_indicator(self): """Update the permanent status indicator""" if self.is_owned: @@ -1319,83 +1393,25 @@ class AlbumCard(QFrame): self.update_status_indicator() # Update the hover overlay - if self.is_owned: - if self.ownership_status and self.ownership_status.is_complete: - # Complete album (90%+) - green checkmark overlay - self.overlay.setStyleSheet(""" - QLabel { - background: rgba(29, 185, 84, 0.8); - border-radius: 6px; - color: white; - font-size: 16px; - font-weight: bold; - } - """) - self.overlay.setText("✓ Complete\nVerify tracks") - self.overlay.setCursor(Qt.CursorShape.PointingHandCursor) - elif self.ownership_status and self.ownership_status.is_nearly_complete: - # Nearly complete album (80-89%) - blue overlay - self.overlay.setStyleSheet(""" - QLabel { - background: rgba(13, 110, 253, 0.8); - border-radius: 6px; - color: white; - font-size: 14px; - font-weight: bold; - } - """) - percentage = int(self.ownership_status.completion_ratio * 100) - missing_tracks = self.ownership_status.expected_tracks - self.ownership_status.owned_tracks - self.overlay.setText(f"◐ {percentage}%\nGet {missing_tracks} missing") - self.overlay.setCursor(Qt.CursorShape.PointingHandCursor) - elif self.ownership_status: - # Partial album (<80%) - yellow warning overlay - self.overlay.setStyleSheet(""" - QLabel { - background: rgba(255, 193, 7, 0.8); - border-radius: 6px; - color: #212529; - font-size: 14px; - font-weight: bold; - } - """) - percentage = int(self.ownership_status.completion_ratio * 100) - missing_tracks = self.ownership_status.expected_tracks - self.ownership_status.owned_tracks - self.overlay.setText(f"⚠ {percentage}%\nGet {missing_tracks} missing") - self.overlay.setCursor(Qt.CursorShape.PointingHandCursor) - else: - # Legacy complete album - green checkmark overlay - self.overlay.setStyleSheet(""" - QLabel { - background: rgba(29, 185, 84, 0.8); - border-radius: 6px; - color: white; - font-size: 16px; - font-weight: bold; - } - """) - self.overlay.setText("✓ Complete\nVerify tracks") - self.overlay.setCursor(Qt.CursorShape.PointingHandCursor) - else: - # Missing album - download overlay - self.overlay.setStyleSheet(""" - QLabel { - background: rgba(0, 0, 0, 0.7); - border-radius: 6px; - color: white; - font-size: 16px; - font-weight: bold; - } - """) - self.overlay.setText("📥\nDownload") - self.overlay.setCursor(Qt.CursorShape.PointingHandCursor) + self._refresh_overlay_text() def set_download_in_progress(self): """Set album card to download in progress state""" # Hide hover overlay and show progress overlay - self.overlay.hide() - self.progress_overlay.setText("⏳\nPreparing...") - self.progress_overlay.show() + try: + if hasattr(self, 'overlay') and self.overlay: + self.overlay.hide() + except (RuntimeError, AttributeError): + # Object has been deleted or is invalid, skip + pass + + try: + if hasattr(self, 'progress_overlay') and self.progress_overlay and not self.progress_overlay.isNull(): + self.progress_overlay.setText("⏳\nPreparing...") + self.progress_overlay.show() + except (RuntimeError, AttributeError): + # Object has been deleted or is invalid, skip + pass # Update status indicator self.status_indicator.setStyleSheet(""" diff --git a/ui/pages/sync.py b/ui/pages/sync.py index c2d4668f..48c57cc8 100644 --- a/ui/pages/sync.py +++ b/ui/pages/sync.py @@ -223,10 +223,10 @@ class PlaylistTrackAnalysisWorker(QRunnable): for query_title in unique_title_variations: if self._cancelled: return None, 0.0 - # Use database check_track_exists method + # Use database check_track_exists method with consistent thresholds db_track, confidence = db.check_track_exists(query_title, artist_name, confidence_threshold=0.7) - if db_track and confidence >= 0.8: + if db_track and confidence >= 0.7: print(f"✔️ Database match found for '{original_title}' by '{artist_name}': '{db_track.title}' with confidence {confidence:.2f}") # Convert database track to format compatible with existing code