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