update variations

This commit is contained in:
Broque Thomas 2025-08-06 00:07:12 -07:00
parent b76c0531e5
commit 684433d857
6 changed files with 147 additions and 97 deletions

View file

@ -9,6 +9,7 @@ import time
from database import get_database, MusicDatabase from database import get_database, MusicDatabase
from utils.logging_config import get_logger from utils.logging_config import get_logger
from config.settings import config_manager
logger = get_logger("database_update_worker") logger = get_logger("database_update_worker")
@ -36,8 +37,10 @@ class DatabaseUpdateWorker(QThread):
self.successful_operations = 0 self.successful_operations = 0
self.failed_operations = 0 self.failed_operations = 0
# Threading control # Threading control - get from config or default to 5
self.max_workers = 3 # Conservative to avoid overwhelming Plex API database_config = config_manager.get('database', {})
self.max_workers = database_config.get('max_workers', 5)
logger.info(f"Using {self.max_workers} worker threads for database update")
self.thread_lock = threading.Lock() self.thread_lock = threading.Lock()
# Database instance # Database instance

View file

@ -54,19 +54,19 @@ class MusicDatabase:
def __init__(self, database_path: str = "database/music_library.db"): def __init__(self, database_path: str = "database/music_library.db"):
self.database_path = Path(database_path) self.database_path = Path(database_path)
self.database_path.parent.mkdir(parents=True, exist_ok=True) self.database_path.parent.mkdir(parents=True, exist_ok=True)
self._connection: Optional[sqlite3.Connection] = None
# Initialize database # Initialize database
self._initialize_database() self._initialize_database()
def _get_connection(self) -> sqlite3.Connection: def _get_connection(self) -> sqlite3.Connection:
"""Get database connection with row factory""" """Get a NEW database connection for each operation (thread-safe)"""
if self._connection is None: connection = sqlite3.connect(str(self.database_path), timeout=30.0)
self._connection = sqlite3.connect(str(self.database_path)) connection.row_factory = sqlite3.Row
self._connection.row_factory = sqlite3.Row # Enable foreign key constraints and WAL mode for better concurrency
# Enable foreign key constraints connection.execute("PRAGMA foreign_keys = ON")
self._connection.execute("PRAGMA foreign_keys = ON") connection.execute("PRAGMA journal_mode = WAL")
return self._connection connection.execute("PRAGMA busy_timeout = 30000") # 30 second timeout
return connection
def _initialize_database(self): def _initialize_database(self):
"""Create database tables if they don't exist""" """Create database tables if they don't exist"""
@ -138,31 +138,30 @@ class MusicDatabase:
raise raise
def close(self): def close(self):
"""Close database connection""" """Close database connection (no-op since we create connections per operation)"""
if self._connection: # Each operation creates and closes its own connection, so nothing to do here
self._connection.close() pass
self._connection = None
def get_statistics(self) -> Dict[str, int]: def get_statistics(self) -> Dict[str, int]:
"""Get database statistics""" """Get database statistics"""
try: try:
conn = self._get_connection() with self._get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("SELECT COUNT(*) FROM artists") cursor.execute("SELECT COUNT(*) FROM artists")
artist_count = cursor.fetchone()[0] artist_count = cursor.fetchone()[0]
cursor.execute("SELECT COUNT(*) FROM albums") cursor.execute("SELECT COUNT(*) FROM albums")
album_count = cursor.fetchone()[0] album_count = cursor.fetchone()[0]
cursor.execute("SELECT COUNT(*) FROM tracks") cursor.execute("SELECT COUNT(*) FROM tracks")
track_count = cursor.fetchone()[0] track_count = cursor.fetchone()[0]
return { return {
'artists': artist_count, 'artists': artist_count,
'albums': album_count, 'albums': album_count,
'tracks': track_count 'tracks': track_count
} }
except Exception as e: except Exception as e:
logger.error(f"Error getting database statistics: {e}") logger.error(f"Error getting database statistics: {e}")
return {'artists': 0, 'albums': 0, 'tracks': 0} return {'artists': 0, 'albums': 0, 'tracks': 0}
@ -170,16 +169,21 @@ class MusicDatabase:
def clear_all_data(self): def clear_all_data(self):
"""Clear all data from database (for full refresh)""" """Clear all data from database (for full refresh)"""
try: try:
conn = self._get_connection() with self._get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("DELETE FROM tracks") cursor.execute("DELETE FROM tracks")
cursor.execute("DELETE FROM albums") cursor.execute("DELETE FROM albums")
cursor.execute("DELETE FROM artists") cursor.execute("DELETE FROM artists")
conn.commit() conn.commit()
logger.info("All database data cleared")
# VACUUM to actually shrink the database file and reclaim disk space
logger.info("Vacuuming database to reclaim disk space...")
cursor.execute("VACUUM")
logger.info("All database data cleared and file compacted")
except Exception as e: except Exception as e:
logger.error(f"Error clearing database: {e}") logger.error(f"Error clearing database: {e}")
raise raise
@ -188,43 +192,43 @@ class MusicDatabase:
def insert_or_update_artist(self, plex_artist) -> bool: def insert_or_update_artist(self, plex_artist) -> bool:
"""Insert or update artist from Plex artist object""" """Insert or update artist from Plex artist object"""
try: try:
conn = self._get_connection() with self._get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
artist_id = int(plex_artist.ratingKey) artist_id = int(plex_artist.ratingKey)
name = plex_artist.title name = plex_artist.title
thumb_url = getattr(plex_artist, 'thumb', None) thumb_url = getattr(plex_artist, 'thumb', None)
summary = getattr(plex_artist, 'summary', None) summary = getattr(plex_artist, 'summary', None)
# Get genres # Get genres
genres = [] genres = []
if hasattr(plex_artist, 'genres') and plex_artist.genres: if hasattr(plex_artist, 'genres') and plex_artist.genres:
genres = [genre.tag if hasattr(genre, 'tag') else str(genre) genres = [genre.tag if hasattr(genre, 'tag') else str(genre)
for genre in plex_artist.genres] for genre in plex_artist.genres]
genres_json = json.dumps(genres) if genres else None genres_json = json.dumps(genres) if genres else None
# Check if artist exists # Check if artist exists
cursor.execute("SELECT id FROM artists WHERE id = ?", (artist_id,)) cursor.execute("SELECT id FROM artists WHERE id = ?", (artist_id,))
exists = cursor.fetchone() exists = cursor.fetchone()
if exists: if exists:
# Update existing artist # Update existing artist
cursor.execute(""" cursor.execute("""
UPDATE artists UPDATE artists
SET name = ?, thumb_url = ?, genres = ?, summary = ?, updated_at = CURRENT_TIMESTAMP SET name = ?, thumb_url = ?, genres = ?, summary = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ? WHERE id = ?
""", (name, thumb_url, genres_json, summary, artist_id)) """, (name, thumb_url, genres_json, summary, artist_id))
else: else:
# Insert new artist # Insert new artist
cursor.execute(""" cursor.execute("""
INSERT INTO artists (id, name, thumb_url, genres, summary) INSERT INTO artists (id, name, thumb_url, genres, summary)
VALUES (?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?)
""", (artist_id, name, thumb_url, genres_json, summary)) """, (artist_id, name, thumb_url, genres_json, summary))
conn.commit() conn.commit()
return True return True
except Exception as e: except Exception as e:
logger.error(f"Error inserting/updating artist {getattr(plex_artist, 'title', 'Unknown')}: {e}") logger.error(f"Error inserting/updating artist {getattr(plex_artist, 'title', 'Unknown')}: {e}")
return False return False
@ -232,25 +236,25 @@ class MusicDatabase:
def get_artist(self, artist_id: int) -> Optional[DatabaseArtist]: def get_artist(self, artist_id: int) -> Optional[DatabaseArtist]:
"""Get artist by ID""" """Get artist by ID"""
try: try:
conn = self._get_connection() with self._get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("SELECT * FROM artists WHERE id = ?", (artist_id,)) cursor.execute("SELECT * FROM artists WHERE id = ?", (artist_id,))
row = cursor.fetchone() row = cursor.fetchone()
if row: if row:
genres = json.loads(row['genres']) if row['genres'] else None genres = json.loads(row['genres']) if row['genres'] else None
return DatabaseArtist( return DatabaseArtist(
id=row['id'], id=row['id'],
name=row['name'], name=row['name'],
thumb_url=row['thumb_url'], thumb_url=row['thumb_url'],
genres=genres, genres=genres,
summary=row['summary'], summary=row['summary'],
created_at=datetime.fromisoformat(row['created_at']) if row['created_at'] else None, created_at=datetime.fromisoformat(row['created_at']) if row['created_at'] else None,
updated_at=datetime.fromisoformat(row['updated_at']) if row['updated_at'] else None updated_at=datetime.fromisoformat(row['updated_at']) if row['updated_at'] else None
) )
return None return None
except Exception as e: except Exception as e:
logger.error(f"Error getting artist {artist_id}: {e}") logger.error(f"Error getting artist {artist_id}: {e}")
return None return None

View file

@ -562,6 +562,15 @@ class SettingsPage(QWidget):
self.download_path_input.setText(soulseek_config.get('download_path', './downloads')) self.download_path_input.setText(soulseek_config.get('download_path', './downloads'))
self.transfer_path_input.setText(soulseek_config.get('transfer_path', './Transfer')) self.transfer_path_input.setText(soulseek_config.get('transfer_path', './Transfer'))
# Load database config
database_config = config_manager.get('database', {})
if hasattr(self, 'max_workers_combo'):
max_workers = database_config.get('max_workers', 5)
# Find the index of the current value in the combo box
index = self.max_workers_combo.findText(str(max_workers))
if index >= 0:
self.max_workers_combo.setCurrentIndex(index)
# Load logging config (read-only display) # Load logging config (read-only display)
logging_config = config_manager.get_logging_config() logging_config = config_manager.get_logging_config()
if hasattr(self, 'log_level_display'): if hasattr(self, 'log_level_display'):
@ -590,6 +599,11 @@ class SettingsPage(QWidget):
config_manager.set('soulseek.download_path', self.download_path_input.text()) config_manager.set('soulseek.download_path', self.download_path_input.text())
config_manager.set('soulseek.transfer_path', self.transfer_path_input.text()) config_manager.set('soulseek.transfer_path', self.transfer_path_input.text())
# Save Database settings
if hasattr(self, 'max_workers_combo'):
max_workers = int(self.max_workers_combo.currentText())
config_manager.set('database.max_workers', max_workers)
# Emit signals for path changes to update other pages immediately # Emit signals for path changes to update other pages immediately
self.settings_changed.emit('soulseek.download_path', self.download_path_input.text()) self.settings_changed.emit('soulseek.download_path', self.download_path_input.text())
self.settings_changed.emit('soulseek.transfer_path', self.transfer_path_input.text()) self.settings_changed.emit('soulseek.transfer_path', self.transfer_path_input.text())
@ -1257,6 +1271,34 @@ class SettingsPage(QWidget):
download_layout.addLayout(path_container) download_layout.addLayout(path_container)
download_layout.addLayout(transfer_path_container) download_layout.addLayout(transfer_path_container)
# Database Settings
database_group = SettingsGroup("Database Settings")
database_layout = QVBoxLayout(database_group)
database_layout.setContentsMargins(16, 20, 16, 16)
database_layout.setSpacing(12)
# Max Workers
workers_layout = QHBoxLayout()
workers_label = QLabel("Concurrent Workers:")
workers_label.setStyleSheet("color: #ffffff; font-size: 12px;")
self.max_workers_combo = QComboBox()
self.max_workers_combo.addItems(["3", "4", "5", "6", "7", "8", "9", "10"])
self.max_workers_combo.setCurrentText("5") # Default value
self.max_workers_combo.setStyleSheet(self.get_combo_style())
self.max_workers_combo.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
workers_layout.addWidget(workers_label)
workers_layout.addWidget(self.max_workers_combo)
# Help text for workers
workers_help = QLabel("Number of parallel threads for database updates. Higher values = faster updates but more server load.")
workers_help.setStyleSheet("color: #888888; font-size: 10px; font-style: italic;")
workers_help.setWordWrap(True)
database_layout.addLayout(workers_layout)
database_layout.addWidget(workers_help)
# Logging Settings # Logging Settings
logging_group = SettingsGroup("Logging Settings") logging_group = SettingsGroup("Logging Settings")
logging_layout = QVBoxLayout(logging_group) logging_layout = QVBoxLayout(logging_group)
@ -1305,6 +1347,7 @@ class SettingsPage(QWidget):
logging_layout.addLayout(log_path_container) logging_layout.addLayout(log_path_container)
layout.addWidget(download_group) layout.addWidget(download_group)
layout.addWidget(database_group)
layout.addWidget(logging_group) layout.addWidget(logging_group)
layout.addStretch() # Push content to top, prevent stretching layout.addStretch() # Push content to top, prevent stretching