Zum Inhalt

M165 NoSQL Datenbanken | Block 06

Inhaltsverzeichnis

mflix

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()

🖊️ Collection(s) erstellen, erste Daten einfügen

⛰️ sample_training