recommend database refresh every 1-2weeks

This is needed if users manually correct failed matches on plex when songs are scanned in. If a db sync is done after the scan but before the manual modification,
This commit is contained in:
Broque Thomas 2025-08-07 20:27:26 -07:00
parent 73679f5709
commit 2ad0dddba4
9 changed files with 157 additions and 12 deletions

View file

@ -4,7 +4,8 @@
"Bash(mkdir:*)", "Bash(mkdir:*)",
"Bash(rm:*)", "Bash(rm:*)",
"Bash(rg:*)", "Bash(rg:*)",
"Bash(grep:*)" "Bash(grep:*)",
"WebFetch(domain:python-plexapi.readthedocs.io)"
], ],
"deny": [] "deny": []
} }

View file

@ -80,6 +80,14 @@ class DatabaseUpdateWorker(QThread):
self.phase_changed.emit("Processing artists, albums, and tracks...") self.phase_changed.emit("Processing artists, albums, and tracks...")
self._process_all_artists(artists_to_process) 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 # Emit final results
self.finished.emit( self.finished.emit(
self.processed_artists, self.processed_artists,
@ -483,7 +491,7 @@ class DatabaseStatsWorker(QThread):
self.should_stop = True self.should_stop = True
def run(self): def run(self):
"""Get database statistics""" """Get database statistics and full info including last refresh"""
try: try:
if self.should_stop: if self.should_stop:
return return
@ -492,9 +500,10 @@ class DatabaseStatsWorker(QThread):
if self.should_stop: if self.should_stop:
return return
stats = database.get_database_info() # Get full database info (includes last_full_refresh)
info = database.get_database_info()
if not self.should_stop: if not self.should_stop:
self.stats_updated.emit(stats) self.stats_updated.emit(info)
except Exception as e: except Exception as e:
logger.error(f"Error getting database stats: {e}") logger.error(f"Error getting database stats: {e}")
if not self.should_stop: if not self.should_stop:
@ -503,5 +512,6 @@ class DatabaseStatsWorker(QThread):
'albums': 0, 'albums': 0,
'tracks': 0, 'tracks': 0,
'database_size_mb': 0.0, 'database_size_mb': 0.0,
'last_update': None 'last_update': None,
'last_full_refresh': None
}) })

View file

@ -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 # 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_albums_artist_id ON albums (artist_id)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_tracks_album_id ON tracks (album_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}") 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} 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]: def get_database_info(self) -> Dict[str, Any]:
"""Get comprehensive database information""" """Get comprehensive database information"""
try: try:
@ -1357,11 +1400,15 @@ class MusicDatabase:
result = cursor.fetchone() result = cursor.fetchone()
last_update = result['last_update'] if result and result['last_update'] else None 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 { return {
**stats, **stats,
'database_size_mb': round(db_size_mb, 2), 'database_size_mb': round(db_size_mb, 2),
'database_path': str(self.database_path), 'database_path': str(self.database_path),
'last_update': last_update 'last_update': last_update,
'last_full_refresh': last_full_refresh
} }
except Exception as e: except Exception as e:
@ -1372,7 +1419,8 @@ class MusicDatabase:
'tracks': 0, 'tracks': 0,
'database_size_mb': 0.0, 'database_size_mb': 0.0,
'database_path': str(self.database_path), 'database_path': str(self.database_path),
'last_update': None 'last_update': None,
'last_full_refresh': None
} }
# Thread-safe singleton pattern for database access # Thread-safe singleton pattern for database access

View file

@ -4,6 +4,9 @@ from PyQt6.QtWidgets import (QFrame, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QProgressBar, QComboBox, QGroupBox) QPushButton, QProgressBar, QComboBox, QGroupBox)
from PyQt6.QtCore import Qt from PyQt6.QtCore import Qt
from PyQt6.QtGui import QFont from PyQt6.QtGui import QFont
from utils.logging_config import get_logger
logger = get_logger("database_updater_widget")
class DatabaseUpdaterWidget(QFrame): class DatabaseUpdaterWidget(QFrame):
"""UI widget for updating SoulSync database with Plex library data""" """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.setStyleSheet("color: #b3b3b3; margin-bottom: 5px;")
info_label.setWordWrap(True) 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 section
control_layout = QVBoxLayout() control_layout = QVBoxLayout()
control_layout.setSpacing(12) control_layout.setSpacing(12)
@ -270,6 +285,8 @@ class DatabaseUpdaterWidget(QFrame):
# Add all sections to main layout # Add all sections to main layout
layout.addWidget(header_label) layout.addWidget(header_label)
layout.addWidget(info_label) layout.addWidget(info_label)
layout.addWidget(self.recommendation_label)
layout.addWidget(self.last_refresh_label)
layout.addLayout(control_layout) layout.addLayout(control_layout)
layout.addLayout(progress_layout) layout.addLayout(progress_layout)
layout.addWidget(stats_group) layout.addWidget(stats_group)
@ -312,4 +329,56 @@ class DatabaseUpdaterWidget(QFrame):
def set_button_enabled(self, enabled: bool): def set_button_enabled(self, enabled: bool):
"""Enable/disable the start button""" """Enable/disable the start button"""
self.start_button.setEnabled(enabled) 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;")

View file

@ -22,6 +22,9 @@ import io
from core.matching_engine import MusicMatchingEngine from core.matching_engine import MusicMatchingEngine
from ui.components.database_updater_widget import DatabaseUpdaterWidget from ui.components.database_updater_widget import DatabaseUpdaterWidget
from core.database_update_worker import DatabaseUpdateWorker, DatabaseStatsWorker from core.database_update_worker import DatabaseUpdateWorker, DatabaseStatsWorker
from utils.logging_config import get_logger
logger = get_logger("dashboard")
class MetadataUpdateWorker(QThread): class MetadataUpdateWorker(QThread):
"""Worker thread for updating Plex artist metadata using Spotify data""" """Worker thread for updating Plex artist metadata using Spotify data"""
@ -1588,7 +1591,7 @@ class DashboardPage(QWidget):
self._active_stats_workers.append(stats_worker) self._active_stats_workers.append(stats_worker)
# Connect signals # 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.finished.connect(lambda: self._cleanup_stats_worker(stats_worker))
stats_worker.start() stats_worker.start()
@ -1596,12 +1599,26 @@ class DashboardPage(QWidget):
logger.error(f"Error refreshing database statistics: {e}") logger.error(f"Error refreshing database statistics: {e}")
# Fallback to default stats to prevent crashes # Fallback to default stats to prevent crashes
if hasattr(self, 'database_widget') and self.database_widget: if hasattr(self, 'database_widget') and self.database_widget:
self.database_widget.update_statistics({ fallback_info = {
'artists': 0, 'artists': 0,
'albums': 0, 'albums': 0,
'tracks': 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): def _cleanup_stats_worker(self, worker):
"""Clean up a finished stats worker""" """Clean up a finished stats worker"""