Youtube Video Downloader – DarkCorners

Date: 16/12/2025

Category: Python

  1. Khởi động phần mềm.
  2. Chọn thư mục chứa tệp được xuất ra. Gồm thư mục chứa video và chứa thumbnails.
  3. Điền danh sách liên kết youtube cần tải xuống.
  4. Chọn thời gian đợi giữa mỗi lần tải.
  5. Chọn dùng cookies và tải thumbnails hay không.
  6. Chọn chất lượng video sẽ tải xuống. Hỗ trợ 360, 480, 720, 1080.
  7. Nhấn nút [Bắt Đầu Tải Xuống].
  8. Theo dõi lịch trình xử lý.
import os
import sys
import tkinter as tk
from tkinter import filedialog, messagebox, ttk, scrolledtext
from threading import Thread
import queue
import urllib.parse
import yt_dlp
import random
import requests

class YtLogger:
    def __init__(self, log_callback):
        self.log_callback = log_callback
    def debug(self, msg): self.log_callback(msg)
    def info(self, msg): self.log_callback(msg)
    def warning(self, msg): self.log_callback(f"⚠️ {msg}")
    def error(self, msg): self.log_callback(f"❌ {msg}")

def center_window(root, width=800, height=600):
    # Lấy kích thước màn hình
    screen_width = root.winfo_screenwidth()
    screen_height = root.winfo_screenheight()
    # Tính vị trí bắt đầu trục ngang và trục đứng
    aaaxxx = (screen_width // 2) - (width // 2)
    bbbyyy = (screen_height // 2) - (height // 2) - (40)
    # Đặt geometry
    root.geometry(f"{width}x{height}+{aaaxxx}+{bbbyyy}")

class YoutubeDownloaderApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Youtube Video Downloader - DarkCorners")
        self.root.resizable(False, False)
        center_window(self.root, 800, 600)
        self.video_folder = tk.StringVar()
        self.thumb_folder = tk.StringVar()
        self.download_thumbs = tk.BooleanVar(value=True)
        self.use_cookies = tk.BooleanVar(value=True)
        self.delay_min = tk.IntVar(value=5)
        self.delay_max = tk.IntVar(value=15)
        self.status_message = tk.StringVar(value="")
        self.stop_download = False
        self.log_queue = queue.Queue()
        self.count_total = 0
        self.count_done = 0
        self.count_error = 0
        self.create_widgets()

    def create_widgets(self):
        # Hàng 1 : Tiêu đề phần mềm
        tk.Label(self.root, text="Youtube Video Downloader - DarkCorners", font=("Arial", 18, "bold")).pack(pady=10)

        # Hàng 2 : Chọn nơi tải xuống
        folder_frame = tk.LabelFrame(self.root, text="CHỌN NƠI TẢI XUỐNG", font=("Arial", 10, "bold"), bd=2, relief='groove', labelanchor='n', padx=10, pady=10)
        folder_frame.pack(fill='x', padx=10, pady=5)
        folder_row = tk.Frame(folder_frame)
        folder_row.pack(fill='x')
        for label_text, var, select_command in [
            ("Thư Mục Videos:", self.video_folder, self.select_video_folder),
            ("Thư Mục Thumbs:", self.thumb_folder, self.select_thumb_folder)
        ]:
            col = tk.Frame(folder_row)
            col.pack(side='left', expand=True, fill='x', padx=5)
            inner = tk.Frame(col)
            inner.pack(fill='x')
            tk.Label(inner, text=label_text, width=14, anchor='w').pack(side='left')
            tk.Entry(inner, textvariable=var).pack(side='left', fill='x', expand=True, padx=5)
            tk.Button(inner, text="Chọn", command=select_command).pack(side='left')

        # Hàng 3
        video_note_row = tk.Frame(self.root)
        video_note_row.pack(fill='x', padx=10, pady=5)
        video_note_row.columnconfigure(0, weight=3)
        video_note_row.columnconfigure(1, weight=2)

        # Hàng 3 Trái : Danh sách video (500px)
        video_input_frame = tk.LabelFrame(video_note_row, text="DANH SÁCH VIDEO CẦN DOWNLOAD", font=("Arial", 10, "bold"), bd=2, relief='groove', labelanchor='n')
        video_input_frame.pack(side='left')
        video_input_frame.config(width=514, height=200)
        video_input_frame.pack_propagate(False)
        self.text_links = scrolledtext.ScrolledText(video_input_frame, height=8)
        self.text_links.pack(fill='both', expand=True)
        # Hàng 3 Phải : Lưu Ý (40%)
        note_frame_container = tk.Frame(video_note_row, width=286, height=200)
        note_frame_container.pack(side='left', padx=(10, 0))
        note_frame_container.pack_propagate(False)
        note_frame = tk.LabelFrame(note_frame_container, text="LƯU Ý SỬ DỤNG", font=("Arial", 10, "bold"), bd=2, relief='groove', labelanchor='n')
        note_frame.pack(fill='both', expand=True)
        tk.Label(note_frame, text="- Danh sách video mỗi dòng phải theo cú pháp : filename|link.", anchor='w', justify='left', wraplength=240).pack(anchor='w', padx=10, pady=2)
        tk.Label(note_frame, text="- Định dạng cookie phải là Netscape(.txt). Bạn có thể xuất bằng tiện ích Cookie-Editor trên Google Chrome.", anchor='w', justify='left', wraplength=240).pack(anchor='w', padx=10, pady=2)
        tk.Label(note_frame, text="- Tải xuống Thumb được ưu tiên với định dạng WEBP. Nếu không khả dụng mới tải JPG", anchor='w', justify='left', wraplength=240).pack(anchor='w', padx=10, pady=2)

        # Hàng 4
        main_status_row = tk.Frame(self.root)
        main_status_row.pack(fill='x', padx=10, pady=10)
        # Hàng 4 Trái : Thống Kê & Điều Khiển
        left_frame = tk.LabelFrame(main_status_row, text="THỐNG KÊ & ĐIỀU KHIỂN", font=("Arial", 10, "bold"), bd=2, relief='groove', labelanchor='n')
        left_frame.pack(side='left', fill='both', expand=True, padx=5, pady=5)
        stats_row = tk.Frame(left_frame)
        stats_row.pack(pady=3)
        self.total_label = tk.Label(stats_row, text="[Tổng Số : 0]", font=('Arial', 10, 'bold'), fg='red')
        self.total_label.pack(side='left', padx=5)
        self.done_label = tk.Label(stats_row, text="[Đã Download: 0]", font=('Arial', 10, 'bold'), fg='green')
        self.done_label.pack(side='left', padx=5)
        self.waiting_label = tk.Label(stats_row, text="[Đang Đợi: 0]", font=('Arial', 10, 'bold'), fg='blue')
        self.waiting_label.pack(side='left', padx=5)
        self.error_label = tk.Label(stats_row, text="[Bị Lỗi : 0]", font=('Arial', 10, 'bold'), fg='black')
        self.error_label.pack(side='left', padx=5)
        options_row = tk.Frame(left_frame)
        options_row.pack(pady=3)
        tk.Label(options_row, text="Thời gian delay :").pack(side='left')
        tk.Entry(options_row, textvariable=self.delay_min, width=5).pack(side='left', padx=2)
        tk.Label(options_row, text="đến").pack(side='left')
        tk.Entry(options_row, textvariable=self.delay_max, width=5).pack(side='left', padx=2)
        tk.Checkbutton(options_row, text="Tải Thumbs", variable=self.download_thumbs).pack(side='left', padx=5)
        tk.Checkbutton(options_row, text="Dùng cookies-youtube.txt", variable=self.use_cookies).pack(side='left', padx=5)
        controls_row = tk.Frame(left_frame)
        controls_row.pack(pady=3)
        tk.Button(controls_row, text="Bắt Đầu Tải Xuống", command=self.start_download).pack(side='left', padx=10)
        tk.Button(controls_row, text="Dừng Lại", command=self.stop_download_func).pack(side='left', padx=10)
        tk.Button(controls_row, text="Thoát", command=self.quit_app).pack(side='left', padx=10)
        # Hàng 4 Phải : Tiến Trình Tải Xuống
        right_frame = tk.LabelFrame(main_status_row, text="TIẾN TRÌNH TẢI XUỐNG", font=("Arial", 10, "bold"), bd=2, relief='groove', labelanchor='n')
        right_frame.pack(side='right', fill='both', expand=True, padx=5, pady=5)
        cur_row = tk.Frame(right_frame)
        cur_row.pack(fill='x', pady=3, padx=10)
        tk.Label(cur_row, text="Video Hiện Tại:").grid(row=0, column=0, sticky='e')
        self.progress_current = ttk.Progressbar(cur_row, maximum=100)
        self.progress_current.grid(row=0, column=1, sticky='ew', padx=(5, 0))
        cur_row.columnconfigure(1, weight=1)
        total_row = tk.Frame(right_frame)
        total_row.pack(fill='x', pady=3, padx=10)
        tk.Label(total_row, text="Toàn Bộ Video:").grid(row=0, column=0, sticky='e')
        self.progress_total = ttk.Progressbar(total_row, maximum=100)
        self.progress_total.grid(row=0, column=1, sticky='ew', padx=(5, 0))
        total_row.columnconfigure(1, weight=1)
        status_row = tk.Frame(right_frame)
        status_row.pack(fill='x', pady=5, padx=10)
        tk.Label(status_row, text="Trạng Thái:").pack(side='left')
        tk.Label(status_row, textvariable=self.status_message, fg="blue").pack(side='left')

        # Hàng 5 : Logs
        log_frame = tk.LabelFrame(self.root, text="LOGS", font=("Arial", 10, "bold"), bd=2, relief='groove', labelanchor='n')
        log_frame.pack(fill='both', padx=10, pady=5, expand=True)
        self.log_text = scrolledtext.ScrolledText(log_frame, height=10)
        self.log_text.pack(fill='both', expand=True)

        self.root.after(100, self.process_log_queue)

    def update_stats_labels(self):
        waiting = self.count_total - self.count_done - self.count_error
        self.total_label.config(text=f"[Tổng Số: {self.count_total}]")
        self.done_label.config(text=f"[Đã Download: {self.count_done}]")
        self.waiting_label.config(text=f"[Đang Đợi: {waiting}]")
        self.error_label.config(text=f"[Bị Lỗi: {self.count_error}]")

    def select_video_folder(self):
        folder = filedialog.askdirectory()
        if folder:
            self.video_folder.set(folder)

    def select_thumb_folder(self):
        folder = filedialog.askdirectory()
        if folder:
            self.thumb_folder.set(folder)

    def log(self, message):
        self.log_queue.put(message)

    def process_log_queue(self):
        while not self.log_queue.empty():
            msg = self.log_queue.get()
            self.log_text.insert(tk.END, msg + "\n")
            self.log_text.see(tk.END)
        self.root.after(100, self.process_log_queue)

    def start_download(self):
        self.stop_download = False
        self.status_message.set("")
        self.progress_total['value'] = 0
        self.progress_current['value'] = 0
        self.link_list = [line.strip() for line in self.text_links.get("1.0", tk.END).strip().splitlines() if '|' in line]
        if not self.link_list:
            messagebox.showerror("Lỗi", "Danh sách link video cần download không hợp lệ.")
            return
        Thread(target=self.download_all).start()

    def stop_download_func(self):
        self.stop_download = True
        self.log("⛔ Đã yêu cầu dừng quá trình tải.")

    def extract_video_id(self, url):
        query = urllib.parse.urlparse(url)
        if query.hostname in ['www.youtube.com', 'youtube.com']:
            qs = urllib.parse.parse_qs(query.query)
            return qs.get('v', [None])[0]
        elif query.hostname == 'youtu.be':
            return query.path[1:]
        return None

    def ydl_progress_hook(self, d):
        if d['status'] == 'downloading' and 'total_bytes' in d and 'downloaded_bytes' in d:
            total = d['total_bytes']
            done = d['downloaded_bytes']
            percent = int((done / total) * 100)
            self.progress_current['value'] = percent
        elif d['status'] == 'finished':
            self.progress_current['value'] = 100

    def download_file(self, url, output_path):
        try:
            r = requests.get(url, stream=True)
            r.raise_for_status()
            with open(output_path, 'wb') as f:
                for chunk in r.iter_content(chunk_size=8192):
                    if self.stop_download:
                        return False
                    f.write(chunk)
            return True
        except Exception as e:
            self.log(f"❌ Lỗi tải thumbnail webp: {e}")
            return False

    def download_all(self):
        self.count_total = len(self.link_list)
        self.count_done = 0
        self.count_error = 0
        self.update_stats_labels()

        total = self.count_total
        for idx, line in enumerate(self.link_list):
            if self.stop_download:
                self.log("⛔ Đã dừng tải.")
                break
            try:
                filename, url = map(str.strip, line.split("|", 1))
                self.log(f"▶ Đang tải video: {filename}")
                self.status_message.set("Đang tải video...")
                self.progress_current['value'] = 0
                self.progress_total['value'] = (idx / total) * 100
                self.root.update_idletasks()

                success = self.download_with_ytdlp(url, filename)

                if self.download_thumbs.get():
                    video_id = self.extract_video_id(url)
                    if video_id:
                        thumb_base = os.path.join(self.thumb_folder.get(), filename)
                        thumb_url = f"https://i.ytimg.com/vi/{video_id}/maxresdefault.webp"
                        self.log(f"🖼️ Tải thumbnail webp: {thumb_url}")
                        self.download_file(thumb_url, thumb_base + ".webp")
                        if not os.path.exists(thumb_base + ".webp"):
                            thumb_url = f"https://i.ytimg.com/vi/{video_id}/maxresdefault.jpg"
                            self.log(f"↩️ Đang thử lại với jpg: {thumb_url}")
                            self.download_file(thumb_url, thumb_base + ".jpg")

                if success:
                    self.count_done += 1
                else:
                    self.count_error += 1

                self.update_stats_labels()
                self.progress_current['value'] = 100
                self.log(f"✅ Hoàn tất tải xuống video {filename}")
            except Exception as e:
                self.count_error += 1
                self.update_stats_labels()
                self.status_message.set("❌ Lỗi trong quá trình tải")
                self.log(f"❌ Lỗi với video \"{filename}\": {e}")

        self.progress_total['value'] = 100
        self.status_message.set("🎉 Đã hoàn tất toàn bộ.")
        self.update_stats_labels()

    def download_with_ytdlp(self, url, filename):
        min_delay = self.delay_min.get()
        max_delay = self.delay_max.get()

        ydl_opts = {
            'outtmpl': os.path.join(self.video_folder.get(), filename + ".%(ext)s"),
            'format': 'bestvideo+bestaudio/best',
            'merge_output_format': 'mp4',
            'noplaylist': True,
            'logger': YtLogger(self.log),
            'progress_hooks': [self.ydl_progress_hook],
            'sleep_interval': min_delay,
            'max_sleep_interval': max_delay,
        }

        if self.use_cookies.get():
            base_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
            cookies_txt = os.path.join(base_dir, "cookies-youtube.txt")
            if not os.path.exists(cookies_txt):
                self.log(f"❌ Không tìm thấy file cookies: {cookies_txt}")
                return False
            with open(cookies_txt, "r", encoding="utf-8") as f:
                lines = f.readlines()
            # ⚠️ LỌC các cookies quan trọng để tránh lỗi từ Firefox
            important_keys = [
                "SAPISID", "HSID", "SSID", "SID", "LOGIN_INFO", "PREF",
                "YSC", "VISITOR_INFO1_LIVE", "APISID", "__Secure-", "SIDCC"
            ]
            filtered_lines = [
                line for line in lines
                if any(key in line for key in important_keys)
            ]
            # Thêm dòng tiêu đề nếu thiếu
            if not filtered_lines or not filtered_lines[0].startswith("# Netscape HTTP Cookie File"):
                self.log("⚠️ Định dạng cookie thiếu dòng tiêu đề, đang tự động thêm...")
                filtered_lines.insert(0, "# Netscape HTTP Cookie File\n")
            # Ghi ra file tạm để yt-dlp sử dụng
            fixed_path = os.path.join(base_dir, "cookies-fixed.txt")
            with open(fixed_path, "w", encoding="utf-8") as f2:
                f2.writelines(filtered_lines)
            ydl_opts['cookiefile'] = fixed_path
        try:
            with yt_dlp.YoutubeDL(ydl_opts) as ydl:
                ydl.download([url])
            return True
        except Exception as e:
            self.log(f"❌ Lỗi tải video với yt-dlp: {e}")
            return False

    def quit_app(self):
        self.stop_download_func()
        self.root.destroy()

if __name__ == "__main__":
    root = tk.Tk()
    app = YoutubeDownloaderApp(root)
    root.mainloop()
import os
import sys
import tkinter as tk
from tkinter import filedialog, messagebox, ttk, scrolledtext
from threading import Thread
import queue
import urllib.parse
import yt_dlp
import random
import requests

class YtLogger:
    def __init__(self, log_callback):
        self.log_callback = log_callback
    def debug(self, msg): self.log_callback(msg)
    def info(self, msg): self.log_callback(msg)
    def warning(self, msg): self.log_callback(f"⚠️ {msg}")
    def error(self, msg): self.log_callback(f"❌ {msg}")

def center_window(root, width=800, height=600):
    # Lấy kích thước màn hình
    screen_width = root.winfo_screenwidth()
    screen_height = root.winfo_screenheight()
    # Tính vị trí bắt đầu trục ngang và trục đứng
    aaaxxx = (screen_width // 2) - (width // 2)
    bbbyyy = (screen_height // 2) - (height // 2) - (40)
    # Đặt geometry
    root.geometry(f"{width}x{height}+{aaaxxx}+{bbbyyy}")

class YoutubeDownloaderApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Youtube Video Downloader - DarkCorners")
        self.root.resizable(False, False)
        center_window(self.root, 800, 600)
        self.video_folder = tk.StringVar()
        self.thumb_folder = tk.StringVar()
        self.download_thumbs = tk.BooleanVar(value=True)
        self.use_cookies = tk.BooleanVar(value=True)
        self.delay_min = tk.IntVar(value=1)
        self.delay_max = tk.IntVar(value=3)
        self.status_message = tk.StringVar(value="")
        self.stop_download = False
        self.log_queue = queue.Queue()
        self.count_total = 0
        self.count_done = 0
        self.count_error = 0
        self.create_widgets()

    def create_widgets(self):
        # Hàng 1 : Tiêu đề phần mềm
        tk.Label(self.root, text="Youtube Video Downloader - DarkCorners", font=("Arial", 18, "bold")).pack(pady=10)

        # Hàng 2 : Chọn nơi tải xuống
        folder_frame = tk.LabelFrame(self.root, text="CHỌN NƠI TẢI XUỐNG", font=("Arial", 10, "bold"), bd=2, relief='groove', labelanchor='n', padx=10, pady=10)
        folder_frame.pack(fill='x', padx=10, pady=5)
        folder_row = tk.Frame(folder_frame)
        folder_row.pack(fill='x')
        for label_text, var, select_command in [
            ("Thư Mục Videos:", self.video_folder, self.select_video_folder),
            ("Thư Mục Thumbs:", self.thumb_folder, self.select_thumb_folder)
        ]:
            col = tk.Frame(folder_row)
            col.pack(side='left', expand=True, fill='x', padx=5)
            inner = tk.Frame(col)
            inner.pack(fill='x')
            tk.Label(inner, text=label_text, width=14, anchor='w').pack(side='left')
            tk.Entry(inner, textvariable=var).pack(side='left', fill='x', expand=True, padx=5)
            tk.Button(inner, text="Chọn", command=select_command).pack(side='left')

        # Hàng 3
        video_note_row = tk.Frame(self.root)
        video_note_row.pack(fill='x', padx=10, pady=5)
        video_note_row.columnconfigure(0, weight=3)
        video_note_row.columnconfigure(1, weight=2)

        # Hàng 3 Trái : Danh sách video (500px)
        video_input_frame = tk.LabelFrame(video_note_row, text="DANH SÁCH VIDEO CẦN DOWNLOAD", font=("Arial", 10, "bold"), bd=2, relief='groove', labelanchor='n')
        video_input_frame.pack(side='left')
        video_input_frame.config(width=514, height=200)
        video_input_frame.pack_propagate(False)
        self.text_links = scrolledtext.ScrolledText(video_input_frame, height=8)
        self.text_links.pack(fill='both', expand=True)
        # Hàng 3 Phải : Lưu Ý (40%)
        note_frame_container = tk.Frame(video_note_row, width=286, height=200)
        note_frame_container.pack(side='left', padx=(10, 0))
        note_frame_container.pack_propagate(False)
        note_frame = tk.LabelFrame(note_frame_container, text="LƯU Ý SỬ DỤNG", font=("Arial", 10, "bold"), bd=2, relief='groove', labelanchor='n')
        note_frame.pack(fill='both', expand=True)
        tk.Label(note_frame, text="- Danh sách video mỗi dòng phải theo cú pháp : filename|link.", anchor='w', justify='left', wraplength=240).pack(anchor='w', padx=10, pady=2)
        tk.Label(note_frame, text="- Định dạng cookie phải là Netscape(.txt). Bạn có thể xuất bằng tiện ích Cookie-Editor trên Google Chrome.", anchor='w', justify='left', wraplength=240).pack(anchor='w', padx=10, pady=2)
        tk.Label(note_frame, text="- Tải xuống Thumb được ưu tiên với định dạng WEBP. Nếu không khả dụng mới tải JPG", anchor='w', justify='left', wraplength=240).pack(anchor='w', padx=10, pady=2)

        # Hàng 4
        main_status_row = tk.Frame(self.root)
        main_status_row.pack(fill='x', padx=10, pady=10)
        # Hàng 4 Trái : Thống Kê & Điều Khiển
        left_frame = tk.LabelFrame(main_status_row, text="THỐNG KÊ & ĐIỀU KHIỂN", font=("Arial", 10, "bold"), bd=2, relief='groove', labelanchor='n')
        left_frame.pack(side='left', fill='both', expand=True, padx=5, pady=5)
        stats_row = tk.Frame(left_frame)
        stats_row.pack(pady=3)
        self.total_label = tk.Label(stats_row, text="[Tổng Số : 0]", font=('Arial', 10, 'bold'), fg='red')
        self.total_label.pack(side='left', padx=5)
        self.done_label = tk.Label(stats_row, text="[Đã Download: 0]", font=('Arial', 10, 'bold'), fg='green')
        self.done_label.pack(side='left', padx=5)
        self.waiting_label = tk.Label(stats_row, text="[Đang Đợi: 0]", font=('Arial', 10, 'bold'), fg='blue')
        self.waiting_label.pack(side='left', padx=5)
        self.error_label = tk.Label(stats_row, text="[Bị Lỗi : 0]", font=('Arial', 10, 'bold'), fg='black')
        self.error_label.pack(side='left', padx=5)
        options_row = tk.Frame(left_frame)
        options_row.pack(pady=3)
        tk.Label(options_row, text="Thời gian delay :").pack(side='left')
        tk.Entry(options_row, textvariable=self.delay_min, width=3).pack(side='left', padx=2)
        tk.Label(options_row, text="đến").pack(side='left')
        tk.Entry(options_row, textvariable=self.delay_max, width=3).pack(side='left', padx=2)
        tk.Checkbutton(options_row, text="Tải Thumbs", variable=self.download_thumbs).pack(side='left', padx=5)
        tk.Checkbutton(options_row, text="Dùng cookies-youtube.txt", variable=self.use_cookies).pack(side='left', padx=5)
        controls_row = tk.Frame(left_frame)
        controls_row.pack(pady=3)
        # Khai báo biến giữ chất lượng (default = 720p)
        self.quality = tk.StringVar(value="720p")
        # Thêm vào giao diện (trong khung TÙY CHỌN)
        tk.Label(controls_row, text="Chất lượng:").pack(side='left', padx=5)
        quality_combo = ttk.Combobox(controls_row, textvariable=self.quality, state="readonly", width=8)
        quality_combo['values'] = ["best", "1080p", "720p", "480p", "360p", "audio"]
        quality_combo.pack(side='left', padx=5)
        # Dãy nút điều khiển
        tk.Button(controls_row, text="Bắt Đầu Tải Xuống", command=self.start_download).pack(side='left', padx=10)
        tk.Button(controls_row, text="Dừng Lại", command=self.stop_download_func).pack(side='left', padx=10)
        tk.Button(controls_row, text="Thoát", command=self.quit_app).pack(side='left', padx=10)
        # Hàng 4 Phải : Tiến Trình Tải Xuống
        right_frame = tk.LabelFrame(main_status_row, text="TIẾN TRÌNH TẢI XUỐNG", font=("Arial", 10, "bold"), bd=2, relief='groove', labelanchor='n')
        right_frame.pack(side='right', fill='both', expand=True, padx=5, pady=5)
        cur_row = tk.Frame(right_frame)
        cur_row.pack(fill='x', pady=3, padx=10)
        tk.Label(cur_row, text="Video Hiện Tại:").grid(row=0, column=0, sticky='e')
        self.progress_current = ttk.Progressbar(cur_row, maximum=100)
        self.progress_current.grid(row=0, column=1, sticky='ew', padx=(5, 0))
        cur_row.columnconfigure(1, weight=1)
        total_row = tk.Frame(right_frame)
        total_row.pack(fill='x', pady=3, padx=10)
        tk.Label(total_row, text="Toàn Bộ Video:").grid(row=0, column=0, sticky='e')
        self.progress_total = ttk.Progressbar(total_row, maximum=100)
        self.progress_total.grid(row=0, column=1, sticky='ew', padx=(5, 0))
        total_row.columnconfigure(1, weight=1)
        status_row = tk.Frame(right_frame)
        status_row.pack(fill='x', pady=5, padx=10)
        tk.Label(status_row, text="Trạng Thái:").pack(side='left')
        tk.Label(status_row, textvariable=self.status_message, fg="blue").pack(side='left')

        # Hàng 5 : Logs
        log_frame = tk.LabelFrame(self.root, text="LOGS", font=("Arial", 10, "bold"), bd=2, relief='groove', labelanchor='n')
        log_frame.pack(fill='both', padx=10, pady=5, expand=True)
        self.log_text = scrolledtext.ScrolledText(log_frame, height=10)
        self.log_text.pack(fill='both', expand=True)

        self.root.after(100, self.process_log_queue)

    def update_stats_labels(self):
        waiting = self.count_total - self.count_done - self.count_error
        self.total_label.config(text=f"[Tổng Số: {self.count_total}]")
        self.done_label.config(text=f"[Đã Download: {self.count_done}]")
        self.waiting_label.config(text=f"[Đang Đợi: {waiting}]")
        self.error_label.config(text=f"[Bị Lỗi: {self.count_error}]")

    def select_video_folder(self):
        folder = filedialog.askdirectory()
        if folder:
            self.video_folder.set(folder)

    def select_thumb_folder(self):
        folder = filedialog.askdirectory()
        if folder:
            self.thumb_folder.set(folder)

    def log(self, message):
        self.log_queue.put(message)

    def process_log_queue(self):
        while not self.log_queue.empty():
            msg = self.log_queue.get()
            self.log_text.insert(tk.END, msg + "\n")
            self.log_text.see(tk.END)
        self.root.after(100, self.process_log_queue)

    def start_download(self):
        self.stop_download = False
        self.status_message.set("")
        self.progress_total['value'] = 0
        self.progress_current['value'] = 0
        self.link_list = [line.strip() for line in self.text_links.get("1.0", tk.END).strip().splitlines() if '|' in line]
        if not self.link_list:
            messagebox.showerror("Lỗi", "Danh sách link video cần download không hợp lệ.")
            return
        Thread(target=self.download_all).start()

    def stop_download_func(self):
        self.stop_download = True
        self.log("⛔ Đã yêu cầu dừng quá trình tải.")

    def extract_video_id(self, url):
        query = urllib.parse.urlparse(url)
        if query.hostname in ['www.youtube.com', 'youtube.com']:
            qs = urllib.parse.parse_qs(query.query)
            return qs.get('v', [None])[0]
        elif query.hostname == 'youtu.be':
            return query.path[1:]
        return None

    def ydl_progress_hook(self, d):
        if d['status'] == 'downloading' and 'total_bytes' in d and 'downloaded_bytes' in d:
            total = d['total_bytes']
            done = d['downloaded_bytes']
            percent = int((done / total) * 100)
            self.progress_current['value'] = percent
        elif d['status'] == 'finished':
            self.progress_current['value'] = 100

    def download_file(self, url, output_path):
        try:
            r = requests.get(url, stream=True)
            r.raise_for_status()
            with open(output_path, 'wb') as f:
                for chunk in r.iter_content(chunk_size=8192):
                    if self.stop_download:
                        return False
                    f.write(chunk)
            return True
        except Exception as e:
            self.log(f"❌ Lỗi tải thumbnail webp: {e}")
            return False

    def download_all(self):
        self.count_total = len(self.link_list)
        self.count_done = 0
        self.count_error = 0
        self.update_stats_labels()

        total = self.count_total
        for idx, line in enumerate(self.link_list):
            if self.stop_download:
                self.log("⛔ Đã dừng tải.")
                break
            try:
                filename, url = map(str.strip, line.split("|", 1))
                self.log(f"▶ Đang tải video: {filename}")
                self.status_message.set("Đang tải video...")
                self.progress_current['value'] = 0
                self.progress_total['value'] = (idx / total) * 100
                self.root.update_idletasks()

                success = self.download_with_ytdlp(url, filename)

                if self.download_thumbs.get():
                    video_id = self.extract_video_id(url)
                    if video_id:
                        thumb_base = os.path.join(self.thumb_folder.get(), filename)
                        thumb_url = f"https://i.ytimg.com/vi/{video_id}/maxresdefault.webp"
                        self.log(f"🖼️ Tải thumbnail webp: {thumb_url}")
                        self.download_file(thumb_url, thumb_base + ".webp")
                        if not os.path.exists(thumb_base + ".webp"):
                            thumb_url = f"https://i.ytimg.com/vi/{video_id}/maxresdefault.jpg"
                            self.log(f"↩️ Đang thử lại với jpg: {thumb_url}")
                            self.download_file(thumb_url, thumb_base + ".jpg")

                if success:
                    self.count_done += 1
                else:
                    self.count_error += 1

                self.update_stats_labels()
                self.progress_current['value'] = 100
                self.log(f"✅ Hoàn tất tải xuống video {filename}")
            except Exception as e:
                self.count_error += 1
                self.update_stats_labels()
                self.status_message.set("❌ Lỗi trong quá trình tải")
                self.log(f"❌ Lỗi với video \"{filename}\": {e}")

        self.progress_total['value'] = 100
        self.status_message.set("🎉 Đã hoàn tất toàn bộ.")
        self.update_stats_labels()

    def download_with_ytdlp(self, url, filename):
        min_delay = self.delay_min.get()
        max_delay = self.delay_max.get()

        output_template = os.path.join(self.video_folder.get(), filename + ".%(ext)s")
        
        # Lấy lựa chọn từ combobox
        quality = self.quality.get()
        if quality == "best":
            format_str = "bv*+ba/best"
        elif quality == "1080p":
            format_str = "bv*[height<=1080]+ba/best[height<=1080]"
        elif quality == "720p":
            format_str = "bv*[height<=720]+ba/best[height<=720]"
        elif quality == "480p":
            format_str = "bv*[height<=480]+ba/best[height<=480]"
        elif quality == "360p":
            format_str = "bv*[height<=360]+ba/best[height<=360]"
        elif quality == "audio":
            format_str = "bestaudio/best"
        else:
            format_str = "bv*+ba/best"

        ydl_opts = {
            'outtmpl': output_template,          # Đặt tên file theo input filename
            'format': format_str,
            'merge_output_format': 'mp4',        # Xuất mp4
            'noplaylist': True,
            'logger': YtLogger(self.log),
            'progress_hooks': [self.ydl_progress_hook],
            'sleep_interval': min_delay,
            'max_sleep_interval': max_delay,
            'ffmpeg_location': r'C:\ffmpeg\bin',  # ⚙️ Đường dẫn tới thư mục chứa ffmpeg.exe
        }

        if self.use_cookies.get():
            base_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
            cookies_txt = os.path.join(base_dir, "cookies-youtube.txt")
            if not os.path.exists(cookies_txt):
                self.log(f"❌ Không tìm thấy file cookies: {cookies_txt}")
                return False
            with open(cookies_txt, "r", encoding="utf-8") as f:
                lines = f.readlines()
            # ⚠️ LỌC các cookies quan trọng để tránh lỗi từ Firefox
            important_keys = [
                "SAPISID", "HSID", "SSID", "SID", "LOGIN_INFO", "PREF",
                "YSC", "VISITOR_INFO1_LIVE", "APISID", "__Secure-", "SIDCC"
            ]
            filtered_lines = [
                line for line in lines
                if any(key in line for key in important_keys)
            ]
            # Thêm dòng tiêu đề nếu thiếu
            if not filtered_lines or not filtered_lines[0].startswith("# Netscape HTTP Cookie File"):
                self.log("⚠️ Định dạng cookie thiếu dòng tiêu đề, đang tự động thêm...")
                filtered_lines.insert(0, "# Netscape HTTP Cookie File\n")
            # Ghi ra file tạm để yt-dlp sử dụng
            fixed_path = os.path.join(base_dir, "cookies-fixed.txt")
            with open(fixed_path, "w", encoding="utf-8") as f2:
                f2.writelines(filtered_lines)
            ydl_opts['cookiefile'] = fixed_path
        try:
            with yt_dlp.YoutubeDL(ydl_opts) as ydl:
                ydl.download([url])
            return True
        except Exception as e:
            self.log(f"❌ Lỗi tải video với yt-dlp: {e}")
            return False

    def quit_app(self):
        self.stop_download_func()
        self.root.destroy()

if __name__ == "__main__":
    root = tk.Tk()
    app = YoutubeDownloaderApp(root)
    root.mainloop()
pyinstaller --noconsole --onefile --windowed --add-data "1-YoutubeVideoDownloader-icon.ico;." --icon=1-YoutubeVideoDownloader-icon.ico 1-YoutubeVideoDownloaderV1-Source.py

Để lại một bình luận