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 utils.logging_config import get_logger
from config.settings import config_manager
logger = get_logger("database_update_worker")
@ -36,8 +37,10 @@ class DatabaseUpdateWorker(QThread):
self.successful_operations = 0
self.failed_operations = 0
# Threading control
self.max_workers = 3 # Conservative to avoid overwhelming Plex API
# Threading control - get from config or default to 5
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()
# Database instance

View file

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

View file

@ -562,6 +562,15 @@ class SettingsPage(QWidget):
self.download_path_input.setText(soulseek_config.get('download_path', './downloads'))
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)
logging_config = config_manager.get_logging_config()
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.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
self.settings_changed.emit('soulseek.download_path', self.download_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(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_group = SettingsGroup("Logging Settings")
logging_layout = QVBoxLayout(logging_group)
@ -1305,6 +1347,7 @@ class SettingsPage(QWidget):
logging_layout.addLayout(log_path_container)
layout.addWidget(download_group)
layout.addWidget(database_group)
layout.addWidget(logging_group)
layout.addStretch() # Push content to top, prevent stretching