maç takip sitelerinin abuk subuk reklamlarından gına gelen bana "neden kendi masaüstü uygulamamı yapmıyorum, gol geldiğinde de bildirim versin" diyerek böyle birşey yaptım. api-football.com apisi ile çalışıyor.
requests ve plyer kurulumu yapmanız gerekiyor.

not: yaptım derken yapay zekaya yaptırdım. hakkını vereyim.




import json
import queue
import threading
import time
import tkinter as tk
from datetime import date
from pathlib import Path
from tkinter import messagebox, ttk
import requests
try:
    from plyer import notification
except Exception:  # plyer yoksa uygulama yine açılır
    notification = None
API_BASE = "https://v3.football.api-sports.io"
CONFIG_FILE = Path(__file__).with_name("config.json")
LIVE_STATUSES = {"1H", "HT", "2H", "ET", "BT", "P", "LIVE", "INT"}

# ----------------------------------------------------------------- veri katmanı
class ApiFootball:
    def __init__(self, api_key: str):
        self.headers = {"x-apisports-key": api_key}
    def _get(self, path: str, params: dict):
        r = requests.get(f"{API_BASE}{path}", headers=self.headers,
                         params=params, timeout=15)
        r.raise_for_status()
        data = r.json()
        if data.get("errors"):
            raise RuntimeError(str(data["errors"]))
        return data.get("response", [])
    def live(self):
        return [self._parse(x) for x in self._get("/fixtures", {"live": "all"})]
    def today(self):
        return [self._parse(x) for x in
                self._get("/fixtures", {"date": date.today().isoformat(),
                                        "timezone": "Europe/Istanbul"})]
    @staticmethod
    def _parse(x: dict) -> dict:
        fx, teams, goals = x["fixture"], x["teams"], x["goals"]
        return {
            "id": fx["id"],
            "league": x["league"]["name"],
            "home": teams["home"]["name"],
            "away": teams["away"]["name"],
            "gh": goals["home"] or 0,
            "ga": goals["away"] or 0,
            "status": fx["status"]["short"],
            "minute": fx["status"].get("elapsed"),
            "time": fx["date"][11:16],
        }

# ------------------------------------------------------------------- uygulama
class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Maç Takip")
        self.geometry("820x560")
        self.cfg = self._load_cfg()
        self.tracked: dict[int, dict] = {}   # id -> son bilinen maç verisi
        self.matches: dict[str, dict] = {}   # treeview iid -> maç
        self.events = queue.Queue()
        self.stop_flag = threading.Event()
        self.worker = None
        self._build_ui()
        self.after(300, self._drain_events)
        self.protocol("WM_DELETE_WINDOW", self._on_close)
    # ---- ayarlar
    def _load_cfg(self):
        try:
            return json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
        except Exception:
            return {"api_key": "", "interval": 90}
    def _save_cfg(self):
        CONFIG_FILE.write_text(json.dumps(self.cfg), encoding="utf-8")
    # ---- arayüz
    def _build_ui(self):
        top = ttk.Frame(self, padding=8)
        top.pack(fill="x")
        ttk.Label(top, text="API anahtarı:").pack(side="left")
        self.key_var = tk.StringVar(value=self.cfg.get("api_key", ""))
        ttk.Entry(top, textvariable=self.key_var, width=34, show="*").pack(side="left", padx=4)
        ttk.Label(top, text="Aralık (sn):").pack(side="left", padx=(10, 0))
        self.interval_var = tk.IntVar(value=self.cfg.get("interval", 90))
        ttk.Spinbox(top, from_=15, to=600, width=5, textvariable=self.interval_var).pack(side="left", padx=4)
        btns = ttk.Frame(self, padding=(8, 0))
        btns.pack(fill="x")
        ttk.Button(btns, text="Canlı maçlar", command=lambda: self._fetch("live")).pack(side="left")
        ttk.Button(btns, text="Bugünün maçları", command=lambda: self._fetch("today")).pack(side="left", padx=4)
        ttk.Button(btns, text="Seçilenleri takibe ekle", command=self._add_selected).pack(side="left", padx=12)
        ttk.Button(btns, text="Seçilenleri çıkar", command=self._remove_selected).pack(side="left")
        self.run_btn = ttk.Button(btns, text="▶ Takibi başlat", command=self._toggle)
        self.run_btn.pack(side="right")
        cols = ("tracked", "league", "match", "score", "status")
        self.tree = ttk.Treeview(self, columns=cols, show="headings", selectmode="extended")
        for c, t, w in (("tracked", "Takip", 50), ("league", "Lig", 170),
                        ("match", "Maç", 280), ("score", "Skor", 70), ("status", "Durum", 100)):
            self.tree.heading(c, text=t)
            self.tree.column(c, width=w, anchor="center" if c != "match" else "w")
        self.tree.pack(fill="both", expand=True, padx=8, pady=8)
        self.log = tk.Text(self, height=7, state="disabled")
        self.log.pack(fill="x", padx=8, pady=(0, 8))
    def _log(self, msg):
        self.log.configure(state="normal")
        self.log.insert("end", f"[{time.strftime('%H:%M:%S')}] {msg}\n")
        self.log.see("end")
        self.log.configure(state="disabled")
    # ---- maç listesi
    def _client(self):
        key = self.key_var.get().strip()
        if not key:
            messagebox.showwarning("Eksik", "Lütfen API anahtarını girin.")
            return None
        self.cfg.update(api_key=key, interval=int(self.interval_var.get()))
        self._save_cfg()
        return ApiFootball(key)
    def _fetch(self, kind):
        client = self._client()
        if not client:
            return
        def job():
            try:
                data = client.live() if kind == "live" else client.today()
                self.events.put(("list", data))
            except Exception as e:
                self.events.put(("log", f"Hata: {e}"))
        threading.Thread(target=job, daemon=True).start()
    def _fill_tree(self, matches):
        self.tree.delete(*self.tree.get_children())
        self.matches.clear()
        for m in matches:
            iid = str(m["id"])
            self.matches[iid] = m
            self.tree.insert("", "end", iid=iid, values=self._row(m))
        self._log(f"{len(matches)} maç listelendi.")
    def _row(self, m):
        mark = "✔" if m["id"] in self.tracked else ""
        status = f"{m['minute']}'" if m["status"] in LIVE_STATUSES and m["minute"] else (
            m["time"] if m["status"] == "NS" else m["status"])
        return (mark, m["league"], f"{m['home']} - {m['away']}", f"{m['gh']} - {m['ga']}", status)
    def _refresh_row(self, m):
        iid = str(m["id"])
        if self.tree.exists(iid):
            self.tree.item(iid, values=self._row(m))
    def _add_selected(self):
        for iid in self.tree.selection():
            m = self.matches[iid]
            self.tracked[m["id"]] = m
            self._refresh_row(m)
        self._log(f"Takipteki maç sayısı: {len(self.tracked)}")
    def _remove_selected(self):
        for iid in self.tree.selection():
            m = self.matches[iid]
            self.tracked.pop(m["id"], None)
            self._refresh_row(m)
        self._log(f"Takipteki maç sayısı: {len(self.tracked)}")
    # ---- takip döngüsü
    def _toggle(self):
        if self.worker and self.worker.is_alive():
            self.stop_flag.set()
            self.run_btn.configure(text="▶ Takibi başlat")
            self._log("Takip durduruldu.")
            return
        client = self._client()
        if not client:
            return
        if not self.tracked:
            messagebox.showinfo("Bilgi", "Önce takip edilecek maçları seçip ekleyin.")
            return
        self.stop_flag.clear()
        interval = max(15, int(self.interval_var.get()))
        self.worker = threading.Thread(target=self._poll, args=(client, interval), daemon=True)
        self.worker.start()
        self.run_btn.configure(text="■ Takibi durdur")
        self._log(f"Takip başladı ({interval} sn aralıkla).")
    def _poll(self, client, interval):
        while not self.stop_flag.is_set():
            try:
                live = {m["id"]: m for m in client.live()}
                for mid, old in list(self.tracked.items()):
                    new = live.get(mid)
                    if not new:
                        continue
                    self.events.put(("update", old, new))
            except Exception as e:
                self.events.put(("log", f"Veri alınamadı: {e}"))
            self.stop_flag.wait(interval)
    # ---- ana thread'de olay işleme
    def _drain_events(self):
        try:
            while True:
                ev = self.events.get_nowait()
                if ev[0] == "list":
                    self._fill_tree(ev[1])
                elif ev[0] == "log":
                    self._log(ev[1])
                elif ev[0] == "update":
                    self._handle_update(ev[1], ev[2])
        except queue.Empty:
            pass
        self.after(300, self._drain_events)
    def _handle_update(self, old, new):
        if new["id"] not in self.tracked:
            return
        self.tracked[new["id"]] = new
        self._refresh_row(new)
        title = f"{new['home']} {new['gh']} - {new['ga']} {new['away']}"
        if new["gh"] > old["gh"]:
            self._notify(f"⚽ GOL! {new['home']}", f"{title}  ({new['minute']}')")
        elif new["ga"] > old["ga"]:
            self._notify(f"⚽ GOL! {new['away']}", f"{title}  ({new['minute']}')")
        elif new["gh"] < old["gh"] or new["ga"] < old["ga"]:
            self._notify("Gol iptal edildi (VAR)", title)
    def _notify(self, title, msg):
        self._log(f"{title} | {msg}")
        if notification:
            try:
                notification.notify(title=title, message=msg, app_name="Maç Takip", timeout=10)
            except Exception as e:
                self._log(f"Bildirim gönderilemedi: {e}")
        else:
            self._log("plyer kurulu değil: pip install plyer")
    def _on_close(self):
        self.stop_flag.set()
        self.destroy()

if __name__ == "__main__":
    App().mainloop()