playlist backup functionality
This commit is contained in:
parent
b1e1dda003
commit
845f8e995f
2 changed files with 170 additions and 49 deletions
|
|
@ -266,14 +266,86 @@ class PlexClient:
|
||||||
logger.error(f"Error creating playlist '{name}': {e}")
|
logger.error(f"Error creating playlist '{name}': {e}")
|
||||||
return False
|
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:
|
def update_playlist(self, playlist_name: str, tracks: List[PlexTrackInfo]) -> bool:
|
||||||
if not self.ensure_connection():
|
if not self.ensure_connection():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
existing_playlist = self.server.playlist(playlist_name)
|
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)
|
return self.create_playlist(playlist_name, tracks)
|
||||||
|
|
||||||
except NotFound:
|
except NotFound:
|
||||||
|
|
|
||||||
|
|
@ -602,6 +602,11 @@ class SettingsPage(QWidget):
|
||||||
if hasattr(self, 'embed_album_art_checkbox'):
|
if hasattr(self, 'embed_album_art_checkbox'):
|
||||||
self.embed_album_art_checkbox.setChecked(metadata_config.get('embed_album_art', True))
|
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:
|
except Exception as e:
|
||||||
QMessageBox.warning(self, "Error", f"Failed to load configuration: {e}")
|
QMessageBox.warning(self, "Error", f"Failed to load configuration: {e}")
|
||||||
|
|
||||||
|
|
@ -1234,7 +1239,55 @@ class SettingsPage(QWidget):
|
||||||
|
|
||||||
api_layout.addLayout(test_layout)
|
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(api_group)
|
||||||
|
layout.addWidget(logging_group)
|
||||||
layout.addStretch()
|
layout.addStretch()
|
||||||
|
|
||||||
return column
|
return column
|
||||||
|
|
@ -1337,53 +1390,6 @@ class SettingsPage(QWidget):
|
||||||
database_layout.addLayout(workers_layout)
|
database_layout.addLayout(workers_layout)
|
||||||
database_layout.addWidget(workers_help)
|
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 Enhancement Settings
|
||||||
metadata_group = SettingsGroup("🎵 Metadata Enhancement")
|
metadata_group = SettingsGroup("🎵 Metadata Enhancement")
|
||||||
metadata_layout = QVBoxLayout(metadata_group)
|
metadata_layout = QVBoxLayout(metadata_group)
|
||||||
|
|
@ -1452,10 +1458,53 @@ class SettingsPage(QWidget):
|
||||||
metadata_layout.addLayout(supported_formats_layout)
|
metadata_layout.addLayout(supported_formats_layout)
|
||||||
metadata_layout.addWidget(help_text)
|
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(download_group)
|
||||||
layout.addWidget(database_group)
|
layout.addWidget(database_group)
|
||||||
layout.addWidget(metadata_group)
|
layout.addWidget(metadata_group)
|
||||||
layout.addWidget(logging_group)
|
layout.addWidget(playlist_sync_group)
|
||||||
layout.addStretch() # Push content to top, prevent stretching
|
layout.addStretch() # Push content to top, prevent stretching
|
||||||
|
|
||||||
return column
|
return column
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue