This commit is contained in:
Broque Thomas 2025-07-09 13:01:11 -07:00
parent 8e52afe57d
commit 36c7e5922e
12 changed files with 508 additions and 74 deletions

View file

@ -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}
{"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"}

View file

@ -9,7 +9,7 @@
},
"soulseek": {
"slskd_url": "http://localhost:5030",
"api_key": "",
"api_key": "123456789123456789",
"download_path": "./downloads"
},
"database": {

View file

@ -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

View file

@ -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()

View file

@ -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)

View file

@ -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")

View file

@ -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)
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)

View file

@ -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;