Kompletter Rewrite
This commit is contained in:
599
serien_checker/ui/options_dialog.py
Normal file
599
serien_checker/ui/options_dialog.py
Normal file
@@ -0,0 +1,599 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user