891 lines
38 KiB
Python
891 lines
38 KiB
Python
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, QCheckBox)
|
||
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", "Bevorzuge deutsche Synchro"])
|
||
settings_layout.addRow("Datum Präferenz:", self.date_pref)
|
||
|
||
# Checkbox für Serien ohne klassische Staffeln
|
||
self.no_seasons_check = QCheckBox("Serie ohne klassische Staffeln")
|
||
self.no_seasons_check.setToolTip("Aktivieren für Serien mit fortlaufender Nummerierung oder Jahresformat")
|
||
settings_layout.addRow("", self.no_seasons_check)
|
||
|
||
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(),
|
||
'no_seasons': self.no_seasons_check.isChecked()
|
||
}
|
||
|
||
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.1 | © Akamaru | <a href="https://git.ponywave.de/Akamaru/Serien-Checker">Source auf PonyGit</a>')
|
||
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", "Bevorzuge deutsche Synchro"])
|
||
settings_layout.addRow("Datum Präferenz:", self.date_pref)
|
||
|
||
# Checkbox für Serien ohne klassische Staffeln
|
||
self.no_seasons_check = QCheckBox("Serie ohne klassische Staffeln")
|
||
self.no_seasons_check.setToolTip("Aktivieren für Serien mit fortlaufender Nummerierung oder Jahresformat")
|
||
settings_layout.addRow("", self.no_seasons_check)
|
||
|
||
# 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(),
|
||
'no_seasons': self.no_seasons_check.isChecked()
|
||
})
|
||
|
||
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)
|
||
|
||
# Keine Staffeln Checkbox laden
|
||
self.no_seasons_check.setChecked(data.get('no_seasons', False))
|
||
|
||
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
|
||
synchro_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}")
|
||
|
||
# Prüfe auf deutsche Daten
|
||
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 and "Deutsche" in titel:
|
||
tv_date = (parsed_date, date_str)
|
||
logging.debug(f"Deutsche TV-Premiere gefunden: {date_str}")
|
||
elif "Streaming-Premiere" in titel and "Deutsche" in titel:
|
||
streaming_date = (parsed_date, date_str)
|
||
logging.debug(f"Deutsche Streaming-Premiere gefunden: {date_str}")
|
||
elif "deutschen Synchronfassung" in titel:
|
||
synchro_date = (parsed_date, date_str)
|
||
logging.debug(f"Deutsche Synchro-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"
|
||
|
||
# Prüfe zuerst auf Synchro-Präferenz
|
||
if pref == "Bevorzuge deutsche Synchro":
|
||
if synchro_date:
|
||
result = synchro_date[1]
|
||
logging.debug(f"Synchro-Premiere gewählt: {result}")
|
||
return result
|
||
else:
|
||
logging.debug("Keine Synchro-Premiere gefunden, zeige TBA")
|
||
return "TBA"
|
||
|
||
# Andere Präferenzen
|
||
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
|
||
dates = []
|
||
if tv_date:
|
||
dates.append(tv_date)
|
||
if streaming_date:
|
||
dates.append(streaming_date)
|
||
if synchro_date:
|
||
dates.append(synchro_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:
|
||
# Prüfe auf "Folge X" Format
|
||
folge_match = re.search(r'Folge (\d+)', episode_info.text)
|
||
if folge_match:
|
||
folge = int(folge_match.group(1))
|
||
# Bei Serien ohne Staffeln setzen wir Staffel auf 1
|
||
staffel = 1
|
||
logging.debug(f"Gefundene Folge aus 'Folge X' Format: {folge}")
|
||
else:
|
||
try:
|
||
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}")
|
||
except ValueError:
|
||
logging.debug(f"Konnte Folge nicht aus Text extrahieren: {episode_info.text}")
|
||
|
||
# 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 = []
|
||
headers = soup.find_all('h2', class_='header-2015')
|
||
|
||
for header in headers:
|
||
link = header.find('a', class_='fs-linkable')
|
||
if not link or not link.get('href'):
|
||
continue
|
||
|
||
href = link['href']
|
||
# Prüfe auf normale Staffel-Links
|
||
match = re.search(r'/staffel-(\d+)/(\d+)$', href)
|
||
if match:
|
||
s_nr = int(match.group(1))
|
||
serie_id = match.group(2)
|
||
staffel_links.append(('staffel', s_nr, serie_id))
|
||
logging.debug(f"Gefundene Staffel: {s_nr} mit ID: {serie_id}")
|
||
continue
|
||
|
||
# Prüfe auf "bisher X Folgen" oder Jahres-Format
|
||
match = re.search(r'episodenguide/(\d+)/(\d+)$', href)
|
||
if match:
|
||
section_id = match.group(1)
|
||
serie_id = match.group(2)
|
||
# Prüfe den Text für die Art des Eintrags
|
||
text = link.text.strip()
|
||
if "bisher" in text and "Folgen" in text:
|
||
logging.debug(f"Gefunden 'bisher X Folgen' mit ID: {serie_id}")
|
||
staffel_links.append(('folgen', section_id, serie_id))
|
||
elif text.isdigit(): # Jahresformat
|
||
logging.debug(f"Gefunden Jahr {text} mit ID: {serie_id}")
|
||
staffel_links.append(('jahr', section_id, serie_id))
|
||
|
||
if not staffel_links:
|
||
logging.warning("Keine Staffeln oder Episodengruppen gefunden!")
|
||
return None
|
||
|
||
if staffel_nr:
|
||
# Suche nach der gewünschten Staffel (nur für normale Staffeln)
|
||
for typ, nr, serie_id in staffel_links:
|
||
if typ == 'staffel' and 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 den neuesten Eintrag
|
||
if not staffel_links:
|
||
return None
|
||
|
||
# Sortiere nach Typ und dann nach Nummer/ID
|
||
def sort_key(x):
|
||
typ, nr, serie_id = x
|
||
type_priority = {'staffel': 3, 'jahr': 2, 'folgen': 1}
|
||
# Bei Staffeln nach Staffelnummer sortieren, sonst nach section_id
|
||
if typ == 'staffel':
|
||
return (type_priority.get(typ, 0), int(nr)) # nr ist hier die Staffelnummer
|
||
else:
|
||
return (type_priority.get(typ, 0), int(nr)) # nr ist hier die section_id
|
||
|
||
newest = max(staffel_links, key=sort_key)
|
||
typ, nr, serie_id = newest
|
||
|
||
if typ == 'staffel':
|
||
url = f"https://www.fernsehserien.de/{slug}/episodenguide/staffel-{nr}/{serie_id}"
|
||
logging.debug(f"Generierte URL für neueste Staffel ({nr}): {url}")
|
||
else:
|
||
url = f"https://www.fernsehserien.de/{slug}/episodenguide/{nr}/{serie_id}"
|
||
logging.debug(f"Generierte URL für Episodengruppe: {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
|
||
|
||
episodes = []
|
||
page = 1
|
||
# Prüfe ob die Serie als "ohne klassische Staffeln" markiert ist
|
||
no_seasons = selected_data.get('no_seasons', False)
|
||
|
||
while True:
|
||
current_url = f"{url}/{page}" if page > 1 and no_seasons else url
|
||
logging.debug(f"Hole Episoden von: {current_url}")
|
||
|
||
try:
|
||
response = requests.get(current_url)
|
||
if response.status_code == 404:
|
||
logging.debug(f"Seite {page} nicht gefunden, beende Suche")
|
||
break
|
||
|
||
# Bei normalen Staffeln nur eine Seite laden
|
||
if not no_seasons:
|
||
logging.debug("Serie hat klassische Staffeln, lade nur aktuelle Seite")
|
||
|
||
response.raise_for_status() # Prüfe auf andere HTTP-Fehler
|
||
soup = BeautifulSoup(response.text, 'html.parser')
|
||
|
||
# Prüfe ob die Seite Episoden enthält
|
||
page_episodes = soup.find_all('section', {'itemprop': 'episode'})
|
||
if not page_episodes:
|
||
logging.debug(f"Keine Episoden auf Seite {page} gefunden, beende Suche")
|
||
break
|
||
|
||
for episode in page_episodes:
|
||
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
|
||
})
|
||
|
||
# Bei normalen Staffeln nach der ersten Seite aufhören
|
||
if not no_seasons:
|
||
break
|
||
page += 1
|
||
|
||
except requests.RequestException as e:
|
||
logging.error(f"Fehler beim Abrufen von Seite {page}: {str(e)}")
|
||
break
|
||
|
||
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 alle gefundenen Episoden an
|
||
# episodes = episodes[:20] # Alte Begrenzung entfernt
|
||
|
||
# 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_()) |