diff --git a/core/plex_client.py b/core/plex_client.py index 1658fa4c..f24fbe85 100644 --- a/core/plex_client.py +++ b/core/plex_client.py @@ -266,14 +266,86 @@ class PlexClient: logger.error(f"Error creating playlist '{name}': {e}") return False + def copy_playlist(self, source_name: str, target_name: str) -> bool: + """Copy a playlist to create a backup""" + if not self.ensure_connection(): + return False + + try: + # Get the source playlist + source_playlist = self.server.playlist(source_name) + + # Get all tracks from source playlist + source_tracks = source_playlist.items() + logger.debug(f"Retrieved {len(source_tracks) if source_tracks else 0} tracks from source playlist") + + # Validate tracks + if not source_tracks: + logger.warning(f"Source playlist '{source_name}' has no tracks to copy") + return False + + # Filter for valid track objects + valid_tracks = [track for track in source_tracks if hasattr(track, 'ratingKey')] + logger.debug(f"Found {len(valid_tracks)} valid tracks with ratingKeys") + + if not valid_tracks: + logger.error(f"No valid tracks found in source playlist '{source_name}'") + return False + + # Delete target playlist if it exists (for overwriting backup) + try: + target_playlist = self.server.playlist(target_name) + target_playlist.delete() + logger.info(f"Deleted existing backup playlist '{target_name}'") + except NotFound: + pass # Target doesn't exist, which is fine + + # Create new playlist with copied tracks + try: + self.server.createPlaylist(target_name, items=valid_tracks) + logger.info(f"✅ Created backup playlist '{target_name}' with {len(valid_tracks)} tracks") + return True + except Exception as create_error: + logger.error(f"Failed to create backup playlist: {create_error}") + # Try alternative method + try: + new_playlist = self.server.createPlaylist(target_name) + new_playlist.addItems(valid_tracks) + logger.info(f"✅ Created backup playlist '{target_name}' with {len(valid_tracks)} tracks (alternative method)") + return True + except Exception as alt_error: + logger.error(f"Alternative backup creation also failed: {alt_error}") + return False + + except NotFound: + logger.error(f"Source playlist '{source_name}' not found") + return False + except Exception as e: + logger.error(f"Error copying playlist '{source_name}' to '{target_name}': {e}") + return False + def update_playlist(self, playlist_name: str, tracks: List[PlexTrackInfo]) -> bool: if not self.ensure_connection(): return False try: existing_playlist = self.server.playlist(playlist_name) - existing_playlist.delete() + # Check if backup is enabled in config + from config.settings import config_manager + create_backup = config_manager.get('playlist_sync.create_backup', True) + + if create_backup: + backup_name = f"{playlist_name} Backup" + logger.info(f"🛡️ Creating backup playlist '{backup_name}' before sync") + + if self.copy_playlist(playlist_name, backup_name): + logger.info(f"✅ Backup created successfully") + else: + logger.warning(f"⚠️ Failed to create backup, continuing with sync") + + # Delete original and recreate + existing_playlist.delete() return self.create_playlist(playlist_name, tracks) except NotFound: diff --git a/ui/pages/settings.py b/ui/pages/settings.py index b0482060..e9771da8 100644 --- a/ui/pages/settings.py +++ b/ui/pages/settings.py @@ -602,6 +602,11 @@ class SettingsPage(QWidget): if hasattr(self, 'embed_album_art_checkbox'): self.embed_album_art_checkbox.setChecked(metadata_config.get('embed_album_art', True)) + # Load playlist sync settings + playlist_sync_config = config_manager.get('playlist_sync', {}) + if hasattr(self, 'create_backup_checkbox'): + self.create_backup_checkbox.setChecked(playlist_sync_config.get('create_backup', True)) + except Exception as e: QMessageBox.warning(self, "Error", f"Failed to load configuration: {e}") @@ -1234,7 +1239,55 @@ class SettingsPage(QWidget): api_layout.addLayout(test_layout) + # Logging Settings + logging_group = SettingsGroup("Logging Settings") + logging_layout = QVBoxLayout(logging_group) + logging_layout.setContentsMargins(16, 20, 16, 16) + logging_layout.setSpacing(12) + + # Log level (read-only) + log_level_layout = QHBoxLayout() + log_level_label = QLabel("Log Level:") + log_level_label.setStyleSheet("color: #ffffff; font-size: 12px;") + + self.log_level_display = QLabel("DEBUG") + self.log_level_display.setStyleSheet(""" + color: #b3b3b3; + font-size: 11px; + background-color: #404040; + border: 1px solid #606060; + border-radius: 4px; + padding: 8px; + """) + self.log_level_display.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) + + log_level_layout.addWidget(log_level_label) + log_level_layout.addWidget(self.log_level_display) + + # Log file path (read-only) + log_path_container = QVBoxLayout() + log_path_label = QLabel("Log File Path:") + log_path_label.setStyleSheet("color: #ffffff; font-size: 12px;") + log_path_container.addWidget(log_path_label) + + self.log_path_display = QLabel("logs/app.log") + self.log_path_display.setStyleSheet(""" + color: #b3b3b3; + font-size: 11px; + background-color: #404040; + border: 1px solid #606060; + border-radius: 4px; + padding: 8px; + font-family: 'Courier New', monospace; + """) + self.log_path_display.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) + log_path_container.addWidget(self.log_path_display) + + logging_layout.addLayout(log_level_layout) + logging_layout.addLayout(log_path_container) + layout.addWidget(api_group) + layout.addWidget(logging_group) layout.addStretch() return column @@ -1337,53 +1390,6 @@ class SettingsPage(QWidget): database_layout.addLayout(workers_layout) database_layout.addWidget(workers_help) - # Logging Settings - logging_group = SettingsGroup("Logging Settings") - logging_layout = QVBoxLayout(logging_group) - logging_layout.setContentsMargins(16, 20, 16, 16) - logging_layout.setSpacing(12) - - # Log level (read-only) - log_level_layout = QHBoxLayout() - log_level_label = QLabel("Log Level:") - log_level_label.setStyleSheet("color: #ffffff; font-size: 12px;") - - self.log_level_display = QLabel("DEBUG") - self.log_level_display.setStyleSheet(""" - color: #b3b3b3; - font-size: 11px; - background-color: #404040; - border: 1px solid #606060; - border-radius: 4px; - padding: 8px; - """) - self.log_level_display.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) - - log_level_layout.addWidget(log_level_label) - log_level_layout.addWidget(self.log_level_display) - - # Log file path (read-only) - log_path_container = QVBoxLayout() - log_path_label = QLabel("Log File Path:") - log_path_label.setStyleSheet("color: #ffffff; font-size: 12px;") - log_path_container.addWidget(log_path_label) - - self.log_path_display = QLabel("logs/app.log") - self.log_path_display.setStyleSheet(""" - color: #b3b3b3; - font-size: 11px; - background-color: #404040; - border: 1px solid #606060; - border-radius: 4px; - padding: 8px; - font-family: 'Courier New', monospace; - """) - self.log_path_display.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) - log_path_container.addWidget(self.log_path_display) - - logging_layout.addLayout(log_level_layout) - logging_layout.addLayout(log_path_container) - # Metadata Enhancement Settings metadata_group = SettingsGroup("🎵 Metadata Enhancement") metadata_layout = QVBoxLayout(metadata_group) @@ -1452,10 +1458,53 @@ class SettingsPage(QWidget): metadata_layout.addLayout(supported_formats_layout) metadata_layout.addWidget(help_text) + # Playlist Sync Settings + playlist_sync_group = SettingsGroup("🎶 Playlist Sync") + playlist_sync_layout = QVBoxLayout(playlist_sync_group) + playlist_sync_layout.setContentsMargins(16, 20, 16, 16) + playlist_sync_layout.setSpacing(12) + + # Create backup checkbox + self.create_backup_checkbox = QCheckBox("🛡️ Create backup of existing playlists before sync") + self.create_backup_checkbox.setChecked(True) + self.create_backup_checkbox.setStyleSheet(""" + QCheckBox { + color: #ffffff; + font-size: 12px; + spacing: 8px; + } + QCheckBox::indicator { + width: 16px; + height: 16px; + border-radius: 3px; + border: 2px solid #606060; + background-color: #404040; + } + QCheckBox::indicator:checked { + background-color: #1db954; + border-color: #1db954; + image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEzLjUgNC41TDYuNSAxMS41TDIuNSA3LjUiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+Cjwvc3ZnPgo=); + } + QCheckBox::indicator:hover { + border-color: #1db954; + } + """) + + # Help text for playlist sync + playlist_help_text = QLabel("When enabled, existing Plex playlists will be backed up as '[Playlist Name] Backup' before being overwritten during sync. Only one backup per playlist is maintained.") + playlist_help_text.setStyleSheet("color: #888888; font-size: 10px; font-style: italic;") + playlist_help_text.setWordWrap(True) + + playlist_sync_layout.addWidget(self.create_backup_checkbox) + playlist_sync_layout.addWidget(playlist_help_text) + + # Add to form inputs for saving + self.form_inputs['playlist_sync.create_backup'] = self.create_backup_checkbox + layout.addWidget(download_group) layout.addWidget(database_group) layout.addWidget(metadata_group) - layout.addWidget(logging_group) + layout.addWidget(playlist_sync_group) layout.addStretch() # Push content to top, prevent stretching return column