mirror of https://github.com/Nezreka/SoulSync.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
391 lines
16 KiB
391 lines
16 KiB
#!/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 Plex library data"""
|
|
|
|
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()
|
|
server_name = "Jellyfin" if active_server == "jellyfin" else "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;") |