import tkinter as tk from tkinter import messagebox, filedialog from tkinter import ttk import requests import re import os import subprocess import json from threading import Thread # Hilfsfunktion zur Extraktion der Video-ID aus Twitch-URL def extract_video_id(url): match = re.search(r"videos/(\d+)", url) return match.group(1) if match else None # Video-Metadaten (Titel) via Twitch GraphQL API holen def fetch_video_title(video_id): client_id = 'kimne78kx3ncx6brgo4mv6wki5h1ko' query = ''' query VideoByID($id: ID!) { video(id: $id) { title } } ''' payload = [{ 'operationName': 'VideoByID', 'variables': {'id': video_id}, 'query': query }] headers = {'Client-ID': client_id, 'Content-Type': 'application/json'} try: res = requests.post('https://gql.twitch.tv/gql', headers=headers, data=json.dumps(payload)) res.raise_for_status() data = res.json() return data[0]['data']['video']['title'] except Exception: return video_id # M3U8-Playlist abrufen über Twitch GraphQL API def get_all_m3u8_urls(video_id): payload = [{ "operationName": "PlaybackAccessToken_Template", "variables": {"vodID": video_id, "playerType": "embed"}, "query": """ query PlaybackAccessToken_Template($vodID: ID!, $playerType: String!) { videoPlaybackAccessToken( id: $vodID, params: { platform: \"web\", playerBackend: \"mediaplayer\", playerType: $playerType } ) { signature value } } """ }] headers = {"Client-ID": "kimne78kx3ncx6brgo4mv6wki5h1ko", "Content-Type": "application/json"} res = requests.post("https://gql.twitch.tv/gql", headers=headers, data=json.dumps(payload)) res.raise_for_status() data = res.json()[0]['data']['videoPlaybackAccessToken'] sig, token = data['signature'], data['value'] m3u8_url = f"https://usher.ttvnw.net/vod/{video_id}.m3u8" params = {"player": "twitchweb", "token": token, "sig": sig, "allow_source": "true", "allow_audio_only": "true", "playlist_include_framerate": "true"} playlist_resp = requests.get(m3u8_url, params=params) playlist_resp.raise_for_status() playlist = playlist_resp.text streams = re.findall(r"#EXT-X-STREAM-INF:.*RESOLUTION=(\d+x\d+).*\n(.*)", playlist, re.IGNORECASE) return {res: url for res, url in streams} # Download-Funktion, Aufrufe GUI-aktualisierend via safe callbacks def download_video(m3u8_url, output_path, safe_log, safe_progress): try: cmd = ["ffmpeg", "-i", m3u8_url, "-c", "copy", "-bsf:a", "aac_adtstoasc", output_path] process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding='utf-8', errors='ignore') for line in process.stdout: safe_log(line.strip()) process.wait() safe_log(f"Video gespeichert: {output_path}") safe_progress(100) except Exception as e: safe_log(f"Fehler beim ffmpeg-Lauf: {e}") safe_progress(0) # Hauptklasse für GUI def main(): root = tk.Tk() root.title("Twitch Downloader") # Widgets tk.Label(root, text="Twitch VOD URL:").pack(anchor='w', padx=10, pady=(10,0)) url_entry = tk.Entry(root, width=60) url_entry.pack(padx=10) tk.Label(root, text="Video-Titel:").pack(anchor='w', padx=10, pady=(10,0)) title_var = tk.StringVar() title_entry = tk.Entry(root, textvariable=title_var, width=60) title_entry.pack(padx=10) tk.Label(root, text="Qualität auswählen:").pack(anchor='w', padx=10, pady=(10,0)) quality_var = tk.StringVar() quality_menu = tk.OptionMenu(root, quality_var, "Bitte URL eingeben") quality_menu.pack(padx=10) progress = tk.DoubleVar() progressbar = ttk.Progressbar(root, variable=progress, maximum=100) progressbar.pack(fill='x', padx=10, pady=5) log_text = tk.Text(root, height=8, width=70, state='disabled') log_text.pack(padx=10, pady=(0,10)) output_path = [None] # mutable container # Thread-safe GUI-Update-Funktionen def safe_log(msg): root.after(0, lambda: log(msg)) def safe_progress(val): root.after(0, lambda: progress.set(val)) def log(msg): log_text.configure(state='normal') log_text.insert(tk.END, msg+"\n") log_text.see(tk.END) log_text.configure(state='disabled') def on_url_change(event=None): vid = extract_video_id(url_entry.get().strip()) if not vid: return title = fetch_video_title(vid) title_var.set(title) log(f"Titel geladen: {title}") streams = get_all_m3u8_urls(vid) menu = quality_menu['menu'] menu.delete(0,'end') for r,u in streams.items(): menu.add_command(label=r, command=lambda r=r: quality_var.set(r)) if streams: q = next(iter(streams)) quality_var.set(q) root.streams = streams log(f"Qualitäten: {', '.join(streams.keys())}") def save_as(): fname = filedialog.asksaveasfilename( defaultextension='.mp4', filetypes=[('MP4','*.mp4'),('All','*.*')], initialfile=f"{title_var.get() or 'video'}.mp4" ) if fname: output_path[0] = fname log(f"Speichern unter: {fname}") def start_dl(): if not output_path[0]: messagebox.showerror('Fehler','Bitte über "Speichern unter..." einen Zielpfad wählen.') return vid = extract_video_id(url_entry.get().strip()) q = quality_var.get() u = getattr(root,'streams',{}).get(q) if not all([vid,q,u]): messagebox.showerror('Fehler','Bitte URL eingeben und Qualität auswählen.') return log('Starte Download...') Thread(target=lambda: download_video(u, output_path[0], safe_log, safe_progress)).start() url_entry.bind('', on_url_change) tk.Button(root, text='Speichern unter...', command=save_as).pack(pady=5) tk.Button(root, text='Download starten', command=start_dl).pack(pady=5) root.mainloop() if __name__=='__main__': main()