From a228affd7b91f6e112c97c3367d2fa380631294e Mon Sep 17 00:00:00 2001 From: Broque Thomas Date: Fri, 8 Aug 2025 20:44:48 -0700 Subject: [PATCH] database fixes --- .../database_update_worker.cpython-312.pyc | Bin 25251 -> 30191 bytes core/database_update_worker.py | 163 ++++++++++++++---- .../music_database.cpython-312.pyc | Bin 71409 -> 75555 bytes database/music_database.py | 97 +++++++++++ 4 files changed, 224 insertions(+), 36 deletions(-) diff --git a/core/__pycache__/database_update_worker.cpython-312.pyc b/core/__pycache__/database_update_worker.cpython-312.pyc index 0bf4f6573ac1f20b1f506c58546f034e7bacddfa..0372f5ce785919d5264a78cf07bfcf19b60e7a90 100644 GIT binary patch delta 10180 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 0 or orphaned_albums > 0: + logger.info(f"๐Ÿงน Cleanup complete: {orphaned_artists} orphaned artists, {orphaned_albums} orphaned albums removed") + else: + logger.debug("๐Ÿงน Cleanup complete: No orphaned records found") + + except Exception as e: + logger.warning(f"Could not cleanup orphaned records: {e}") + # Emit final results self.finished.emit( self.processed_artists, @@ -149,21 +164,39 @@ class DatabaseUpdateWorker(QThread): self.full_refresh = True return self._get_all_artists() - # Strategy: Get recently added albums and extract artists from them - # Process artists in reverse chronological order until we hit one that's already current + # Enhanced Strategy: Get both recently added AND recently updated content + # This catches both new content and metadata corrections done in Plex - logger.info("Getting recently added albums to find new artists...") + logger.info("Getting recently added and recently updated content...") + + # Get both recently added and recently updated albums + all_recent_content = [] - # Get recently added albums (up to 500 to cast a wide net) try: - # Try to get specifically albums first + # Get recently added albums (up to 400 to catch more recent content) try: - recent_content = self.plex_client.music_library.recentlyAdded(libtype='album', maxresults=500) - logger.info(f"Found {len(recent_content)} recently added albums (album-specific)") + recently_added = self.plex_client.music_library.recentlyAdded(libtype='album', maxresults=400) + all_recent_content.extend(recently_added) + logger.info(f"Found {len(recently_added)} recently added albums") except: # Fallback to general recently added - recent_content = self.plex_client.music_library.recentlyAdded(maxresults=500) - logger.info(f"Found {len(recent_content)} recently added items (mixed types)") + recently_added = self.plex_client.music_library.recentlyAdded(maxresults=400) + all_recent_content.extend(recently_added) + logger.info(f"Found {len(recently_added)} recently added items (mixed types)") + + # Get recently updated albums (catches metadata corrections) - increased limit + try: + recently_updated = self.plex_client.music_library.search(sort='updatedAt:desc', libtype='album', limit=400) + # Remove duplicates (items that are both recently added and updated) + added_keys = {getattr(item, 'ratingKey', None) for item in all_recent_content} + unique_updated = [item for item in recently_updated if getattr(item, 'ratingKey', None) not in added_keys] + all_recent_content.extend(unique_updated) + logger.info(f"Found {len(unique_updated)} additional recently updated albums (after deduplication)") + except Exception as e: + logger.warning(f"Could not get recently updated content: {e}") + + recent_content = all_recent_content + logger.info(f"Combined total: {len(recent_content)} recent albums (added + updated)") # Filter to only get Album objects and convert Artist objects to their albums recent_albums = [] @@ -253,8 +286,10 @@ class DatabaseUpdateWorker(QThread): logger.warning("No albums found to process - incremental update cannot proceed") return [] - # New approach: Track-level incremental update with 3-consecutive-tracks stopping - consecutive_existing_tracks = 0 + # Improved approach: Album-level incremental update with smart stopping + # Check entire albums at a time and use more robust stopping criteria + albums_with_new_content = 0 + consecutive_complete_albums = 0 processed_artist_ids = set() total_tracks_checked = 0 @@ -270,6 +305,7 @@ class DatabaseUpdateWorker(QThread): album_title = getattr(album, 'title', f'Album_{i}') album_has_new_tracks = False + missing_tracks_count = 0 # Check each individual track in this album try: @@ -282,39 +318,48 @@ class DatabaseUpdateWorker(QThread): track_id = int(track.ratingKey) track_title = getattr(track, 'title', 'Unknown Track') - if self.database.track_exists(track_id): - consecutive_existing_tracks += 1 - logger.debug(f"Track '{track_title}' already exists (consecutive: {consecutive_existing_tracks})") - - # Stop after 3 consecutive existing tracks - if consecutive_existing_tracks >= 3: - logger.info(f"๐Ÿ›‘ Found 3 consecutive existing tracks - stopping incremental scan after checking {total_tracks_checked} tracks") - stopped_early = True - break - else: - # Found missing track - reset counter - if consecutive_existing_tracks > 0: - logger.debug(f"Track '{track_title}' missing - resetting consecutive count (was {consecutive_existing_tracks})") - consecutive_existing_tracks = 0 + if not self.database.track_exists(track_id): + missing_tracks_count += 1 album_has_new_tracks = True - logger.debug(f"๐Ÿ“€ Track '{track_title}' is new - will process album's artist") + logger.debug(f"๐Ÿ“€ Track '{track_title}' is new - album needs processing") + else: + logger.debug(f"โœ… Track '{track_title}' already exists") except Exception as track_error: logger.debug(f"Error checking individual track: {track_error}") - # Reset counter on error to be safe - consecutive_existing_tracks = 0 album_has_new_tracks = True # Assume needs processing if can't check + missing_tracks_count += 1 continue + + # Evaluate album completion status + if album_has_new_tracks: + albums_with_new_content += 1 + consecutive_complete_albums = 0 # Reset counter + logger.info(f"๐Ÿ“€ Album '{album_title}' has {missing_tracks_count} new tracks - needs processing") + else: + # Check if existing tracks have metadata changes (catches Plex corrections) + metadata_changed = self._check_for_metadata_changes(tracks) + if metadata_changed: + albums_with_new_content += 1 + consecutive_complete_albums = 0 # Reset counter + logger.info(f"๐Ÿ”„ Album '{album_title}' has metadata changes - needs processing") + album_has_new_tracks = True # Mark for artist processing + else: + consecutive_complete_albums += 1 + logger.debug(f"โœ… Album '{album_title}' is fully up-to-date (consecutive complete: {consecutive_complete_albums})") - # If we hit the stop condition, break out of album loop too - if stopped_early: - break + # Very conservative stopping criteria: 25 consecutive complete albums after metadata fixes + # This ensures we don't miss scattered updated content from manual corrections + if consecutive_complete_albums >= 25: + logger.info(f"๐Ÿ›‘ Found 25 consecutive complete albums - stopping incremental scan after checking {total_tracks_checked} tracks from {i+1} albums") + stopped_early = True + break except Exception as tracks_error: logger.warning(f"Error getting tracks for album '{album_title}': {tracks_error}") # Assume album needs processing if we can't check tracks album_has_new_tracks = True - consecutive_existing_tracks = 0 + consecutive_complete_albums = 0 # Reset the correct variable # If album has new tracks, queue its artist for processing if album_has_new_tracks: @@ -334,14 +379,16 @@ class DatabaseUpdateWorker(QThread): except Exception as e: logger.warning(f"Error processing album at index {i} (type: {type(album).__name__}): {e}") # Reset consecutive count on error to be safe - consecutive_existing_tracks = 0 + consecutive_complete_albums = 0 continue - result_msg = f"Smart incremental scan result: {len(artists_to_process)} artists to process" + result_msg = f"Smart incremental scan result: {len(artists_to_process)} artists to process from {albums_with_new_content} albums with new content" if stopped_early: - result_msg += f" (stopped early after finding 3 consecutive existing tracks)" + result_msg += f" (stopped early after finding 25 consecutive complete albums)" else: - result_msg += f" (checked {total_tracks_checked} tracks from {len(recent_albums)} albums)" + result_msg += f" (checked all {total_tracks_checked} tracks from {len(recent_albums)} recent albums)" + + logger.info(f"๐Ÿ“Š Incremental scan stats: {len(recent_albums)} recent albums examined, {albums_with_new_content} needed processing") logger.info(result_msg) return artists_to_process @@ -351,6 +398,50 @@ class DatabaseUpdateWorker(QThread): # Fallback to empty list - user can try full refresh return [] + def _check_for_metadata_changes(self, plex_tracks) -> bool: + """Check if any tracks in the list have metadata changes compared to database""" + try: + if not self.database or not plex_tracks: + return False + + changes_detected = 0 + for track in plex_tracks: + try: + track_id = int(track.ratingKey) + + # Get current data from database + db_track = self.database.get_track_by_id(track_id) + if not db_track: + continue # Track doesn't exist in DB, not a metadata change + + # Compare key metadata fields that users commonly fix + current_title = track.title + current_artist = track.artist().title if track.artist() else "Unknown" + current_album = track.album().title if track.album() else "Unknown" + + if (db_track.title != current_title or + db_track.artist_name != current_artist or + db_track.album_title != current_album): + logger.debug(f"๐Ÿ”„ Metadata change detected for track ID {track_id}:") + logger.debug(f" Title: '{db_track.title}' โ†’ '{current_title}'") + logger.debug(f" Artist: '{db_track.artist_name}' โ†’ '{current_artist}'") + logger.debug(f" Album: '{db_track.album_title}' โ†’ '{current_album}'") + changes_detected += 1 + + except Exception as e: + logger.debug(f"Error checking metadata for track: {e}") + continue + + if changes_detected > 0: + logger.info(f"๐Ÿ”„ Found {changes_detected} tracks with metadata changes") + return True + + return False + + except Exception as e: + logger.debug(f"Error checking for metadata changes: {e}") + return False # Assume no changes if we can't check + def _process_all_artists(self, artists: List): """Process all artists and their albums/tracks using thread pool""" total_artists = len(artists) diff --git a/database/__pycache__/music_database.cpython-312.pyc b/database/__pycache__/music_database.cpython-312.pyc index 8e7825f08cdde280391ee0877f16e76ecc4825dc..c8c0ed3d0f5ce556589b6ebe8de7e6468838426b 100644 GIT binary patch delta 9996 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 eJYK}Al^NqB9~QBa6x`<_UFbj!v6!LV#_}O diff --git a/database/music_database.py b/database/music_database.py index 954f5869..0ac752a9 100644 --- a/database/music_database.py +++ b/database/music_database.py @@ -60,6 +60,20 @@ class DatabaseTrack: created_at: Optional[datetime] = None updated_at: Optional[datetime] = None +@dataclass +class DatabaseTrackWithMetadata: + """Track with joined artist and album names for metadata comparison""" + id: int + album_id: int + artist_id: int + title: str + artist_name: str + album_title: str + track_number: Optional[int] = None + duration: Optional[int] = None + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + class MusicDatabase: """SQLite database manager for SoulSync music library data""" @@ -226,6 +240,53 @@ class MusicDatabase: logger.error(f"Error clearing database: {e}") raise + def cleanup_orphaned_records(self) -> Dict[str, int]: + """Remove artists and albums that have no associated tracks""" + try: + with self._get_connection() as conn: + cursor = conn.cursor() + + # Find orphaned artists (no tracks) + cursor.execute(""" + SELECT COUNT(*) FROM artists + WHERE id NOT IN (SELECT DISTINCT artist_id FROM tracks WHERE artist_id IS NOT NULL) + """) + orphaned_artists_count = cursor.fetchone()[0] + + # Find orphaned albums (no tracks) + cursor.execute(""" + SELECT COUNT(*) FROM albums + WHERE id NOT IN (SELECT DISTINCT album_id FROM tracks WHERE album_id IS NOT NULL) + """) + orphaned_albums_count = cursor.fetchone()[0] + + # Delete orphaned artists + if orphaned_artists_count > 0: + cursor.execute(""" + DELETE FROM artists + WHERE id NOT IN (SELECT DISTINCT artist_id FROM tracks WHERE artist_id IS NOT NULL) + """) + logger.info(f"๐Ÿงน Removed {orphaned_artists_count} orphaned artists") + + # Delete orphaned albums + if orphaned_albums_count > 0: + cursor.execute(""" + DELETE FROM albums + WHERE id NOT IN (SELECT DISTINCT album_id FROM tracks WHERE album_id IS NOT NULL) + """) + logger.info(f"๐Ÿงน Removed {orphaned_albums_count} orphaned albums") + + conn.commit() + + return { + 'orphaned_artists_removed': orphaned_artists_count, + 'orphaned_albums_removed': orphaned_albums_count + } + + except Exception as e: + logger.error(f"Error cleaning up orphaned records: {e}") + return {'orphaned_artists_removed': 0, 'orphaned_albums_removed': 0} + # Artist operations def insert_or_update_artist(self, plex_artist) -> bool: """Insert or update artist from Plex artist object""" @@ -442,6 +503,42 @@ class MusicDatabase: logger.error(f"Error checking if track {track_id} exists: {e}") return False + def get_track_by_id(self, track_id: int) -> Optional[DatabaseTrackWithMetadata]: + """Get a track with artist and album names by Plex ID""" + try: + conn = self._get_connection() + cursor = conn.cursor() + + cursor.execute(""" + SELECT t.id, t.album_id, t.artist_id, t.title, t.track_number, + t.duration, t.created_at, t.updated_at, + a.name as artist_name, al.title as album_title + FROM tracks t + JOIN artists a ON t.artist_id = a.id + JOIN albums al ON t.album_id = al.id + WHERE t.id = ? + """, (track_id,)) + + row = cursor.fetchone() + if row: + return DatabaseTrackWithMetadata( + id=row['id'], + album_id=row['album_id'], + artist_id=row['artist_id'], + title=row['title'], + artist_name=row['artist_name'], + album_title=row['album_title'], + track_number=row['track_number'], + duration=row['duration'], + created_at=row['created_at'], + updated_at=row['updated_at'] + ) + return None + + except Exception as e: + logger.error(f"Error getting track {track_id}: {e}") + return None + def get_tracks_by_album(self, album_id: int) -> List[DatabaseTrack]: """Get all tracks by album ID""" try: