- Phần mềm hỗ trợ tải xuống video từ youtube hàng loạt.
- Nhập danh sách tải xuống theo cấu trúc : [filename]|[link\
- Phần mềm sẽ tải xuống video từ youtube. Sau đó lưu với tên mà bạn quy định.
- Cần chuẩn bị file cookies-youtube.txt được xuất từ Chrome Extension : https://chromewebstore.google.com/detail/cookie-editor/hlkenndednhfkekhgcdicdfddnkalmdm
- Khởi động phần mềm.
- 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.
- Điền danh sách liên kết youtube cần tải xuống.
- Chọn thời gian đợi giữa mỗi lần tải.
- Chọn dùng cookies và tải thumbnails hay không.
- Chọn chất lượng video sẽ tải xuống. Hỗ trợ 360, 480, 720, 1080.
- Nhấn nút [Bắt Đầu Tải Xuống].
- 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







