diff --git a/.spotify_cache b/.spotify_cache index 09e1b530..fda602d8 100644 --- a/.spotify_cache +++ b/.spotify_cache @@ -1 +1 @@ -{"access_token": "BQDHuavPHFxKGfglVQ7zVOIVbF8ufUuQlH_d-mqEsy0PLmbsHR9OxUyB-QVgF_5-Nrbqwni67byBlNFhBg4dwx45A-SCedirLIMu5IIy6CSCszxwjHXLwhc4Kmz3iGbMytpLQpbsDfqTCTHk2xlfo2DY29XM_Be3ToRWv7LWFCadeIOBxjz2td_xRCDezEFjoZ-g1S9dbVO52dOwo2LPvUk4na5HKE8aH7fJ4uhZNDSyg-Isqm54JQGmWUOnGwxI", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "AQDmfQkPCGObfJeTUIbW1hAAwhSqkuHRA3Qh2dqVYMRh0eCkFMQgPNJDDzF8y-BiaVbj80zePkK_XSfYH1aJutMtNbnsqRKWuxP31BTrMc7pdUdbE7Fma4oH8wpDUKdG3MM", "scope": "user-library-read user-read-private playlist-read-private playlist-read-collaborative user-read-email", "expires_at": 1752090138} \ No newline at end of file +{"access_token": "BQA7NSoqYKl8-HgT1MaTChCe-DbSBPXUZG9_4AzQYIqZsO4MgnVjJyl35gsDKR36HejqrJK8DziDO-HIecF2nhX4OSro0ROIY71CCtjCboE_-BAHk7w-x1XtUyINP7ZNBARToEaketFUiguJAMXIH7NALut2AtQelLyuT7X_NZdINBNpUY6St3BoEAiOIJe5dkxy7UFdHgLZdBLDUZhXLs40zB73qONG4Ie3WjkCAH_RTa4owXn7wvSMLToXd3VH", "token_type": "Bearer", "expires_in": 3600, "scope": "user-library-read user-read-private playlist-read-private playlist-read-collaborative user-read-email", "expires_at": 1752094286, "refresh_token": "AQDmfQkPCGObfJeTUIbW1hAAwhSqkuHRA3Qh2dqVYMRh0eCkFMQgPNJDDzF8y-BiaVbj80zePkK_XSfYH1aJutMtNbnsqRKWuxP31BTrMc7pdUdbE7Fma4oH8wpDUKdG3MM"} \ No newline at end of file diff --git a/config/config.json b/config/config.json index 4339e9e8..b4d13ba8 100644 --- a/config/config.json +++ b/config/config.json @@ -9,7 +9,7 @@ }, "soulseek": { "slskd_url": "http://localhost:5030", - "api_key": "", + "api_key": "123456789123456789", "download_path": "./downloads" }, "database": { diff --git a/logs/app.log b/logs/app.log index d1df26ad..101364c0 100644 --- a/logs/app.log +++ b/logs/app.log @@ -104,3 +104,62 @@ 2025-07-09 12:28:42 - newmusic.main - INFO - change_page:139 - Changed to page: downloads 2025-07-09 12:28:43 - newmusic.main - INFO - change_page:139 - Changed to page: artists 2025-07-09 12:28:44 - newmusic.main - INFO - change_page:139 - Changed to page: settings +2025-07-09 12:29:52 - newmusic.main - INFO - change_page:139 - Changed to page: downloads +2025-07-09 12:29:53 - newmusic.main - INFO - change_page:139 - Changed to page: artists +2025-07-09 12:29:54 - newmusic.main - INFO - change_page:139 - Changed to page: dashboard +2025-07-09 12:30:23 - newmusic.main - INFO - change_page:139 - Changed to page: settings +2025-07-09 12:30:24 - newmusic.main - INFO - change_page:139 - Changed to page: artists +2025-07-09 12:34:29 - newmusic.main - INFO - change_page:139 - Changed to page: settings +2025-07-09 12:34:56 - newmusic.main - INFO - change_page:139 - Changed to page: artists +2025-07-09 12:34:57 - newmusic.main - INFO - change_page:139 - Changed to page: downloads +2025-07-09 12:36:03 - newmusic.main - INFO - change_page:139 - Changed to page: sync +2025-07-09 12:37:31 - newmusic.main - INFO - closeEvent:152 - Closing application... +2025-07-09 12:40:45 - newmusic - INFO - setup_logging:57 - Logging initialized with level: DEBUG +2025-07-09 12:40:45 - newmusic.main - INFO - main:173 - Starting NewMusic application +2025-07-09 12:40:45 - newmusic.spotify_client - INFO - _setup_client:84 - Successfully authenticated with Spotify as broquethomas +2025-07-09 12:40:45 - newmusic.plex_client - INFO - _find_music_library:98 - Found music library: Music +2025-07-09 12:40:45 - newmusic.plex_client - INFO - _setup_client:84 - Successfully connected to Plex server: PLEX-MACHINE +2025-07-09 12:40:45 - newmusic.soulseek_client - INFO - _setup_client:88 - Soulseek client configured with slskd at http://localhost:5030 +2025-07-09 12:40:46 - newmusic.main - INFO - change_page:139 - Changed to page: dashboard +2025-07-09 12:40:48 - newmusic.main - INFO - change_page:139 - Changed to page: sync +2025-07-09 12:40:59 - newmusic.main - INFO - change_page:139 - Changed to page: downloads +2025-07-09 12:41:00 - newmusic.main - INFO - change_page:139 - Changed to page: artists +2025-07-09 12:41:41 - newmusic.main - INFO - change_page:139 - Changed to page: settings +2025-07-09 12:42:45 - newmusic.main - INFO - change_page:139 - Changed to page: dashboard +2025-07-09 12:42:47 - newmusic.main - INFO - change_page:139 - Changed to page: sync +2025-07-09 12:43:27 - newmusic.main - INFO - change_page:139 - Changed to page: settings +2025-07-09 12:47:49 - newmusic.main - INFO - closeEvent:152 - Closing application... +2025-07-09 12:51:25 - newmusic - INFO - setup_logging:57 - Logging initialized with level: DEBUG +2025-07-09 12:51:25 - newmusic.main - INFO - main:173 - Starting NewMusic application +2025-07-09 12:51:26 - newmusic.spotify_client - INFO - _setup_client:84 - Successfully authenticated with Spotify as broquethomas +2025-07-09 12:51:26 - newmusic.plex_client - INFO - _find_music_library:98 - Found music library: Music +2025-07-09 12:51:26 - newmusic.plex_client - INFO - _setup_client:84 - Successfully connected to Plex server: PLEX-MACHINE +2025-07-09 12:51:26 - newmusic.soulseek_client - INFO - _setup_client:88 - Soulseek client configured with slskd at http://localhost:5030 +2025-07-09 12:51:26 - newmusic.spotify_client - INFO - get_user_playlists:106 - Fetching tracks for playlist: Aether +2025-07-09 12:51:26 - newmusic.spotify_client - INFO - get_user_playlists:106 - Fetching tracks for playlist: Favorite Artists +2025-07-09 12:51:29 - newmusic.spotify_client - INFO - get_user_playlists:106 - Fetching tracks for playlist: Brittnea +2025-07-09 12:51:30 - newmusic.spotify_client - INFO - get_user_playlists:106 - Fetching tracks for playlist: Baleigh +2025-07-09 12:51:31 - newmusic.spotify_client - INFO - get_user_playlists:106 - Fetching tracks for playlist: Extra Music +2025-07-09 12:51:32 - newmusic.spotify_client - INFO - get_user_playlists:106 - Fetching tracks for playlist: Maggi Main +2025-07-09 12:51:38 - newmusic.spotify_client - INFO - get_user_playlists:106 - Fetching tracks for playlist: Broque Main +2025-07-09 12:51:45 - newmusic.spotify_client - INFO - get_user_playlists:113 - Retrieved 7 playlists +2025-07-09 12:58:18 - newmusic - INFO - setup_logging:57 - Logging initialized with level: DEBUG +2025-07-09 12:58:18 - newmusic.main - INFO - main:173 - Starting NewMusic application +2025-07-09 12:58:18 - newmusic.spotify_client - INFO - _setup_client:84 - Successfully authenticated with Spotify as broquethomas +2025-07-09 12:58:18 - newmusic.plex_client - INFO - _find_music_library:98 - Found music library: Music +2025-07-09 12:58:18 - newmusic.plex_client - INFO - _setup_client:84 - Successfully connected to Plex server: PLEX-MACHINE +2025-07-09 12:58:18 - newmusic.soulseek_client - INFO - _setup_client:88 - Soulseek client configured with slskd at http://localhost:5030 +2025-07-09 12:58:19 - newmusic.main - INFO - change_page:139 - Changed to page: dashboard +2025-07-09 12:58:19 - newmusic.spotify_client - INFO - get_user_playlists:106 - Fetching tracks for playlist: Aether +2025-07-09 12:58:19 - newmusic.spotify_client - INFO - get_user_playlists:106 - Fetching tracks for playlist: Favorite Artists +2025-07-09 12:58:22 - newmusic.spotify_client - INFO - get_user_playlists:106 - Fetching tracks for playlist: Brittnea +2025-07-09 12:58:23 - newmusic.spotify_client - INFO - get_user_playlists:106 - Fetching tracks for playlist: Baleigh +2025-07-09 12:58:24 - newmusic.main - INFO - change_page:139 - Changed to page: sync +2025-07-09 12:58:24 - newmusic.spotify_client - INFO - get_user_playlists:106 - Fetching tracks for playlist: Extra Music +2025-07-09 12:58:25 - newmusic.spotify_client - INFO - get_user_playlists:106 - Fetching tracks for playlist: Maggi Main +2025-07-09 12:58:30 - newmusic.spotify_client - INFO - get_user_playlists:106 - Fetching tracks for playlist: Broque Main +2025-07-09 12:58:37 - newmusic.spotify_client - INFO - get_user_playlists:113 - Retrieved 7 playlists +2025-07-09 12:58:50 - newmusic.main - INFO - change_page:139 - Changed to page: settings +2025-07-09 12:59:15 - newmusic.plex_client - INFO - _find_music_library:98 - Found music library: Music +2025-07-09 12:59:15 - newmusic.plex_client - INFO - _setup_client:84 - Successfully connected to Plex server: PLEX-MACHINE +2025-07-09 12:59:17 - newmusic.spotify_client - INFO - _setup_client:84 - Successfully authenticated with Spotify as broquethomas diff --git a/main.py b/main.py index c11c1a03..6edea7a3 100644 --- a/main.py +++ b/main.py @@ -99,7 +99,7 @@ class MainWindow(QMainWindow): # Create and add pages self.dashboard_page = DashboardPage() - self.sync_page = SyncPage() + self.sync_page = SyncPage(self.spotify_client, self.plex_client) self.downloads_page = DownloadsPage() self.artists_page = ArtistsPage() self.settings_page = SettingsPage() diff --git a/ui/__pycache__/sidebar.cpython-312.pyc b/ui/__pycache__/sidebar.cpython-312.pyc index b90eedab..9ffac9b1 100644 Binary files a/ui/__pycache__/sidebar.cpython-312.pyc and b/ui/__pycache__/sidebar.cpython-312.pyc differ diff --git a/ui/pages/__pycache__/artists.cpython-312.pyc b/ui/pages/__pycache__/artists.cpython-312.pyc index 818ed777..0fb7e8f0 100644 Binary files a/ui/pages/__pycache__/artists.cpython-312.pyc and b/ui/pages/__pycache__/artists.cpython-312.pyc differ diff --git a/ui/pages/__pycache__/settings.cpython-312.pyc b/ui/pages/__pycache__/settings.cpython-312.pyc index b7f8f532..066aa527 100644 Binary files a/ui/pages/__pycache__/settings.cpython-312.pyc and b/ui/pages/__pycache__/settings.cpython-312.pyc differ diff --git a/ui/pages/__pycache__/sync.cpython-312.pyc b/ui/pages/__pycache__/sync.cpython-312.pyc index aecfe7a5..9c8770cd 100644 Binary files a/ui/pages/__pycache__/sync.cpython-312.pyc and b/ui/pages/__pycache__/sync.cpython-312.pyc differ diff --git a/ui/pages/artists.py b/ui/pages/artists.py index 1d50098a..b214b716 100644 --- a/ui/pages/artists.py +++ b/ui/pages/artists.py @@ -81,6 +81,7 @@ class ArtistCard(QFrame): update_btn = QPushButton("Update") update_btn.setFixedSize(60, 25) + update_btn.setToolTip("Update metadata for this artist only") update_btn.setStyleSheet(""" QPushButton { background: #1db954; @@ -97,6 +98,7 @@ class ArtistCard(QFrame): download_btn = QPushButton("Download") download_btn.setFixedSize(60, 25) + download_btn.setToolTip("Download complete discography for this artist") download_btn.setStyleSheet(""" QPushButton { background: transparent; @@ -177,9 +179,14 @@ class ArtistsPage(QWidget): def create_controls(self): controls = QWidget() - layout = QHBoxLayout(controls) + layout = QVBoxLayout(controls) layout.setSpacing(15) + # First row - search and filters + first_row = QWidget() + first_row_layout = QHBoxLayout(first_row) + first_row_layout.setSpacing(15) + # Search bar search_bar = QLineEdit() search_bar.setPlaceholderText("Search artists...") @@ -241,6 +248,7 @@ class ArtistsPage(QWidget): # Update all button update_btn = QPushButton("📝 Update All Metadata") update_btn.setFixedHeight(40) + update_btn.setToolTip("Batch update metadata for all artists (skips already completed)") update_btn.setStyleSheet(""" QPushButton { background: transparent; @@ -256,14 +264,73 @@ class ArtistsPage(QWidget): } """) - layout.addWidget(search_bar) - layout.addWidget(filter_combo) - layout.addStretch() - layout.addWidget(scan_btn) - layout.addWidget(update_btn) + first_row_layout.addWidget(search_bar) + first_row_layout.addWidget(filter_combo) + first_row_layout.addStretch() + first_row_layout.addWidget(scan_btn) + first_row_layout.addWidget(update_btn) + + # Second row - alphabet navigation + alphabet_row = self.create_alphabet_navigation() + + layout.addWidget(first_row) + layout.addWidget(alphabet_row) return controls + def create_alphabet_navigation(self): + alphabet_widget = QWidget() + alphabet_layout = QHBoxLayout(alphabet_widget) + alphabet_layout.setContentsMargins(0, 0, 0, 0) + alphabet_layout.setSpacing(3) + + # Create alphabet buttons + letters = ['All', '#'] + [chr(i) for i in range(ord('A'), ord('Z')+1)] + + for letter in letters: + btn = QPushButton(letter) + btn.setFixedSize(30, 30) + btn.setStyleSheet(""" + QPushButton { + background: #282828; + border: 1px solid #404040; + border-radius: 15px; + color: #b3b3b3; + font-size: 10px; + font-weight: bold; + } + QPushButton:hover { + background: #1db954; + border: 1px solid #1db954; + color: #000000; + } + QPushButton:pressed { + background: #1aa34a; + } + """) + + # Set "All" as active by default + if letter == 'All': + btn.setStyleSheet(""" + QPushButton { + background: #1db954; + border: 1px solid #1db954; + border-radius: 15px; + color: #000000; + font-size: 10px; + font-weight: bold; + } + QPushButton:hover { + background: #1ed760; + } + """) + + alphabet_layout.addWidget(btn) + + alphabet_layout.addStretch() + + return alphabet_widget + def create_artists_section(self): section = QWidget() layout = QVBoxLayout(section) diff --git a/ui/pages/settings.py b/ui/pages/settings.py index 22f40977..9f3644c9 100644 --- a/ui/pages/settings.py +++ b/ui/pages/settings.py @@ -1,8 +1,9 @@ from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QFrame, QPushButton, QLineEdit, QComboBox, - QCheckBox, QSpinBox, QTextEdit, QGroupBox, QFormLayout) + QCheckBox, QSpinBox, QTextEdit, QGroupBox, QFormLayout, QMessageBox) from PyQt6.QtCore import Qt from PyQt6.QtGui import QFont +from config.settings import config_manager class SettingsGroup(QGroupBox): def __init__(self, title: str, parent=None): @@ -28,7 +29,10 @@ class SettingsGroup(QGroupBox): class SettingsPage(QWidget): def __init__(self, parent=None): super().__init__(parent) + self.config_manager = None + self.form_inputs = {} self.setup_ui() + self.load_config_values() def setup_ui(self): self.setStyleSheet(""" @@ -61,9 +65,10 @@ class SettingsPage(QWidget): main_layout.addStretch() # Save button - save_btn = QPushButton("💾 Save Settings") - save_btn.setFixedHeight(45) - save_btn.setStyleSheet(""" + self.save_btn = QPushButton("💾 Save Settings") + self.save_btn.setFixedHeight(45) + self.save_btn.clicked.connect(self.save_settings) + self.save_btn.setStyleSheet(""" QPushButton { background: #1db954; border: none; @@ -77,7 +82,173 @@ class SettingsPage(QWidget): } """) - main_layout.addWidget(save_btn) + main_layout.addWidget(self.save_btn) + + def load_config_values(self): + """Load current configuration values into form inputs""" + try: + # Load Spotify config + spotify_config = config_manager.get_spotify_config() + self.client_id_input.setText(spotify_config.get('client_id', '')) + self.client_secret_input.setText(spotify_config.get('client_secret', '')) + + # Load Plex config + plex_config = config_manager.get_plex_config() + self.plex_url_input.setText(plex_config.get('base_url', '')) + self.plex_token_input.setText(plex_config.get('token', '')) + + # Load Soulseek config + soulseek_config = config_manager.get_soulseek_config() + self.slskd_url_input.setText(soulseek_config.get('slskd_url', '')) + self.api_key_input.setText(soulseek_config.get('api_key', '')) + + except Exception as e: + QMessageBox.warning(self, "Error", f"Failed to load configuration: {e}") + + def save_settings(self): + """Save current form values to configuration""" + try: + # Save Spotify settings + config_manager.set('spotify.client_id', self.client_id_input.text()) + config_manager.set('spotify.client_secret', self.client_secret_input.text()) + + # Save Plex settings + config_manager.set('plex.base_url', self.plex_url_input.text()) + config_manager.set('plex.token', self.plex_token_input.text()) + + # Save Soulseek settings + config_manager.set('soulseek.slskd_url', self.slskd_url_input.text()) + config_manager.set('soulseek.api_key', self.api_key_input.text()) + + # Show success message + QMessageBox.information(self, "Success", "Settings saved successfully!") + + # Update button text temporarily + original_text = self.save_btn.text() + self.save_btn.setText("✓ Saved!") + self.save_btn.setStyleSheet(""" + QPushButton { + background: #1aa34a; + border: none; + border-radius: 22px; + color: #ffffff; + font-size: 14px; + font-weight: bold; + } + """) + + # Reset button after 2 seconds + from PyQt6.QtCore import QTimer + QTimer.singleShot(2000, lambda: self.reset_save_button(original_text)) + + except Exception as e: + QMessageBox.critical(self, "Error", f"Failed to save settings: {e}") + + def reset_save_button(self, original_text): + """Reset save button to original state""" + self.save_btn.setText(original_text) + self.save_btn.setStyleSheet(""" + QPushButton { + background: #1db954; + border: none; + border-radius: 22px; + color: #000000; + font-size: 14px; + font-weight: bold; + } + QPushButton:hover { + background: #1ed760; + } + """) + + def test_spotify_connection(self): + """Test Spotify API connection""" + try: + from core.spotify_client import SpotifyClient + + # Create temporary client with current form values + temp_config = config_manager.get_spotify_config().copy() + temp_config['client_id'] = self.client_id_input.text() + temp_config['client_secret'] = self.client_secret_input.text() + + # Save temporarily to test + original_client_id = config_manager.get('spotify.client_id') + original_client_secret = config_manager.get('spotify.client_secret') + + config_manager.set('spotify.client_id', temp_config['client_id']) + config_manager.set('spotify.client_secret', temp_config['client_secret']) + + # Test connection + client = SpotifyClient() + if client.is_authenticated(): + user_info = client.get_user_info() + username = user_info.get('display_name', 'Unknown') if user_info else 'Unknown' + QMessageBox.information(self, "Success", f"✓ Spotify connection successful!\nConnected as: {username}") + else: + QMessageBox.warning(self, "Failed", "✗ Spotify connection failed.\nCheck your credentials and try again.") + + # Restore original values + config_manager.set('spotify.client_id', original_client_id) + config_manager.set('spotify.client_secret', original_client_secret) + + except Exception as e: + QMessageBox.critical(self, "Error", f"✗ Spotify test failed:\n{str(e)}") + + def test_plex_connection(self): + """Test Plex server connection""" + try: + from core.plex_client import PlexClient + + # Save temporarily to test + original_base_url = config_manager.get('plex.base_url') + original_token = config_manager.get('plex.token') + + config_manager.set('plex.base_url', self.plex_url_input.text()) + config_manager.set('plex.token', self.plex_token_input.text()) + + # Test connection + client = PlexClient() + if client.is_connected(): + server_name = client.server.friendlyName if client.server else 'Unknown' + QMessageBox.information(self, "Success", f"✓ Plex connection successful!\nServer: {server_name}") + else: + QMessageBox.warning(self, "Failed", "✗ Plex connection failed.\nCheck your server URL and token.") + + # Restore original values + config_manager.set('plex.base_url', original_base_url) + config_manager.set('plex.token', original_token) + + except Exception as e: + QMessageBox.critical(self, "Error", f"✗ Plex test failed:\n{str(e)}") + + def test_soulseek_connection(self): + """Test Soulseek slskd connection""" + try: + import requests + + slskd_url = self.slskd_url_input.text() + api_key = self.api_key_input.text() + + if not slskd_url: + QMessageBox.warning(self, "Error", "Please enter slskd URL") + return + + # Test API endpoint + headers = {} + if api_key: + headers['X-API-Key'] = api_key + + response = requests.get(f"{slskd_url}/api/v0/session", headers=headers, timeout=5) + + if response.status_code == 200: + QMessageBox.information(self, "Success", "✓ Soulseek connection successful!\nslskd is responding.") + else: + QMessageBox.warning(self, "Failed", f"✗ Soulseek connection failed.\nHTTP {response.status_code}") + + except requests.exceptions.RequestException as e: + QMessageBox.critical(self, "Error", f"✗ Soulseek test failed:\n{str(e)}") + except Exception as e: + QMessageBox.critical(self, "Error", f"✗ Soulseek test failed:\n{str(e)}") def create_header(self): header = QWidget() @@ -120,16 +291,18 @@ class SettingsPage(QWidget): spotify_title.setFont(QFont("Arial", 12, QFont.Weight.Bold)) spotify_title.setStyleSheet("color: #1db954;") - client_id_input = QLineEdit("512b25bd9e0d4ecd82140f6d1ce0c8e6") - client_id_input.setStyleSheet(self.get_input_style()) + self.client_id_input = QLineEdit() + self.client_id_input.setStyleSheet(self.get_input_style()) + self.form_inputs['spotify.client_id'] = self.client_id_input - client_secret_input = QLineEdit("c3844dcbedbc4e09a6242a14b2e89e89") - client_secret_input.setEchoMode(QLineEdit.EchoMode.Password) - client_secret_input.setStyleSheet(self.get_input_style()) + self.client_secret_input = QLineEdit() + self.client_secret_input.setEchoMode(QLineEdit.EchoMode.Password) + self.client_secret_input.setStyleSheet(self.get_input_style()) + self.form_inputs['spotify.client_secret'] = self.client_secret_input spotify_layout.addRow(spotify_title) - spotify_layout.addRow("Client ID:", client_id_input) - spotify_layout.addRow("Client Secret:", client_secret_input) + spotify_layout.addRow("Client ID:", self.client_id_input) + spotify_layout.addRow("Client Secret:", self.client_secret_input) # Plex settings plex_frame = QFrame() @@ -140,16 +313,18 @@ class SettingsPage(QWidget): plex_title.setFont(QFont("Arial", 12, QFont.Weight.Bold)) plex_title.setStyleSheet("color: #e5a00d;") - plex_url_input = QLineEdit("http://192.168.86.36:32400") - plex_url_input.setStyleSheet(self.get_input_style()) + self.plex_url_input = QLineEdit() + self.plex_url_input.setStyleSheet(self.get_input_style()) + self.form_inputs['plex.base_url'] = self.plex_url_input - plex_token_input = QLineEdit("a9hTgvasV1aJMLSdoBkr") - plex_token_input.setEchoMode(QLineEdit.EchoMode.Password) - plex_token_input.setStyleSheet(self.get_input_style()) + self.plex_token_input = QLineEdit() + self.plex_token_input.setEchoMode(QLineEdit.EchoMode.Password) + self.plex_token_input.setStyleSheet(self.get_input_style()) + self.form_inputs['plex.token'] = self.plex_token_input plex_layout.addRow(plex_title) - plex_layout.addRow("Server URL:", plex_url_input) - plex_layout.addRow("Token:", plex_token_input) + plex_layout.addRow("Server URL:", self.plex_url_input) + plex_layout.addRow("Token:", self.plex_token_input) # Soulseek settings soulseek_frame = QFrame() @@ -160,17 +335,19 @@ class SettingsPage(QWidget): soulseek_title.setFont(QFont("Arial", 12, QFont.Weight.Bold)) soulseek_title.setStyleSheet("color: #ff6b35;") - slskd_url_input = QLineEdit("http://localhost:5030") - slskd_url_input.setStyleSheet(self.get_input_style()) + self.slskd_url_input = QLineEdit() + self.slskd_url_input.setStyleSheet(self.get_input_style()) + self.form_inputs['soulseek.slskd_url'] = self.slskd_url_input - api_key_input = QLineEdit() - api_key_input.setPlaceholderText("Enter your slskd API key") - api_key_input.setEchoMode(QLineEdit.EchoMode.Password) - api_key_input.setStyleSheet(self.get_input_style()) + self.api_key_input = QLineEdit() + self.api_key_input.setPlaceholderText("Enter your slskd API key") + self.api_key_input.setEchoMode(QLineEdit.EchoMode.Password) + self.api_key_input.setStyleSheet(self.get_input_style()) + self.form_inputs['soulseek.api_key'] = self.api_key_input soulseek_layout.addRow(soulseek_title) - soulseek_layout.addRow("slskd URL:", slskd_url_input) - soulseek_layout.addRow("API Key:", api_key_input) + soulseek_layout.addRow("slskd URL:", self.slskd_url_input) + soulseek_layout.addRow("API Key:", self.api_key_input) api_layout.addWidget(spotify_frame) api_layout.addWidget(plex_frame) @@ -182,14 +359,17 @@ class SettingsPage(QWidget): test_spotify = QPushButton("Test Spotify") test_spotify.setFixedHeight(30) + test_spotify.clicked.connect(self.test_spotify_connection) test_spotify.setStyleSheet(self.get_test_button_style()) test_plex = QPushButton("Test Plex") test_plex.setFixedHeight(30) + test_plex.clicked.connect(self.test_plex_connection) test_plex.setStyleSheet(self.get_test_button_style()) test_soulseek = QPushButton("Test Soulseek") test_soulseek.setFixedHeight(30) + test_soulseek.clicked.connect(self.test_soulseek_connection) test_soulseek.setStyleSheet(self.get_test_button_style()) test_layout.addWidget(test_spotify) @@ -288,19 +468,22 @@ class SettingsPage(QWidget): auto_sync.setChecked(True) auto_sync.setStyleSheet(self.get_checkbox_style()) - # Update metadata checkbox - update_metadata = QCheckBox("Update metadata from Spotify") - update_metadata.setChecked(True) - update_metadata.setStyleSheet(self.get_checkbox_style()) + # Sync interval + interval_layout = QHBoxLayout() + interval_label = QLabel("Sync Interval (minutes):") + interval_label.setStyleSheet("color: #ffffff; font-size: 12px;") - # Download missing tracks checkbox - download_missing = QCheckBox("Automatically download missing tracks") - download_missing.setChecked(False) - download_missing.setStyleSheet(self.get_checkbox_style()) + interval_spin = QSpinBox() + interval_spin.setRange(5, 1440) # 5 minutes to 24 hours + interval_spin.setValue(60) + interval_spin.setStyleSheet(self.get_spin_style()) + + interval_layout.addWidget(interval_label) + interval_layout.addWidget(interval_spin) + interval_layout.addStretch() sync_layout.addWidget(auto_sync) - sync_layout.addWidget(update_metadata) - sync_layout.addWidget(download_missing) + sync_layout.addLayout(interval_layout) # Logging Settings logging_group = SettingsGroup("Logging Settings") diff --git a/ui/pages/sync.py b/ui/pages/sync.py index 380c5c53..e9e1424b 100644 --- a/ui/pages/sync.py +++ b/ui/pages/sync.py @@ -1,10 +1,40 @@ from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QFrame, QPushButton, QListWidget, QListWidgetItem, QProgressBar, QTextEdit, QCheckBox, QComboBox, - QScrollArea, QSizePolicy) -from PyQt6.QtCore import Qt, QThread, pyqtSignal + QScrollArea, QSizePolicy, QMessageBox) +from PyQt6.QtCore import Qt, QThread, pyqtSignal, QTimer from PyQt6.QtGui import QFont +class PlaylistLoaderThread(QThread): + playlist_loaded = pyqtSignal(object) # Single playlist + loading_finished = pyqtSignal(int) # Total count + loading_failed = pyqtSignal(str) # Error message + progress_updated = pyqtSignal(str) # Progress text + + def __init__(self, spotify_client): + super().__init__() + self.spotify_client = spotify_client + + def run(self): + try: + self.progress_updated.emit("Connecting to Spotify...") + if not self.spotify_client or not self.spotify_client.is_authenticated(): + self.loading_failed.emit("Spotify not authenticated") + return + + self.progress_updated.emit("Fetching playlists...") + playlists = self.spotify_client.get_user_playlists() + + for i, playlist in enumerate(playlists): + self.progress_updated.emit(f"Loading playlist {i+1}/{len(playlists)}: {playlist.name}") + self.playlist_loaded.emit(playlist) + self.msleep(50) # Small delay to show progressive loading + + self.loading_finished.emit(len(playlists)) + + except Exception as e: + self.loading_failed.emit(str(e)) + class PlaylistItem(QFrame): def __init__(self, name: str, track_count: int, sync_status: str, parent=None): super().__init__(parent) @@ -191,9 +221,16 @@ class SyncOptionsPanel(QFrame): layout.addLayout(quality_layout) class SyncPage(QWidget): - def __init__(self, parent=None): + def __init__(self, spotify_client=None, plex_client=None, parent=None): super().__init__(parent) + self.spotify_client = spotify_client + self.plex_client = plex_client + self.current_playlists = [] + self.playlist_loader = None self.setup_ui() + + # Start loading playlists asynchronously after UI is ready + QTimer.singleShot(100, self.load_playlists_async) def setup_ui(self): self.setStyleSheet(""" @@ -261,9 +298,10 @@ class SyncPage(QWidget): section_title.setFont(QFont("Arial", 16, QFont.Weight.Bold)) section_title.setStyleSheet("color: #ffffff;") - refresh_btn = QPushButton("🔄 Refresh") - refresh_btn.setFixedSize(100, 35) - refresh_btn.setStyleSheet(""" + self.refresh_btn = QPushButton("🔄 Refresh") + self.refresh_btn.setFixedSize(100, 35) + self.refresh_btn.clicked.connect(self.refresh_playlists) + self.refresh_btn.setStyleSheet(""" QPushButton { background: #1db954; border: none; @@ -282,7 +320,7 @@ class SyncPage(QWidget): header_layout.addWidget(section_title) header_layout.addStretch() - header_layout.addWidget(refresh_btn) + header_layout.addWidget(self.refresh_btn) # Playlist container playlist_container = QScrollArea() @@ -303,26 +341,14 @@ class SyncPage(QWidget): } """) - playlist_widget = QWidget() - playlist_layout = QVBoxLayout(playlist_widget) - playlist_layout.setSpacing(10) + self.playlist_widget = QWidget() + self.playlist_layout = QVBoxLayout(self.playlist_widget) + self.playlist_layout.setSpacing(10) - # Sample playlists - playlists = [ - ("Liked Songs", 247, "Synced"), - ("Discover Weekly", 30, "Needs Sync"), - ("Chill Vibes", 89, "Synced"), - ("Workout Mix", 156, "Needs Sync"), - ("Road Trip", 67, "Never Synced"), - ("Focus Music", 45, "Synced") - ] + # Playlists will be loaded asynchronously after UI setup - for name, count, status in playlists: - item = PlaylistItem(name, count, status) - playlist_layout.addWidget(item) - - playlist_layout.addStretch() - playlist_container.setWidget(playlist_widget) + self.playlist_layout.addStretch() + playlist_container.setWidget(self.playlist_widget) layout.addLayout(header_layout) layout.addWidget(playlist_container) @@ -455,4 +481,103 @@ class SyncPage(QWidget): layout.addWidget(progress_header) layout.addWidget(self.progress_bar) layout.addWidget(self.progress_text) - layout.addWidget(self.log_area) \ No newline at end of file + layout.addWidget(self.log_area) + + return section + + def load_playlists_async(self): + """Start asynchronous playlist loading""" + if self.playlist_loader and self.playlist_loader.isRunning(): + return + + # Clear existing playlists + self.clear_playlists() + + # Show loading state + self.refresh_btn.setText("🔄 Loading...") + self.refresh_btn.setEnabled(False) + self.log_area.append("Starting playlist loading...") + + # Create and start loader thread + self.playlist_loader = PlaylistLoaderThread(self.spotify_client) + self.playlist_loader.playlist_loaded.connect(self.add_playlist_to_ui) + self.playlist_loader.loading_finished.connect(self.on_loading_finished) + self.playlist_loader.loading_failed.connect(self.on_loading_failed) + self.playlist_loader.progress_updated.connect(self.update_progress) + self.playlist_loader.start() + + def add_playlist_to_ui(self, playlist): + """Add a single playlist to the UI as it's loaded""" + # Simple sync status (placeholder for now) + sync_status = "Never Synced" # TODO: Check actual sync status + item = PlaylistItem(playlist.name, playlist.total_tracks, sync_status) + # Insert before the stretch item + self.playlist_layout.insertWidget(self.playlist_layout.count() - 1, item) + self.current_playlists.append(playlist) + + # Update log + self.log_area.append(f"Added playlist: {playlist.name} ({playlist.total_tracks} tracks)") + + def on_loading_finished(self, count): + """Handle completion of playlist loading""" + self.refresh_btn.setText("🔄 Refresh") + self.refresh_btn.setEnabled(True) + self.log_area.append(f"✓ Loaded {count} Spotify playlists successfully") + + def on_loading_failed(self, error_msg): + """Handle playlist loading failure""" + self.refresh_btn.setText("🔄 Refresh") + self.refresh_btn.setEnabled(True) + self.log_area.append(f"✗ Failed to load playlists: {error_msg}") + QMessageBox.critical(self, "Error", f"Failed to load playlists: {error_msg}") + + def update_progress(self, message): + """Update progress text""" + self.log_area.append(message) + + def load_initial_playlists(self): + """Load initial playlist data (placeholder or real)""" + if self.spotify_client and self.spotify_client.is_authenticated(): + self.refresh_playlists() + else: + # Show placeholder playlists + playlists = [ + ("Liked Songs", 247, "Synced"), + ("Discover Weekly", 30, "Needs Sync"), + ("Chill Vibes", 89, "Synced"), + ("Workout Mix", 156, "Needs Sync"), + ("Road Trip", 67, "Never Synced"), + ("Focus Music", 45, "Synced") + ] + + for name, count, status in playlists: + item = PlaylistItem(name, count, status) + self.playlist_layout.addWidget(item) + + def refresh_playlists(self): + """Refresh playlists from Spotify API using async loader""" + if not self.spotify_client: + QMessageBox.warning(self, "Error", "Spotify client not available") + return + + if not self.spotify_client.is_authenticated(): + QMessageBox.warning(self, "Error", "Spotify not authenticated. Please check your settings.") + return + + # Use the async loader + self.load_playlists_async() + + def clear_playlists(self): + """Clear all playlist items from the layout""" + # Clear the current playlists list + self.current_playlists = [] + + # Remove all items except the stretch + for i in reversed(range(self.playlist_layout.count())): + item = self.playlist_layout.itemAt(i) + if item.widget(): + item.widget().deleteLater() + elif item.spacerItem(): + continue # Keep the stretch spacer + else: + self.playlist_layout.removeItem(item) \ No newline at end of file diff --git a/ui/sidebar.py b/ui/sidebar.py index 9cc8e876..e0006c36 100644 --- a/ui/sidebar.py +++ b/ui/sidebar.py @@ -63,7 +63,7 @@ class SidebarButton(QPushButton): transform: scale(1.02); } """) - self.text_label.setStyleSheet("color: #1db954; font-weight: bold;") + self.text_label.setStyleSheet("color: #1db954; font-weight: bold; background: transparent;") self.icon_label.setStyleSheet(""" QLabel { color: #1db954; @@ -87,7 +87,7 @@ class SidebarButton(QPushButton): border-left: 2px solid rgba(255, 255, 255, 0.3); } """) - self.text_label.setStyleSheet("color: #b3b3b3;") + self.text_label.setStyleSheet("color: #b3b3b3; background: transparent;") self.icon_label.setStyleSheet(""" QLabel { color: #b3b3b3;