import tkinter as tk
from tkinter import ttk, messagebox
from pymongo import MongoClient
# --- MODEL: DATENBANK CLIENT ---
class AtlasClient:
def __init__(self, uri, db_name, collection_name):
self.client = MongoClient(uri)
self.db = self.client[db_name]
self.collection = self.db[collection_name]
def ping(self):
try:
self.client.admin.command('ping')
return True
except Exception as e:
messagebox.showerror("Verbindungsfehler", f"Datenbank offline:\n{e}")
return False
def find_movies(self, filter_query, sort_criteria=None, limit=0):
try:
cursor = self.collection.find(filter_query)
if sort_criteria:
cursor = cursor.sort(sort_criteria)
if limit > 0:
cursor = cursor.limit(limit)
return list(cursor)
except Exception as e:
print(f"Datenbankfehler: {e}")
return []
def aggregate_movies(self, pipeline):
try:
return list(self.collection.aggregate(pipeline))
except Exception as e:
print(f"Aggregation Fehler: {e}")
return []
# --- VIEW / CONTROLLER: GUI APP ---
class MflixApp:
def __init__(self, root, db_client):
self.root = root
self.db_client = db_client
self.root.title("Mflix Analytics | Dashboard v3.6")
self.root.geometry("700x850")
self.colors = {
"bg_main": "#ecf0f1",
"bg_card": "#ffffff",
"text_main": "#2c3e50",
"accent": "#3498db",
"success": "#27ae60",
"danger": "#e74c3c",
"header_bg": "#34495e",
"input_bg": "#f5f6fa"
}
self.root.configure(bg=self.colors["bg_main"])
self.style = ttk.Style()
self.style.theme_use('clam')
self.setup_styles()
self.create_widgets()
def setup_styles(self):
self.style.configure("TFrame", background=self.colors["bg_card"])
self.style.configure("Main.TFrame", background=self.colors["bg_main"])
self.style.configure("TLabelframe", background=self.colors["bg_card"], relief="flat", borderwidth=0)
self.style.configure("TLabelframe.Label", background=self.colors["bg_card"], foreground=self.colors["accent"], font=("Segoe UI", 11, "bold"))
self.style.configure("TButton", font=("Segoe UI", 9, "bold"), padding=6, background=self.colors["accent"], foreground="white")
self.style.map("TButton", background=[('active', '#2980b9')])
self.style.configure("Danger.TButton", background=self.colors["danger"])
self.style.configure("Success.TButton", background=self.colors["success"])
# --- HILFSFUNKTION: EINGABE VALIDIERUNG ---
def get_safe_int(self, entry_widget, name, min_val=1, max_val=2100):
"""Prüft ob die Eingabe eine gültige Zahl im Bereich ist."""
val_str = entry_widget.get().strip()
try:
val = int(val_str)
if val < min_val or val > max_val:
raise ValueError
return val
except ValueError:
messagebox.showwarning("Eingabefehler", f"Bitte gib für '{name}' eine gültige Zahl zwischen {min_val} und {max_val} ein.")
return None
def get_safe_float(self, entry_widget, name, min_val=0.0, max_val=10.0):
val_str = entry_widget.get().replace(",", ".").strip() # Komma zu Punkt wandeln
try:
val = float(val_str)
if val < min_val or val > max_val:
raise ValueError
return val
except ValueError:
messagebox.showwarning("Eingabefehler", f"Bitte gib für '{name}' eine Zahl zwischen {min_val} und {max_val} ein.")
return None
def create_widgets(self):
header_frame = tk.Frame(self.root, bg=self.colors["header_bg"], pady=20)
header_frame.pack(fill="x")
tk.Label(header_frame, text="MFLIX ANALYTICS PRO", font=("Segoe UI", 22, "bold"), bg=self.colors["header_bg"], fg="white").pack()
main_scroll_frame = ttk.Frame(self.root, style="Main.TFrame")
main_scroll_frame.pack(padx=25, pady=25, fill="both", expand=True)
self.create_query_card(main_scroll_frame, "1. Historische Auswertung", [
("Anzahl:", "limit1", "5"),
("Jahr:", "year1", "1999")
], self.query_1)
self.create_query_card_genre(main_scroll_frame)
self.create_query_card(main_scroll_frame, "3. Qualitäts-Filter (Rating)", [
("Anzahl:", "limit3", "5"),
("Min. Rating (0-10):", "rating3", "8.0")
], self.query_3)
self.create_query_card(main_scroll_frame, "4. Laufzeit-Analyse", [
("Anzahl:", "limit4", "10"),
("Min. Stunden:", "hours4", "2")
], self.query_4)
self.create_query_card_votes(main_scroll_frame)
ttk.Button(self.root, text="DASHBOARD SCHLIESSEN", command=self.root.destroy, style="Danger.TButton").pack(pady=(0, 20), fill="x", padx=50)
def create_query_card(self, parent, title, fields, command):
card_outer = tk.Frame(parent, bg=self.colors["bg_main"], pady=5)
card_outer.pack(fill="x")
frame = ttk.LabelFrame(card_outer, text=f" {title} ")
frame.pack(fill="x", ipady=5)
inner = tk.Frame(frame, bg=self.colors["bg_card"])
inner.pack(padx=15, pady=10, fill="x")
for label_text, var_name, default in fields:
tk.Label(inner, text=label_text, bg=self.colors["bg_card"]).pack(side="left", padx=(0, 5))
ent = tk.Entry(inner, width=8, bg=self.colors["input_bg"], relief="solid", bd=1)
ent.insert(0, default)
ent.pack(side="left", padx=(0, 15))
setattr(self, f"entry_{var_name}", ent)
ttk.Button(inner, text="Starten", command=command, style="Success.TButton").pack(side="right")
def create_query_card_genre(self, parent):
card_outer = tk.Frame(parent, bg=self.colors["bg_main"], pady=5)
card_outer.pack(fill="x")
frame = ttk.LabelFrame(card_outer, text=" 2. Genre & Besetzung ")
frame.pack(fill="x", ipady=5)
inner = tk.Frame(frame, bg=self.colors["bg_card"])
inner.pack(padx=15, pady=10, fill="x")
tk.Label(inner, text="Genre:", bg=self.colors["bg_card"]).pack(side="left")
self.combo_genre = ttk.Combobox(inner, values=["Action", "Comedy", "Drama", "Horror", "Sci-Fi", "Western", "Romance"], width=10, state="readonly")
self.combo_genre.current(0)
self.combo_genre.pack(side="left", padx=5)
tk.Label(inner, text="Schauspieler:", bg=self.colors["bg_card"]).pack(side="left", padx=(10, 0))
self.entry_actor = tk.Entry(inner, width=15, bg=self.colors["input_bg"], relief="solid", bd=1)
self.entry_actor.insert(0, "Dwayne Johnson")
self.entry_actor.pack(side="left", padx=5)
ttk.Button(inner, text="Starten", command=self.query_2, style="Success.TButton").pack(side="right")
def create_query_card_votes(self, parent):
card_outer = tk.Frame(parent, bg=self.colors["bg_main"], pady=5)
card_outer.pack(fill="x")
frame = ttk.LabelFrame(card_outer, text=" 5. Global Ranking (Meiste Votes) ")
frame.pack(fill="x", ipady=5)
inner = tk.Frame(frame, bg=self.colors["bg_card"])
inner.pack(padx=15, pady=10, fill="x")
tk.Label(inner, text="Limit:", bg=self.colors["bg_card"]).pack(side="left")
self.entry_limit5 = tk.Entry(inner, width=5, bg=self.colors["input_bg"], relief="solid", bd=1)
self.entry_limit5.insert(0, "5")
self.entry_limit5.pack(side="left", padx=5)
ttk.Button(inner, text="Pipeline Ausführen", command=self.query_5, style="TButton").pack(side="right")
def format_output(self, title, movies):
print(f"\n{'='*75}")
print(f"REPORT: {title.upper()}")
print(f"{'='*75}")
if not movies:
print(">> Keine Ergebnisse gefunden.")
else:
print(f"{'TITEL':<35} | {'JAHR':<6} | {'RTG':<4} | {'VOTES':<9} | {'DAUER'}")
print("-" * 75)
for m in movies:
t = str(m.get('title', 'Unbekannt'))[:34]
y = str(m.get('year', 'N/A'))
imdb = m.get('imdb', {}) if isinstance(m.get('imdb'), dict) else {}
r = imdb.get('rating', '-')
v = imdb.get('votes', '-')
rt = m.get('runtime', '-')
print(f"{t:<35} | {y:<6} | {str(r):<4} | {str(v):<9} | {rt} min")
print("-" * 75)
# --- QUERY METHODEN MIT SICHERHEITS-CHECK ---
def query_1(self):
y = self.get_safe_int(self.entry_year1, "Jahr", 1880, 2026)
lim = self.get_safe_int(self.entry_limit1, "Anzahl", 1, 100)
if y is not None and lim is not None:
res = self.db_client.find_movies({"year": y, "type": "movie"}, [("imdb.rating", -1)], lim)
self.format_output(f"Beste Filme aus {y}", res)
def query_2(self):
g, a = self.combo_genre.get(), self.entry_actor.get().strip()
if not a:
messagebox.showwarning("Eingabe fehlt", "Bitte gib einen Namen bei 'Actor' ein.")
return
res = self.db_client.find_movies({"genres": g, "cast": a, "type": "movie"}, [("year", -1)])
self.format_output(f"{g} Filme mit {a}", res)
def query_3(self):
lim = self.get_safe_int(self.entry_limit3, "Anzahl", 1, 100)
rat = self.get_safe_float(self.entry_rating3, "Rating", 0.0, 10.0)
if lim is not None and rat is not None:
q = {"imdb.rating": {"$gte": rat}, "imdb.votes": {"$gt": 1000}, "type": "movie"}
res = self.db_client.find_movies(q, [("imdb.rating", -1)], lim)
self.format_output(f"Top-Filme (Rating >= {rat})", res)
def query_4(self):
lim = self.get_safe_int(self.entry_limit4, "Anzahl", 1, 100)
hrs = self.get_safe_float(self.entry_hours4, "Stunden", 0.1, 10.0)
if lim is not None and hrs is not None:
minutes = hrs * 60
res = self.db_client.find_movies({"runtime": {"$gte": minutes}, "type": "movie"}, [("runtime", -1)], lim)
self.format_output(f"Filme länger als {hrs}h ({minutes} min)", res)
def query_5(self):
lim = self.get_safe_int(self.entry_limit5, "Limit", 1, 50)
if lim:
pipeline = [
{"$match": {"imdb.votes": {"$type": "number"}, "type": "movie"}},
{"$sort": {"imdb.votes": -1}},
{"$limit": lim * 2}, # Puffer für Dubletten
{"$group": {
"_id": "$title",
"title": {"$first": "$title"},
"year": {"$first": "$year"},
"imdb": {"$first": "$imdb"},
"runtime": {"$first": "$runtime"}
}},
{"$sort": {"imdb.votes": -1}},
{"$limit": lim}
]
res = self.db_client.aggregate_movies(pipeline)
self.format_output("Meistbewertete Filme", res)
if __name__ == "__main__":
URI = "mongodb+srv://tlangenauer_db_user:WcX8CbTuPjw6INOX@cluster0.u4yfkok.mongodb.net/?appName=Cluster0"
client = AtlasClient(URI, "sample_mflix", "movies")
if client.ping():
root = tk.Tk()
app = MflixApp(root, client)
root.mainloop()