diff --git a/.claude/settings.local.json b/.claude/settings.local.json index e3e0fee4..d76aadf0 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -4,7 +4,8 @@ "Bash(mkdir:*)", "Bash(rm:*)", "Bash(rg:*)", - "Bash(grep:*)" + "Bash(grep:*)", + "WebFetch(domain:python-plexapi.readthedocs.io)" ], "deny": [] } diff --git a/core/__pycache__/database_update_worker.cpython-312.pyc b/core/__pycache__/database_update_worker.cpython-312.pyc index 58e50de3..0bf4f657 100644 Binary files a/core/__pycache__/database_update_worker.cpython-312.pyc and b/core/__pycache__/database_update_worker.cpython-312.pyc differ diff --git a/core/database_update_worker.py b/core/database_update_worker.py index cba9428b..5cd68475 100644 --- a/core/database_update_worker.py +++ b/core/database_update_worker.py @@ -80,6 +80,14 @@ class DatabaseUpdateWorker(QThread): self.phase_changed.emit("Processing artists, albums, and tracks...") self._process_all_artists(artists_to_process) + # Record full refresh completion for tracking purposes + if self.full_refresh and self.database: + try: + self.database.record_full_refresh_completion() + logger.info("Full refresh completion recorded in database") + except Exception as e: + logger.warning(f"Could not record full refresh completion: {e}") + # Emit final results self.finished.emit( self.processed_artists, @@ -483,7 +491,7 @@ class DatabaseStatsWorker(QThread): self.should_stop = True def run(self): - """Get database statistics""" + """Get database statistics and full info including last refresh""" try: if self.should_stop: return @@ -492,9 +500,10 @@ class DatabaseStatsWorker(QThread): if self.should_stop: return - stats = database.get_database_info() + # Get full database info (includes last_full_refresh) + info = database.get_database_info() if not self.should_stop: - self.stats_updated.emit(stats) + self.stats_updated.emit(info) except Exception as e: logger.error(f"Error getting database stats: {e}") if not self.should_stop: @@ -503,5 +512,6 @@ class DatabaseStatsWorker(QThread): 'albums': 0, 'tracks': 0, 'database_size_mb': 0.0, - 'last_update': None + 'last_update': None, + 'last_full_refresh': None }) \ No newline at end of file diff --git a/database/__pycache__/music_database.cpython-312.pyc b/database/__pycache__/music_database.cpython-312.pyc index d3af11d0..234de155 100644 Binary files a/database/__pycache__/music_database.cpython-312.pyc and b/database/__pycache__/music_database.cpython-312.pyc differ diff --git a/database/music_database.py b/database/music_database.py index 2572fbbb..7c05a4e8 100644 --- a/database/music_database.py +++ b/database/music_database.py @@ -126,6 +126,15 @@ class MusicDatabase: ) """) + # Metadata table for storing system information like last refresh dates + cursor.execute(""" + CREATE TABLE IF NOT EXISTS metadata ( + key TEXT PRIMARY KEY, + value TEXT, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """) + # Create indexes for performance cursor.execute("CREATE INDEX IF NOT EXISTS idx_albums_artist_id ON albums (artist_id)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_tracks_album_id ON tracks (album_id)") @@ -1330,6 +1339,40 @@ class MusicDatabase: logger.error(f"Error getting album completion stats for artist '{artist_name}': {e}") return {'complete': 0, 'nearly_complete': 0, 'partial': 0, 'missing': 0, 'total': 0} + def set_metadata(self, key: str, value: str): + """Set a metadata value""" + try: + with self._get_connection() as conn: + cursor = conn.cursor() + cursor.execute(""" + INSERT OR REPLACE INTO metadata (key, value, updated_at) + VALUES (?, ?, CURRENT_TIMESTAMP) + """, (key, value)) + conn.commit() + except Exception as e: + logger.error(f"Error setting metadata {key}: {e}") + + def get_metadata(self, key: str) -> Optional[str]: + """Get a metadata value""" + try: + with self._get_connection() as conn: + cursor = conn.cursor() + cursor.execute("SELECT value FROM metadata WHERE key = ?", (key,)) + result = cursor.fetchone() + return result['value'] if result else None + except Exception as e: + logger.error(f"Error getting metadata {key}: {e}") + return None + + def record_full_refresh_completion(self): + """Record when a full refresh was completed""" + from datetime import datetime + self.set_metadata('last_full_refresh', datetime.now().isoformat()) + + def get_last_full_refresh(self) -> Optional[str]: + """Get the date of the last full refresh""" + return self.get_metadata('last_full_refresh') + def get_database_info(self) -> Dict[str, Any]: """Get comprehensive database information""" try: @@ -1357,11 +1400,15 @@ class MusicDatabase: result = cursor.fetchone() last_update = result['last_update'] if result and result['last_update'] else None + # Get last full refresh + last_full_refresh = self.get_last_full_refresh() + return { **stats, 'database_size_mb': round(db_size_mb, 2), 'database_path': str(self.database_path), - 'last_update': last_update + 'last_update': last_update, + 'last_full_refresh': last_full_refresh } except Exception as e: @@ -1372,7 +1419,8 @@ class MusicDatabase: 'tracks': 0, 'database_size_mb': 0.0, 'database_path': str(self.database_path), - 'last_update': None + 'last_update': None, + 'last_full_refresh': None } # Thread-safe singleton pattern for database access diff --git a/ui/components/__pycache__/database_updater_widget.cpython-312.pyc b/ui/components/__pycache__/database_updater_widget.cpython-312.pyc index 11560c50..1e3723a3 100644 Binary files a/ui/components/__pycache__/database_updater_widget.cpython-312.pyc and b/ui/components/__pycache__/database_updater_widget.cpython-312.pyc differ diff --git a/ui/components/database_updater_widget.py b/ui/components/database_updater_widget.py index ab2238be..59eff0be 100644 --- a/ui/components/database_updater_widget.py +++ b/ui/components/database_updater_widget.py @@ -4,6 +4,9 @@ from PyQt6.QtWidgets import (QFrame, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QProgressBar, QComboBox, QGroupBox) from PyQt6.QtCore import Qt from PyQt6.QtGui import QFont +from utils.logging_config import get_logger + +logger = get_logger("database_updater_widget") class DatabaseUpdaterWidget(QFrame): """UI widget for updating SoulSync database with Plex library data""" @@ -36,6 +39,18 @@ class DatabaseUpdaterWidget(QFrame): info_label.setStyleSheet("color: #b3b3b3; margin-bottom: 5px;") info_label.setWordWrap(True) + # Recommendation label + self.recommendation_label = QLabel("💡 Tip: Run a Full Refresh every 1-2 weeks to ensure database accuracy") + self.recommendation_label.setFont(QFont("Arial", 9)) + self.recommendation_label.setStyleSheet("color: #ffaa00; margin-bottom: 8px; padding: 6px 8px; background: #332200; border-radius: 4px;") + self.recommendation_label.setWordWrap(True) + + # Last full refresh label + self.last_refresh_label = QLabel("") + self.last_refresh_label.setFont(QFont("Arial", 8)) + self.last_refresh_label.setStyleSheet("color: #888888; margin-bottom: 5px;") + self.last_refresh_label.setWordWrap(True) + # Control section control_layout = QVBoxLayout() control_layout.setSpacing(12) @@ -270,6 +285,8 @@ class DatabaseUpdaterWidget(QFrame): # Add all sections to main layout layout.addWidget(header_label) layout.addWidget(info_label) + layout.addWidget(self.recommendation_label) + layout.addWidget(self.last_refresh_label) layout.addLayout(control_layout) layout.addLayout(progress_layout) layout.addWidget(stats_group) @@ -312,4 +329,56 @@ class DatabaseUpdaterWidget(QFrame): def set_button_enabled(self, enabled: bool): """Enable/disable the start button""" - self.start_button.setEnabled(enabled) \ No newline at end of file + self.start_button.setEnabled(enabled) + + def update_last_refresh_info(self, last_refresh_date: str = None): + """Update the last refresh information with color-coded warnings""" + if not last_refresh_date: + self.last_refresh_label.setText("No full refresh recorded") + self.last_refresh_label.setStyleSheet("color: #ff6666; margin-bottom: 5px; font-style: italic;") + self._update_recommendation_urgency(urgent=True) + return + + try: + from datetime import datetime + last_date = datetime.fromisoformat(last_refresh_date.replace('Z', '+00:00')) + days_ago = (datetime.now() - last_date.replace(tzinfo=None)).days + + if days_ago == 0: + time_text = "today" + color = "#1db954" # Green + urgent = False + elif days_ago == 1: + time_text = "yesterday" + color = "#1db954" # Green + urgent = False + elif days_ago < 7: + time_text = f"{days_ago} days ago" + color = "#1db954" # Green + urgent = False + elif days_ago < 14: + time_text = f"{days_ago} days ago" + color = "#ffaa00" # Orange warning + urgent = False + else: + time_text = f"{days_ago} days ago" + color = "#ff6666" # Red warning + urgent = True + + self.last_refresh_label.setText(f"Last full refresh: {time_text}") + self.last_refresh_label.setStyleSheet(f"color: {color}; margin-bottom: 5px;") + self._update_recommendation_urgency(urgent=urgent) + + except Exception: + self.last_refresh_label.setText("Last full refresh: unknown") + self.last_refresh_label.setStyleSheet("color: #888888; margin-bottom: 5px;") + self._update_recommendation_urgency(urgent=False) + + def _update_recommendation_urgency(self, urgent: bool = False): + """Update the recommendation label styling based on urgency""" + if urgent: + self.recommendation_label.setText("⚠️ Recommended: Run a Full Refresh - it's been over 2 weeks!") + self.recommendation_label.setStyleSheet("color: #ffffff; margin-bottom: 8px; padding: 6px 8px; background: #cc3300; border-radius: 4px;") + else: + self.recommendation_label.setText("💡 Tip: Run a Full Refresh every 1-2 weeks to ensure database accuracy") + self.recommendation_label.setStyleSheet("color: #ffaa00; margin-bottom: 8px; padding: 6px 8px; background: #332200; border-radius: 4px;") \ No newline at end of file diff --git a/ui/pages/__pycache__/dashboard.cpython-312.pyc b/ui/pages/__pycache__/dashboard.cpython-312.pyc index 88b705f0..7c515797 100644 Binary files a/ui/pages/__pycache__/dashboard.cpython-312.pyc and b/ui/pages/__pycache__/dashboard.cpython-312.pyc differ diff --git a/ui/pages/dashboard.py b/ui/pages/dashboard.py index c278c4a9..dcf91d1b 100644 --- a/ui/pages/dashboard.py +++ b/ui/pages/dashboard.py @@ -22,6 +22,9 @@ import io from core.matching_engine import MusicMatchingEngine from ui.components.database_updater_widget import DatabaseUpdaterWidget from core.database_update_worker import DatabaseUpdateWorker, DatabaseStatsWorker +from utils.logging_config import get_logger + +logger = get_logger("dashboard") class MetadataUpdateWorker(QThread): """Worker thread for updating Plex artist metadata using Spotify data""" @@ -1588,7 +1591,7 @@ class DashboardPage(QWidget): self._active_stats_workers.append(stats_worker) # Connect signals - stats_worker.stats_updated.connect(self.database_widget.update_statistics) + stats_worker.stats_updated.connect(self.update_database_info) stats_worker.finished.connect(lambda: self._cleanup_stats_worker(stats_worker)) stats_worker.start() @@ -1596,12 +1599,26 @@ class DashboardPage(QWidget): logger.error(f"Error refreshing database statistics: {e}") # Fallback to default stats to prevent crashes if hasattr(self, 'database_widget') and self.database_widget: - self.database_widget.update_statistics({ + fallback_info = { 'artists': 0, 'albums': 0, 'tracks': 0, - 'database_size_mb': 0.0 - }) + 'database_size_mb': 0.0, + 'last_full_refresh': None + } + self.update_database_info(fallback_info) + + def update_database_info(self, info: dict): + """Update database statistics and last refresh info""" + try: + # Update basic statistics + self.database_widget.update_statistics(info) + + # Update last refresh information + last_refresh_date = info.get('last_full_refresh') + self.database_widget.update_last_refresh_info(last_refresh_date) + except Exception as e: + logger.error(f"Error updating database info: {e}") def _cleanup_stats_worker(self, worker): """Clean up a finished stats worker"""