Kompletter Rewrite

This commit is contained in:
Akamaru
2025-12-21 14:35:08 +01:00
parent 79b8aa2a34
commit 5fa6bfeb62
27 changed files with 3946 additions and 1197 deletions

View File

@@ -0,0 +1 @@
"""PyQt5 user interface components"""

View File

@@ -0,0 +1,622 @@
"""
Main window for Serien-Checker application
"""
from PyQt5.QtWidgets import (
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QSplitter,
QListWidget, QTableWidget, QTableWidgetItem, QLabel, QPushButton,
QStatusBar, QMessageBox, QMenu, QAction, QHeaderView, QGroupBox
)
from PyQt5.QtCore import Qt, QUrl
from PyQt5.QtGui import QDesktopServices, QColor, QIcon
from datetime import datetime
from typing import Optional
from ..database.db_manager import DatabaseManager
from ..database.models import Series, Season, Episode, DatePreference
from ..utils.logger import setup_logger
from ..utils.threading import UpdateWorker
from .options_dialog import OptionsDialog
logger = setup_logger()
class MainWindow(QMainWindow):
"""Main application window with 3-column layout"""
def __init__(self, db_manager: DatabaseManager):
super().__init__()
self.db = db_manager
self.current_series: Optional[Series] = None
self.current_season: Optional[Season] = None
self.worker = None # Keep reference to worker thread
self.init_ui()
self.load_series_list()
def init_ui(self):
"""Initialize user interface"""
self.setWindowTitle("Serien-Checker")
self.setGeometry(100, 100, 1400, 800)
# Set window icon if available
try:
import sys
from pathlib import Path
if getattr(sys, 'frozen', False):
# Running as EXE - PyInstaller extracts to _MEIPASS
if hasattr(sys, '_MEIPASS'):
icon_path = Path(sys._MEIPASS) / "icon.ico"
else:
icon_path = Path(sys.executable).parent / "icon.ico"
else:
# Running as script
icon_path = Path(__file__).parent.parent.parent / "icon.ico"
if icon_path.exists():
self.setWindowIcon(QIcon(str(icon_path)))
except Exception:
pass
# Central widget
central_widget = QWidget()
self.setCentralWidget(central_widget)
# Main layout
main_layout = QVBoxLayout(central_widget)
# Create splitter for 3-column layout
splitter = QSplitter(Qt.Horizontal)
# Left column: Series list
self.series_list_widget = self._create_series_list()
splitter.addWidget(self.series_list_widget)
# Middle column: Episode list
self.episode_list_widget = self._create_episode_list()
splitter.addWidget(self.episode_list_widget)
# Right column: Series info
self.info_widget = self._create_info_panel()
splitter.addWidget(self.info_widget)
# Set initial splitter sizes (30% - 40% - 30%)
splitter.setSizes([400, 600, 400])
main_layout.addWidget(splitter)
# Status bar
self.status_bar = QStatusBar()
self.setStatusBar(self.status_bar)
self.status_label = QLabel("Bereit")
self.last_update_label = QLabel("Letztes Update: Nie")
self.new_episodes_label = QLabel("Neue Folgen: 0")
self.status_bar.addWidget(self.status_label)
self.status_bar.addPermanentWidget(self.new_episodes_label)
self.status_bar.addPermanentWidget(self.last_update_label)
# Menu bar
self._create_menu_bar()
def _create_series_list(self) -> QWidget:
"""Create series list widget"""
widget = QWidget()
layout = QVBoxLayout(widget)
# Title
title = QLabel("Serien")
title.setStyleSheet("font-weight: bold; font-size: 14px;")
layout.addWidget(title)
# List widget
self.series_list = QListWidget()
self.series_list.currentItemChanged.connect(self.on_series_selected)
self.series_list.setContextMenuPolicy(Qt.CustomContextMenu)
self.series_list.customContextMenuRequested.connect(self.show_series_context_menu)
layout.addWidget(self.series_list)
# Add series button
add_btn = QPushButton("+ Serie hinzufügen")
add_btn.clicked.connect(self.add_series)
layout.addWidget(add_btn)
return widget
def _create_episode_list(self) -> QWidget:
"""Create episode list widget"""
widget = QWidget()
layout = QVBoxLayout(widget)
# Title
title = QLabel("Folgen")
title.setStyleSheet("font-weight: bold; font-size: 14px;")
layout.addWidget(title)
# Table widget
self.episode_table = QTableWidget()
self.episode_table.setColumnCount(5)
self.episode_table.setHorizontalHeaderLabels(["Nr.", "Staffel", "Folge", "Titel", "Datum"])
self.episode_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.Stretch) # Titel column stretches
self.episode_table.setSelectionBehavior(QTableWidget.SelectRows)
self.episode_table.setEditTriggers(QTableWidget.NoEditTriggers)
self.episode_table.verticalHeader().setVisible(False) # Hide row numbers (1, 2, 3, 4...)
layout.addWidget(self.episode_table)
return widget
def _create_info_panel(self) -> QWidget:
"""Create series info panel"""
widget = QWidget()
layout = QVBoxLayout(widget)
# Title
title = QLabel("Informationen")
title.setStyleSheet("font-weight: bold; font-size: 14px;")
layout.addWidget(title)
# Info group
info_group = QGroupBox("Serie")
info_layout = QVBoxLayout(info_group)
self.info_title = QLabel("Keine Serie ausgewählt")
self.info_title.setWordWrap(True)
self.info_title.setStyleSheet("font-weight: bold;")
info_layout.addWidget(self.info_title)
self.info_seasons = QLabel("")
info_layout.addWidget(self.info_seasons)
self.info_episodes = QLabel("")
info_layout.addWidget(self.info_episodes)
self.info_link = QLabel("")
self.info_link.setOpenExternalLinks(True)
self.info_link.setTextFormat(Qt.RichText)
self.info_link.setTextInteractionFlags(Qt.TextBrowserInteraction)
info_layout.addWidget(self.info_link)
info_layout.addStretch()
layout.addWidget(info_group)
# Seasons group
seasons_group = QGroupBox("Staffeln")
seasons_layout = QVBoxLayout(seasons_group)
self.seasons_list = QListWidget()
self.seasons_list.currentItemChanged.connect(self.on_season_selected)
seasons_layout.addWidget(self.seasons_list)
layout.addWidget(seasons_group)
return widget
def _create_menu_bar(self):
"""Create menu bar"""
menubar = self.menuBar()
# File menu
file_menu = menubar.addMenu("Datei")
add_action = QAction("Serie hinzufügen", self)
add_action.triggered.connect(self.add_series)
file_menu.addAction(add_action)
file_menu.addSeparator()
exit_action = QAction("Beenden", self)
exit_action.triggered.connect(self.close)
file_menu.addAction(exit_action)
# Series menu
series_menu = menubar.addMenu("Serien")
update_action = QAction("Aktualisieren", self)
update_action.triggered.connect(self.update_current_series)
series_menu.addAction(update_action)
update_all_action = QAction("Alle aktualisieren", self)
update_all_action.triggered.connect(self.update_all_series)
series_menu.addAction(update_all_action)
# Settings menu
settings_menu = menubar.addMenu("Einstellungen")
options_action = QAction("Optionen", self)
options_action.triggered.connect(self.show_options)
settings_menu.addAction(options_action)
# Help menu
help_menu = menubar.addMenu("Hilfe")
about_action = QAction("Über", self)
about_action.triggered.connect(self.show_about)
help_menu.addAction(about_action)
def load_series_list(self):
"""Load all series into the list"""
self.series_list.clear()
series_list = self.db.get_all_series()
for series in series_list:
# Check if series has future episodes
has_new = self.db.has_future_episodes(series.id)
title = f"⭐️ {series.title}" if has_new else series.title
self.series_list.addItem(title)
self.status_label.setText(f"{len(series_list)} Serien geladen")
def on_series_selected(self, current, previous):
"""Handle series selection"""
if not current:
return
series_title = current.text()
# Remove star emoji if present
series_title = series_title.replace("⭐️ ", "")
series_list = self.db.get_all_series()
self.current_series = next((s for s in series_list if s.title == series_title), None)
if self.current_series:
self.load_series_info()
self.load_seasons_list()
# Show recent episodes automatically
self.load_recent_episodes()
def load_series_info(self):
"""Load series information panel"""
if not self.current_series:
return
self.info_title.setText(self.current_series.title)
seasons = self.db.get_seasons_by_series(self.current_series.id)
self.info_seasons.setText(f"Staffeln: {len(seasons)}")
episode_count = self.db.get_episode_count(self.current_series.id)
self.info_episodes.setText(f"Folgen: {episode_count}")
self.info_link.setText(f'<a href="{self.current_series.url}">fernsehserien.de öffnen</a>')
# Update last update time
if self.current_series.last_updated:
last_update_str = self.current_series.last_updated.strftime("%d.%m.%Y %H:%M")
self.last_update_label.setText(f"Letztes Update: {last_update_str}")
def load_seasons_list(self):
"""Load seasons list"""
if not self.current_series:
return
self.seasons_list.clear()
# Add "Neuste Folgen" as first item
self.seasons_list.addItem("Neuste Folgen")
# Add regular seasons
seasons = self.db.get_seasons_by_series(self.current_series.id)
for season in seasons:
self.seasons_list.addItem(season.name)
# Auto-select "Neuste Folgen" by default
self.seasons_list.setCurrentRow(0)
def on_season_selected(self, current, previous):
"""Handle season selection"""
if not current or not self.current_series:
return
season_name = current.text()
# Check if "Neuste Folgen" was selected
if season_name == "Neuste Folgen":
self.current_season = None # No specific season
self.load_recent_episodes()
return
# Regular season selection
seasons = self.db.get_seasons_by_series(self.current_series.id)
self.current_season = next((s for s in seasons if s.name == season_name), None)
if self.current_season:
self.load_episodes()
def load_recent_episodes(self):
"""Load 20 most recent episodes for current series"""
if not self.current_series:
return
recent_episodes = self.db.get_recent_episodes(self.current_series.id, limit=20)
self.episode_table.setRowCount(len(recent_episodes))
now = datetime.now()
for row, (season, episode) in enumerate(recent_episodes):
# Nr. (fortlaufende Gesamtnummer)
nr_text = str(episode.episode_number) if episode.episode_number else ""
nr_item = QTableWidgetItem(nr_text)
self.episode_table.setItem(row, 0, nr_item)
# Staffel
season_item = QTableWidgetItem(season.name)
self.episode_table.setItem(row, 1, season_item)
# Folge (Episodennummer der Staffel)
episode_item = QTableWidgetItem(episode.episode_code)
self.episode_table.setItem(row, 2, episode_item)
# Title
title_item = QTableWidgetItem(episode.title)
self.episode_table.setItem(row, 3, title_item)
# Date
date_str = episode.comparison_date.strftime("%d.%m.%Y") if episode.comparison_date else ""
date_item = QTableWidgetItem(date_str)
self.episode_table.setItem(row, 4, date_item)
# Highlight future episodes in green
highlight_enabled = self.db.get_setting("highlight_future", "true") == "true"
if highlight_enabled and episode.comparison_date and episode.comparison_date > now:
green = QColor(200, 255, 200)
nr_item.setBackground(green)
season_item.setBackground(green)
episode_item.setBackground(green)
title_item.setBackground(green)
date_item.setBackground(green)
def load_episodes(self):
"""Load episodes for current season"""
if not self.current_season:
return
episodes = self.db.get_episodes_by_season(self.current_season.id)
self.episode_table.setRowCount(len(episodes))
now = datetime.now()
for row, episode in enumerate(episodes):
# Nr. (episode_number from fernsehserien.de)
nr_text = str(episode.episode_number) if episode.episode_number else ""
nr_item = QTableWidgetItem(nr_text)
self.episode_table.setItem(row, 0, nr_item)
# Staffel (current season name)
season_item = QTableWidgetItem(self.current_season.name)
self.episode_table.setItem(row, 1, season_item)
# Folge (episode code)
episode_item = QTableWidgetItem(episode.episode_code)
self.episode_table.setItem(row, 2, episode_item)
# Title
title_item = QTableWidgetItem(episode.title)
self.episode_table.setItem(row, 3, title_item)
# Date
date_str = episode.comparison_date.strftime("%d.%m.%Y") if episode.comparison_date else ""
date_item = QTableWidgetItem(date_str)
self.episode_table.setItem(row, 4, date_item)
# Highlight future episodes in green
highlight_enabled = self.db.get_setting("highlight_future", "true") == "true"
if highlight_enabled and episode.comparison_date and episode.comparison_date > now:
green = QColor(200, 255, 200)
nr_item.setBackground(green)
season_item.setBackground(green)
episode_item.setBackground(green)
title_item.setBackground(green)
date_item.setBackground(green)
def show_series_context_menu(self, position):
"""Show context menu for series list"""
menu = QMenu()
update_action = QAction("Aktualisieren", self)
update_action.triggered.connect(self.update_current_series)
menu.addAction(update_action)
settings_action = QAction("Einstellungen", self)
settings_action.triggered.connect(self.show_series_settings)
menu.addAction(settings_action)
remove_action = QAction("Entfernen", self)
remove_action.triggered.connect(self.remove_current_series)
menu.addAction(remove_action)
menu.exec_(self.series_list.mapToGlobal(position))
def add_series(self):
"""Add a new series"""
from .options_dialog import OptionsDialog
dialog = OptionsDialog(self, self.db)
if dialog.exec_():
self.load_series_list()
def update_current_series(self):
"""Update currently selected series"""
if not self.current_series:
QMessageBox.warning(self, "Warnung", "Bitte wählen Sie zuerst eine Serie aus")
return
from ..scraper.browser_scraper import BrowserScraper, SeriesUpdater
from ..utils.threading import UpdateWorker
from .widgets import ProgressDialog, UpdateResultDialog
# Create progress dialog
progress = ProgressDialog(self, "Serie aktualisieren")
progress.set_determinate() # Use determinate mode for progress bar
progress.show()
# Create worker
scraper = BrowserScraper()
def do_update():
# Progress callback that will be called from the worker thread
def progress_callback(percent, message):
self.worker.signals.progress.emit(percent, message)
updater = SeriesUpdater(self.db, scraper, progress_callback=progress_callback)
return updater.update_series(self.current_series.id)
self.worker = UpdateWorker(do_update)
def on_progress(percent, message):
progress.set_progress(percent, message)
def on_finished(stats):
progress.close()
UpdateResultDialog(self, stats).exec_()
# Reload UI
self.load_series_info()
self.load_seasons_list()
if self.current_season:
self.load_episodes()
self.worker = None # Clear worker reference
def on_error(error, traceback):
progress.close()
QMessageBox.critical(self, "Fehler", f"Update fehlgeschlagen:\n{error}")
self.worker = None # Clear worker reference
self.worker.signals.progress.connect(on_progress)
self.worker.signals.finished.connect(on_finished)
self.worker.signals.error.connect(on_error)
self.worker.start()
def update_all_series(self):
"""Update all series"""
all_series = self.db.get_all_series()
if not all_series:
QMessageBox.information(self, "Info", "Keine Serien vorhanden")
return
reply = QMessageBox.question(
self,
"Alle Serien aktualisieren",
f"Möchten Sie alle {len(all_series)} Serien aktualisieren?\nDies kann einige Minuten dauern.",
QMessageBox.Yes | QMessageBox.No
)
if reply != QMessageBox.Yes:
return
from ..scraper.browser_scraper import BrowserScraper, SeriesUpdater
from ..utils.threading import UpdateWorker
from .widgets import ProgressDialog, UpdateResultDialog
# Create progress dialog
progress = ProgressDialog(self, "Alle Serien aktualisieren")
progress.set_determinate()
progress.show()
# Create worker
scraper = BrowserScraper()
def do_update_all():
total_stats = {
'new_seasons': 0,
'new_episodes': 0,
'updated_episodes': 0,
'unchanged': 0
}
total_series = len(all_series)
for idx, series in enumerate(all_series):
try:
# Progress callback for individual series
def progress_callback(percent, message):
# Calculate overall progress: each series gets an equal portion
overall_percent = int((idx / total_series) * 100 + (percent / total_series))
overall_message = f"[{idx + 1}/{total_series}] {series.title}: {message}"
self.worker.signals.progress.emit(overall_percent, overall_message)
updater = SeriesUpdater(self.db, scraper, progress_callback=progress_callback)
stats = updater.update_series(series.id)
for key in total_stats:
total_stats[key] += stats.get(key, 0)
except Exception as e:
logger.error(f"Error updating {series.title}: {e}")
return total_stats
self.worker = UpdateWorker(do_update_all)
def on_progress(percent, message):
progress.set_progress(percent, message)
def on_finished(stats):
progress.close()
UpdateResultDialog(self, stats).exec_()
# Reload UI
self.load_series_list()
if self.current_series:
self.load_series_info()
self.load_seasons_list()
if self.current_season:
self.load_episodes()
self.worker = None # Clear worker reference
def on_error(error, traceback):
progress.close()
QMessageBox.critical(self, "Fehler", f"Update fehlgeschlagen:\n{error}")
self.worker = None # Clear worker reference
self.worker.signals.progress.connect(on_progress)
self.worker.signals.finished.connect(on_finished)
self.worker.signals.error.connect(on_error)
self.worker.start()
def remove_current_series(self):
"""Remove currently selected series"""
if not self.current_series:
return
reply = QMessageBox.question(
self,
"Serie entfernen",
f"Möchten Sie '{self.current_series.title}' wirklich entfernen?",
QMessageBox.Yes | QMessageBox.No
)
if reply == QMessageBox.Yes:
self.db.delete_series(self.current_series.id)
self.current_series = None
self.current_season = None
self.load_series_list()
self.episode_table.setRowCount(0)
self.seasons_list.clear()
self.info_title.setText("Keine Serie ausgewählt")
def show_series_settings(self):
"""Show settings for current series"""
QMessageBox.information(self, "Hinweis", "Einstellungen werden noch implementiert")
def show_options(self):
"""Show options dialog"""
dialog = OptionsDialog(self, self.db)
if dialog.exec_():
self.load_series_list()
def show_about(self):
"""Show about dialog"""
QMessageBox.about(
self,
"Über Serien-Checker",
"Serien-Checker v2.0\n\n"
"TV-Serien Episode Tracker\n"
"Datenquelle: fernsehserien.de"
)
def closeEvent(self, event):
"""Handle window close event"""
# Wait for worker thread to finish if running
if self.worker and self.worker.isRunning():
logger.info("Waiting for worker thread to finish...")
self.worker.wait(5000) # Wait max 5 seconds
if self.worker.isRunning():
logger.warning("Worker thread did not finish, terminating...")
self.worker.terminate()
self.worker.wait()
event.accept()

View 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

View File

@@ -0,0 +1,105 @@
"""
Custom widgets for Serien-Checker UI
"""
from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QProgressBar,
QPushButton, QTextEdit
)
from PyQt5.QtCore import Qt
class ProgressDialog(QDialog):
"""Dialog showing progress for long-running operations"""
def __init__(self, parent, title: str = "Bitte warten..."):
super().__init__(parent)
self.setWindowTitle(title)
self.setModal(True)
self.setFixedSize(400, 150)
self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint)
self.init_ui()
def init_ui(self):
"""Initialize UI"""
layout = QVBoxLayout(self)
# Status label
self.status_label = QLabel("Initialisiere...")
layout.addWidget(self.status_label)
# Progress bar
self.progress_bar = QProgressBar()
self.progress_bar.setRange(0, 100)
self.progress_bar.setValue(0)
layout.addWidget(self.progress_bar)
# Cancel button
button_layout = QHBoxLayout()
button_layout.addStretch()
self.cancel_btn = QPushButton("Abbrechen")
self.cancel_btn.clicked.connect(self.reject)
button_layout.addWidget(self.cancel_btn)
layout.addLayout(button_layout)
def set_progress(self, value: int, message: str = None):
"""Update progress"""
self.progress_bar.setValue(value)
if message:
self.status_label.setText(message)
def set_indeterminate(self):
"""Set progress bar to indeterminate mode"""
self.progress_bar.setRange(0, 0)
def set_determinate(self):
"""Set progress bar to determinate mode"""
self.progress_bar.setRange(0, 100)
class UpdateResultDialog(QDialog):
"""Dialog showing update results"""
def __init__(self, parent, stats: dict):
super().__init__(parent)
self.setWindowTitle("Update-Ergebnis")
self.setModal(True)
self.resize(450, 300)
self.init_ui(stats)
def init_ui(self, stats: dict):
"""Initialize UI"""
layout = QVBoxLayout(self)
# Title
title = QLabel("Update abgeschlossen")
title.setStyleSheet("font-weight: bold; font-size: 14px;")
layout.addWidget(title)
# Results text
results = QTextEdit()
results.setReadOnly(True)
text = "Zusammenfassung:\n\n"
text += f"Neue Staffeln: {stats.get('new_seasons', 0)}\n"
text += f"Neue Folgen: {stats.get('new_episodes', 0)}\n"
text += f"Aktualisierte Folgen: {stats.get('updated_episodes', 0)}\n"
text += f"Unverändert: {stats.get('unchanged', 0)}\n"
results.setText(text)
layout.addWidget(results)
# OK button
button_layout = QHBoxLayout()
button_layout.addStretch()
ok_btn = QPushButton("OK")
ok_btn.clicked.connect(self.accept)
ok_btn.setDefault(True)
button_layout.addWidget(ok_btn)
layout.addLayout(button_layout)