commit f6f65696c6c1281be42262fee4fd79b8f8c3aec6 Author: Akamaru Date: Tue Jul 29 21:03:24 2025 +0200 Erste Version diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a533f8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/build +/dist +/versions +series_config.json +release.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..8ad0ad1 --- /dev/null +++ b/README.md @@ -0,0 +1,96 @@ +# Serien-Checker + +Ein Programm zum Überprüfen von Ausstrahlungsterminen deutscher TV-Serien. Die Daten werden von fernsehserien.de abgerufen. + +## Features + +- Verfolgen Sie mehrere Serien gleichzeitig +- Anzeige deutscher Ausstrahlungstermine (TV und Streaming) +- Staffel-spezifische Filterung +- Datumspräferenz (TV, Streaming oder früheste Ausstrahlung) +- Übersichtliche Episodenliste mit Datum, Staffel, Folge und Titel + +## Installation + +### Option 1: Ausführbare Datei (Windows) + +1. Laden Sie die neueste Version von der [Releases](https://git.ponywave.de/Akamaru/Serien-Checker/releases) Seite herunter +2. Entpacken Sie die ZIP-Datei +3. Starten Sie `Serien-Checker.exe` + +### Option 2: Aus dem Quellcode + +1. Stellen Sie sicher, dass Python 3.8 oder höher installiert ist +2. Klonen Sie das Repository: + ```bash + git clone https://git.ponywave.de/Akamaru/Serien-Checker.git + cd Serien-Checker + ``` +3. Installieren Sie die Abhängigkeiten: + ```bash + pip install -r requirements.txt + ``` +4. Starten Sie das Programm: + ```bash + python serien_checker.py + ``` + +### Executable erstellen + +Um Ihre eigene ausführbare Datei zu erstellen: + +1. Führen Sie `build.bat` aus, oder +2. Manuell: + ```bash + pip install -r requirements.txt + pip install pyinstaller + python build.py + ``` + +Die ausführbare Datei finden Sie dann im `dist` Ordner. + +## Verwendung + +### Serien hinzufügen + +1. Klicken Sie auf "Serien verwalten" +2. Klicken Sie auf "Neue Serie" +3. Geben Sie die URL oder den Slug von fernsehserien.de ein + - Beispiel URL: `https://www.fernsehserien.de/9-1-1-notruf-l-a` + - Beispiel Slug: `9-1-1-notruf-l-a` +4. Wählen Sie die gewünschten Einstellungen: + - Staffel-Modus (Neuste, Alle, Bestimmte) + - Datumspräferenz (Erstausstrahlung, TV, Streaming) + +### Serien verwalten + +- Wählen Sie eine Serie aus der Liste +- Ändern Sie die Einstellungen nach Bedarf +- Klicken Sie auf "Einstellungen speichern" +- Löschen Sie unerwünschte Serien mit dem "Löschen" Button + +### Episoden anzeigen + +- Wählen Sie eine Serie aus der Liste im Hauptfenster +- Die Episoden werden automatisch geladen +- Die Liste wird alle 30 Minuten automatisch aktualisiert +- Klicken Sie auf "Aktualisieren" für sofortige Aktualisierung + +## Konfiguration + +Die Einstellungen werden automatisch in `series_config.json` gespeichert. Diese Datei wird beim ersten Start erstellt und enthält: +- Liste der Serien +- Staffel-Einstellungen pro Serie +- Datumspräferenzen pro Serie + +## Fehlerbehebung + +### Keine Episoden werden angezeigt +- Prüfen Sie Ihre Internetverbindung +- Prüfen Sie, ob die Serie auf fernsehserien.de verfügbar ist +- Prüfen Sie die Staffel-Einstellungen + +### Keine deutschen Titel +- Einige Episoden haben noch keine deutschen Titel +- Diese werden als "Noch kein Titel" angezeigt +- Die Titel werden automatisch aktualisiert, sobald sie verfügbar sind \ No newline at end of file diff --git a/Serien-Checker.spec b/Serien-Checker.spec new file mode 100644 index 0000000..3a94dd7 --- /dev/null +++ b/Serien-Checker.spec @@ -0,0 +1,45 @@ +# -*- mode: python ; coding: utf-8 -*- + + +block_cipher = None + + +a = Analysis( + ['serien_checker.py'], + pathex=[], + binaries=[], + datas=[('series_config.json', '.')], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False, +) +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + [], + name='Serien-Checker', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=False, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon=['icon.ico'], +) diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..79ee5e9 --- /dev/null +++ b/build.bat @@ -0,0 +1,15 @@ +@echo off +echo Installing required packages... +pip install -r requirements.txt +pip install pyinstaller + +echo Building executable... +python build.py + +echo Done! +if exist "dist\Serien-Checker.exe" ( + echo Executable created successfully at dist\Serien-Checker.exe +) else ( + echo Error: Build failed! +) +pause \ No newline at end of file diff --git a/build.py b/build.py new file mode 100644 index 0000000..864908c --- /dev/null +++ b/build.py @@ -0,0 +1,20 @@ +import PyInstaller.__main__ +import os +import shutil + +# Lösche alte build und dist Ordner +if os.path.exists('build'): + shutil.rmtree('build') +if os.path.exists('dist'): + shutil.rmtree('dist') + +# PyInstaller Konfiguration +PyInstaller.__main__.run([ + 'serien_checker.py', + '--onefile', + '--windowed', + '--name=Serien-Checker', + '--icon=icon.ico', # Optional: Fügen Sie ein Icon hinzu wenn gewünscht + '--add-data=series_config.json;.', # Fügt die Konfigurationsdatei hinzu wenn sie existiert + '--clean' +]) \ No newline at end of file diff --git a/icon.ico b/icon.ico new file mode 100644 index 0000000..c1c035b Binary files /dev/null and b/icon.ico differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..72acf73 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +PyQt5==5.15.9 +requests==2.31.0 +beautifulsoup4==4.12.2 \ No newline at end of file diff --git a/serien_checker.py b/serien_checker.py new file mode 100644 index 0000000..6eeab6f --- /dev/null +++ b/serien_checker.py @@ -0,0 +1,776 @@ +import sys +import json +import requests +import re +import logging +from bs4 import BeautifulSoup +from datetime import datetime +from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, + QHBoxLayout, QPushButton, QLineEdit, QListWidget, + QLabel, QMessageBox, QSpinBox, QComboBox, QTableWidget, + QTableWidgetItem, QHeaderView, QDialog, QDialogButtonBox, + QTextEdit, QGroupBox, QToolBar, QSplitter, QFormLayout, + QListWidgetItem) +from PyQt5.QtCore import Qt, QTimer + +# Logging Konfiguration +logging.basicConfig(level=logging.DEBUG, + format='%(asctime)s - %(levelname)s - %(message)s') + +class LogWindow(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("Debug Log") + self.setGeometry(100, 100, 800, 400) + + layout = QVBoxLayout(self) + + self.log_text = QTextEdit() + self.log_text.setReadOnly(True) + layout.addWidget(self.log_text) + + close_button = QPushButton("Schließen") + close_button.clicked.connect(self.close) + layout.addWidget(close_button) + + def append_log(self, message): + self.log_text.append(message) + +class DebugHandler(logging.Handler): + def __init__(self, log_window): + super().__init__() + self.log_window = log_window + + def emit(self, record): + msg = self.format(record) + self.log_window.append_log(msg) + +class NewSeriesDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.setup_ui() + + def setup_ui(self): + self.setWindowTitle("Neue Serie hinzufügen") + self.setMinimumWidth(400) + self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) + + layout = QVBoxLayout(self) + + # Einstellungen + settings_group = QGroupBox("Serie hinzufügen") + settings_layout = QFormLayout() + + # Serien-Eingabe + self.slug_input = QLineEdit() + self.slug_input.setPlaceholderText("Serien-URL oder Slug") + settings_layout.addRow("Serie:", self.slug_input) + + # Staffel-Einstellungen + self.staffel_mode = QComboBox() + self.staffel_mode.addItems(["Neuste Staffel", "Alle Staffeln", "Bestimmte Staffel"]) + self.staffel_mode.currentIndexChanged.connect(self.on_staffel_mode_changed) + settings_layout.addRow("Staffel-Modus:", self.staffel_mode) + + self.staffel_spin = QSpinBox() + self.staffel_spin.setMinimum(1) + self.staffel_spin.setMaximum(100) + self.staffel_spin.setEnabled(False) + settings_layout.addRow("Staffel:", self.staffel_spin) + + # Datumspräferenz + self.date_pref = QComboBox() + self.date_pref.addItems(["Bevorzuge Erstausstrahlung", "Bevorzuge TV", "Bevorzuge Streaming"]) + settings_layout.addRow("Datum Präferenz:", self.date_pref) + + settings_group.setLayout(settings_layout) + layout.addWidget(settings_group) + + # Dialog Buttons + buttons = QDialogButtonBox( + QDialogButtonBox.Ok | QDialogButtonBox.Cancel, + Qt.Horizontal, self) + buttons.accepted.connect(self.accept) + buttons.rejected.connect(self.reject) + layout.addWidget(buttons) + + def on_staffel_mode_changed(self, index): + """Wird aufgerufen, wenn sich der Staffel-Modus ändert""" + self.staffel_spin.setEnabled(self.staffel_mode.currentText() == "Bestimmte Staffel") + + def get_series_data(self): + """Extrahiert die Seriendaten aus den Eingabefeldern""" + input_text = self.slug_input.text().strip() + if not input_text: + QMessageBox.warning(self, "Fehler", "Bitte geben Sie eine Serie ein.") + return None + + # Extrahiere Slug aus URL wenn nötig + if "/" in input_text: + try: + # Prüfe ob es eine fernsehserien.de URL ist + if "fernsehserien.de" not in input_text: + QMessageBox.warning(self, "Fehler", "Die URL muss von fernsehserien.de sein.") + return None + # Extrahiere den Slug (Teil nach dem letzten /) + slug = input_text.rstrip("/").split("/")[-1] + if slug == "episodenguide": + # Wenn die URL auf /episodenguide endet, nimm den Teil davor + slug = input_text.rstrip("/").split("/")[-2] + except Exception as e: + QMessageBox.warning(self, "Fehler", f"Ungültige URL: {str(e)}") + return None + else: + slug = input_text + + # Hole den Seriennamen von fernsehserien.de + try: + url = f"https://www.fernsehserien.de/{slug}/episodenguide" + response = requests.get(url) + soup = BeautifulSoup(response.text, 'html.parser') + title_elem = soup.find('h1') + if title_elem: + series_name = title_elem.text.strip() + else: + QMessageBox.warning(self, "Fehler", "Serie nicht gefunden.") + return None + except Exception as e: + QMessageBox.warning(self, "Fehler", f"Fehler beim Abrufen der Serie: {str(e)}") + return None + + return { + 'name': series_name, + 'slug': slug, + 'staffel_setting': { + 'mode': self.staffel_mode.currentText(), + 'staffel': self.staffel_spin.value() + }, + 'date_preference': self.date_pref.currentText() + } + +class SeriesEditDialog(QDialog): + def __init__(self, series_data, parent=None): + super().__init__(parent) + self.series_data = series_data.copy() + self.parent = parent + self.setup_ui() + + def setup_ui(self): + self.setWindowTitle("Serien verwalten") + self.setMinimumWidth(500) + self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) + + layout = QVBoxLayout(self) + + # Serien Liste und Einstellungen nebeneinander + content_layout = QHBoxLayout() + + # Linke Seite - Serien Liste und Info + left_layout = QVBoxLayout() + + list_group = QGroupBox("Gespeicherte Serien") + list_layout = QVBoxLayout() + + self.series_list = QListWidget() + self.series_list.itemClicked.connect(self.on_series_selected) + list_layout.addWidget(self.series_list) + + # Buttons für die Liste + button_layout = QHBoxLayout() + add_button = QPushButton("Neue Serie") + add_button.clicked.connect(self.add_series) + delete_button = QPushButton("Löschen") + delete_button.clicked.connect(self.delete_series) + + button_layout.addWidget(add_button) + button_layout.addWidget(delete_button) + list_layout.addLayout(button_layout) + + list_group.setLayout(list_layout) + left_layout.addWidget(list_group) + + # Info Label + info_label = QLabel('v1.0 | © Akamaru | Source auf PonyGit') + info_label.setOpenExternalLinks(True) # Erlaubt das Öffnen des Links + info_label.setTextFormat(Qt.RichText) # Aktiviert HTML-Formatierung + left_layout.addWidget(info_label) + + content_layout.addLayout(left_layout) + + # Rechte Seite - Einstellungen + self.settings_group = QGroupBox("Einstellungen") + settings_layout = QFormLayout() + + # Serien-Eingabe + self.slug_input = QLineEdit() + self.slug_input.setPlaceholderText("Serien-URL oder Slug") + self.slug_input.setEnabled(False) # Deaktiviert, da nur zum Anzeigen + settings_layout.addRow("Serie:", self.slug_input) + + # Staffel-Einstellungen + self.staffel_mode = QComboBox() + self.staffel_mode.addItems(["Neuste Staffel", "Alle Staffeln", "Bestimmte Staffel"]) + self.staffel_mode.currentIndexChanged.connect(self.on_staffel_mode_changed) + settings_layout.addRow("Staffel-Modus:", self.staffel_mode) + + self.staffel_spin = QSpinBox() + self.staffel_spin.setMinimum(1) + self.staffel_spin.setMaximum(100) + self.staffel_spin.setEnabled(False) + settings_layout.addRow("Staffel:", self.staffel_spin) + + # Datumspräferenz + self.date_pref = QComboBox() + self.date_pref.addItems(["Bevorzuge Erstausstrahlung", "Bevorzuge TV", "Bevorzuge Streaming"]) + settings_layout.addRow("Datum Präferenz:", self.date_pref) + + # Speichern Button für Einstellungen + save_button = QPushButton("Einstellungen speichern") + save_button.clicked.connect(self.save_settings) + settings_layout.addRow("", save_button) + + self.settings_group.setLayout(settings_layout) + content_layout.addWidget(self.settings_group) + + # Füge das Content-Layout zum Hauptlayout hinzu + layout.addLayout(content_layout) + + # Dialog Buttons + buttons = QDialogButtonBox( + QDialogButtonBox.Ok | QDialogButtonBox.Cancel, + Qt.Horizontal, self) + buttons.accepted.connect(self.accept) + buttons.rejected.connect(self.reject) + layout.addWidget(buttons) + + # Initialisiere die Liste und deaktiviere die Einstellungen + self.update_series_list() + self.settings_group.setEnabled(False) + + def accept(self): + """Schließt den Dialog""" + super().accept() # Schließe den Dialog einfach + + def save_settings(self): + """Speichert die Einstellungen für die aktuelle Serie""" + current_item = self.series_list.currentItem() + if not current_item: + QMessageBox.warning(self, "Fehler", "Bitte wählen Sie eine Serie zum Bearbeiten aus!") + return + + slug = current_item.data(Qt.UserRole) + if slug not in self.series_data: + QMessageBox.warning(self, "Fehler", "Serie nicht gefunden!") + return + + # Aktualisiere die Einstellungen + self.series_data[slug].update({ + 'staffel_setting': { + 'mode': self.staffel_mode.currentText(), + 'staffel': self.staffel_spin.value() + }, + 'date_preference': self.date_pref.currentText() + }) + + QMessageBox.information(self, "Erfolg", "Einstellungen wurden gespeichert!") + + def add_series(self): + """Öffnet den Dialog zum Hinzufügen einer neuen Serie""" + dialog = NewSeriesDialog(self) + if dialog.exec_() == QDialog.Accepted: + series_data = dialog.get_series_data() + if series_data: + slug = series_data['slug'] + self.series_data[slug] = series_data + self.update_series_list() + # Wähle die neue Serie aus + for i in range(self.series_list.count()): + if self.series_list.item(i).data(Qt.UserRole) == slug: + self.series_list.setCurrentRow(i) + break + QMessageBox.information(self, "Erfolg", "Serie wurde hinzugefügt!") + + def on_staffel_mode_changed(self, index): + """Wird aufgerufen, wenn sich der Staffel-Modus ändert""" + self.staffel_spin.setEnabled(self.staffel_mode.currentText() == "Bestimmte Staffel") + + def update_series_list(self): + """Aktualisiert die Liste der Serien""" + self.series_list.clear() + # Sortiere nach Namen + sorted_series = sorted(self.series_data.items(), key=lambda x: x[1]['name'].lower()) + for slug, data in sorted_series: + item = QListWidgetItem(data['name']) + item.setData(Qt.UserRole, slug) # Speichere den Slug als Zusatzdaten + self.series_list.addItem(item) + + def on_series_selected(self, item): + """Wird aufgerufen, wenn eine Serie ausgewählt wird""" + self.settings_group.setEnabled(bool(item)) + if item: + slug = item.data(Qt.UserRole) # Hole den Slug aus den Zusatzdaten + data = self.series_data.get(slug, {}) + self.slug_input.setText(data.get('slug', slug)) + + # Staffel-Einstellungen laden + staffel_setting = data.get('staffel_setting', {}) + mode = staffel_setting.get('mode', "Neuste Staffel") + staffel = staffel_setting.get('staffel', 1) + + index = self.staffel_mode.findText(mode) + if index >= 0: + self.staffel_mode.setCurrentIndex(index) + self.staffel_spin.setValue(staffel) + + # Datumspräferenz laden + date_pref = data.get('date_preference', "Bevorzuge Erstausstrahlung") + index = self.date_pref.findText(date_pref) + if index >= 0: + self.date_pref.setCurrentIndex(index) + + def delete_series(self): + """Löscht die ausgewählte Serie""" + current_item = self.series_list.currentItem() + if not current_item: + QMessageBox.warning(self, "Fehler", "Bitte wählen Sie eine Serie zum Löschen aus!") + return + + slug = current_item.data(Qt.UserRole) + name = self.series_data[slug]['name'] + + reply = QMessageBox.question( + self, + "Serie löschen", + f"Möchten Sie die Serie '{name}' wirklich löschen?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No + ) + + if reply == QMessageBox.Yes: + del self.series_data[slug] + self.update_series_list() + self.slug_input.clear() + QMessageBox.information(self, "Erfolg", f"Serie '{name}' wurde gelöscht!") + +class SerienChecker(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("Serien Checker") + self.setGeometry(100, 100, 1000, 800) + + # Initialisiere series als leeres Dictionary + self.series = {} + + # Log-Fenster erstellen + self.log_window = LogWindow(self) + + # Zentral-Widget und Layout + central_widget = QWidget() + self.setCentralWidget(central_widget) + layout = QVBoxLayout(central_widget) + + # Toolbar + toolbar = QToolBar() + self.addToolBar(toolbar) + + # Serien verwalten Button + manage_button = QPushButton("Serien verwalten") + manage_button.clicked.connect(self.manage_series) + toolbar.addWidget(manage_button) + + # Debug Log Button + debug_button = QPushButton("Debug Log") + debug_button.clicked.connect(self.show_debug_log) + toolbar.addWidget(debug_button) + + # Serien und Episoden Layout + content_layout = QHBoxLayout() + + # Linke Seite - Serien Liste + list_group = QGroupBox("Gespeicherte Serien") + list_layout = QVBoxLayout() + self.series_list = QListWidget() + self.series_list.currentItemChanged.connect(self.on_series_selected) + list_layout.addWidget(self.series_list) + list_group.setLayout(list_layout) + content_layout.addWidget(list_group) + + # Rechte Seite - Episoden + episodes_group = QGroupBox("Episoden") + episodes_layout = QVBoxLayout() + + self.episodes_table = QTableWidget() + self.episodes_table.setColumnCount(4) + self.episodes_table.setHorizontalHeaderLabels(["Datum", "Staffel", "Folge", "Titel"]) + header = self.episodes_table.horizontalHeader() + header.setSectionResizeMode(0, QHeaderView.ResizeToContents) + header.setSectionResizeMode(1, QHeaderView.ResizeToContents) + header.setSectionResizeMode(2, QHeaderView.ResizeToContents) + header.setSectionResizeMode(3, QHeaderView.Stretch) + episodes_layout.addWidget(self.episodes_table) + + # Aktualisieren Button + refresh_button = QPushButton("Aktualisieren") + refresh_button.clicked.connect(self.refresh_selected_series) + episodes_layout.addWidget(refresh_button) + + episodes_group.setLayout(episodes_layout) + content_layout.addWidget(episodes_group) + + # Füge das Content-Layout zum Hauptlayout hinzu + layout.addLayout(content_layout) + + # Timer für automatische Aktualisierung (alle 30 Minuten) + self.timer = QTimer() + self.timer.timeout.connect(self.refresh_selected_series) + self.timer.start(30 * 60 * 1000) + + # Lade die Konfiguration und aktualisiere die Liste + self.load_config() + self.update_series_list() + + self.show() + + def manage_series(self): + dialog = SeriesEditDialog(self.series, self) + if dialog.exec_() == QDialog.Accepted: + self.series = dialog.series_data + self.save_config() + self.update_series_list() + self.refresh_all() + + def load_config(self): + """Lädt die Konfiguration""" + try: + with open('series_config.json', 'r', encoding='utf-8') as f: + config = json.load(f) + if isinstance(config, dict) and 'series' in config: + self.series = config['series'] + else: + # Alte Konfiguration kompatibel machen + self.series = {} + for slug, data in config.items(): + self.series[slug] = { + 'name': data.get('name', slug), + 'staffel_setting': { + 'mode': data.get('settings', {}).get('mode', "Neuste Staffel"), + 'staffel': data.get('settings', {}).get('staffel', 1) + }, + 'date_preference': "Bevorzuge Erstausstrahlung" + } + except FileNotFoundError: + logging.debug("Keine Konfigurationsdatei gefunden, verwende leeres Dictionary") + self.series = {} + + def save_config(self): + """Speichert die Konfiguration""" + with open('series_config.json', 'w', encoding='utf-8') as f: + json.dump({'series': self.series}, f, indent=4, ensure_ascii=False) + + def update_series_list(self): + """Aktualisiert die Liste der Serien""" + self.series_list.clear() + # Sortiere nach Namen + sorted_series = sorted(self.series.items(), key=lambda x: x[1]['name'].lower()) + for slug, data in sorted_series: + item = QListWidgetItem(data['name']) + item.setData(Qt.UserRole, slug) # Speichere den Slug als Zusatzdaten + self.series_list.addItem(item) + + def parse_date(self, date_str): + try: + return datetime.strptime(date_str.split()[0], "%d.%m.%Y") + except: + return None + + def get_premiere_date(self, episode): + """Extrahiert das erste deutsche Ausstrahlungsdatum (TV oder Streaming) basierend auf der Präferenz""" + logging.debug("Suche nach Premierendaten") + try: + tv_date = None + streaming_date = None + + # Suche nach allen deutschen Premieren + for ea_angabe in episode.find_all('ea-angabe'): + titel_elem = ea_angabe.find('ea-angabe-titel') + if not titel_elem: + continue + + titel = titel_elem.text.strip() + logging.debug(f"Gefundene Premiere: {titel}") + + if "Deutsche" in titel: + datum_elem = ea_angabe.find('ea-angabe-datum') + if datum_elem: + date_str = datum_elem.text.strip() + if '. ' in date_str: + date_str = date_str.split('. ', 1)[1] + + parsed_date = self.parse_date(date_str) + if parsed_date: + if "TV-Premiere" in titel: + tv_date = (parsed_date, date_str) + logging.debug(f"Deutsche TV-Premiere gefunden: {date_str}") + elif "Streaming-Premiere" in titel: + streaming_date = (parsed_date, date_str) + logging.debug(f"Deutsche Streaming-Premiere gefunden: {date_str}") + + # Hole die Datumspräferenz für diese Serie + current_series = self.series_list.currentItem() + if current_series: + slug = current_series.data(Qt.UserRole) # Hole den Slug aus den Zusatzdaten + pref = self.series[slug].get('date_preference', "Bevorzuge Erstausstrahlung") + else: + pref = "Bevorzuge Erstausstrahlung" + + if pref == "Bevorzuge TV" and tv_date: + result = tv_date[1] + logging.debug(f"TV-Premiere gewählt: {result}") + elif pref == "Bevorzuge Streaming" and streaming_date: + result = streaming_date[1] + logging.debug(f"Streaming-Premiere gewählt: {result}") + else: # Bevorzuge Erstausstrahlung oder wenn bevorzugtes Datum nicht verfügbar + dates = [] + if tv_date: + dates.append(tv_date) + if streaming_date: + dates.append(streaming_date) + + if dates: + result = min(dates, key=lambda x: x[0])[1] + logging.debug(f"Frühestes Datum gewählt: {result}") + else: + logging.warning("Keine deutschen Premierendaten gefunden!") + return "TBA" + + return result + + except Exception as e: + logging.error(f"Fehler beim Extrahieren des Premierendatums: {str(e)}") + return "TBA" + + def on_date_preference_changed(self, series_slug): + """Wird aufgerufen, wenn sich die Datumspräferenz einer Serie ändert""" + logging.debug(f"Datumspräferenz für Serie {series_slug} wurde geändert") + self.refresh_selected_series() + + def get_episode_info(self, episode): + """Extrahiert Folgeninformationen aus einem Episode-Element""" + try: + logging.debug("Versuche Episodeninformationen zu extrahieren") + + # Suche nach Staffel und Folge in der Episodennummer (z.B. "7.01") + header = episode.find('h3', class_='episode-output-titel') + if header: + episode_link = header.find('a') + if episode_link: + # URL enthält die Episodennummer (z.B. "7x01") + href = episode_link.get('href', '') + match = re.search(r'(\d+)x(\d+)-', href) + if match: + staffel = int(match.group(1)) + folge = int(match.group(2)) + logging.debug(f"Gefundene Staffel/Folge aus URL: {staffel}/{folge}") + else: + # Alternative: Suche in der Episoden-Zeile + episode_info = episode.find('div', {'itemprop': 'episodeNumber'}) + if episode_info and episode_info.text: + folge = int(episode_info.text) + # Staffel aus übergeordnetem Element + staffel_info = episode.find_previous('h2', class_='header-2015') + if staffel_info: + staffel_match = re.search(r'Staffel (\d+)', staffel_info.text) + if staffel_match: + staffel = int(staffel_match.group(1)) + logging.debug(f"Gefundene Staffel/Folge aus Text: {staffel}/{folge}") + + # Suche nach deutschem Titel + if episode_link: + # Prüfe zuerst, ob ein deutscher Titel existiert + title_spans = episode_link.find_all('span') + title = "Noch kein Titel" + + for span in title_spans: + # Überspringe Folgen-Nummer + if span.text.isdigit(): + continue + # Überspringe englischen Titel + if span.get('class') and 'episode-output-originaltitel' in span.get('class'): + continue + # Überspringe Platzhalter für fehlenden Titel + if span.get('title') == "Titel unbekannt": + continue + # Wenn wir hier sind und der span itemprop="name" hat, ist es der deutsche Titel + if span.get('itemprop') == 'name' and span.text.strip() not in ['–', '-']: + title = span.text.strip() + logging.debug(f"Gefundener deutscher Titel: {title}") + break + + logging.debug(f"Finaler Titel: {title}") + else: + title = "Noch kein Titel" + logging.debug("Kein Link gefunden, verwende Standardtitel") + + if not all([staffel, folge]): + logging.warning(f"Unvollständige Daten: Staffel={staffel}, Folge={folge}") + return None, None, None + + return staffel, folge, title + + except Exception as e: + logging.error(f"Fehler beim Extrahieren der Episodeninformationen: {str(e)}") + return None, None, None + + def get_staffel_url(self, slug, staffel_nr=None): + """Generiert die URL für eine bestimmte Staffel""" + logging.debug(f"Generiere Staffel-URL für Slug: {slug}, Staffel: {staffel_nr}") + try: + # Hole die Übersichtsseite + url = f"https://www.fernsehserien.de/{slug}/episodenguide" + logging.debug(f"Hole Übersichtsseite: {url}") + response = requests.get(url) + soup = BeautifulSoup(response.text, 'html.parser') + + staffel_links = [] + for link in soup.find_all('a', href=True): + # Suche nach Staffel-Links und extrahiere die ID + if 'staffel-' in link['href']: + match = re.search(r'/staffel-(\d+)/(\d+)$', link['href']) + if match: + s_nr = int(match.group(1)) + serie_id = match.group(2) + staffel_links.append((s_nr, serie_id)) + logging.debug(f"Gefundene Staffel: {s_nr} mit ID: {serie_id}") + + if not staffel_links: + logging.warning("Keine Staffeln gefunden!") + return None + + if staffel_nr: + # Suche nach der gewünschten Staffel + for s_nr, serie_id in staffel_links: + if s_nr == staffel_nr: + url = f"https://www.fernsehserien.de/{slug}/episodenguide/staffel-{staffel_nr}/{serie_id}" + logging.debug(f"Generierte URL für spezifische Staffel: {url}") + return url + logging.warning(f"Staffel {staffel_nr} nicht gefunden!") + return None + else: + # Nehme die neueste Staffel + newest_staffel, serie_id = max(staffel_links, key=lambda x: x[0]) + url = f"https://www.fernsehserien.de/{slug}/episodenguide/staffel-{newest_staffel}/{serie_id}" + logging.debug(f"Generierte URL für neueste Staffel ({newest_staffel}): {url}") + return url + + except Exception as e: + logging.error(f"Fehler beim Generieren der Staffel-URL: {str(e)}") + return None + + def on_series_selected(self, current, previous): + """Wird aufgerufen, wenn eine Serie in der Liste ausgewählt wird""" + if current: + slug = current.data(Qt.UserRole) # Hole den Slug aus den Zusatzdaten + if slug in self.series: + self.refresh_selected_series() + else: + logging.warning(f"Serie {slug} nicht gefunden!") + else: + self.episodes_table.setRowCount(0) + + def refresh_selected_series(self): + """Aktualisiert die Episodenliste für die ausgewählte Serie""" + current_item = self.series_list.currentItem() + if not current_item: + logging.warning("Keine Serie ausgewählt!") + return + + slug = current_item.data(Qt.UserRole) # Hole den Slug aus den Zusatzdaten + if slug not in self.series: + logging.warning(f"Serie {slug} nicht gefunden!") + return + + selected_data = self.series[slug] + logging.debug(f"Aktualisiere Serie: {selected_data['name']}") + + try: + # Bestimme die Staffel-URL basierend auf den Einstellungen + settings = selected_data.get('staffel_setting', {}) + mode = settings.get('mode', "Neuste Staffel") + staffel_nr = settings.get('staffel') if mode == "Bestimmte Staffel" else None + + url = self.get_staffel_url(slug, staffel_nr) + if not url: + self.episodes_table.setRowCount(1) + if mode == "Bestimmte Staffel": + error_msg = f"Staffel {staffel_nr} wurde nicht gefunden!" + else: + error_msg = "Keine Staffeln gefunden!" + self.episodes_table.setItem(0, 0, QTableWidgetItem(error_msg)) + for i in range(1, 4): + self.episodes_table.setItem(0, i, QTableWidgetItem("")) + logging.error(error_msg) + return + + logging.debug(f"Hole Episoden von: {url}") + response = requests.get(url) + soup = BeautifulSoup(response.text, 'html.parser') + + episodes = [] + for episode in soup.find_all('section', {'itemprop': 'episode'}): + staffel, folge, titel = self.get_episode_info(episode) + if all([staffel, folge, titel]): + datum = self.get_premiere_date(episode) + episodes.append({ + 'date': datum, + 'staffel': staffel, + 'folge': folge, + 'titel': titel + }) + + if not episodes: + self.episodes_table.setRowCount(1) + error_msg = "Keine Episoden gefunden!" + self.episodes_table.setItem(0, 0, QTableWidgetItem(error_msg)) + for i in range(1, 4): + self.episodes_table.setItem(0, i, QTableWidgetItem("")) + logging.warning(error_msg) + return + + # Sortiere nach Datum (wenn verfügbar) und Staffel/Folge + episodes.sort(key=lambda x: ( + datetime.strptime(x['date'], '%d.%m.%Y') if x['date'] != 'TBA' else datetime.max, + x['staffel'], + x['folge'] + )) + + # Zeige maximal 20 Episoden an + episodes = episodes[:20] + + # Aktualisiere die Tabelle + self.episodes_table.setRowCount(len(episodes)) + for row, episode in enumerate(episodes): + self.episodes_table.setItem(row, 0, QTableWidgetItem(episode['date'])) + self.episodes_table.setItem(row, 1, QTableWidgetItem(str(episode['staffel']))) + self.episodes_table.setItem(row, 2, QTableWidgetItem(str(episode['folge']))) + self.episodes_table.setItem(row, 3, QTableWidgetItem(episode['titel'])) + + except Exception as e: + logging.error(f"Fehler beim Aktualisieren der Serie: {str(e)}") + self.episodes_table.setRowCount(1) + self.episodes_table.setItem(0, 0, QTableWidgetItem(f"Fehler: {str(e)}")) + for i in range(1, 4): + self.episodes_table.setItem(0, i, QTableWidgetItem("")) + + def refresh_all(self): + self.update_series_list() + if self.series_list.selectedItems(): + self.refresh_selected_series() + + def show_debug_log(self): + self.log_window.show() + +if __name__ == '__main__': + app = QApplication(sys.argv) + window = SerienChecker() + window.show() + sys.exit(app.exec_()) \ No newline at end of file diff --git a/start.bat b/start.bat new file mode 100644 index 0000000..03fa647 --- /dev/null +++ b/start.bat @@ -0,0 +1,2 @@ +@echo off +start "" /b pythonw.exe serien_checker.py \ No newline at end of file