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:
parent
73679f5709
commit
2ad0dddba4
9 changed files with 157 additions and 12 deletions
|
|
@ -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": []
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -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
|
||||||
})
|
})
|
||||||
Binary file not shown.
|
|
@ -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
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -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;")
|
||||||
Binary file not shown.
|
|
@ -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"""
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue