#!/usr/bin/env python3 import sys import asyncio import time from pathlib import Path from PyQt6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QHBoxLayout, QWidget, QStackedWidget from PyQt6.QtCore import QThread, pyqtSignal, QTimer from PyQt6.QtGui import QFont, QPalette, QColor from config.settings import config_manager from utils.logging_config import setup_logging, get_logger from core.spotify_client import SpotifyClient from core.plex_client import PlexClient from core.soulseek_client import SoulseekClient from ui.sidebar import ModernSidebar from ui.pages.dashboard import DashboardPage from ui.pages.sync import SyncPage from ui.pages.downloads import DownloadsPage from ui.pages.artists import ArtistsPage from ui.pages.settings import SettingsPage logger = get_logger("main") class ServiceStatusThread(QThread): status_updated = pyqtSignal(str, bool) def __init__(self, spotify_client, plex_client, soulseek_client): super().__init__() self.spotify_client = spotify_client self.plex_client = plex_client self.soulseek_client = soulseek_client self.running = True def run(self): while self.running: try: # Check Spotify authentication spotify_status = self.spotify_client.is_authenticated() self.status_updated.emit("spotify", spotify_status) # Check Plex connection plex_status = self.plex_client.is_connected() self.status_updated.emit("plex", plex_status) # Check Soulseek connection (simplified check to avoid event loop issues) soulseek_status = self.soulseek_client.is_configured() self.status_updated.emit("soulseek", soulseek_status) self.msleep(3000) # Check every 3 seconds except Exception as e: logger.error(f"Error checking service status: {e}") self.msleep(5000) def stop(self): self.running = False self.quit() self.wait() class MainWindow(QMainWindow): def __init__(self): super().__init__() # Track application start time for uptime calculation self.app_start_time = time.time() self.spotify_client = SpotifyClient() self.plex_client = PlexClient() self.soulseek_client = SoulseekClient() self.status_thread = None self.init_ui() self.setup_status_monitoring() # Setup periodic search maintenance (rolling 50-search window) self.setup_search_maintenance() def setup_search_maintenance(self): """Setup periodic search history maintenance to keep only the 50 most recent searches""" try: # Create timer for periodic search maintenance self.search_maintenance_timer = QTimer() self.search_maintenance_timer.timeout.connect(self._run_search_maintenance) # Run maintenance every 2 minutes (120 seconds) # This keeps search history clean without being too frequent self.search_maintenance_timer.start(120000) logger.info("Search maintenance timer started (every 2 minutes, keeps 200 most recent searches)") except Exception as e: logger.error(f"Error setting up search maintenance: {e}") def _run_search_maintenance(self): """Run search maintenance in background thread to avoid blocking UI""" try: # Only run if Soulseek client seems to be available if hasattr(self.soulseek_client, 'base_url') and self.soulseek_client.base_url: # Run maintenance in background thread import threading def maintenance_thread(): try: import asyncio loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) # Run the maintenance (keep 200 most recent searches) success = loop.run_until_complete(self.soulseek_client.maintain_search_history(200)) if not success: logger.warning("Search maintenance completed with some failures") except Exception as e: logger.error(f"Error in search maintenance thread: {e}") finally: loop.close() thread = threading.Thread(target=maintenance_thread, daemon=True) thread.start() else: logger.debug("Soulseek client not configured, skipping search maintenance") except Exception as e: logger.error(f"Error running search maintenance: {e}") def init_ui(self): self.setWindowTitle("NewMusic - Music Sync & Manager") self.setGeometry(100, 100, 1400, 900) # Set dark theme palette self.setStyleSheet(""" QMainWindow { background: #121212; } """) # Create central widget central_widget = QWidget() self.setCentralWidget(central_widget) # Main layout main_layout = QHBoxLayout(central_widget) main_layout.setContentsMargins(0, 0, 0, 0) main_layout.setSpacing(0) # Create sidebar self.sidebar = ModernSidebar() self.sidebar.page_changed.connect(self.change_page) main_layout.addWidget(self.sidebar) # Create stacked widget for pages self.stacked_widget = QStackedWidget() # Create and add pages self.dashboard_page = DashboardPage() self.downloads_page = DownloadsPage(self.soulseek_client) self.sync_page = SyncPage(self.spotify_client, self.plex_client, self.soulseek_client, self.downloads_page) self.artists_page = ArtistsPage(downloads_page=self.downloads_page) self.settings_page = SettingsPage() # Configure dashboard with service clients and page references self.dashboard_page.set_service_clients(self.spotify_client, self.plex_client, self.soulseek_client) self.dashboard_page.set_page_references(self.downloads_page, self.sync_page) self.dashboard_page.set_app_start_time(self.app_start_time) # Connect download completion signal for session tracking self.downloads_page.download_session_completed.connect( self.dashboard_page.data_provider.increment_completed_downloads ) self.stacked_widget.addWidget(self.dashboard_page) self.stacked_widget.addWidget(self.sync_page) self.stacked_widget.addWidget(self.downloads_page) self.stacked_widget.addWidget(self.artists_page) self.stacked_widget.addWidget(self.settings_page) main_layout.addWidget(self.stacked_widget) # Set dashboard as default page self.change_page("dashboard") # Connect media player signals between sidebar and downloads page self.setup_media_player_connections() # Connect settings change signals for live updates self.setup_settings_connections() def setup_status_monitoring(self): # Start status monitoring thread self.status_thread = ServiceStatusThread( self.spotify_client, self.plex_client, self.soulseek_client ) self.status_thread.status_updated.connect(self.update_service_status) self.status_thread.start() def setup_media_player_connections(self): """Connect signals between downloads page and sidebar media player""" # Connect downloads page signals to sidebar media player self.downloads_page.track_started.connect(self.sidebar.media_player.set_track_info) self.downloads_page.track_paused.connect(lambda: self.sidebar.media_player.set_playing_state(False)) self.downloads_page.track_resumed.connect(lambda: self.sidebar.media_player.set_playing_state(True)) self.downloads_page.track_stopped.connect(self.sidebar.media_player.clear_track) self.downloads_page.track_finished.connect(self.sidebar.media_player.clear_track) # Connect loading animation signals self.downloads_page.track_loading_started.connect(lambda result: self.sidebar.media_player.show_loading()) self.downloads_page.track_loading_finished.connect(lambda result: self.sidebar.media_player.hide_loading()) self.downloads_page.track_loading_progress.connect(lambda progress, result: self.sidebar.media_player.set_loading_progress(progress)) # Connect sidebar media player signals to downloads page self.sidebar.media_player.play_pause_requested.connect(self.downloads_page.handle_sidebar_play_pause) self.sidebar.media_player.stop_requested.connect(self.downloads_page.handle_sidebar_stop) self.sidebar.media_player.volume_changed.connect(self.downloads_page.handle_sidebar_volume) logger.info("Media player connections established between sidebar and downloads page") def setup_settings_connections(self): """Connect settings change signals for live updates across pages""" self.settings_page.settings_changed.connect(self.on_settings_changed) logger.info("Settings change connections established") def on_settings_changed(self, key: str, value: str): """Handle settings changes and broadcast to relevant pages""" # Reinitialize service clients when their settings change if key.startswith('spotify.'): try: self.spotify_client._setup_client() except Exception as e: logger.error("Failed to reinitialize Spotify client") elif key.startswith('plex.'): try: # Reset Plex connection to force reconnection with new settings self.plex_client.server = None self.plex_client.music_library = None self.plex_client._connection_attempted = False except Exception as e: logger.error("Failed to reset Plex client") elif key.startswith('soulseek.'): try: self.soulseek_client._setup_client() except Exception as e: logger.error("Failed to reinitialize Soulseek client") # Broadcast to all pages that need to know about path changes if hasattr(self.downloads_page, 'on_paths_updated'): self.downloads_page.on_paths_updated(key, value) if hasattr(self.artists_page, 'on_paths_updated'): self.artists_page.on_paths_updated(key, value) def change_page(self, page_id: str): page_map = { "dashboard": 0, "sync": 1, "downloads": 2, "artists": 3, "settings": 4 } if page_id in page_map: self.stacked_widget.setCurrentIndex(page_map[page_id]) logger.info(f"Changed to page: {page_id}") def update_service_status(self, service: str, connected: bool): self.sidebar.update_service_status(service, connected) # Update dashboard with service status if hasattr(self.dashboard_page, 'data_provider'): self.dashboard_page.data_provider.update_service_status(service, connected) # Force a refresh of the Spotify client if needed if service == "spotify" and not connected: try: self.spotify_client._setup_client() except Exception as e: logger.error(f"Error refreshing Spotify client: {e}") def closeEvent(self, event): logger.info("Closing application...") try: # Stop all page threads first if hasattr(self, 'downloads_page') and self.downloads_page: logger.info("Cleaning up Downloads page threads...") self.downloads_page.cleanup_all_threads() # Stop status monitoring thread if self.status_thread: logger.info("Stopping status monitoring thread...") self.status_thread.stop() # Stop search maintenance timer if hasattr(self, 'search_maintenance_timer') and self.search_maintenance_timer: logger.info("Stopping search maintenance timer...") self.search_maintenance_timer.stop() # Close Soulseek client try: logger.info("Closing Soulseek client...") loop = asyncio.get_event_loop() loop.run_until_complete(self.soulseek_client.close()) except Exception as e: logger.error(f"Error closing Soulseek client: {e}") logger.info("Application closed successfully") event.accept() except Exception as e: logger.error(f"Error during application shutdown: {e}") # Force accept the event to prevent hanging event.accept() def main(): logging_config = config_manager.get_logging_config() log_level = logging_config.get('level', 'INFO') log_file = logging_config.get('path', 'logs/newmusic.log') setup_logging(level=log_level, log_file=log_file) logger.info("Starting NewMusic application") if not config_manager.config_path.exists(): logger.error("Configuration file not found. Please check config/config.json") sys.exit(1) app = QApplication(sys.argv) app.setApplicationName("NewMusic") app.setApplicationVersion("1.0.0") main_window = MainWindow() main_window.show() try: sys.exit(app.exec()) except KeyboardInterrupt: logger.info("Application interrupted by user") sys.exit(0) except Exception as e: logger.error(f"Unexpected error: {e}") sys.exit(1) if __name__ == "__main__": main()