""" Options dialog for Serien-Checker """ from PyQt5.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QTabWidget, QWidget, QLabel, QLineEdit, QPushButton, QComboBox, QCheckBox, QListWidget, QMessageBox, QGroupBox, QFormLayout, QFileDialog, QProgressDialog ) from PyQt5.QtCore import Qt import json import os import sys from ..database.db_manager import DatabaseManager from ..database.models import Series, DatePreference from ..utils.logger import setup_logger logger = setup_logger() class OptionsDialog(QDialog): """Options and settings dialog""" def __init__(self, parent, db_manager: DatabaseManager): super().__init__(parent) self.db = db_manager self.setWindowTitle("Optionen") self.setModal(True) self.resize(700, 500) self.init_ui() def init_ui(self): """Initialize UI""" layout = QVBoxLayout(self) # Tab widget tabs = QTabWidget() tabs.addTab(self._create_series_tab(), "Serien") tabs.addTab(self._create_settings_tab(), "Einstellungen") layout.addWidget(tabs) # Buttons button_layout = QHBoxLayout() button_layout.addStretch() close_btn = QPushButton("Schließen") close_btn.clicked.connect(self.accept) button_layout.addWidget(close_btn) layout.addLayout(button_layout) def _create_series_tab(self) -> QWidget: """Create series management tab""" widget = QWidget() layout = QVBoxLayout(widget) # Add series group add_group = QGroupBox("Serie hinzufügen") add_layout = QFormLayout(add_group) self.series_url_input = QLineEdit() self.series_url_input.setPlaceholderText("https://www.fernsehserien.de/.../episodenguide") add_layout.addRow("URL:", self.series_url_input) self.date_pref_combo = QComboBox() self.date_pref_combo.addItem("Deutsche Erstausstrahlung (frühestes DE-Datum)", DatePreference.DE_FIRST.value) self.date_pref_combo.addItem("Deutsche TV-Premiere", DatePreference.DE_TV.value) self.date_pref_combo.addItem("Deutsche Streaming-Premiere", DatePreference.DE_STREAMING.value) self.date_pref_combo.addItem("Deutsche Home-Media-Premiere", DatePreference.DE_HOME_MEDIA.value) self.date_pref_combo.addItem("Deutsche Synchronfassung", DatePreference.DE_SYNC.value) self.date_pref_combo.addItem("Original-Erstausstrahlung", DatePreference.ORIGINAL.value) add_layout.addRow("Bevorzugtes Datum:", self.date_pref_combo) add_btn = QPushButton("Serie hinzufügen") add_btn.clicked.connect(self.add_series) add_layout.addRow("", add_btn) layout.addWidget(add_group) # Manage series group manage_group = QGroupBox("Serien verwalten") manage_layout = QVBoxLayout(manage_group) self.series_list = QListWidget() self.load_series_list() manage_layout.addWidget(self.series_list) button_row = QHBoxLayout() edit_btn = QPushButton("Bearbeiten") edit_btn.clicked.connect(self.edit_series) button_row.addWidget(edit_btn) remove_btn = QPushButton("Entfernen") remove_btn.clicked.connect(self.remove_series) button_row.addWidget(remove_btn) manage_layout.addLayout(button_row) layout.addWidget(manage_group) return widget def _create_settings_tab(self) -> QWidget: """Create settings tab""" widget = QWidget() layout = QVBoxLayout(widget) # General settings general_group = QGroupBox("Allgemein") general_layout = QVBoxLayout(general_group) self.portable_mode_check = QCheckBox("Portable Modus (Datenbank im Programmverzeichnis)") current_portable = self.db.get_setting("portable_mode", "false") self.portable_mode_check.setChecked(current_portable == "true") general_layout.addWidget(self.portable_mode_check) layout.addWidget(general_group) # Update settings update_group = QGroupBox("Aktualisierung") update_layout = QVBoxLayout(update_group) self.auto_update_check = QCheckBox("Automatisch beim Start aktualisieren") auto_update = self.db.get_setting("auto_update", "false") self.auto_update_check.setChecked(auto_update == "true") update_layout.addWidget(self.auto_update_check) layout.addWidget(update_group) # Display settings display_group = QGroupBox("Anzeige") display_layout = QVBoxLayout(display_group) self.highlight_future_check = QCheckBox("Zukünftige Folgen grün markieren") highlight = self.db.get_setting("highlight_future", "true") self.highlight_future_check.setChecked(highlight == "true") display_layout.addWidget(self.highlight_future_check) layout.addWidget(display_group) # Import/Export settings import_group = QGroupBox("Import/Export") import_layout = QVBoxLayout(import_group) import_btn = QPushButton("Serien aus series_config.json importieren") import_btn.clicked.connect(self.import_from_config) import_layout.addWidget(import_btn) import_info = QLabel("Importiert Serien aus der alten series_config.json Datei.\n" "Duplikate werden automatisch übersprungen.") import_info.setWordWrap(True) import_info.setStyleSheet("color: gray; font-size: 10px;") import_layout.addWidget(import_info) layout.addWidget(import_group) layout.addStretch() # Save button save_btn = QPushButton("Einstellungen speichern") save_btn.clicked.connect(self.save_settings) layout.addWidget(save_btn) return widget def load_series_list(self): """Load series into list""" self.series_list.clear() series_list = self.db.get_all_series() for series in series_list: pref_name = self._get_date_pref_name(series.date_preference) self.series_list.addItem(f"{series.title} ({pref_name})") def _get_date_pref_name(self, pref: DatePreference) -> str: """Get display name for date preference""" names = { DatePreference.DE_FIRST: "DE Erst.", DatePreference.DE_TV: "DE TV", DatePreference.DE_STREAMING: "DE Stream", DatePreference.DE_HOME_MEDIA: "DE HMedia", DatePreference.DE_SYNC: "DE Sync", DatePreference.ORIGINAL: "Original" } return names.get(pref, "Unbekannt") def add_series(self): """Add a new series""" url = self.series_url_input.text().strip() if not url: QMessageBox.warning(self, "Fehler", "Bitte geben Sie eine URL ein") return if "fernsehserien.de" not in url: QMessageBox.warning(self, "Fehler", "Bitte geben Sie eine gültige fernsehserien.de URL ein") return # Get date preference date_pref_value = self.date_pref_combo.currentData() date_pref = DatePreference(date_pref_value) # Extract series title from URL (placeholder) # In real implementation, this would scrape the title import re match = re.search(r'fernsehserien\.de/([^/]+)', url) if match: title = match.group(1).replace('-', ' ').title() else: title = "Unbekannte Serie" # Check if series already exists existing = self.db.get_all_series() if any(s.url == url for s in existing): QMessageBox.warning(self, "Fehler", "Diese Serie wurde bereits hinzugefügt") return # Add series series = Series( id=None, title=title, url=url, date_preference=date_pref ) try: series_id = self.db.add_series(series) self.series_url_input.clear() self.load_series_list() # Automatically update the newly added series reply = QMessageBox.question( self, "Serie hinzugefügt", f"Serie '{title}' wurde hinzugefügt.\n\n" "Möchten Sie die Episodendaten jetzt abrufen?", QMessageBox.Yes | QMessageBox.No ) if reply == QMessageBox.Yes: self._auto_update_series(series_id) except Exception as e: logger.error(f"Error adding series: {e}") QMessageBox.critical(self, "Fehler", f"Serie konnte nicht hinzugefügt werden:\n{str(e)}") def edit_series(self): """Edit selected series""" current_item = self.series_list.currentItem() if not current_item: QMessageBox.warning(self, "Fehler", "Bitte wählen Sie eine Serie aus") return # Extract series title text = current_item.text() title = text.split(" (")[0] series_list = self.db.get_all_series() series = next((s for s in series_list if s.title == title), None) if not series: return # Show edit dialog (simplified) QMessageBox.information( self, "Info", f"Bearbeiten von '{series.title}' wird noch implementiert" ) def remove_series(self): """Remove selected series""" current_item = self.series_list.currentItem() if not current_item: QMessageBox.warning(self, "Fehler", "Bitte wählen Sie eine Serie aus") return # Extract series title text = current_item.text() title = text.split(" (")[0] series_list = self.db.get_all_series() series = next((s for s in series_list if s.title == title), None) if not series: return reply = QMessageBox.question( self, "Serie entfernen", f"Möchten Sie '{series.title}' wirklich entfernen?", QMessageBox.Yes | QMessageBox.No ) if reply == QMessageBox.Yes: self.db.delete_series(series.id) self.load_series_list() def _auto_update_series(self, series_id: int): """Automatically update newly added series""" from ..scraper.browser_scraper import BrowserScraper, SeriesUpdater from ..utils.threading import UpdateWorker from .widgets import ProgressDialog, UpdateResultDialog # Create progress dialog progress = ProgressDialog(self, "Serie wird aktualisiert") progress.set_determinate() progress.show() # Create worker scraper = BrowserScraper() def do_update(): # Progress callback def progress_callback(percent, message): worker.signals.progress.emit(percent, message) updater = SeriesUpdater(self.db, scraper, progress_callback=progress_callback) return updater.update_series(series_id) worker = UpdateWorker(do_update) def on_progress(percent, message): progress.set_progress(percent, message) def on_finished(stats): progress.close() UpdateResultDialog(self, stats).exec_() self.load_series_list() def on_error(error, traceback): progress.close() QMessageBox.critical(self, "Fehler", f"Aktualisierung fehlgeschlagen:\n{error}") worker.signals.progress.connect(on_progress) worker.signals.finished.connect(on_finished) worker.signals.error.connect(on_error) worker.start() # Keep reference to prevent garbage collection self._update_worker = worker def save_settings(self): """Save settings to database""" self.db.set_setting("portable_mode", "true" if self.portable_mode_check.isChecked() else "false") self.db.set_setting("auto_update", "true" if self.auto_update_check.isChecked() else "false") self.db.set_setting("highlight_future", "true" if self.highlight_future_check.isChecked() else "false") QMessageBox.information(self, "Erfolg", "Einstellungen wurden gespeichert") def import_from_config(self): """Import series from old series_config.json file""" # Get directory of the executable (or script) if getattr(sys, 'frozen', False): # Running as compiled executable app_dir = os.path.dirname(sys.executable) else: # Running as script app_dir = os.getcwd() default_path = os.path.join(app_dir, "series_config.json") # File dialog to select config file file_path, _ = QFileDialog.getOpenFileName( self, "series_config.json auswählen", default_path, "JSON Dateien (*.json);;Alle Dateien (*.*)" ) if not file_path: return try: # Load and parse JSON file with open(file_path, 'r', encoding='utf-8') as f: config = json.load(f) if 'series' not in config: QMessageBox.warning( self, "Fehler", "Ungültige Datei: 'series' Schlüssel nicht gefunden" ) return # Get existing series URLs to check for duplicates existing_series = self.db.get_all_series() existing_urls = {s.url for s in existing_series} # Import statistics imported_count = 0 skipped_count = 0 error_count = 0 errors = [] series_to_import = [] # Process each series in config for slug, series_data in config['series'].items(): try: # Construct URL from slug url = f"https://www.fernsehserien.de/{slug}/episodenguide" # Skip if already exists if url in existing_urls: skipped_count += 1 continue # Map date preference from German to enum date_pref = self._map_date_preference( series_data.get('date_preference', 'Bevorzuge Erstausstrahlung') ) # Get series title title = series_data.get('name', slug.replace('-', ' ').title()) # Create series object series = Series( id=None, title=title, url=url, date_preference=date_pref ) series_to_import.append(series) except Exception as e: error_count += 1 errors.append(f"{slug}: {str(e)}") logger.error(f"Error processing series {slug}: {e}") # Import series to database imported_series_ids = [] for series in series_to_import: try: series_id = self.db.add_series(series) imported_series_ids.append(series_id) imported_count += 1 except Exception as e: error_count += 1 errors.append(f"{series.title}: {str(e)}") logger.error(f"Error adding series {series.title}: {e}") # Reload series list self.load_series_list() # Show result dialog self._show_import_results( imported_count, skipped_count, error_count, errors, imported_series_ids ) except json.JSONDecodeError as e: QMessageBox.critical( self, "Fehler", f"Fehler beim Parsen der JSON-Datei:\n{str(e)}" ) except Exception as e: logger.error(f"Error importing config: {e}") QMessageBox.critical( self, "Fehler", f"Fehler beim Importieren:\n{str(e)}" ) def _map_date_preference(self, pref_string: str) -> DatePreference: """Map German date preference string to DatePreference enum""" mapping = { "Bevorzuge deutsche Synchro": DatePreference.DE_SYNC, "Bevorzuge Streaming": DatePreference.DE_STREAMING, "Bevorzuge Erstausstrahlung": DatePreference.DE_FIRST, } return mapping.get(pref_string, DatePreference.DE_FIRST) def _show_import_results(self, imported: int, skipped: int, errors: int, error_list: list, imported_ids: list): """Show import results dialog and ask about updating""" # Build result message msg = f"Import abgeschlossen:\n\n" msg += f"✓ {imported} Serien importiert\n" msg += f"⊘ {skipped} Duplikate übersprungen\n" if errors > 0: msg += f"✗ {errors} Fehler\n\n" msg += "Fehlerdetails:\n" for error in error_list[:5]: # Show first 5 errors msg += f" • {error}\n" if len(error_list) > 5: msg += f" ... und {len(error_list) - 5} weitere\n" # Show result message QMessageBox.information(self, "Import abgeschlossen", msg) # Ask about updating imported series if imported > 0: reply = QMessageBox.question( self, "Serien aktualisieren?", f"{imported} Serien wurden importiert.\n\n" "Möchten Sie die Episodendaten für alle importierten Serien jetzt abrufen?\n\n" f"Hinweis: Dies kann bei {imported} Serien einige Zeit dauern.", QMessageBox.Yes | QMessageBox.No ) if reply == QMessageBox.Yes: self._batch_update_series(imported_ids) def _batch_update_series(self, series_ids: list): """Batch update multiple series""" from ..scraper.browser_scraper import BrowserScraper, SeriesUpdater from ..utils.threading import UpdateWorker # Create progress dialog progress = QProgressDialog( "Serien werden aktualisiert...", "Abbrechen", 0, len(series_ids), self ) progress.setWindowTitle("Batch-Update") progress.setWindowModality(Qt.WindowModal) progress.show() # Create worker scraper = BrowserScraper() self._batch_update_cancelled = False def do_batch_update(): results = { 'success': 0, 'failed': 0, 'episodes_added': 0, 'episodes_updated': 0 } for i, series_id in enumerate(series_ids): if self._batch_update_cancelled: break # Update progress series = self.db.get_series(series_id) worker.signals.progress.emit( i, f"Aktualisiere {series.title}... ({i + 1}/{len(series_ids)})" ) try: updater = SeriesUpdater(self.db, scraper) stats = updater.update_series(series_id) results['success'] += 1 results['episodes_added'] += stats.get('episodes_added', 0) results['episodes_updated'] += stats.get('episodes_updated', 0) except Exception as e: logger.error(f"Error updating series {series_id}: {e}") results['failed'] += 1 return results worker = UpdateWorker(do_batch_update) def on_progress(value, message): progress.setValue(value) progress.setLabelText(message) def on_finished(stats): progress.close() msg = f"Batch-Update abgeschlossen:\n\n" msg += f"✓ {stats['success']} Serien erfolgreich aktualisiert\n" msg += f"✗ {stats['failed']} Fehlgeschlagen\n" msg += f"+ {stats['episodes_added']} neue Episoden\n" msg += f"⟳ {stats['episodes_updated']} Episoden aktualisiert" QMessageBox.information(self, "Update abgeschlossen", msg) self.load_series_list() def on_error(error, traceback): progress.close() QMessageBox.critical(self, "Fehler", f"Batch-Update fehlgeschlagen:\n{error}") def on_cancelled(): self._batch_update_cancelled = True progress.canceled.connect(on_cancelled) worker.signals.progress.connect(on_progress) worker.signals.finished.connect(on_finished) worker.signals.error.connect(on_error) worker.start() # Keep reference to prevent garbage collection self._batch_update_worker = worker