1
0
Files
MyAnimeList-Editor/main.py
2025-07-29 17:36:19 +02:00

341 lines
13 KiB
Python

import sys
import os
import xml.etree.ElementTree as ET
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QTableWidget, QTableWidgetItem, QPushButton,
QMessageBox, QHeaderView, QLabel, QComboBox, QSpinBox,
QFileDialog, QCheckBox, QScrollBar)
from PyQt5.QtCore import Qt, QObject, QEvent
class CustomTable(QTableWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.installEventFilter(self)
self.setVerticalScrollMode(QTableWidget.ScrollPerItem)
self.verticalScrollBar().setSingleStep(1)
self.verticalScrollBar().setPageStep(1)
def eventFilter(self, obj, event):
if event.type() == QEvent.KeyPress:
if event.key() == Qt.Key_Up:
current_row = self.currentRow()
if current_row > 0:
self.selectRow(current_row - 1)
self.setCurrentCell(current_row - 1, self.currentColumn())
# Stelle sicher, dass die ausgewählte Zeile sichtbar ist
self.scrollToItem(self.item(current_row - 1, 0))
return True
elif event.key() == Qt.Key_Down:
current_row = self.currentRow()
if current_row < self.rowCount() - 1:
self.selectRow(current_row + 1)
self.setCurrentCell(current_row + 1, self.currentColumn())
# Stelle sicher, dass die ausgewählte Zeile sichtbar ist
self.scrollToItem(self.item(current_row + 1, 0))
return True
return super().eventFilter(obj, event)
class AnimeListEditor(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("MyAnimeList Editor")
self.setGeometry(100, 100, 1200, 600)
# Initialisiere Variablen
self.current_file = None
self.tree = None
self.root = None
# Erstelle das zentrale Widget und Layout
central_widget = QWidget()
self.setCentralWidget(central_widget)
layout = QVBoxLayout(central_widget)
# Erstelle die obere Button-Leiste
top_button_layout = QHBoxLayout()
open_button = QPushButton("XML Öffnen")
open_button.clicked.connect(self.open_file)
top_button_layout.addWidget(open_button)
save_button = QPushButton("Speichern")
save_button.clicked.connect(self.save_changes)
save_button.setEnabled(False)
self.save_button = save_button
top_button_layout.addWidget(save_button)
save_as_button = QPushButton("Speichern unter")
save_as_button.clicked.connect(self.save_as)
save_as_button.setEnabled(False)
self.save_as_button = save_as_button
top_button_layout.addWidget(save_as_button)
# Füge Massenbearbeitung-Buttons hinzu
top_button_layout.addStretch()
select_all_button = QPushButton("Alle auswählen")
select_all_button.clicked.connect(self.select_all_entries)
top_button_layout.addWidget(select_all_button)
deselect_all_button = QPushButton("Alle abwählen")
deselect_all_button.clicked.connect(self.deselect_all_entries)
top_button_layout.addWidget(deselect_all_button)
delete_selected_button = QPushButton("Ausgewählte löschen")
delete_selected_button.clicked.connect(self.delete_selected_entries)
top_button_layout.addWidget(delete_selected_button)
layout.addLayout(top_button_layout)
# Erstelle die Tabelle
self.table = CustomTable()
self.table.setColumnCount(8) # Eine zusätzliche Spalte für Checkboxen
self.table.setHorizontalHeaderLabels([
"Auswahl", "Titel", "Typ", "Episoden", "Status",
"Gesehene Episoden", "Bewertung", "Aktionen"
])
# Setze die Spaltenbreiten
header = self.table.horizontalHeader()
header.setSectionResizeMode(0, QHeaderView.ResizeToContents) # Checkbox-Spalte
header.setSectionResizeMode(1, QHeaderView.Stretch) # Titel-Spalte
for i in range(2, 8):
header.setSectionResizeMode(i, QHeaderView.ResizeToContents)
layout.addWidget(self.table)
def select_all_entries(self):
for row in range(self.table.rowCount()):
checkbox = self.table.cellWidget(row, 0)
if checkbox:
checkbox.setChecked(True)
def deselect_all_entries(self):
for row in range(self.table.rowCount()):
checkbox = self.table.cellWidget(row, 0)
if checkbox:
checkbox.setChecked(False)
def delete_selected_entries(self):
selected_rows = []
for row in range(self.table.rowCount()):
checkbox = self.table.cellWidget(row, 0)
if checkbox and checkbox.isChecked():
selected_rows.append(row)
if not selected_rows:
QMessageBox.information(self, "Information", "Keine Einträge ausgewählt!")
return
reply = QMessageBox.question(
self, 'Bestätigung',
f'Möchten Sie die ausgewählten {len(selected_rows)} Einträge wirklich löschen?',
QMessageBox.Yes | QMessageBox.No
)
if reply == QMessageBox.Yes:
# Lösche von unten nach oben, um die Indizes nicht zu verschieben
for row in sorted(selected_rows, reverse=True):
self.table.removeRow(row)
def open_file(self):
file_name, _ = QFileDialog.getOpenFileName(
self,
"MyAnimeList XML öffnen",
"",
"XML Dateien (*.xml);;Alle Dateien (*.*)"
)
if file_name:
try:
self.tree = ET.parse(file_name)
self.root = self.tree.getroot()
self.current_file = file_name
# Aktualisiere den Fenstertitel
self.setWindowTitle(f"MyAnimeList Editor - {os.path.basename(file_name)}")
# Aktiviere die Buttons
self.save_button.setEnabled(True)
self.save_as_button.setEnabled(True)
# Lade die Daten
self.load_anime_data()
except ET.ParseError:
QMessageBox.critical(
self,
"Fehler",
"Die ausgewählte Datei ist keine gültige XML-Datei."
)
except Exception as e:
QMessageBox.critical(
self,
"Fehler",
f"Fehler beim Öffnen der Datei: {str(e)}"
)
def load_anime_data(self):
if not self.root:
return
# Finde alle Anime-Einträge
anime_entries = self.root.findall(".//anime")
# Setze die Anzahl der Zeilen
self.table.setRowCount(len(anime_entries))
# Deaktiviere die Sortierung während des Ladens
self.table.setSortingEnabled(False)
# Fülle die Tabelle
for row, anime in enumerate(anime_entries):
# Checkbox für Massenbearbeitung
checkbox = QCheckBox()
self.table.setCellWidget(row, 0, checkbox)
# Titel
title = anime.find("series_title").text.replace("<![CDATA[", "").replace("]]>", "")
title_item = QTableWidgetItem(title)
title_item.setFlags(title_item.flags() & ~Qt.ItemIsEditable) # Mache nicht editierbar
self.table.setItem(row, 1, title_item)
# Typ
type_item = QTableWidgetItem(anime.find("series_type").text)
type_item.setFlags(type_item.flags() & ~Qt.ItemIsEditable)
self.table.setItem(row, 2, type_item)
# Episoden
episodes = QTableWidgetItem(anime.find("series_episodes").text)
episodes.setFlags(episodes.flags() & ~Qt.ItemIsEditable)
self.table.setItem(row, 3, episodes)
# Status
status_combo = QComboBox()
status_combo.addItems(["Watching", "Completed", "On-Hold", "Dropped", "Plan to Watch"])
status_combo.setCurrentText(anime.find("my_status").text)
self.table.setCellWidget(row, 4, status_combo)
# Gesehene Episoden
watched_spin = QSpinBox()
watched_spin.setRange(0, int(anime.find("series_episodes").text))
watched_spin.setValue(int(anime.find("my_watched_episodes").text))
self.table.setCellWidget(row, 5, watched_spin)
# Bewertung
score_combo = QComboBox()
score_combo.addItems([str(i) for i in range(11)]) # 0-10
score_combo.setCurrentText(anime.find("my_score").text)
self.table.setCellWidget(row, 6, score_combo)
# Löschen Button
delete_button = QPushButton("Löschen")
delete_button.clicked.connect(lambda checked, row=row: self.delete_entry(row))
self.table.setCellWidget(row, 7, delete_button)
# Aktiviere die Sortierung wieder
self.table.setSortingEnabled(True)
# Setze die Scrollbar auf den Anfang
self.table.verticalScrollBar().setValue(0)
def delete_entry(self, row):
reply = QMessageBox.question(
self, 'Bestätigung',
'Möchten Sie diesen Eintrag wirklich löschen?',
QMessageBox.Yes | QMessageBox.No
)
if reply == QMessageBox.Yes:
self.table.removeRow(row)
def save_changes(self):
if not self.current_file or not self.root:
return
self.save_to_file(self.current_file)
def save_as(self):
if not self.root:
return
file_name, _ = QFileDialog.getSaveFileName(
self,
"Speichern unter",
"",
"XML Dateien (*.xml);;Alle Dateien (*.*)"
)
if file_name:
self.current_file = file_name
self.save_to_file(file_name)
self.setWindowTitle(f"MyAnimeList Editor - {os.path.basename(file_name)}")
def save_to_file(self, file_path):
# Entferne alle bestehenden Anime-Einträge
for anime in self.root.findall(".//anime"):
self.root.remove(anime)
# Füge die aktualisierten Einträge hinzu
for row in range(self.table.rowCount()):
anime = ET.SubElement(self.root, "anime")
# Titel
title = ET.SubElement(anime, "series_title")
title.text = f"<![CDATA[{self.table.item(row, 1).text()}]]>"
# Typ
type_elem = ET.SubElement(anime, "series_type")
type_elem.text = self.table.item(row, 2).text()
# Episoden
episodes = ET.SubElement(anime, "series_episodes")
episodes.text = self.table.item(row, 3).text()
# Status
status = ET.SubElement(anime, "my_status")
status.text = self.table.cellWidget(row, 4).currentText()
# Gesehene Episoden
watched = ET.SubElement(anime, "my_watched_episodes")
watched.text = str(self.table.cellWidget(row, 5).value())
# Bewertung
score = ET.SubElement(anime, "my_score")
score.text = self.table.cellWidget(row, 6).currentText()
# Füge weitere benötigte Felder hinzu
ET.SubElement(anime, "series_animedb_id").text = "0"
ET.SubElement(anime, "my_id").text = "0"
ET.SubElement(anime, "my_start_date").text = ""
ET.SubElement(anime, "my_finish_date").text = ""
ET.SubElement(anime, "my_rated").text = ""
ET.SubElement(anime, "my_storage").text = ""
ET.SubElement(anime, "my_storage_value").text = "0.00"
ET.SubElement(anime, "my_comments").text = "<![CDATA[]]>"
ET.SubElement(anime, "my_times_watched").text = "0"
ET.SubElement(anime, "my_rewatch_value").text = ""
ET.SubElement(anime, "my_priority").text = "LOW"
ET.SubElement(anime, "my_tags").text = "<![CDATA[]]>"
ET.SubElement(anime, "my_rewatching").text = "0"
ET.SubElement(anime, "my_rewatching_ep").text = "0"
ET.SubElement(anime, "my_discuss").text = "1"
ET.SubElement(anime, "my_sns").text = "default"
ET.SubElement(anime, "update_on_import").text = "0"
try:
# Speichere die Änderungen
self.tree.write(file_path, encoding='UTF-8', xml_declaration=True)
QMessageBox.information(self, "Erfolg", "Die Änderungen wurden gespeichert!")
except Exception as e:
QMessageBox.critical(
self,
"Fehler",
f"Fehler beim Speichern der Datei: {str(e)}"
)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = AnimeListEditor()
window.show()
sys.exit(app.exec_())