Erste Version
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/build
|
||||
/dist
|
||||
/versions
|
||||
series_config.json
|
||||
release.md
|
96
README.md
Normal file
96
README.md
Normal file
@@ -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
|
45
Serien-Checker.spec
Normal file
45
Serien-Checker.spec
Normal file
@@ -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'],
|
||||
)
|
15
build.bat
Normal file
15
build.bat
Normal file
@@ -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
|
20
build.py
Normal file
20
build.py
Normal file
@@ -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'
|
||||
])
|
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
PyQt5==5.15.9
|
||||
requests==2.31.0
|
||||
beautifulsoup4==4.12.2
|
776
serien_checker.py
Normal file
776
serien_checker.py
Normal file
@@ -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 | <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"])
|
||||
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_())
|
Reference in New Issue
Block a user