YouTube-Downloader für Linux: Flexible Video-Auflösung

26. März 2025

YouTube-Videos herunterladen war noch nie so einfach: Mit einem kleinen Python-Skript, das ich mit Hilfe von Grok 3 (xAI) entwickelt habe, kannst du Videos in wählbarer Auflösung oder als MP3 speichern – und das mit einer übersichtlichen grafischen Oberfläche. Open Source, für Linux (Debian/Ubuntu) kompiliert und kinderleicht zu bedienen, ist es perfekt für alle, die Inhalte schnell offline verfügbar machen wollen.

Beachte vor dem Download der Werke die Rechtslage hinsichtlich der Urheber- und Nutzungsrechte, die von Land zu Land verschieden sein können. Sei besonders vorsichtig, wenn du heruntergeladene Videos und Musik teilst. Dies kann unter Umständen strafbar sein und sehr teuer werden.

Screenshot
Die grafische Oberfläche des kompakten Programms für Linux

Download in einer Zip-Datei: http://www.janson-soft.de/ubuntu/yt-playlist-downloader.zip

Die etwa 30 MByte große Zip-Datei enthält das Python-Skript, eine Kurzanleitung und das für Debian / Ubuntu kompilierte Programm.

Bedienung: Nach dem Start öffnet sich ein Fenster mit einem sanften Beige-Braun-Design, in dem du die YouTube-URL eingibst, den Speicherort wählst und einen Dateinamen festlegst. Über Dropdown-Menüs entscheidest du, ob du ein MP4-Video (in 360p, 720p oder 1080p) oder eine MP3-Datei möchtest. Ein Klick auf „Download starten“ genügt, und das Skript lädt den Inhalt herunter. Ein Fortschrittsbalken zeigt den Status an, während eine Textzeile darunter meldet, ob der Download läuft, die MP3-Konvertierung erfolgt oder temporäre Dateien bereinigt werden. Fehler werden in Pop-ups angezeigt – simpel und nutzerfreundlich.

Funktion: Das Skript nutzt die Bibliothek yt-dlp, um YouTube-Videos effizient herunterzuladen. Für MP4 wird die gewählte Auflösung (z. B. „bestvideo[height<=720]+bestaudio“) an yt-dlp übergeben, das Video und Audio kombiniert und in einer MP4-Datei speichert. Bei MP3 wählt es die niedrigste Videoqualität, lädt diese mit dem besten Audio und konvertiert das Ergebnis per ffmpeg in eine MP3-Datei mit 192 kBit/s. Die GUI basiert auf tkinter, während threading dafür sorgt, dass der Download im Hintergrund läuft, ohne das Fenster einzufrieren. Temporäre Dateien (wie .part oder .ytdl) werden nach Abschluss automatisch gelöscht, was den Prozess sauber hält.

Das vollständige Python-Skript:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# 2025-03-24 21:30:31 SM5ZBS

# getestet mit: 
# https://youtu.be/4HbyziUROS8?si=OkXN1qF1hJ7cgmod
# On the Edge of Dream and Reality: Steampunk, Baroque, Sci-Fi - AI Short Film

# https://youtu.be/DVI7rYdzH9g?si=ENfNS8mLC85wlsy_
# What If MARS COLONIZATION Was Possible? - Sci-Fi AI Short Film

# Abhängigkeiten:
# sudo apt update
# sudo apt install ffmpeg
# pip3 install yt-dlp

# Ausführbare Datei für Ubuntu erzeugen
# pyinstaller --onefile yt-mp4-mp3-downloader-video-res-gui.py

import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import yt_dlp
import os
import threading
import time
import subprocess
import glob
import fnmatch

class YouTubeDownloader:
    def __init__(self, root):
        self.root = root
        self.root.title("YouTube Downloader mit wählbarer Video-Auflösung")
        self.root.geometry("500x530")
        self.root.configure(bg="#E6D7B2")  # Sanftes Beige-Braun als Hintergrund

        # GUI-Elemente
        self.create_widgets()

    def create_widgets(self):
        style = ttk.Style()
        style.configure("TProgressbar", background="#A87C4D")  # Mittelbraun für Fortschrittsbalken
        style.configure("TButton", background="#D3A875", foreground="#3C2F2F")  # Warmer Braunton für Buttons
        style.configure("TCombobox", fieldbackground="#F5E8C7", foreground="#3C2F2F")  # Helles Beige für Dropdowns

        url_label = tk.Label(self.root, text="YouTube URL:", bg="#E6D7B2", fg="#3C2F2F")
        url_label.pack(pady=10, anchor="center")
        self.url_entry = tk.Entry(self.root, width=50, bg="#F5E8C7", fg="#3C2F2F", insertbackground="#3C2F2F")
        self.url_entry.pack(pady=5, anchor="center")

        save_label = tk.Label(self.root, text="Speicherort:", bg="#E6D7B2", fg="#3C2F2F")
        save_label.pack(pady=10, anchor="center")
        save_frame = tk.Frame(self.root, bg="#E6D7B2")
        save_frame.pack(anchor="center")
        self.save_path_var = tk.StringVar(value=os.getcwd())
        self.save_path_entry = tk.Entry(save_frame, textvariable=self.save_path_var, width=40, bg="#F5E8C7", fg="#3C2F2F", insertbackground="#3C2F2F")
        self.save_path_entry.pack(side=tk.LEFT, pady=5)
        browse_button = tk.Button(save_frame, text="Durchsuchen", command=self.browse_save_path, bg="#D3A875", fg="#3C2F2F")
        browse_button.pack(side=tk.LEFT, padx=5)

        filename_label = tk.Label(self.root, text="Dateiname (ohne Endung):", bg="#E6D7B2", fg="#3C2F2F")
        filename_label.pack(pady=10, anchor="center")
        self.filename_entry = tk.Entry(self.root, width=50, bg="#F5E8C7", fg="#3C2F2F", insertbackground="#3C2F2F")
        self.filename_entry.pack(pady=5, anchor="center")

        format_label = tk.Label(self.root, text="Format wählen:", bg="#E6D7B2", fg="#3C2F2F")
        format_label.pack(pady=10, anchor="center")
        self.format_var = tk.StringVar(value="MP4")
        format_combo = ttk.Combobox(self.root, textvariable=self.format_var, values=["MP4", "MP3"], state="readonly", style="TCombobox")
        format_combo.pack(pady=5, anchor="center")

        resolution_label = tk.Label(self.root, text="Auflösung (für MP4):", bg="#E6D7B2", fg="#3C2F2F")
        resolution_label.pack(pady=10, anchor="center")
        self.resolution_var = tk.StringVar(value="720p")
        resolution_combo = ttk.Combobox(self.root, textvariable=self.resolution_var, values=["360p", "720p", "1080p"], state="readonly", style="TCombobox")
        resolution_combo.pack(pady=5, anchor="center")

        self.download_button = tk.Button(self.root, text="Download starten", command=self.start_download, bg="#D3A875", fg="#3C2F2F")
        self.download_button.pack(pady=15, anchor="center")

        self.progress = ttk.Progressbar(self.root, length=400, mode="determinate", style="TProgressbar")
        self.progress.pack(pady=10, anchor="center")

        self.status_label = tk.Label(self.root, text="Bereit", bg="#E6D7B2", fg="#3C2F2F")
        self.status_label.pack(pady=10, anchor="center")

    def browse_save_path(self):
        folder = filedialog.askdirectory()
        if folder:
            self.save_path_var.set(folder)

    def download_video(self, url, save_path, filename):
        format_choice = self.format_var.get()
        resolution = self.resolution_var.get()
        mp4_file = os.path.join(save_path, f"{filename}.mp4")
        mp3_file = os.path.join(save_path, f"{filename}.mp3")
        output_template = os.path.join(save_path, f"{filename}.%(ext)s")

        # yt-dlp Optionen basierend auf Format und Auflösung
        if format_choice == "MP3":
            # Für MP3: Niedrigste Auflösung verwenden
            ydl_opts = {
                "format": "worstvideo+bestaudio/worst",  # Niedrigste Videoqualität
                "outtmpl": output_template,
                "merge_output_format": "mp4",
                "quiet": True,
                "progress_hooks": [self.progress_hook],
                "nopart": False,
            }
        else:
            # Für MP4: Gewählte Auflösung verwenden
            resolution_map = {
                "360p": "bestvideo[height<=360]+bestaudio/best[height<=360]",
                "720p": "bestvideo[height<=720]+bestaudio/best[height<=720]",
                "1080p": "bestvideo[height<=1080]+bestaudio/best[height<=1080]"
            }
            ydl_opts = {
                "format": resolution_map.get(resolution, "bestvideo[height<=720]+bestaudio/best[height<=720]"),  # Standard: 720p
                "outtmpl": output_template,
                "merge_output_format": "mp4",
                "quiet": True,
                "progress_hooks": [self.progress_hook],
                "nopart": False,
            }

        try:
            with yt_dlp.YoutubeDL(ydl_opts) as ydl:
                self.status_label.config(text="Download läuft...")
                self.download_button.config(state="disabled")
                ydl.download([url])
                if not os.path.exists(mp4_file):
                    raise Exception("MP4-Datei wurde nicht erstellt")

            if format_choice == "MP3":
                self.status_label.config(text="Konvertiere zu MP3...")
                ffmpeg_cmd = [
                    "ffmpeg", "-i", mp4_file, "-vn", "-acodec", "mp3", "-ab", "192k", "-y", mp3_file
                ]
                result = subprocess.run(ffmpeg_cmd, capture_output=True, text=True)
                if result.returncode != 0:
                    raise Exception(f"Konvertierung fehlgeschlagen: {result.stderr}")
                os.remove(mp4_file)
                final_file = mp3_file
                file_extension = "mp3"
            else:
                final_file = mp4_file
                file_extension = "mp4"

            self.status_label.config(text=f"Download abgeschlossen: {filename}.{file_extension}")
            self.progress["value"] = 100
            messagebox.showinfo("Erfolg", f"Datei wurde als {filename}.{file_extension} gespeichert!")
        except Exception as e:
            messagebox.showerror("Fehler", f"Download oder Konvertierung fehlgeschlagen: {str(e)}")
            self.status_label.config(text="Fehler beim Download/Konvertierung")
        finally:
            time.sleep(5)  # Erhöhte Wartezeit auf 5 Sekunden
            self.cleanup_temp_files(save_path, filename)
            self.download_button.config(state="normal")

    def progress_hook(self, d):
        if d["status"] == "downloading":
            total = d.get("total_bytes") or d.get("total_bytes_estimate", 0)
            downloaded = d.get("downloaded_bytes", 0)
            if total > 0:
                percentage = (downloaded / total) * 100
                self.progress["value"] = percentage
                self.root.update_idletasks()
        elif d["status"] == "finished":
            self.progress["value"] = 100

    def cleanup_temp_files(self, save_path, filename):
        self.status_label.config(text="Bereinige temporäre Dateien...")
        
        # Erweiterte Liste temporärer Endungen und Muster
        temp_patterns = [
            ".part", ".ytdl", ".temp", 
            r"\.f\d+", r"\.part-Frag\d*", 
            "*.webm", "*.part", "*.temp", 
            "*.ytdl", "*.mhtml", "*.part-*"
        ]
        
        deleted_files = []
        remaining_files = []

        try:
            # Kombiniere alle möglichen temporären Dateimuster
            search_patterns = [
                os.path.join(save_path, f"{filename}*"),
                os.path.join(save_path, f"*{filename}*")
            ]

            # Durchsuche alle Muster
            for pattern in search_patterns:
                for temp_file in glob.glob(pattern):
                    # Überprüfe, ob die Datei ein temporäres Muster enthält
                    if any(fnmatch.fnmatch(os.path.basename(temp_file), pat) for pat in temp_patterns):
                        try:
                            os.remove(temp_file)
                            deleted_files.append(os.path.basename(temp_file))
                        except Exception as e:
                            remaining_files.append(f"{os.path.basename(temp_file)} (Fehler: {str(e)})")
            
            # Zusätzliche Bereinigung für spezifische Dateitypen
            extra_patterns = [
                os.path.join(save_path, f"{filename}*.webm"),
                os.path.join(save_path, f"{filename}*.part"),
                os.path.join(save_path, f"{filename}*.temp")
            ]
            
            for pattern in extra_patterns:
                for extra_file in glob.glob(pattern):
                    try:
                        os.remove(extra_file)
                        if os.path.basename(extra_file) not in deleted_files:
                            deleted_files.append(os.path.basename(extra_file))
                    except Exception as e:
                        if os.path.basename(extra_file) not in remaining_files:
                            remaining_files.append(f"{os.path.basename(extra_file)} (Fehler: {str(e)})")

        except Exception as global_error:
            self.status_label.config(text=f"Kritischer Bereinigungsfehler: {str(global_error)}")

        # Statusmeldung mit Details
        if deleted_files:
            self.status_label.config(text=f"Gelöscht: {', '.join(deleted_files)}")
        if remaining_files:
            messagebox.showwarning("Bereinigungswarnungen", 
                                  f"Folgende Dateien konnten nicht gelöscht werden:\n{', '.join(remaining_files)}")
        else:
            self.status_label.config(text="Bereinigung abgeschlossen")

    def start_download(self):
        url = self.url_entry.get().strip()
        save_path = self.save_path_var.get().strip()
        filename = self.filename_entry.get().strip()

        if not url:
            messagebox.showwarning("Eingabe fehlt", "Bitte gib eine YouTube-URL ein!")
            return
        if not save_path:
            messagebox.showwarning("Eingabe fehlt", "Bitte wähle einen Speicherort!")
            return
        if not filename:
            messagebox.showwarning("Eingabe fehlt", "Bitte gib einen Dateinamen ein!")
            return

        self.progress["value"] = 0
        threading.Thread(target=self.download_video, args=(url, save_path, filename), daemon=True).start()

    def on_closing(self):
        self.root.destroy()

if __name__ == "__main__":
    root = tk.Tk()
    app = YouTubeDownloader(root)
    root.protocol("WM_DELETE_WINDOW", app.on_closing)
    root.mainloop()

Datenablauf: Alles beginnt mit der YouTube-URL, die an yt-dlp übergeben wird. Diese Bibliothek kontaktiert den YouTube-Server, analysiert die verfügbaren Formate und lädt die passenden Streams (Video und Audio) herunter. Die Daten werden zunächst als temporäre Dateien auf die Festplatte geschrieben. Bei MP4 werden Video- und Audiospur direkt zu einer finalen Datei gemerged; bei MP3 erfolgt nach dem Download eine Konvertierung mit ffmpeg, wobei die MP4-Zwischendatei gelöscht wird. Der Fortschrittsbalken wird über einen „progress_hook“ aktualisiert, der die heruntergeladenen Bytes mit der Gesamtgröße vergleicht. Am Ende liegt die fertige Datei im gewählten Ordner – bereit zur Nutzung.

Notwendige Abhängigkeiten: Damit das Skript läuft, brauchst du Python 3, yt-dlp („pip3 install yt-dlp“), ffmpeg („sudo apt install ffmpeg“) und tkinter, das meist vorinstalliert ist. Für die kompilierte Version unter Debian/Ubuntu habe ich PyInstaller verwendet („pyinstaller –onefile“), sodass du nur die ausführbare Datei brauchst. Der Quellcode ist Open Source und frei verfügbar – ideal zum Anpassen oder Erweitern.

Installiere folgende Abhängigkeiten:

pip3 install yt-dlp
sudo apt update
sudo apt install ffmpeg

Fazit: Dieses Tool ist ein praktischer Helfer für alle, die YouTube-Inhalte offline nutzen möchten, ohne sich mit komplizierten Kommandozeilen herumzuschlagen. Die Kombination aus einfacher Bedienung, flexibler Auflösungswahl und Open-Source-Charakter macht es zu einem Gewinn für Bastler und Technikfans. Ich nutze es selbst regelmäßig – und mit ein paar Zeilen Code kannst du es an deine Bedürfnisse anpassen.

Auch interessant:

Bildschirmfoto zu 2025 03 17 13 45 41
Mit kostenlosen Tools und Programmen baust du dir deine persönliche und private Musiksammlung auf.
Vom Video zur Playlist: YouTube-Downloads mit kostenlosen Tools meistern – 17. März 2025: YouTube ist eine Schatztruhe voller Videos und Musik, doch die Inhalte einfach herunterzuladen, ist oft eine Herausforderung. Browser-Plug-ins für Firefox, die früher funktionierten, sind meist veraltet oder inkompatibel. Zum Glück gibt es Alternativen! In diesem Artikel zeige ich dir, wie du mit dem 4K Video Downloader, Audacity, MusicBrainz Picard und ein paar Python-Skripten Videos und Musik von YouTube herunterladen, bearbeiten und organisieren kannst.