Playlist page
This commit is contained in:
parent
92d0f060f4
commit
4973a53369
2 changed files with 628 additions and 15 deletions
|
|
@ -174,22 +174,40 @@ class SpotifyClient:
|
|||
)
|
||||
|
||||
self.sp = spotipy.Spotify(auth_manager=auth_manager)
|
||||
user_info = self.sp.current_user()
|
||||
self.user_id = user_info['id']
|
||||
logger.info(f"Successfully authenticated with Spotify as {user_info['display_name']}")
|
||||
# Don't fetch user info on startup - do it lazily to avoid blocking UI
|
||||
self.user_id = None
|
||||
logger.info("Spotify client initialized (user info will be fetched when needed)")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to authenticate with Spotify: {e}")
|
||||
self.sp = None
|
||||
|
||||
def is_authenticated(self) -> bool:
|
||||
return self.sp is not None and self.user_id is not None
|
||||
"""Check if Spotify client is set up (fast check, no API calls)"""
|
||||
return self.sp is not None
|
||||
|
||||
def _ensure_user_id(self) -> bool:
|
||||
"""Ensure user_id is loaded (may make API call)"""
|
||||
if self.user_id is None and self.sp is not None:
|
||||
try:
|
||||
user_info = self.sp.current_user()
|
||||
self.user_id = user_info['id']
|
||||
logger.info(f"Successfully authenticated with Spotify as {user_info['display_name']}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to fetch user info: {e}")
|
||||
return False
|
||||
return self.user_id is not None
|
||||
|
||||
def get_user_playlists(self) -> List[Playlist]:
|
||||
if not self.is_authenticated():
|
||||
logger.error("Not authenticated with Spotify")
|
||||
return []
|
||||
|
||||
if not self._ensure_user_id():
|
||||
logger.error("Failed to get user ID")
|
||||
return []
|
||||
|
||||
playlists = []
|
||||
|
||||
try:
|
||||
|
|
@ -212,6 +230,36 @@ class SpotifyClient:
|
|||
logger.error(f"Error fetching user playlists: {e}")
|
||||
return []
|
||||
|
||||
def get_user_playlists_metadata_only(self) -> List[Playlist]:
|
||||
"""Get playlists without fetching all track details for faster loading"""
|
||||
if not self.is_authenticated():
|
||||
logger.error("Not authenticated with Spotify")
|
||||
return []
|
||||
|
||||
if not self._ensure_user_id():
|
||||
logger.error("Failed to get user ID")
|
||||
return []
|
||||
|
||||
playlists = []
|
||||
|
||||
try:
|
||||
# Only fetch first batch initially for faster loading
|
||||
results = self.sp.current_user_playlists(limit=20)
|
||||
|
||||
if results and 'items' in results:
|
||||
for playlist_data in results['items']:
|
||||
if playlist_data['owner']['id'] == self.user_id or playlist_data['collaborative']:
|
||||
# Create playlist with empty tracks list for now
|
||||
playlist = Playlist.from_spotify_playlist(playlist_data, [])
|
||||
playlists.append(playlist)
|
||||
|
||||
logger.info(f"Retrieved {len(playlists)} playlist metadata (first batch)")
|
||||
return playlists
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching user playlists metadata: {e}")
|
||||
return []
|
||||
|
||||
def _get_playlist_tracks(self, playlist_id: str) -> List[Track]:
|
||||
if not self.is_authenticated():
|
||||
return []
|
||||
|
|
|
|||
587
ui/pages/sync.py
587
ui/pages/sync.py
|
|
@ -1,8 +1,9 @@
|
|||
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
||||
QFrame, QPushButton, QListWidget, QListWidgetItem,
|
||||
QProgressBar, QTextEdit, QCheckBox, QComboBox,
|
||||
QScrollArea, QSizePolicy, QMessageBox)
|
||||
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QTimer
|
||||
QScrollArea, QSizePolicy, QMessageBox, QDialog,
|
||||
QTableWidget, QTableWidgetItem, QHeaderView)
|
||||
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QTimer, QPropertyAnimation, QEasingCurve, QRunnable, QThreadPool, QObject
|
||||
from PyQt6.QtGui import QFont
|
||||
|
||||
class PlaylistLoaderThread(QThread):
|
||||
|
|
@ -23,24 +24,398 @@ class PlaylistLoaderThread(QThread):
|
|||
return
|
||||
|
||||
self.progress_updated.emit("Fetching playlists...")
|
||||
playlists = self.spotify_client.get_user_playlists()
|
||||
playlists = self.spotify_client.get_user_playlists_metadata_only()
|
||||
|
||||
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.msleep(20) # Reduced delay for faster but visible progressive loading
|
||||
|
||||
self.loading_finished.emit(len(playlists))
|
||||
|
||||
except Exception as e:
|
||||
self.loading_failed.emit(str(e))
|
||||
|
||||
class TrackLoadingWorkerSignals(QObject):
|
||||
"""Signals for async track loading worker"""
|
||||
tracks_loaded = pyqtSignal(str, list) # playlist_id, tracks
|
||||
loading_failed = pyqtSignal(str, str) # playlist_id, error_message
|
||||
loading_started = pyqtSignal(str) # playlist_id
|
||||
|
||||
class TrackLoadingWorker(QRunnable):
|
||||
"""Async worker for loading playlist tracks (following downloads.py pattern)"""
|
||||
|
||||
def __init__(self, spotify_client, playlist_id, playlist_name):
|
||||
super().__init__()
|
||||
self.spotify_client = spotify_client
|
||||
self.playlist_id = playlist_id
|
||||
self.playlist_name = playlist_name
|
||||
self.signals = TrackLoadingWorkerSignals()
|
||||
|
||||
def run(self):
|
||||
"""Load tracks in background thread"""
|
||||
try:
|
||||
self.signals.loading_started.emit(self.playlist_id)
|
||||
|
||||
# Fetch tracks from Spotify API
|
||||
tracks = self.spotify_client._get_playlist_tracks(self.playlist_id)
|
||||
|
||||
# Emit success signal
|
||||
self.signals.tracks_loaded.emit(self.playlist_id, tracks)
|
||||
|
||||
except Exception as e:
|
||||
# Emit error signal
|
||||
self.signals.loading_failed.emit(self.playlist_id, str(e))
|
||||
|
||||
class PlaylistDetailsModal(QDialog):
|
||||
def __init__(self, playlist, parent=None):
|
||||
super().__init__(parent)
|
||||
self.playlist = playlist
|
||||
self.parent_page = parent
|
||||
self.spotify_client = parent.spotify_client if parent else None
|
||||
|
||||
self.setup_ui()
|
||||
|
||||
# Load tracks asynchronously if not already cached
|
||||
if not self.playlist.tracks and self.spotify_client:
|
||||
# Check cache first
|
||||
if hasattr(parent, 'track_cache') and playlist.id in parent.track_cache:
|
||||
self.playlist.tracks = parent.track_cache[playlist.id]
|
||||
self.refresh_track_table()
|
||||
else:
|
||||
self.load_tracks_async()
|
||||
|
||||
def setup_ui(self):
|
||||
self.setWindowTitle(f"Playlist Details - {self.playlist.name}")
|
||||
self.setFixedSize(900, 700)
|
||||
self.setStyleSheet("""
|
||||
QDialog {
|
||||
background: #191414;
|
||||
color: #ffffff;
|
||||
}
|
||||
""")
|
||||
|
||||
main_layout = QVBoxLayout(self)
|
||||
main_layout.setContentsMargins(30, 30, 30, 30)
|
||||
main_layout.setSpacing(20)
|
||||
|
||||
# Header section
|
||||
header = self.create_header()
|
||||
main_layout.addWidget(header)
|
||||
|
||||
# Track list section
|
||||
track_list = self.create_track_list()
|
||||
main_layout.addWidget(track_list)
|
||||
|
||||
# Button section
|
||||
button_widget = QWidget()
|
||||
button_layout = self.create_buttons()
|
||||
button_widget.setLayout(button_layout)
|
||||
main_layout.addWidget(button_widget)
|
||||
|
||||
def create_header(self):
|
||||
header = QFrame()
|
||||
header.setStyleSheet("""
|
||||
QFrame {
|
||||
background: #282828;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #404040;
|
||||
}
|
||||
""")
|
||||
|
||||
layout = QVBoxLayout(header)
|
||||
layout.setContentsMargins(25, 20, 25, 20)
|
||||
layout.setSpacing(10)
|
||||
|
||||
# Playlist name
|
||||
name_label = QLabel(self.playlist.name)
|
||||
name_label.setFont(QFont("Arial", 20, QFont.Weight.Bold))
|
||||
name_label.setStyleSheet("color: #ffffff; border: none; background: transparent;")
|
||||
|
||||
# Playlist info
|
||||
info_layout = QHBoxLayout()
|
||||
info_layout.setSpacing(20)
|
||||
|
||||
# Track count
|
||||
track_count = QLabel(f"{self.playlist.total_tracks} tracks")
|
||||
track_count.setFont(QFont("Arial", 12))
|
||||
track_count.setStyleSheet("color: #b3b3b3; border: none; background: transparent;")
|
||||
|
||||
# Owner
|
||||
owner = QLabel(f"by {self.playlist.owner}")
|
||||
owner.setFont(QFont("Arial", 12))
|
||||
owner.setStyleSheet("color: #b3b3b3; border: none; background: transparent;")
|
||||
|
||||
# Public/Private status
|
||||
visibility = "Public" if self.playlist.public else "Private"
|
||||
if self.playlist.collaborative:
|
||||
visibility = "Collaborative"
|
||||
status = QLabel(visibility)
|
||||
status.setFont(QFont("Arial", 12))
|
||||
status.setStyleSheet("color: #1db954; border: none; background: transparent;")
|
||||
|
||||
info_layout.addWidget(track_count)
|
||||
info_layout.addWidget(owner)
|
||||
info_layout.addWidget(status)
|
||||
info_layout.addStretch()
|
||||
|
||||
# Description (if available)
|
||||
if self.playlist.description:
|
||||
desc_label = QLabel(self.playlist.description)
|
||||
desc_label.setFont(QFont("Arial", 11))
|
||||
desc_label.setStyleSheet("color: #b3b3b3; border: none; background: transparent;")
|
||||
desc_label.setWordWrap(True)
|
||||
desc_label.setMaximumHeight(60)
|
||||
layout.addWidget(desc_label)
|
||||
|
||||
layout.addWidget(name_label)
|
||||
layout.addLayout(info_layout)
|
||||
|
||||
return header
|
||||
|
||||
def create_track_list(self):
|
||||
container = QFrame()
|
||||
container.setStyleSheet("""
|
||||
QFrame {
|
||||
background: #282828;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #404040;
|
||||
}
|
||||
""")
|
||||
|
||||
layout = QVBoxLayout(container)
|
||||
layout.setContentsMargins(25, 20, 25, 20)
|
||||
layout.setSpacing(15)
|
||||
|
||||
# Section title
|
||||
title = QLabel("Tracks")
|
||||
title.setFont(QFont("Arial", 16, QFont.Weight.Bold))
|
||||
title.setStyleSheet("color: #ffffff; border: none; background: transparent;")
|
||||
|
||||
# Track table
|
||||
self.track_table = QTableWidget()
|
||||
self.track_table.setColumnCount(4)
|
||||
self.track_table.setHorizontalHeaderLabels(["Track", "Artist", "Album", "Duration"])
|
||||
|
||||
# Set initial row count (may be 0 if tracks not loaded yet)
|
||||
track_count = len(self.playlist.tracks) if self.playlist.tracks else 1
|
||||
self.track_table.setRowCount(track_count)
|
||||
|
||||
# Style the table
|
||||
self.track_table.setStyleSheet("""
|
||||
QTableWidget {
|
||||
background: #181818;
|
||||
border: 1px solid #404040;
|
||||
border-radius: 8px;
|
||||
gridline-color: #404040;
|
||||
color: #ffffff;
|
||||
font-size: 11px;
|
||||
}
|
||||
QTableWidget::item {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #333333;
|
||||
}
|
||||
QTableWidget::item:selected {
|
||||
background: #1db954;
|
||||
color: #000000;
|
||||
}
|
||||
QHeaderView::section {
|
||||
background: #404040;
|
||||
color: #ffffff;
|
||||
padding: 10px;
|
||||
border: none;
|
||||
font-weight: bold;
|
||||
font-size: 11px;
|
||||
}
|
||||
""")
|
||||
|
||||
# Populate table
|
||||
if self.playlist.tracks:
|
||||
for row, track in enumerate(self.playlist.tracks):
|
||||
# Track name
|
||||
track_item = QTableWidgetItem(track.name)
|
||||
track_item.setFlags(track_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
||||
self.track_table.setItem(row, 0, track_item)
|
||||
|
||||
# Artist(s)
|
||||
artists = ", ".join(track.artists)
|
||||
artist_item = QTableWidgetItem(artists)
|
||||
artist_item.setFlags(artist_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
||||
self.track_table.setItem(row, 1, artist_item)
|
||||
|
||||
# Album
|
||||
album_item = QTableWidgetItem(track.album)
|
||||
album_item.setFlags(album_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
||||
self.track_table.setItem(row, 2, album_item)
|
||||
|
||||
# Duration
|
||||
duration = self.format_duration(track.duration_ms)
|
||||
duration_item = QTableWidgetItem(duration)
|
||||
duration_item.setFlags(duration_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
||||
self.track_table.setItem(row, 3, duration_item)
|
||||
else:
|
||||
# Show placeholder while tracks are being loaded
|
||||
placeholder_item = QTableWidgetItem("Tracks will load momentarily...")
|
||||
placeholder_item.setFlags(placeholder_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
||||
self.track_table.setItem(0, 0, placeholder_item)
|
||||
self.track_table.setSpan(0, 0, 1, 4)
|
||||
|
||||
# Set column widths
|
||||
header = self.track_table.horizontalHeader()
|
||||
header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) # Track name
|
||||
header.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents) # Artist
|
||||
header.setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents) # Album
|
||||
header.setSectionResizeMode(3, QHeaderView.ResizeMode.Fixed) # Duration
|
||||
self.track_table.setColumnWidth(3, 80)
|
||||
|
||||
# Hide row numbers
|
||||
self.track_table.verticalHeader().setVisible(False)
|
||||
|
||||
layout.addWidget(title)
|
||||
layout.addWidget(self.track_table)
|
||||
|
||||
return container
|
||||
|
||||
def create_buttons(self):
|
||||
button_layout = QHBoxLayout()
|
||||
button_layout.setSpacing(15)
|
||||
|
||||
# Close button
|
||||
close_btn = QPushButton("Close")
|
||||
close_btn.setFixedSize(100, 40)
|
||||
close_btn.clicked.connect(self.close)
|
||||
close_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background: #404040;
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
color: #ffffff;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background: #505050;
|
||||
}
|
||||
""")
|
||||
|
||||
# Sync button
|
||||
sync_btn = QPushButton("Sync This Playlist")
|
||||
sync_btn.setFixedSize(150, 40)
|
||||
sync_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background: #1db954;
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
color: #000000;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background: #1ed760;
|
||||
}
|
||||
""")
|
||||
|
||||
button_layout.addStretch()
|
||||
button_layout.addWidget(close_btn)
|
||||
button_layout.addWidget(sync_btn)
|
||||
|
||||
return button_layout
|
||||
|
||||
def format_duration(self, duration_ms):
|
||||
"""Convert milliseconds to MM:SS format"""
|
||||
seconds = duration_ms // 1000
|
||||
minutes = seconds // 60
|
||||
seconds = seconds % 60
|
||||
return f"{minutes}:{seconds:02d}"
|
||||
|
||||
def load_tracks_async(self):
|
||||
"""Load tracks asynchronously using worker thread"""
|
||||
if not self.spotify_client:
|
||||
return
|
||||
|
||||
# Show loading state in track table
|
||||
if hasattr(self, 'track_table'):
|
||||
self.track_table.setRowCount(1)
|
||||
loading_item = QTableWidgetItem("Loading tracks...")
|
||||
loading_item.setFlags(loading_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
||||
self.track_table.setItem(0, 0, loading_item)
|
||||
self.track_table.setSpan(0, 0, 1, 4)
|
||||
|
||||
# Create and submit worker to thread pool
|
||||
worker = TrackLoadingWorker(self.spotify_client, self.playlist.id, self.playlist.name)
|
||||
worker.signals.tracks_loaded.connect(self.on_tracks_loaded)
|
||||
worker.signals.loading_failed.connect(self.on_tracks_loading_failed)
|
||||
|
||||
# Submit to parent's thread pool if available, otherwise create one
|
||||
if hasattr(self.parent_page, 'thread_pool'):
|
||||
self.parent_page.thread_pool.start(worker)
|
||||
else:
|
||||
# Fallback thread pool
|
||||
thread_pool = QThreadPool()
|
||||
thread_pool.start(worker)
|
||||
|
||||
def on_tracks_loaded(self, playlist_id, tracks):
|
||||
"""Handle successful track loading"""
|
||||
if playlist_id == self.playlist.id:
|
||||
self.playlist.tracks = tracks
|
||||
|
||||
# Cache tracks in parent for future use
|
||||
if hasattr(self.parent_page, 'track_cache'):
|
||||
self.parent_page.track_cache[playlist_id] = tracks
|
||||
|
||||
# Refresh the track table
|
||||
self.refresh_track_table()
|
||||
|
||||
def on_tracks_loading_failed(self, playlist_id, error_message):
|
||||
"""Handle track loading failure"""
|
||||
if playlist_id == self.playlist.id and hasattr(self, 'track_table'):
|
||||
self.track_table.setRowCount(1)
|
||||
error_item = QTableWidgetItem(f"Error loading tracks: {error_message}")
|
||||
error_item.setFlags(error_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
||||
self.track_table.setItem(0, 0, error_item)
|
||||
self.track_table.setSpan(0, 0, 1, 4)
|
||||
|
||||
def refresh_track_table(self):
|
||||
"""Refresh the track table with loaded tracks"""
|
||||
if not hasattr(self, 'track_table'):
|
||||
return
|
||||
|
||||
self.track_table.setRowCount(len(self.playlist.tracks))
|
||||
self.track_table.clearSpans() # Remove any spans from loading state
|
||||
|
||||
# Populate table
|
||||
for row, track in enumerate(self.playlist.tracks):
|
||||
# Track name
|
||||
track_item = QTableWidgetItem(track.name)
|
||||
track_item.setFlags(track_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
||||
self.track_table.setItem(row, 0, track_item)
|
||||
|
||||
# Artist(s)
|
||||
artists = ", ".join(track.artists)
|
||||
artist_item = QTableWidgetItem(artists)
|
||||
artist_item.setFlags(artist_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
||||
self.track_table.setItem(row, 1, artist_item)
|
||||
|
||||
# Album
|
||||
album_item = QTableWidgetItem(track.album)
|
||||
album_item.setFlags(album_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
||||
self.track_table.setItem(row, 2, album_item)
|
||||
|
||||
# Duration
|
||||
duration = self.format_duration(track.duration_ms)
|
||||
duration_item = QTableWidgetItem(duration)
|
||||
duration_item.setFlags(duration_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
||||
self.track_table.setItem(row, 3, duration_item)
|
||||
|
||||
class PlaylistItem(QFrame):
|
||||
def __init__(self, name: str, track_count: int, sync_status: str, parent=None):
|
||||
view_details_clicked = pyqtSignal(object) # Signal to emit playlist object
|
||||
|
||||
def __init__(self, name: str, track_count: int, sync_status: str, playlist=None, parent=None):
|
||||
super().__init__(parent)
|
||||
self.name = name
|
||||
self.track_count = track_count
|
||||
self.sync_status = sync_status
|
||||
self.playlist = playlist # Store playlist object reference
|
||||
self.is_selected = False
|
||||
self.setup_ui()
|
||||
|
||||
|
|
@ -117,6 +492,7 @@ class PlaylistItem(QFrame):
|
|||
# Action button
|
||||
action_btn = QPushButton("View Details")
|
||||
action_btn.setFixedSize(100, 30)
|
||||
action_btn.clicked.connect(self.on_view_details_clicked)
|
||||
action_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background: transparent;
|
||||
|
|
@ -141,6 +517,11 @@ class PlaylistItem(QFrame):
|
|||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self.checkbox.setChecked(not self.checkbox.isChecked())
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def on_view_details_clicked(self):
|
||||
"""Handle View Details button click"""
|
||||
if self.playlist:
|
||||
self.view_details_clicked.emit(self.playlist)
|
||||
|
||||
class SyncOptionsPanel(QFrame):
|
||||
def __init__(self, parent=None):
|
||||
|
|
@ -227,10 +608,81 @@ class SyncPage(QWidget):
|
|||
self.plex_client = plex_client
|
||||
self.current_playlists = []
|
||||
self.playlist_loader = None
|
||||
|
||||
# Track cache for performance
|
||||
self.track_cache = {} # playlist_id -> tracks
|
||||
|
||||
# Thread pool for async operations (like downloads.py)
|
||||
self.thread_pool = QThreadPool()
|
||||
self.thread_pool.setMaxThreadCount(3) # Limit concurrent Spotify API calls
|
||||
|
||||
self.setup_ui()
|
||||
|
||||
# Start loading playlists asynchronously after UI is ready
|
||||
QTimer.singleShot(100, self.load_playlists_async)
|
||||
# Don't auto-load on startup, but do auto-load when page becomes visible
|
||||
self.show_initial_state()
|
||||
self.playlists_loaded = False
|
||||
|
||||
def showEvent(self, event):
|
||||
"""Auto-load playlists when page becomes visible (but not during app startup)"""
|
||||
super().showEvent(event)
|
||||
|
||||
# Only auto-load once and only if we have a spotify client
|
||||
if (not self.playlists_loaded and
|
||||
self.spotify_client and
|
||||
self.spotify_client.is_authenticated()):
|
||||
|
||||
# Small delay to ensure UI is fully rendered
|
||||
QTimer.singleShot(100, self.auto_load_playlists)
|
||||
|
||||
def auto_load_playlists(self):
|
||||
"""Auto-load playlists with proper UI transition"""
|
||||
# Clear the welcome state first
|
||||
self.clear_playlists()
|
||||
|
||||
# Start loading (this will set playlists_loaded = True)
|
||||
self.load_playlists_async()
|
||||
|
||||
def show_initial_state(self):
|
||||
"""Show initial state with option to load playlists"""
|
||||
# Add welcome message to playlist area
|
||||
welcome_message = QLabel("Ready to sync playlists!\nClick 'Load Playlists' to get started.")
|
||||
welcome_message.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
welcome_message.setStyleSheet("""
|
||||
QLabel {
|
||||
color: #b3b3b3;
|
||||
font-size: 16px;
|
||||
padding: 60px;
|
||||
background: #282828;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #404040;
|
||||
line-height: 1.5;
|
||||
}
|
||||
""")
|
||||
|
||||
# Add load button
|
||||
load_btn = QPushButton("🎵 Load Playlists")
|
||||
load_btn.setFixedSize(200, 50)
|
||||
load_btn.clicked.connect(self.load_playlists_async)
|
||||
load_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background: #1db954;
|
||||
border: none;
|
||||
border-radius: 25px;
|
||||
color: #000000;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin-top: 20px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background: #1ed760;
|
||||
}
|
||||
""")
|
||||
|
||||
# Add them to the playlist layout
|
||||
if hasattr(self, 'playlist_layout'):
|
||||
self.playlist_layout.addWidget(welcome_message)
|
||||
self.playlist_layout.addWidget(load_btn)
|
||||
self.playlist_layout.addStretch()
|
||||
|
||||
def setup_ui(self):
|
||||
self.setStyleSheet("""
|
||||
|
|
@ -300,7 +752,7 @@ class SyncPage(QWidget):
|
|||
|
||||
self.refresh_btn = QPushButton("🔄 Refresh")
|
||||
self.refresh_btn.setFixedSize(100, 35)
|
||||
self.refresh_btn.clicked.connect(self.refresh_playlists)
|
||||
self.refresh_btn.clicked.connect(self.load_playlists_async)
|
||||
self.refresh_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background: #1db954;
|
||||
|
|
@ -490,9 +942,27 @@ class SyncPage(QWidget):
|
|||
if self.playlist_loader and self.playlist_loader.isRunning():
|
||||
return
|
||||
|
||||
# Mark as loaded to prevent duplicate auto-loading
|
||||
self.playlists_loaded = True
|
||||
|
||||
# Clear existing playlists
|
||||
self.clear_playlists()
|
||||
|
||||
# Add loading placeholder
|
||||
loading_label = QLabel("🔄 Loading playlists...")
|
||||
loading_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
loading_label.setStyleSheet("""
|
||||
QLabel {
|
||||
color: #b3b3b3;
|
||||
font-size: 14px;
|
||||
padding: 40px;
|
||||
background: #282828;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #404040;
|
||||
}
|
||||
""")
|
||||
self.playlist_layout.insertWidget(0, loading_label)
|
||||
|
||||
# Show loading state
|
||||
self.refresh_btn.setText("🔄 Loading...")
|
||||
self.refresh_btn.setEnabled(False)
|
||||
|
|
@ -510,22 +980,111 @@ class SyncPage(QWidget):
|
|||
"""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)
|
||||
item = PlaylistItem(playlist.name, playlist.total_tracks, sync_status, playlist)
|
||||
item.view_details_clicked.connect(self.show_playlist_details)
|
||||
|
||||
# Add subtle fade-in animation
|
||||
item.setStyleSheet(item.styleSheet() + "background: rgba(40, 40, 40, 0);")
|
||||
|
||||
# Insert before the stretch item
|
||||
self.playlist_layout.insertWidget(self.playlist_layout.count() - 1, item)
|
||||
self.current_playlists.append(playlist)
|
||||
|
||||
# Animate the item appearing
|
||||
self.animate_item_fade_in(item)
|
||||
|
||||
# Update log
|
||||
self.log_area.append(f"Added playlist: {playlist.name} ({playlist.total_tracks} tracks)")
|
||||
|
||||
def animate_item_fade_in(self, item):
|
||||
"""Add a subtle fade-in animation to playlist items"""
|
||||
# Start with reduced opacity
|
||||
item.setStyleSheet("""
|
||||
PlaylistItem {
|
||||
background: #282828;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #404040;
|
||||
opacity: 0.3;
|
||||
}
|
||||
PlaylistItem:hover {
|
||||
background: #333333;
|
||||
border: 1px solid #1db954;
|
||||
}
|
||||
""")
|
||||
|
||||
# Animate to full opacity after a short delay
|
||||
QTimer.singleShot(50, lambda: item.setStyleSheet("""
|
||||
PlaylistItem {
|
||||
background: #282828;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #404040;
|
||||
}
|
||||
PlaylistItem:hover {
|
||||
background: #333333;
|
||||
border: 1px solid #1db954;
|
||||
}
|
||||
"""))
|
||||
|
||||
def on_loading_finished(self, count):
|
||||
"""Handle completion of playlist loading"""
|
||||
# Remove loading placeholder if it exists
|
||||
for i in range(self.playlist_layout.count()):
|
||||
item = self.playlist_layout.itemAt(i)
|
||||
if item and item.widget() and isinstance(item.widget(), QLabel):
|
||||
if "Loading playlists" in item.widget().text():
|
||||
item.widget().deleteLater()
|
||||
break
|
||||
|
||||
self.refresh_btn.setText("🔄 Refresh")
|
||||
self.refresh_btn.setEnabled(True)
|
||||
self.log_area.append(f"✓ Loaded {count} Spotify playlists successfully")
|
||||
|
||||
# Start background preloading of tracks for smaller playlists
|
||||
self.start_background_preloading()
|
||||
|
||||
def start_background_preloading(self):
|
||||
"""Start background preloading of tracks for smaller playlists"""
|
||||
if not self.spotify_client:
|
||||
return
|
||||
|
||||
# Preload tracks for playlists with < 100 tracks to improve responsiveness
|
||||
for playlist in self.current_playlists:
|
||||
if (playlist.total_tracks < 100 and
|
||||
playlist.id not in self.track_cache and
|
||||
not playlist.tracks):
|
||||
|
||||
# Create background worker
|
||||
worker = TrackLoadingWorker(self.spotify_client, playlist.id, playlist.name)
|
||||
worker.signals.tracks_loaded.connect(self.on_background_tracks_loaded)
|
||||
# Don't connect error signals for background loading to avoid spam
|
||||
|
||||
# Submit with low priority
|
||||
self.thread_pool.start(worker)
|
||||
|
||||
# Add delay between requests to be nice to Spotify API
|
||||
QTimer.singleShot(2000, lambda: None) # 2 second delay
|
||||
|
||||
def on_background_tracks_loaded(self, playlist_id, tracks):
|
||||
"""Handle background track loading completion"""
|
||||
# Cache the tracks for future use
|
||||
self.track_cache[playlist_id] = tracks
|
||||
|
||||
# Update the playlist object if we can find it
|
||||
for playlist in self.current_playlists:
|
||||
if playlist.id == playlist_id:
|
||||
playlist.tracks = tracks
|
||||
break
|
||||
|
||||
def on_loading_failed(self, error_msg):
|
||||
"""Handle playlist loading failure"""
|
||||
# Remove loading placeholder if it exists
|
||||
for i in range(self.playlist_layout.count()):
|
||||
item = self.playlist_layout.itemAt(i)
|
||||
if item and item.widget() and isinstance(item.widget(), QLabel):
|
||||
if "Loading playlists" in item.widget().text():
|
||||
item.widget().deleteLater()
|
||||
break
|
||||
|
||||
self.refresh_btn.setText("🔄 Refresh")
|
||||
self.refresh_btn.setEnabled(True)
|
||||
self.log_area.append(f"✗ Failed to load playlists: {error_msg}")
|
||||
|
|
@ -551,7 +1110,7 @@ class SyncPage(QWidget):
|
|||
]
|
||||
|
||||
for name, count, status in playlists:
|
||||
item = PlaylistItem(name, count, status)
|
||||
item = PlaylistItem(name, count, status, None) # No playlist object for placeholders
|
||||
self.playlist_layout.addWidget(item)
|
||||
|
||||
def refresh_playlists(self):
|
||||
|
|
@ -567,12 +1126,18 @@ class SyncPage(QWidget):
|
|||
# Use the async loader
|
||||
self.load_playlists_async()
|
||||
|
||||
def show_playlist_details(self, playlist):
|
||||
"""Show playlist details modal"""
|
||||
if playlist:
|
||||
modal = PlaylistDetailsModal(playlist, self)
|
||||
modal.exec()
|
||||
|
||||
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
|
||||
# Remove all items including welcome state
|
||||
for i in reversed(range(self.playlist_layout.count())):
|
||||
item = self.playlist_layout.itemAt(i)
|
||||
if item.widget():
|
||||
|
|
|
|||
Loading…
Reference in a new issue