Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- 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('<FocusOut>', 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()
Add Comment
Please, Sign In to add comment