update variations
This commit is contained in:
parent
b76c0531e5
commit
684433d857
6 changed files with 147 additions and 97 deletions
Binary file not shown.
|
|
@ -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
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -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,15 +169,20 @@ 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}")
|
||||||
|
|
@ -188,42 +192,42 @@ 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}")
|
||||||
|
|
@ -232,24 +236,24 @@ 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}")
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue