basic db structure
This commit is contained in:
parent
df475692a9
commit
b76c0531e5
19 changed files with 1560 additions and 2 deletions
BIN
core/__pycache__/database_update_worker.cpython-310.pyc
Normal file
BIN
core/__pycache__/database_update_worker.cpython-310.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/database_update_worker.cpython-312.pyc
Normal file
BIN
core/__pycache__/database_update_worker.cpython-312.pyc
Normal file
Binary file not shown.
409
core/database_update_worker.py
Normal file
409
core/database_update_worker.py
Normal file
|
|
@ -0,0 +1,409 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
from PyQt6.QtCore import QThread, pyqtSignal
|
||||
import threading
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
import time
|
||||
|
||||
from database import get_database, MusicDatabase
|
||||
from utils.logging_config import get_logger
|
||||
|
||||
logger = get_logger("database_update_worker")
|
||||
|
||||
class DatabaseUpdateWorker(QThread):
|
||||
"""Worker thread for updating SoulSync database with Plex library data"""
|
||||
|
||||
# Signals for progress reporting
|
||||
progress_updated = pyqtSignal(str, int, int, float) # current_item, processed, total, percentage
|
||||
artist_processed = pyqtSignal(str, bool, str, int, int) # artist_name, success, details, albums_count, tracks_count
|
||||
finished = pyqtSignal(int, int, int, int, int) # total_artists, total_albums, total_tracks, successful, failed
|
||||
error = pyqtSignal(str) # error_message
|
||||
phase_changed = pyqtSignal(str) # current_phase (artists, albums, tracks)
|
||||
|
||||
def __init__(self, plex_client, database_path: str = "database/music_library.db", full_refresh: bool = False):
|
||||
super().__init__()
|
||||
self.plex_client = plex_client
|
||||
self.database_path = database_path
|
||||
self.full_refresh = full_refresh
|
||||
self.should_stop = False
|
||||
|
||||
# Statistics tracking
|
||||
self.processed_artists = 0
|
||||
self.processed_albums = 0
|
||||
self.processed_tracks = 0
|
||||
self.successful_operations = 0
|
||||
self.failed_operations = 0
|
||||
|
||||
# Threading control
|
||||
self.max_workers = 3 # Conservative to avoid overwhelming Plex API
|
||||
self.thread_lock = threading.Lock()
|
||||
|
||||
# Database instance
|
||||
self.database: Optional[MusicDatabase] = None
|
||||
|
||||
def stop(self):
|
||||
"""Stop the database update process"""
|
||||
self.should_stop = True
|
||||
|
||||
def run(self):
|
||||
"""Main worker thread execution"""
|
||||
try:
|
||||
# Initialize database
|
||||
self.database = get_database(self.database_path)
|
||||
|
||||
if self.full_refresh:
|
||||
logger.info("Performing full database refresh - clearing existing data")
|
||||
self.database.clear_all_data()
|
||||
# For full refresh, use the old method (all artists)
|
||||
artists_to_process = self._get_all_artists()
|
||||
if not artists_to_process:
|
||||
self.error.emit("No artists found in Plex library or connection failed")
|
||||
return
|
||||
logger.info(f"Full refresh: Found {len(artists_to_process)} artists in Plex library")
|
||||
else:
|
||||
logger.info("Performing smart incremental update - checking recently added content")
|
||||
# For incremental, use smart recent-first approach
|
||||
self.phase_changed.emit("Finding recently added content...")
|
||||
artists_to_process = self._get_artists_for_incremental_update()
|
||||
if not artists_to_process:
|
||||
logger.info("No new content found - database is up to date")
|
||||
self.finished.emit(0, 0, 0, 0, 0)
|
||||
return
|
||||
logger.info(f"Incremental update: Found {len(artists_to_process)} artists to process")
|
||||
|
||||
# Phase 2: Process artists and their albums/tracks
|
||||
self.phase_changed.emit("Processing artists, albums, and tracks...")
|
||||
self._process_all_artists(artists_to_process)
|
||||
|
||||
# Emit final results
|
||||
self.finished.emit(
|
||||
self.processed_artists,
|
||||
self.processed_albums,
|
||||
self.processed_tracks,
|
||||
self.successful_operations,
|
||||
self.failed_operations
|
||||
)
|
||||
|
||||
update_type = "Full refresh" if self.full_refresh else "Incremental update"
|
||||
logger.info(f"{update_type} completed: {self.processed_artists} artists, "
|
||||
f"{self.processed_albums} albums, {self.processed_tracks} tracks processed")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Database update failed: {str(e)}")
|
||||
self.error.emit(f"Database update failed: {str(e)}")
|
||||
|
||||
def _get_all_artists(self) -> List:
|
||||
"""Get all artists from Plex library"""
|
||||
try:
|
||||
if not self.plex_client.ensure_connection():
|
||||
logger.error("Could not connect to Plex server")
|
||||
return []
|
||||
|
||||
artists = self.plex_client.get_all_artists()
|
||||
return artists
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting artists from Plex: {e}")
|
||||
return []
|
||||
|
||||
def _get_artists_for_incremental_update(self) -> List:
|
||||
"""Get artists that need processing for incremental update using smart early-stopping logic"""
|
||||
try:
|
||||
if not self.plex_client.ensure_connection():
|
||||
logger.error("Could not connect to Plex server")
|
||||
return []
|
||||
|
||||
if not self.plex_client.music_library:
|
||||
logger.error("No music library found in Plex")
|
||||
return []
|
||||
|
||||
# Strategy: Get recently added albums and extract artists from them
|
||||
# Process artists in reverse chronological order until we hit one that's already current
|
||||
|
||||
logger.info("Getting recently added albums to find new artists...")
|
||||
|
||||
# Get recently added albums (up to 500 to cast a wide net)
|
||||
try:
|
||||
recent_albums = self.plex_client.music_library.recentlyAdded(maxresults=500)
|
||||
logger.info(f"Found {len(recent_albums)} recently added albums")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not get recently added albums: {e}")
|
||||
# Fallback: get recently added tracks instead
|
||||
try:
|
||||
recent_tracks = self.plex_client.music_library.recentlyAdded(libtype='track', maxresults=1000)
|
||||
logger.info(f"Fallback: Found {len(recent_tracks)} recently added tracks")
|
||||
# Extract albums from tracks
|
||||
recent_albums = []
|
||||
seen_albums = set()
|
||||
for track in recent_tracks:
|
||||
try:
|
||||
album = track.album()
|
||||
if album and album.ratingKey not in seen_albums:
|
||||
recent_albums.append(album)
|
||||
seen_albums.add(album.ratingKey)
|
||||
except:
|
||||
continue
|
||||
logger.info(f"Extracted {len(recent_albums)} unique albums from tracks")
|
||||
except Exception as e2:
|
||||
logger.error(f"Could not get recently added content: {e2}")
|
||||
return []
|
||||
|
||||
if not recent_albums:
|
||||
logger.info("No recently added albums found")
|
||||
return []
|
||||
|
||||
# Sort albums by added date (newest first)
|
||||
try:
|
||||
recent_albums.sort(key=lambda x: getattr(x, 'addedAt', 0), reverse=True)
|
||||
logger.info("Sorted albums by recently added date (newest first)")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not sort albums by date: {e}")
|
||||
|
||||
# Extract artists from recent albums with early stopping logic
|
||||
artists_to_process = []
|
||||
processed_artist_ids = set()
|
||||
stopped_early = False
|
||||
|
||||
logger.info("Checking artists from recent albums (with early stopping)...")
|
||||
|
||||
for i, album in enumerate(recent_albums):
|
||||
if self.should_stop:
|
||||
break
|
||||
|
||||
try:
|
||||
# Get the artist for this album
|
||||
album_artist = album.artist()
|
||||
if not album_artist:
|
||||
continue
|
||||
|
||||
artist_id = int(album_artist.ratingKey)
|
||||
|
||||
# Skip if we've already checked this artist
|
||||
if artist_id in processed_artist_ids:
|
||||
continue
|
||||
|
||||
processed_artist_ids.add(artist_id)
|
||||
|
||||
# Check if this artist is already current in our database
|
||||
if self._artist_is_already_current(album_artist):
|
||||
logger.info(f"Hit already-current artist '{album_artist.title}' at position {i+1} - stopping early!")
|
||||
stopped_early = True
|
||||
break
|
||||
|
||||
# Artist needs processing
|
||||
artists_to_process.append(album_artist)
|
||||
logger.debug(f"Added artist '{album_artist.title}' for processing")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error checking album artist: {e}")
|
||||
continue
|
||||
|
||||
result_msg = f"Smart incremental scan result: {len(artists_to_process)} artists to process"
|
||||
if stopped_early:
|
||||
result_msg += f" (stopped early after checking {len(processed_artist_ids)} artists)"
|
||||
else:
|
||||
result_msg += f" (checked all {len(processed_artist_ids)} artists from recent albums)"
|
||||
|
||||
logger.info(result_msg)
|
||||
return artists_to_process
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in smart incremental update: {e}")
|
||||
# Fallback to empty list - user can try full refresh
|
||||
return []
|
||||
|
||||
def _artist_is_already_current(self, plex_artist) -> bool:
|
||||
"""Check if an artist is already current in our database"""
|
||||
try:
|
||||
artist_id = int(plex_artist.ratingKey)
|
||||
|
||||
# Get artist from database
|
||||
db_artist = self.database.get_artist(artist_id)
|
||||
|
||||
if not db_artist:
|
||||
# Not in database at all
|
||||
return False
|
||||
|
||||
# Check if artist was updated recently (within last 24 hours)
|
||||
if db_artist.updated_at:
|
||||
from datetime import datetime, timedelta
|
||||
hours_since_update = (datetime.now() - db_artist.updated_at).total_seconds() / 3600
|
||||
|
||||
# Consider "current" if updated within last 24 hours
|
||||
if hours_since_update < 24:
|
||||
logger.debug(f"Artist '{plex_artist.title}' is current (updated {hours_since_update:.1f} hours ago)")
|
||||
return True
|
||||
|
||||
# Artist exists but hasn't been updated recently
|
||||
logger.debug(f"Artist '{plex_artist.title}' exists but needs refresh")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error checking if artist is current: {e}")
|
||||
# When in doubt, process the artist
|
||||
return False
|
||||
|
||||
def _process_all_artists(self, artists: List):
|
||||
"""Process all artists and their albums/tracks using thread pool"""
|
||||
total_artists = len(artists)
|
||||
|
||||
def process_single_artist(artist):
|
||||
"""Process a single artist and return results"""
|
||||
if self.should_stop:
|
||||
return None
|
||||
|
||||
try:
|
||||
artist_name = getattr(artist, 'title', 'Unknown Artist')
|
||||
|
||||
# Update progress
|
||||
with self.thread_lock:
|
||||
self.processed_artists += 1
|
||||
progress_percent = (self.processed_artists / total_artists) * 100
|
||||
|
||||
self.progress_updated.emit(
|
||||
f"Processing {artist_name}",
|
||||
self.processed_artists,
|
||||
total_artists,
|
||||
progress_percent
|
||||
)
|
||||
|
||||
# Process the artist
|
||||
success, details, album_count, track_count = self._process_artist_with_content(artist)
|
||||
|
||||
# Track statistics
|
||||
with self.thread_lock:
|
||||
if success:
|
||||
self.successful_operations += 1
|
||||
else:
|
||||
self.failed_operations += 1
|
||||
|
||||
self.processed_albums += album_count
|
||||
self.processed_tracks += track_count
|
||||
|
||||
return (artist_name, success, details, album_count, track_count)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing artist {getattr(artist, 'title', 'Unknown')}: {e}")
|
||||
return (getattr(artist, 'title', 'Unknown'), False, f"Error: {str(e)}", 0, 0)
|
||||
|
||||
# Process artists in parallel using ThreadPoolExecutor
|
||||
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
|
||||
# Submit all tasks
|
||||
future_to_artist = {executor.submit(process_single_artist, artist): artist
|
||||
for artist in artists}
|
||||
|
||||
# Process completed tasks as they finish
|
||||
for future in as_completed(future_to_artist):
|
||||
if self.should_stop:
|
||||
break
|
||||
|
||||
result = future.result()
|
||||
if result is None: # Task was cancelled
|
||||
continue
|
||||
|
||||
artist_name, success, details, album_count, track_count = result
|
||||
|
||||
# Emit progress signal
|
||||
self.artist_processed.emit(artist_name, success, details, album_count, track_count)
|
||||
|
||||
def _process_artist_with_content(self, plex_artist) -> tuple[bool, str, int, int]:
|
||||
"""Process an artist and all their albums and tracks"""
|
||||
try:
|
||||
artist_name = getattr(plex_artist, 'title', 'Unknown Artist')
|
||||
|
||||
# 1. Insert/update the artist
|
||||
artist_success = self.database.insert_or_update_artist(plex_artist)
|
||||
if not artist_success:
|
||||
return False, "Failed to update artist data", 0, 0
|
||||
|
||||
artist_id = int(plex_artist.ratingKey)
|
||||
|
||||
# 2. Get all albums for this artist
|
||||
try:
|
||||
albums = list(plex_artist.albums())
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not get albums for artist '{artist_name}': {e}")
|
||||
return True, "Artist updated (no albums accessible)", 0, 0
|
||||
|
||||
album_count = 0
|
||||
track_count = 0
|
||||
|
||||
# 3. Process each album
|
||||
for album in albums:
|
||||
if self.should_stop:
|
||||
break
|
||||
|
||||
try:
|
||||
# Insert/update album
|
||||
album_success = self.database.insert_or_update_album(album, artist_id)
|
||||
if album_success:
|
||||
album_count += 1
|
||||
album_id = int(album.ratingKey)
|
||||
|
||||
# 4. Process tracks in this album
|
||||
try:
|
||||
tracks = list(album.tracks())
|
||||
|
||||
for track in tracks:
|
||||
if self.should_stop:
|
||||
break
|
||||
|
||||
try:
|
||||
track_success = self.database.insert_or_update_track(track, album_id, artist_id)
|
||||
if track_success:
|
||||
track_count += 1
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to process track '{getattr(track, 'title', 'Unknown')}': {e}")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not get tracks for album '{getattr(album, 'title', 'Unknown')}': {e}")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to process album '{getattr(album, 'title', 'Unknown')}': {e}")
|
||||
|
||||
details = f"Updated with {album_count} albums, {track_count} tracks"
|
||||
return True, details, album_count, track_count
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing artist '{getattr(plex_artist, 'title', 'Unknown')}': {e}")
|
||||
return False, f"Processing error: {str(e)}", 0, 0
|
||||
|
||||
class DatabaseStatsWorker(QThread):
|
||||
"""Simple worker for getting database statistics without blocking UI"""
|
||||
|
||||
stats_updated = pyqtSignal(dict) # Database statistics
|
||||
|
||||
def __init__(self, database_path: str = "database/music_library.db"):
|
||||
super().__init__()
|
||||
self.database_path = database_path
|
||||
self.should_stop = False
|
||||
|
||||
def stop(self):
|
||||
"""Stop the worker"""
|
||||
self.should_stop = True
|
||||
|
||||
def run(self):
|
||||
"""Get database statistics"""
|
||||
try:
|
||||
if self.should_stop:
|
||||
return
|
||||
|
||||
database = get_database(self.database_path)
|
||||
if self.should_stop:
|
||||
return
|
||||
|
||||
stats = database.get_database_info()
|
||||
if not self.should_stop:
|
||||
self.stats_updated.emit(stats)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting database stats: {e}")
|
||||
if not self.should_stop:
|
||||
self.stats_updated.emit({
|
||||
'artists': 0,
|
||||
'albums': 0,
|
||||
'tracks': 0,
|
||||
'database_size_mb': 0.0,
|
||||
'last_update': None
|
||||
})
|
||||
39
database/__init__.py
Normal file
39
database/__init__.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
SoulSync Database Module
|
||||
|
||||
This module provides database functionality for storing and managing
|
||||
music library metadata from Plex. It includes:
|
||||
|
||||
- SQLite database management for artists, albums, and tracks
|
||||
- Singleton database access pattern
|
||||
- Data models for database entities
|
||||
- Search and query capabilities
|
||||
|
||||
Usage:
|
||||
from database import get_database
|
||||
|
||||
db = get_database()
|
||||
stats = db.get_statistics()
|
||||
"""
|
||||
|
||||
from .music_database import (
|
||||
MusicDatabase,
|
||||
DatabaseArtist,
|
||||
DatabaseAlbum,
|
||||
DatabaseTrack,
|
||||
get_database,
|
||||
close_database
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'MusicDatabase',
|
||||
'DatabaseArtist',
|
||||
'DatabaseAlbum',
|
||||
'DatabaseTrack',
|
||||
'get_database',
|
||||
'close_database'
|
||||
]
|
||||
|
||||
__version__ = '1.0.0'
|
||||
BIN
database/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
database/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
database/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
database/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
database/__pycache__/music_database.cpython-310.pyc
Normal file
BIN
database/__pycache__/music_database.cpython-310.pyc
Normal file
Binary file not shown.
BIN
database/__pycache__/music_database.cpython-312.pyc
Normal file
BIN
database/__pycache__/music_database.cpython-312.pyc
Normal file
Binary file not shown.
522
database/music_database.py
Normal file
522
database/music_database.py
Normal file
|
|
@ -0,0 +1,522 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import sqlite3
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
from datetime import datetime
|
||||
from typing import List, Optional, Dict, Any, Tuple
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from utils.logging_config import get_logger
|
||||
|
||||
logger = get_logger("music_database")
|
||||
|
||||
@dataclass
|
||||
class DatabaseArtist:
|
||||
id: int
|
||||
name: str
|
||||
thumb_url: Optional[str] = None
|
||||
genres: Optional[List[str]] = None
|
||||
summary: Optional[str] = None
|
||||
created_at: Optional[datetime] = None
|
||||
updated_at: Optional[datetime] = None
|
||||
|
||||
@dataclass
|
||||
class DatabaseAlbum:
|
||||
id: int
|
||||
artist_id: int
|
||||
title: str
|
||||
year: Optional[int] = None
|
||||
thumb_url: Optional[str] = None
|
||||
genres: Optional[List[str]] = None
|
||||
track_count: Optional[int] = None
|
||||
duration: Optional[int] = None
|
||||
created_at: Optional[datetime] = None
|
||||
updated_at: Optional[datetime] = None
|
||||
|
||||
@dataclass
|
||||
class DatabaseTrack:
|
||||
id: int
|
||||
album_id: int
|
||||
artist_id: int
|
||||
title: str
|
||||
track_number: Optional[int] = None
|
||||
duration: Optional[int] = None
|
||||
file_path: Optional[str] = None
|
||||
bitrate: Optional[int] = None
|
||||
created_at: Optional[datetime] = None
|
||||
updated_at: Optional[datetime] = None
|
||||
|
||||
class MusicDatabase:
|
||||
"""SQLite database manager for SoulSync music library data"""
|
||||
|
||||
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
|
||||
|
||||
def _initialize_database(self):
|
||||
"""Create database tables if they don't exist"""
|
||||
try:
|
||||
conn = self._get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Artists table
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS artists (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
thumb_url TEXT,
|
||||
genres TEXT, -- JSON array
|
||||
summary TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
|
||||
# Albums table
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS albums (
|
||||
id INTEGER PRIMARY KEY,
|
||||
artist_id INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
year INTEGER,
|
||||
thumb_url TEXT,
|
||||
genres TEXT, -- JSON array
|
||||
track_count INTEGER,
|
||||
duration INTEGER, -- milliseconds
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (artist_id) REFERENCES artists (id) ON DELETE CASCADE
|
||||
)
|
||||
""")
|
||||
|
||||
# Tracks table
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS tracks (
|
||||
id INTEGER PRIMARY KEY,
|
||||
album_id INTEGER NOT NULL,
|
||||
artist_id INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
track_number INTEGER,
|
||||
duration INTEGER, -- milliseconds
|
||||
file_path TEXT,
|
||||
bitrate INTEGER,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (album_id) REFERENCES albums (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (artist_id) REFERENCES artists (id) ON DELETE CASCADE
|
||||
)
|
||||
""")
|
||||
|
||||
# Create indexes for performance
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_albums_artist_id ON albums (artist_id)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_tracks_album_id ON tracks (album_id)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_tracks_artist_id ON tracks (artist_id)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_artists_name ON artists (name)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_albums_title ON albums (title)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_tracks_title ON tracks (title)")
|
||||
|
||||
conn.commit()
|
||||
logger.info("Database initialized successfully")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error initializing database: {e}")
|
||||
raise
|
||||
|
||||
def close(self):
|
||||
"""Close database connection"""
|
||||
if self._connection:
|
||||
self._connection.close()
|
||||
self._connection = None
|
||||
|
||||
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
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting database statistics: {e}")
|
||||
return {'artists': 0, 'albums': 0, 'tracks': 0}
|
||||
|
||||
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")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error clearing database: {e}")
|
||||
raise
|
||||
|
||||
# Artist operations
|
||||
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
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error inserting/updating artist {getattr(plex_artist, 'title', 'Unknown')}: {e}")
|
||||
return False
|
||||
|
||||
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
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting artist {artist_id}: {e}")
|
||||
return None
|
||||
|
||||
# Album operations
|
||||
def insert_or_update_album(self, plex_album, artist_id: int) -> bool:
|
||||
"""Insert or update album from Plex album object"""
|
||||
try:
|
||||
conn = self._get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
album_id = int(plex_album.ratingKey)
|
||||
title = plex_album.title
|
||||
year = getattr(plex_album, 'year', None)
|
||||
thumb_url = getattr(plex_album, 'thumb', None)
|
||||
|
||||
# Get track count and duration
|
||||
track_count = getattr(plex_album, 'leafCount', None)
|
||||
duration = getattr(plex_album, 'duration', None)
|
||||
|
||||
# Get genres
|
||||
genres = []
|
||||
if hasattr(plex_album, 'genres') and plex_album.genres:
|
||||
genres = [genre.tag if hasattr(genre, 'tag') else str(genre)
|
||||
for genre in plex_album.genres]
|
||||
|
||||
genres_json = json.dumps(genres) if genres else None
|
||||
|
||||
# Check if album exists
|
||||
cursor.execute("SELECT id FROM albums WHERE id = ?", (album_id,))
|
||||
exists = cursor.fetchone()
|
||||
|
||||
if exists:
|
||||
# Update existing album
|
||||
cursor.execute("""
|
||||
UPDATE albums
|
||||
SET artist_id = ?, title = ?, year = ?, thumb_url = ?, genres = ?,
|
||||
track_count = ?, duration = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
""", (artist_id, title, year, thumb_url, genres_json, track_count, duration, album_id))
|
||||
else:
|
||||
# Insert new album
|
||||
cursor.execute("""
|
||||
INSERT INTO albums (id, artist_id, title, year, thumb_url, genres, track_count, duration)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (album_id, artist_id, title, year, thumb_url, genres_json, track_count, duration))
|
||||
|
||||
conn.commit()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error inserting/updating album {getattr(plex_album, 'title', 'Unknown')}: {e}")
|
||||
return False
|
||||
|
||||
def get_albums_by_artist(self, artist_id: int) -> List[DatabaseAlbum]:
|
||||
"""Get all albums by artist ID"""
|
||||
try:
|
||||
conn = self._get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("SELECT * FROM albums WHERE artist_id = ? ORDER BY year, title", (artist_id,))
|
||||
rows = cursor.fetchall()
|
||||
|
||||
albums = []
|
||||
for row in rows:
|
||||
genres = json.loads(row['genres']) if row['genres'] else None
|
||||
albums.append(DatabaseAlbum(
|
||||
id=row['id'],
|
||||
artist_id=row['artist_id'],
|
||||
title=row['title'],
|
||||
year=row['year'],
|
||||
thumb_url=row['thumb_url'],
|
||||
genres=genres,
|
||||
track_count=row['track_count'],
|
||||
duration=row['duration'],
|
||||
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 albums
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting albums for artist {artist_id}: {e}")
|
||||
return []
|
||||
|
||||
# Track operations
|
||||
def insert_or_update_track(self, plex_track, album_id: int, artist_id: int) -> bool:
|
||||
"""Insert or update track from Plex track object"""
|
||||
try:
|
||||
conn = self._get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
track_id = int(plex_track.ratingKey)
|
||||
title = plex_track.title
|
||||
track_number = getattr(plex_track, 'trackNumber', None)
|
||||
duration = getattr(plex_track, 'duration', None)
|
||||
|
||||
# Get file path and media info
|
||||
file_path = None
|
||||
bitrate = None
|
||||
if hasattr(plex_track, 'media') and plex_track.media:
|
||||
media = plex_track.media[0] if plex_track.media else None
|
||||
if media:
|
||||
if hasattr(media, 'parts') and media.parts:
|
||||
part = media.parts[0]
|
||||
file_path = getattr(part, 'file', None)
|
||||
bitrate = getattr(media, 'bitrate', None)
|
||||
|
||||
# Check if track exists
|
||||
cursor.execute("SELECT id FROM tracks WHERE id = ?", (track_id,))
|
||||
exists = cursor.fetchone()
|
||||
|
||||
if exists:
|
||||
# Update existing track
|
||||
cursor.execute("""
|
||||
UPDATE tracks
|
||||
SET album_id = ?, artist_id = ?, title = ?, track_number = ?,
|
||||
duration = ?, file_path = ?, bitrate = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
""", (album_id, artist_id, title, track_number, duration, file_path, bitrate, track_id))
|
||||
else:
|
||||
# Insert new track
|
||||
cursor.execute("""
|
||||
INSERT INTO tracks (id, album_id, artist_id, title, track_number, duration, file_path, bitrate)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (track_id, album_id, artist_id, title, track_number, duration, file_path, bitrate))
|
||||
|
||||
conn.commit()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error inserting/updating track {getattr(plex_track, 'title', 'Unknown')}: {e}")
|
||||
return False
|
||||
|
||||
def get_tracks_by_album(self, album_id: int) -> List[DatabaseTrack]:
|
||||
"""Get all tracks by album ID"""
|
||||
try:
|
||||
conn = self._get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("SELECT * FROM tracks WHERE album_id = ? ORDER BY track_number, title", (album_id,))
|
||||
rows = cursor.fetchall()
|
||||
|
||||
tracks = []
|
||||
for row in rows:
|
||||
tracks.append(DatabaseTrack(
|
||||
id=row['id'],
|
||||
album_id=row['album_id'],
|
||||
artist_id=row['artist_id'],
|
||||
title=row['title'],
|
||||
track_number=row['track_number'],
|
||||
duration=row['duration'],
|
||||
file_path=row['file_path'],
|
||||
bitrate=row['bitrate'],
|
||||
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 tracks
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting tracks for album {album_id}: {e}")
|
||||
return []
|
||||
|
||||
def search_artists(self, query: str, limit: int = 50) -> List[DatabaseArtist]:
|
||||
"""Search artists by name"""
|
||||
try:
|
||||
conn = self._get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
SELECT * FROM artists
|
||||
WHERE name LIKE ?
|
||||
ORDER BY name
|
||||
LIMIT ?
|
||||
""", (f"%{query}%", limit))
|
||||
|
||||
rows = cursor.fetchall()
|
||||
|
||||
artists = []
|
||||
for row in rows:
|
||||
genres = json.loads(row['genres']) if row['genres'] else None
|
||||
artists.append(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 artists
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error searching artists with query '{query}': {e}")
|
||||
return []
|
||||
|
||||
def get_database_info(self) -> Dict[str, Any]:
|
||||
"""Get comprehensive database information"""
|
||||
try:
|
||||
stats = self.get_statistics()
|
||||
|
||||
# Get database file size
|
||||
db_size = self.database_path.stat().st_size if self.database_path.exists() else 0
|
||||
db_size_mb = db_size / (1024 * 1024)
|
||||
|
||||
# Get last update time (most recent updated_at timestamp)
|
||||
conn = self._get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
SELECT MAX(updated_at) as last_update
|
||||
FROM (
|
||||
SELECT updated_at FROM artists
|
||||
UNION ALL
|
||||
SELECT updated_at FROM albums
|
||||
UNION ALL
|
||||
SELECT updated_at FROM tracks
|
||||
)
|
||||
""")
|
||||
|
||||
result = cursor.fetchone()
|
||||
last_update = result['last_update'] if result and result['last_update'] else None
|
||||
|
||||
return {
|
||||
**stats,
|
||||
'database_size_mb': round(db_size_mb, 2),
|
||||
'database_path': str(self.database_path),
|
||||
'last_update': last_update
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting database info: {e}")
|
||||
return {
|
||||
'artists': 0,
|
||||
'albums': 0,
|
||||
'tracks': 0,
|
||||
'database_size_mb': 0.0,
|
||||
'database_path': str(self.database_path),
|
||||
'last_update': None
|
||||
}
|
||||
|
||||
# Thread-safe singleton pattern for database access
|
||||
_database_instances: Dict[int, MusicDatabase] = {} # Thread ID -> Database instance
|
||||
_database_lock = threading.Lock()
|
||||
|
||||
def get_database(database_path: str = "database/music_library.db") -> MusicDatabase:
|
||||
"""Get thread-local database instance"""
|
||||
thread_id = threading.get_ident()
|
||||
|
||||
with _database_lock:
|
||||
if thread_id not in _database_instances:
|
||||
_database_instances[thread_id] = MusicDatabase(database_path)
|
||||
return _database_instances[thread_id]
|
||||
|
||||
def close_database():
|
||||
"""Close database instances (safe to call from any thread)"""
|
||||
global _database_instances
|
||||
|
||||
with _database_lock:
|
||||
# Close all database instances
|
||||
for thread_id, db_instance in list(_database_instances.items()):
|
||||
try:
|
||||
db_instance.close()
|
||||
except Exception as e:
|
||||
# Ignore threading errors during shutdown
|
||||
pass
|
||||
_database_instances.clear()
|
||||
BIN
database/music_library.db
Normal file
BIN
database/music_library.db
Normal file
Binary file not shown.
75
logs/app.log
75
logs/app.log
|
|
@ -8846,3 +8846,78 @@
|
|||
2025-08-05 15:51:19 - newmusic.main - [92mINFO[0m - closeEvent:321 - Stopping search maintenance timer...
|
||||
2025-08-05 15:51:19 - newmusic.main - [92mINFO[0m - closeEvent:326 - Closing Soulseek client...
|
||||
2025-08-05 15:51:19 - newmusic.main - [92mINFO[0m - closeEvent:332 - Application closed successfully
|
||||
2025-08-05 22:16:07 - newmusic - [92mINFO[0m - setup_logging:57 - Logging initialized with level: INFO
|
||||
2025-08-05 22:16:07 - newmusic.main - [92mINFO[0m - main:346 - Starting Soulsync application
|
||||
2025-08-05 22:16:07 - newmusic.spotify_client - [92mINFO[0m - _setup_client:179 - Spotify client initialized (user info will be fetched when needed)
|
||||
2025-08-05 22:16:07 - newmusic.soulseek_client - [92mINFO[0m - _setup_client:220 - Soulseek client configured with slskd at http://localhost:5030
|
||||
2025-08-05 22:16:07 - newmusic.spotify_client - [92mINFO[0m - _setup_client:179 - Spotify client initialized (user info will be fetched when needed)
|
||||
2025-08-05 22:16:08 - newmusic.spotify_client - [92mINFO[0m - _setup_client:179 - Spotify client initialized (user info will be fetched when needed)
|
||||
2025-08-05 22:16:08 - newmusic.soulseek_client - [92mINFO[0m - _setup_client:220 - Soulseek client configured with slskd at http://localhost:5030
|
||||
2025-08-05 22:16:08 - newmusic.main - [92mINFO[0m - change_page:289 - Changed to page: dashboard
|
||||
2025-08-05 22:16:08 - newmusic.main - [92mINFO[0m - setup_media_player_connections:241 - Media player connections established between sidebar and downloads page
|
||||
2025-08-05 22:16:08 - newmusic.main - [92mINFO[0m - setup_settings_connections:246 - Settings change connections established
|
||||
2025-08-05 22:16:08 - newmusic.main - [92mINFO[0m - setup_search_maintenance:92 - Search maintenance timer started (every 2 minutes, keeps 200 most recent searches)
|
||||
2025-08-05 22:16:08 - newmusic.plex_client - [92mINFO[0m - _find_music_library:129 - Found music library: Music
|
||||
2025-08-05 22:16:08 - newmusic.plex_client - [92mINFO[0m - _setup_client:115 - Successfully connected to Plex server: PLEX-MACHINE
|
||||
2025-08-05 22:18:08 - newmusic.soulseek_client - [92mINFO[0m - get_all_searches:949 - Retrieved 202 searches from slskd
|
||||
2025-08-05 22:18:08 - newmusic.soulseek_client - [92mINFO[0m - maintain_search_history:1057 - Maintaining search history: deleting 2 oldest searches (keeping 200)
|
||||
2025-08-05 22:18:08 - newmusic.soulseek_client - [92mINFO[0m - maintain_search_history:1075 - Search maintenance complete: 2 deleted, 0 failed
|
||||
2025-08-05 22:20:08 - newmusic.soulseek_client - [92mINFO[0m - get_all_searches:949 - Retrieved 200 searches from slskd
|
||||
2025-08-05 22:22:08 - newmusic.soulseek_client - [92mINFO[0m - get_all_searches:949 - Retrieved 200 searches from slskd
|
||||
2025-08-05 22:24:08 - newmusic.soulseek_client - [92mINFO[0m - get_all_searches:949 - Retrieved 200 searches from slskd
|
||||
2025-08-05 22:26:08 - newmusic.soulseek_client - [92mINFO[0m - get_all_searches:949 - Retrieved 200 searches from slskd
|
||||
2025-08-05 22:26:51 - newmusic.main - [92mINFO[0m - closeEvent:306 - Closing application...
|
||||
2025-08-05 22:26:51 - newmusic.main - [92mINFO[0m - closeEvent:311 - Cleaning up Downloads page threads...
|
||||
2025-08-05 22:26:51 - newmusic.main - [92mINFO[0m - closeEvent:316 - Stopping status monitoring thread...
|
||||
2025-08-05 22:26:58 - newmusic.main - [92mINFO[0m - closeEvent:321 - Stopping search maintenance timer...
|
||||
2025-08-05 22:26:58 - newmusic.main - [92mINFO[0m - closeEvent:326 - Closing Soulseek client...
|
||||
2025-08-05 22:26:58 - newmusic.main - [92mINFO[0m - closeEvent:332 - Application closed successfully
|
||||
2025-08-05 22:35:21 - newmusic - [92mINFO[0m - setup_logging:57 - Logging initialized with level: INFO
|
||||
2025-08-05 22:35:21 - newmusic.main - [92mINFO[0m - main:346 - Starting Soulsync application
|
||||
2025-08-05 22:35:21 - newmusic.spotify_client - [92mINFO[0m - _setup_client:179 - Spotify client initialized (user info will be fetched when needed)
|
||||
2025-08-05 22:35:21 - newmusic.soulseek_client - [92mINFO[0m - _setup_client:220 - Soulseek client configured with slskd at http://localhost:5030
|
||||
2025-08-05 22:35:36 - newmusic - [92mINFO[0m - setup_logging:57 - Logging initialized with level: INFO
|
||||
2025-08-05 22:35:36 - newmusic.main - [92mINFO[0m - main:346 - Starting Soulsync application
|
||||
2025-08-05 22:35:36 - newmusic.spotify_client - [92mINFO[0m - _setup_client:179 - Spotify client initialized (user info will be fetched when needed)
|
||||
2025-08-05 22:35:36 - newmusic.soulseek_client - [92mINFO[0m - _setup_client:220 - Soulseek client configured with slskd at http://localhost:5030
|
||||
2025-08-05 22:36:15 - newmusic - [92mINFO[0m - setup_logging:57 - Logging initialized with level: INFO
|
||||
2025-08-05 22:36:15 - newmusic.main - [92mINFO[0m - main:359 - Starting Soulsync application
|
||||
2025-08-05 22:36:15 - newmusic.spotify_client - [92mINFO[0m - _setup_client:179 - Spotify client initialized (user info will be fetched when needed)
|
||||
2025-08-05 22:36:15 - newmusic.soulseek_client - [92mINFO[0m - _setup_client:220 - Soulseek client configured with slskd at http://localhost:5030
|
||||
2025-08-05 22:36:59 - newmusic - [92mINFO[0m - setup_logging:57 - Logging initialized with level: INFO
|
||||
2025-08-05 22:36:59 - newmusic.main - [92mINFO[0m - main:359 - Starting Soulsync application
|
||||
2025-08-05 22:36:59 - newmusic.spotify_client - [92mINFO[0m - _setup_client:179 - Spotify client initialized (user info will be fetched when needed)
|
||||
2025-08-05 22:36:59 - newmusic.soulseek_client - [92mINFO[0m - _setup_client:220 - Soulseek client configured with slskd at http://localhost:5030
|
||||
2025-08-05 22:37:00 - newmusic.spotify_client - [92mINFO[0m - _setup_client:179 - Spotify client initialized (user info will be fetched when needed)
|
||||
2025-08-05 22:37:00 - newmusic.spotify_client - [92mINFO[0m - _setup_client:179 - Spotify client initialized (user info will be fetched when needed)
|
||||
2025-08-05 22:37:00 - newmusic.soulseek_client - [92mINFO[0m - _setup_client:220 - Soulseek client configured with slskd at http://localhost:5030
|
||||
2025-08-05 22:37:00 - newmusic.main - [92mINFO[0m - change_page:289 - Changed to page: dashboard
|
||||
2025-08-05 22:37:00 - newmusic.main - [92mINFO[0m - setup_media_player_connections:241 - Media player connections established between sidebar and downloads page
|
||||
2025-08-05 22:37:00 - newmusic.main - [92mINFO[0m - setup_settings_connections:246 - Settings change connections established
|
||||
2025-08-05 22:37:00 - newmusic.main - [92mINFO[0m - setup_search_maintenance:92 - Search maintenance timer started (every 2 minutes, keeps 200 most recent searches)
|
||||
2025-08-05 22:37:00 - newmusic.plex_client - [92mINFO[0m - _find_music_library:129 - Found music library: Music
|
||||
2025-08-05 22:37:00 - newmusic.plex_client - [92mINFO[0m - _setup_client:115 - Successfully connected to Plex server: PLEX-MACHINE
|
||||
2025-08-05 22:37:59 - newmusic - [92mINFO[0m - setup_logging:57 - Logging initialized with level: INFO
|
||||
2025-08-05 22:37:59 - newmusic.main - [92mINFO[0m - main:359 - Starting Soulsync application
|
||||
2025-08-05 22:37:59 - newmusic.spotify_client - [92mINFO[0m - _setup_client:179 - Spotify client initialized (user info will be fetched when needed)
|
||||
2025-08-05 22:37:59 - newmusic.soulseek_client - [92mINFO[0m - _setup_client:220 - Soulseek client configured with slskd at http://localhost:5030
|
||||
2025-08-05 22:37:59 - newmusic.spotify_client - [92mINFO[0m - _setup_client:179 - Spotify client initialized (user info will be fetched when needed)
|
||||
2025-08-05 22:38:00 - newmusic.spotify_client - [92mINFO[0m - _setup_client:179 - Spotify client initialized (user info will be fetched when needed)
|
||||
2025-08-05 22:38:00 - newmusic.soulseek_client - [92mINFO[0m - _setup_client:220 - Soulseek client configured with slskd at http://localhost:5030
|
||||
2025-08-05 22:38:00 - newmusic.main - [92mINFO[0m - change_page:289 - Changed to page: dashboard
|
||||
2025-08-05 22:38:00 - newmusic.main - [92mINFO[0m - setup_media_player_connections:241 - Media player connections established between sidebar and downloads page
|
||||
2025-08-05 22:38:00 - newmusic.main - [92mINFO[0m - setup_settings_connections:246 - Settings change connections established
|
||||
2025-08-05 22:38:00 - newmusic.main - [92mINFO[0m - setup_search_maintenance:92 - Search maintenance timer started (every 2 minutes, keeps 200 most recent searches)
|
||||
2025-08-05 22:38:00 - newmusic.plex_client - [92mINFO[0m - _find_music_library:129 - Found music library: Music
|
||||
2025-08-05 22:38:00 - newmusic.plex_client - [92mINFO[0m - _setup_client:115 - Successfully connected to Plex server: PLEX-MACHINE
|
||||
2025-08-05 22:38:00 - newmusic.music_database - [92mINFO[0m - _initialize_database:133 - Database initialized successfully
|
||||
2025-08-05 22:40:00 - newmusic.soulseek_client - [92mINFO[0m - get_all_searches:949 - Retrieved 200 searches from slskd
|
||||
2025-08-05 22:41:09 - newmusic.main - [92mINFO[0m - closeEvent:306 - Closing application...
|
||||
2025-08-05 22:41:09 - newmusic.main - [92mINFO[0m - closeEvent:311 - Cleaning up Downloads page threads...
|
||||
2025-08-05 22:41:09 - newmusic.main - [92mINFO[0m - closeEvent:316 - Cleaning up Dashboard page threads...
|
||||
2025-08-05 22:41:09 - newmusic.main - [92mINFO[0m - closeEvent:321 - Stopping status monitoring thread...
|
||||
2025-08-05 22:41:10 - newmusic.main - [92mINFO[0m - closeEvent:326 - Stopping search maintenance timer...
|
||||
2025-08-05 22:41:10 - newmusic.main - [92mINFO[0m - closeEvent:331 - Closing Soulseek client...
|
||||
2025-08-05 22:41:10 - newmusic.main - [92mINFO[0m - closeEvent:339 - Closing database connection...
|
||||
2025-08-05 22:41:10 - newmusic.main - [91mERROR[0m - closeEvent:343 - Error closing database: SQLite objects created in a thread can only be used in that same thread. The object was created in thread id 24524 and this is thread id 92856.
|
||||
2025-08-05 22:41:10 - newmusic.main - [92mINFO[0m - closeEvent:345 - Application closed successfully
|
||||
|
|
|
|||
13
main.py
13
main.py
|
|
@ -311,6 +311,11 @@ class MainWindow(QMainWindow):
|
|||
logger.info("Cleaning up Downloads page threads...")
|
||||
self.downloads_page.cleanup_all_threads()
|
||||
|
||||
# Stop dashboard threads
|
||||
if hasattr(self, 'dashboard_page') and self.dashboard_page:
|
||||
logger.info("Cleaning up Dashboard page threads...")
|
||||
self.dashboard_page.cleanup_threads()
|
||||
|
||||
# Stop status monitoring thread
|
||||
if self.status_thread:
|
||||
logger.info("Stopping status monitoring thread...")
|
||||
|
|
@ -329,6 +334,14 @@ class MainWindow(QMainWindow):
|
|||
except Exception as e:
|
||||
logger.error(f"Error closing Soulseek client: {e}")
|
||||
|
||||
# Close database connection
|
||||
try:
|
||||
logger.info("Closing database connection...")
|
||||
from database import close_database
|
||||
close_database()
|
||||
except Exception as e:
|
||||
logger.error(f"Error closing database: {e}")
|
||||
|
||||
logger.info("Application closed successfully")
|
||||
event.accept()
|
||||
|
||||
|
|
|
|||
Binary file not shown.
315
ui/components/database_updater_widget.py
Normal file
315
ui/components/database_updater_widget.py
Normal file
|
|
@ -0,0 +1,315 @@
|
|||
#!/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
|
||||
|
||||
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
|
||||
info_label = QLabel("Syncs your Plex 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)
|
||||
|
||||
# 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.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)
|
||||
BIN
ui/pages/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
ui/pages/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
ui/pages/__pycache__/dashboard.cpython-310.pyc
Normal file
BIN
ui/pages/__pycache__/dashboard.cpython-310.pyc
Normal file
Binary file not shown.
Binary file not shown.
|
|
@ -20,6 +20,8 @@ import requests
|
|||
from PIL import Image
|
||||
import io
|
||||
from core.matching_engine import MusicMatchingEngine
|
||||
from ui.components.database_updater_widget import DatabaseUpdaterWidget
|
||||
from core.database_update_worker import DatabaseUpdateWorker, DatabaseStatsWorker
|
||||
|
||||
class MetadataUpdateWorker(QThread):
|
||||
"""Worker thread for updating Plex artist metadata using Spotify data"""
|
||||
|
|
@ -1205,6 +1207,12 @@ class DashboardPage(QWidget):
|
|||
self.stats_cards = {}
|
||||
|
||||
self.setup_ui()
|
||||
|
||||
# Initialize list to track active stats workers
|
||||
self._active_stats_workers = []
|
||||
|
||||
# Load initial database statistics (with delay to avoid startup issues)
|
||||
QTimer.singleShot(1000, self.refresh_database_statistics)
|
||||
|
||||
def set_service_clients(self, spotify_client, plex_client, soulseek_client):
|
||||
"""Called from main window to provide service client references"""
|
||||
|
|
@ -1383,11 +1391,16 @@ class DashboardPage(QWidget):
|
|||
header_label.setFont(QFont("Arial", 16, QFont.Weight.Bold))
|
||||
header_label.setStyleSheet("color: #ffffff;")
|
||||
|
||||
# Metadata updater widget
|
||||
# Database updater widget (FIRST)
|
||||
self.database_widget = DatabaseUpdaterWidget()
|
||||
self.database_widget.start_button.clicked.connect(self.toggle_database_update)
|
||||
|
||||
# Metadata updater widget (SECOND)
|
||||
self.metadata_widget = MetadataUpdaterWidget()
|
||||
self.metadata_widget.start_button.clicked.connect(self.toggle_metadata_update)
|
||||
|
||||
layout.addWidget(header_label)
|
||||
layout.addWidget(self.database_widget)
|
||||
layout.addWidget(self.metadata_widget)
|
||||
|
||||
return section
|
||||
|
|
@ -1451,6 +1464,154 @@ class DashboardPage(QWidget):
|
|||
# Start test
|
||||
self.data_provider.test_service_connection(service)
|
||||
|
||||
def toggle_database_update(self):
|
||||
"""Toggle database update process"""
|
||||
current_text = self.database_widget.start_button.text()
|
||||
if "Update Database" in current_text:
|
||||
# Start database update
|
||||
self.start_database_update()
|
||||
else:
|
||||
# Stop database update
|
||||
self.stop_database_update()
|
||||
|
||||
def start_database_update(self):
|
||||
"""Start the SoulSync database update process"""
|
||||
if not hasattr(self, 'data_provider') or not self.data_provider.service_clients.get('plex'):
|
||||
self.add_activity_item("❌", "Database Update", "Plex client not available", "Now")
|
||||
return
|
||||
|
||||
try:
|
||||
# Get update type from dropdown
|
||||
full_refresh = self.database_widget.is_full_refresh()
|
||||
|
||||
# Start the database update worker
|
||||
self.database_worker = DatabaseUpdateWorker(
|
||||
self.data_provider.service_clients['plex'],
|
||||
"database/music_library.db",
|
||||
full_refresh
|
||||
)
|
||||
|
||||
# Connect signals
|
||||
self.database_worker.progress_updated.connect(self.on_database_progress)
|
||||
self.database_worker.artist_processed.connect(self.on_database_artist_processed)
|
||||
self.database_worker.finished.connect(self.on_database_finished)
|
||||
self.database_worker.error.connect(self.on_database_error)
|
||||
self.database_worker.phase_changed.connect(self.on_database_phase_changed)
|
||||
|
||||
# Update UI and start
|
||||
self.database_widget.update_progress(True, "Initializing...", 0, 0, 0.0)
|
||||
update_type = "Full refresh" if full_refresh else "Incremental update"
|
||||
self.add_activity_item("🗄️", "Database Update", f"Starting {update_type.lower()}...", "Now")
|
||||
|
||||
self.database_worker.start()
|
||||
|
||||
# Start a timer to refresh database statistics during update
|
||||
self.start_database_stats_refresh()
|
||||
|
||||
except Exception as e:
|
||||
self.add_activity_item("❌", "Database Update", f"Failed to start: {str(e)}", "Now")
|
||||
|
||||
def stop_database_update(self):
|
||||
"""Stop the database update process"""
|
||||
if hasattr(self, 'database_worker') and self.database_worker.isRunning():
|
||||
self.database_worker.stop()
|
||||
self.database_worker.wait(3000) # Wait up to 3 seconds
|
||||
if self.database_worker.isRunning():
|
||||
self.database_worker.terminate()
|
||||
|
||||
self.database_widget.update_progress(False, "", 0, 0, 0.0)
|
||||
self.add_activity_item("⏹️", "Database Update", "Stopped database update process", "Now")
|
||||
|
||||
# Stop statistics refresh timer
|
||||
self.stop_database_stats_refresh()
|
||||
|
||||
def on_database_progress(self, current_item: str, processed: int, total: int, percentage: float):
|
||||
"""Handle database update progress"""
|
||||
self.database_widget.update_progress(True, current_item, processed, total, percentage)
|
||||
|
||||
def on_database_artist_processed(self, artist_name: str, success: bool, details: str, album_count: int, track_count: int):
|
||||
"""Handle individual artist processing completion"""
|
||||
if success:
|
||||
self.add_activity_item("✅", "Artist Processed", f"'{artist_name}' - {details}", "Now")
|
||||
else:
|
||||
self.add_activity_item("❌", "Artist Failed", f"'{artist_name}' - {details}", "Now")
|
||||
|
||||
def on_database_finished(self, total_artists: int, total_albums: int, total_tracks: int, successful: int, failed: int):
|
||||
"""Handle database update completion"""
|
||||
self.database_widget.update_progress(False, "", 0, 0, 0.0)
|
||||
summary = f"Processed {total_artists} artists, {total_albums} albums, {total_tracks} tracks"
|
||||
self.add_activity_item("🗄️", "Database Complete", summary, "Now")
|
||||
|
||||
# Stop statistics refresh timer and do final update
|
||||
self.stop_database_stats_refresh()
|
||||
self.refresh_database_statistics()
|
||||
|
||||
def on_database_error(self, error_message: str):
|
||||
"""Handle database update error"""
|
||||
self.database_widget.update_progress(False, "", 0, 0, 0.0)
|
||||
self.add_activity_item("❌", "Database Error", error_message, "Now")
|
||||
|
||||
# Stop statistics refresh timer
|
||||
self.stop_database_stats_refresh()
|
||||
|
||||
def on_database_phase_changed(self, phase: str):
|
||||
"""Handle database update phase changes"""
|
||||
self.database_widget.update_phase(phase)
|
||||
|
||||
def start_database_stats_refresh(self):
|
||||
"""Start periodic database statistics refresh during update"""
|
||||
# Create timer to refresh stats every 5 seconds during update
|
||||
if not hasattr(self, 'database_stats_timer'):
|
||||
self.database_stats_timer = QTimer()
|
||||
self.database_stats_timer.timeout.connect(self.refresh_database_statistics)
|
||||
|
||||
self.database_stats_timer.start(5000) # Every 5 seconds
|
||||
|
||||
def stop_database_stats_refresh(self):
|
||||
"""Stop periodic database statistics refresh"""
|
||||
if hasattr(self, 'database_stats_timer'):
|
||||
self.database_stats_timer.stop()
|
||||
|
||||
def refresh_database_statistics(self):
|
||||
"""Refresh database statistics display"""
|
||||
try:
|
||||
# Check if database widget exists first
|
||||
if not hasattr(self, 'database_widget') or self.database_widget is None:
|
||||
return
|
||||
|
||||
# Get statistics in background thread to avoid blocking UI
|
||||
stats_worker = DatabaseStatsWorker("database/music_library.db")
|
||||
|
||||
# Track the worker for cleanup
|
||||
if not hasattr(self, '_active_stats_workers'):
|
||||
self._active_stats_workers = []
|
||||
self._active_stats_workers.append(stats_worker)
|
||||
|
||||
# Connect signals
|
||||
stats_worker.stats_updated.connect(self.database_widget.update_statistics)
|
||||
stats_worker.finished.connect(lambda: self._cleanup_stats_worker(stats_worker))
|
||||
|
||||
stats_worker.start()
|
||||
except Exception as e:
|
||||
logger.error(f"Error refreshing database statistics: {e}")
|
||||
# Fallback to default stats to prevent crashes
|
||||
if hasattr(self, 'database_widget') and self.database_widget:
|
||||
self.database_widget.update_statistics({
|
||||
'artists': 0,
|
||||
'albums': 0,
|
||||
'tracks': 0,
|
||||
'database_size_mb': 0.0
|
||||
})
|
||||
|
||||
def _cleanup_stats_worker(self, worker):
|
||||
"""Clean up a finished stats worker"""
|
||||
try:
|
||||
if hasattr(self, '_active_stats_workers') and worker in self._active_stats_workers:
|
||||
self._active_stats_workers.remove(worker)
|
||||
worker.deleteLater()
|
||||
except Exception as e:
|
||||
logger.error(f"Error cleaning up stats worker: {e}")
|
||||
|
||||
def toggle_metadata_update(self):
|
||||
"""Toggle metadata update process"""
|
||||
current_text = self.metadata_widget.start_button.text()
|
||||
|
|
@ -1719,4 +1880,28 @@ class DashboardPage(QWidget):
|
|||
if hasattr(self.data_provider, 'download_stats_timer'):
|
||||
self.data_provider.download_stats_timer.stop()
|
||||
if hasattr(self.data_provider, 'system_stats_timer'):
|
||||
self.data_provider.system_stats_timer.stop()
|
||||
self.data_provider.system_stats_timer.stop()
|
||||
|
||||
# Clean up database-related threads and timers
|
||||
if hasattr(self, 'database_worker') and self.database_worker.isRunning():
|
||||
self.database_worker.stop()
|
||||
self.database_worker.wait(1000)
|
||||
self.database_worker.deleteLater()
|
||||
|
||||
if hasattr(self, 'database_stats_timer'):
|
||||
self.database_stats_timer.stop()
|
||||
|
||||
# Clean up any running stats workers (keep track of them)
|
||||
if hasattr(self, '_active_stats_workers'):
|
||||
for worker in self._active_stats_workers:
|
||||
if worker.isRunning():
|
||||
worker.stop()
|
||||
worker.wait(1000)
|
||||
worker.deleteLater()
|
||||
self._active_stats_workers.clear()
|
||||
|
||||
# Clean up metadata worker as well
|
||||
if hasattr(self, 'metadata_worker') and self.metadata_worker.isRunning():
|
||||
self.metadata_worker.stop()
|
||||
self.metadata_worker.wait(1000)
|
||||
self.metadata_worker.deleteLater()
|
||||
BIN
utils/__pycache__/logging_config.cpython-310.pyc
Normal file
BIN
utils/__pycache__/logging_config.cpython-310.pyc
Normal file
Binary file not shown.
Loading…
Reference in a new issue