Stripped 4,200+ emoji characters from print(), logger calls across 39 Python files. Logs are now clean text — easier to grep, more professional, no encoding issues on terminals without Unicode support. Seasonal config icons preserved for UI display.
396 lines
No EOL
16 KiB
Python
396 lines
No EOL
16 KiB
Python
#!/usr/bin/env python3
|
|
|
|
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 media server library data (Plex, Jellyfin, or Navidrome)"""
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.setup_ui()
|
|
|
|
def setup_ui(self):
|
|
self.setStyleSheet("""
|
|
DatabaseUpdaterWidget {
|
|
background: #282828;
|
|
border-radius: 8px;
|
|
border: 1px solid #404040;
|
|
}
|
|
""")
|
|
|
|
layout = QVBoxLayout(self)
|
|
layout.setContentsMargins(20, 15, 20, 15)
|
|
layout.setSpacing(12)
|
|
|
|
# Header
|
|
header_label = QLabel("Update SoulSync Database")
|
|
header_label.setFont(QFont("Arial", 14, QFont.Weight.Bold))
|
|
header_label.setStyleSheet("color: #ffffff;")
|
|
|
|
# Info label - dynamic based on active server
|
|
try:
|
|
from config.settings import config_manager
|
|
active_server = config_manager.get_active_media_server()
|
|
if active_server == "jellyfin":
|
|
server_name = "Jellyfin"
|
|
elif active_server == "navidrome":
|
|
server_name = "Navidrome"
|
|
else:
|
|
server_name = "Plex"
|
|
except:
|
|
server_name = "Plex" # Fallback
|
|
|
|
info_label = QLabel(f"Syncs your {server_name} music library into the local database for faster searches and analytics")
|
|
info_label.setFont(QFont("Arial", 9))
|
|
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)
|
|
|
|
# Top row: Button
|
|
button_layout = QHBoxLayout()
|
|
self.start_button = QPushButton("Update Database")
|
|
self.start_button.setFixedHeight(36)
|
|
self.start_button.setFont(QFont("Arial", 10, QFont.Weight.Medium))
|
|
self.start_button.setStyleSheet("""
|
|
QPushButton {
|
|
background: #1db954;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 6px;
|
|
padding: 8px 16px;
|
|
}
|
|
QPushButton:hover {
|
|
background: #1ed760;
|
|
}
|
|
QPushButton:pressed {
|
|
background: #169c46;
|
|
}
|
|
QPushButton:disabled {
|
|
background: #555555;
|
|
color: #999999;
|
|
}
|
|
""")
|
|
button_layout.addWidget(self.start_button)
|
|
button_layout.addStretch()
|
|
|
|
# Bottom row: Settings and status
|
|
settings_layout = QHBoxLayout()
|
|
settings_layout.setSpacing(25)
|
|
|
|
# Update type dropdown
|
|
update_type_layout = QVBoxLayout()
|
|
update_type_layout.setSpacing(4)
|
|
|
|
type_label = QLabel("Update Type:")
|
|
type_label.setFont(QFont("Arial", 9))
|
|
type_label.setStyleSheet("color: #b3b3b3;")
|
|
|
|
self.update_type_combo = QComboBox()
|
|
self.update_type_combo.setFixedHeight(32)
|
|
self.update_type_combo.setFont(QFont("Arial", 10))
|
|
self.update_type_combo.addItems([
|
|
"Incremental Update",
|
|
"Full Refresh"
|
|
])
|
|
self.update_type_combo.setCurrentText("Incremental Update")
|
|
self.update_type_combo.setStyleSheet("""
|
|
QComboBox {
|
|
background: #333333;
|
|
color: #ffffff;
|
|
border: 1px solid #555555;
|
|
border-radius: 4px;
|
|
padding: 4px 8px;
|
|
min-width: 140px;
|
|
}
|
|
QComboBox:hover {
|
|
border: 1px solid #1db954;
|
|
}
|
|
QComboBox::drop-down {
|
|
border: none;
|
|
width: 20px;
|
|
}
|
|
QComboBox::down-arrow {
|
|
image: none;
|
|
border-left: 5px solid transparent;
|
|
border-right: 5px solid transparent;
|
|
border-top: 5px solid #ffffff;
|
|
margin-right: 5px;
|
|
}
|
|
QComboBox QAbstractItemView {
|
|
background: #333333;
|
|
color: #ffffff;
|
|
border: 1px solid #555555;
|
|
selection-background-color: #1db954;
|
|
}
|
|
""")
|
|
|
|
update_type_layout.addWidget(type_label)
|
|
update_type_layout.addWidget(self.update_type_combo)
|
|
|
|
# Current status display
|
|
status_layout = QVBoxLayout()
|
|
status_layout.setSpacing(4)
|
|
|
|
current_label = QLabel("Current Status:")
|
|
current_label.setFont(QFont("Arial", 9))
|
|
current_label.setStyleSheet("color: #b3b3b3;")
|
|
|
|
self.current_status_label = QLabel("Ready")
|
|
self.current_status_label.setFont(QFont("Arial", 11, QFont.Weight.Medium))
|
|
self.current_status_label.setStyleSheet("color: #ffffff;")
|
|
|
|
status_layout.addWidget(current_label)
|
|
status_layout.addWidget(self.current_status_label)
|
|
|
|
settings_layout.addLayout(update_type_layout)
|
|
settings_layout.addLayout(status_layout)
|
|
settings_layout.addStretch()
|
|
|
|
control_layout.addLayout(button_layout)
|
|
control_layout.addLayout(settings_layout)
|
|
|
|
# Progress section
|
|
progress_layout = QVBoxLayout()
|
|
progress_layout.setSpacing(8)
|
|
|
|
progress_info_layout = QHBoxLayout()
|
|
|
|
self.progress_label = QLabel("Progress: 0%")
|
|
self.progress_label.setFont(QFont("Arial", 10))
|
|
self.progress_label.setStyleSheet("color: #ffffff;")
|
|
|
|
self.count_label = QLabel("0 artists processed")
|
|
self.count_label.setFont(QFont("Arial", 9))
|
|
self.count_label.setStyleSheet("color: #b3b3b3;")
|
|
|
|
progress_info_layout.addWidget(self.progress_label)
|
|
progress_info_layout.addStretch()
|
|
progress_info_layout.addWidget(self.count_label)
|
|
|
|
self.progress_bar = QProgressBar()
|
|
self.progress_bar.setFixedHeight(8)
|
|
self.progress_bar.setRange(0, 100)
|
|
self.progress_bar.setValue(0)
|
|
self.progress_bar.setStyleSheet("""
|
|
QProgressBar {
|
|
border: none;
|
|
border-radius: 4px;
|
|
background: #555555;
|
|
}
|
|
QProgressBar::chunk {
|
|
background: #1db954;
|
|
border-radius: 4px;
|
|
}
|
|
""")
|
|
|
|
progress_layout.addLayout(progress_info_layout)
|
|
progress_layout.addWidget(self.progress_bar)
|
|
|
|
# Statistics section (shows current database info)
|
|
stats_group = QGroupBox("Database Statistics")
|
|
stats_group.setFont(QFont("Arial", 10, QFont.Weight.Bold))
|
|
stats_group.setStyleSheet("""
|
|
QGroupBox {
|
|
color: #ffffff;
|
|
border: 1px solid #555555;
|
|
border-radius: 6px;
|
|
margin-top: 6px;
|
|
padding-top: 10px;
|
|
}
|
|
QGroupBox::title {
|
|
subcontrol-origin: margin;
|
|
left: 10px;
|
|
padding: 0 5px 0 5px;
|
|
}
|
|
""")
|
|
|
|
stats_layout = QHBoxLayout(stats_group)
|
|
stats_layout.setSpacing(20)
|
|
|
|
# Artists stat
|
|
artists_layout = QVBoxLayout()
|
|
self.artists_count_label = QLabel("0")
|
|
self.artists_count_label.setFont(QFont("Arial", 16, QFont.Weight.Bold))
|
|
self.artists_count_label.setStyleSheet("color: #1db954;")
|
|
self.artists_count_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
|
|
artists_text_label = QLabel("Artists")
|
|
artists_text_label.setFont(QFont("Arial", 9))
|
|
artists_text_label.setStyleSheet("color: #b3b3b3;")
|
|
artists_text_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
|
|
artists_layout.addWidget(self.artists_count_label)
|
|
artists_layout.addWidget(artists_text_label)
|
|
|
|
# Albums stat
|
|
albums_layout = QVBoxLayout()
|
|
self.albums_count_label = QLabel("0")
|
|
self.albums_count_label.setFont(QFont("Arial", 16, QFont.Weight.Bold))
|
|
self.albums_count_label.setStyleSheet("color: #1db954;")
|
|
self.albums_count_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
|
|
albums_text_label = QLabel("Albums")
|
|
albums_text_label.setFont(QFont("Arial", 9))
|
|
albums_text_label.setStyleSheet("color: #b3b3b3;")
|
|
albums_text_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
|
|
albums_layout.addWidget(self.albums_count_label)
|
|
albums_layout.addWidget(albums_text_label)
|
|
|
|
# Tracks stat
|
|
tracks_layout = QVBoxLayout()
|
|
self.tracks_count_label = QLabel("0")
|
|
self.tracks_count_label.setFont(QFont("Arial", 16, QFont.Weight.Bold))
|
|
self.tracks_count_label.setStyleSheet("color: #1db954;")
|
|
self.tracks_count_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
|
|
tracks_text_label = QLabel("Tracks")
|
|
tracks_text_label.setFont(QFont("Arial", 9))
|
|
tracks_text_label.setStyleSheet("color: #b3b3b3;")
|
|
tracks_text_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
|
|
tracks_layout.addWidget(self.tracks_count_label)
|
|
tracks_layout.addWidget(tracks_text_label)
|
|
|
|
# Database size stat
|
|
size_layout = QVBoxLayout()
|
|
self.size_label = QLabel("0.0 MB")
|
|
self.size_label.setFont(QFont("Arial", 16, QFont.Weight.Bold))
|
|
self.size_label.setStyleSheet("color: #1db954;")
|
|
self.size_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
|
|
size_text_label = QLabel("DB Size")
|
|
size_text_label.setFont(QFont("Arial", 9))
|
|
size_text_label.setStyleSheet("color: #b3b3b3;")
|
|
size_text_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
|
|
size_layout.addWidget(self.size_label)
|
|
size_layout.addWidget(size_text_label)
|
|
|
|
stats_layout.addLayout(artists_layout)
|
|
stats_layout.addLayout(albums_layout)
|
|
stats_layout.addLayout(tracks_layout)
|
|
stats_layout.addLayout(size_layout)
|
|
stats_layout.addStretch()
|
|
|
|
# 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)
|
|
|
|
def update_progress(self, is_running: bool, current_item: str, processed: int, total: int, percentage: float):
|
|
"""Update progress display during database update"""
|
|
if is_running:
|
|
self.start_button.setText("Stop Update")
|
|
self.start_button.setEnabled(True)
|
|
self.current_status_label.setText(current_item if current_item else "Processing...")
|
|
self.progress_label.setText(f"Progress: {percentage:.1f}%")
|
|
self.count_label.setText(f"{processed} / {total} artists processed")
|
|
self.progress_bar.setValue(int(percentage))
|
|
else:
|
|
self.start_button.setText("Update Database")
|
|
self.start_button.setEnabled(True)
|
|
self.current_status_label.setText("Ready")
|
|
self.progress_label.setText("Progress: 0%")
|
|
self.count_label.setText("0 artists processed")
|
|
self.progress_bar.setValue(0)
|
|
|
|
def update_statistics(self, stats: dict):
|
|
"""Update database statistics display"""
|
|
self.artists_count_label.setText(str(stats.get('artists', 0)))
|
|
self.albums_count_label.setText(str(stats.get('albums', 0)))
|
|
self.tracks_count_label.setText(str(stats.get('tracks', 0)))
|
|
self.size_label.setText(f"{stats.get('database_size_mb', 0.0):.1f} MB")
|
|
|
|
def update_phase(self, phase: str):
|
|
"""Update current phase display"""
|
|
self.current_status_label.setText(phase)
|
|
|
|
def is_full_refresh(self) -> bool:
|
|
"""Check if full refresh is selected"""
|
|
return self.update_type_combo.currentText() == "Full Refresh"
|
|
|
|
def set_button_text(self, text: str):
|
|
"""Set custom button text"""
|
|
self.start_button.setText(text)
|
|
|
|
def set_button_enabled(self, enabled: bool):
|
|
"""Enable/disable the start button"""
|
|
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;") |