Cài đặt Live Stream Recorder

Phần mềm tải luồng trực tiếp không giới hạn dung lượng chạy trên máy cá nhân.

Quay lại Apps

1Chuẩn bị Môi trường Lõi (Bắt buộc)

❖ Dành cho Windows

Mở phần mềm Command Prompt (cmd) hoặc PowerShell dưới quyền Administrator và chạy lệnh sau để tự động cài đặt:

winget install ffmpeg
winget install yt-dlp
winget install python

❖ Dành cho macOS

Mở phần mềm Terminal và chạy lần lượt các lệnh sau (Yêu cầu máy đã cài sẵn Homebrew):

brew install ffmpeg
brew install yt-dlp
brew install python

Sau khi cài xong môi trường, chạy tiếp lệnh này để cài Giao diện (UI) cho Python:

pip install customtkinter

2Lưu Mã Nguồn

Tạo một file mới trên máy tính của bạn với tên tdg_downloader.py và dán toàn bộ đoạn code dưới đây vào.

import tkinter as tk
import customtkinter as ctk
import subprocess
import threading
import os
import re
import signal
import sys
import shutil
import time
from datetime import datetime

ctk.set_appearance_mode("Dark")
ctk.set_default_color_theme("blue")

class TDGDownloader(ctk.CTk):
    def __init__(self):
        super().__init__()
        self.title("TDG - Universal Engine v3.0 (DOM Injector)")
        self.geometry("850x650")
        self.minsize(600, 450)
        
        self.recording = False
        self.paused = False
        self.bulk_mode = False
        self.download_process = None
        self.part_count = 1
        self.session_id = ""
        
        self.download_path = os.path.join(os.path.expanduser("~"), "Desktop", "TDG_Downloads")
        if not os.path.exists(self.download_path):
            os.makedirs(self.download_path)

        self.setup_ui()
        self.log(">>> Logic: JS DOM Scraping | Rate Limit Defense | Live+VOD Support")

    def setup_ui(self):
        self.grid_columnconfigure(0, weight=1)
        self.grid_rowconfigure(5, weight=1)
        
        self.header_frame = ctk.CTkFrame(self, fg_color="transparent")
        self.header_frame.grid(row=0, column=0, padx=20, pady=20, sticky="ew")
        self.title_label = ctk.CTkLabel(self.header_frame, text="TDG UNIVERSAL RECORDER", font=ctk.CTkFont(size=20, weight="bold"))
        self.title_label.pack(side="left")
        self.status_signal = ctk.CTkLabel(self.header_frame, text="● READY", text_color="#3498db", font=ctk.CTkFont(size=14, weight="bold"))
        self.status_signal.pack(side="right")

        self.url_entry = ctk.CTkEntry(self, placeholder_text="Nhập link TikTok Live, hoặc quét FB Reels...", height=40)
        self.url_entry.grid(row=1, column=0, padx=20, pady=5, sticky="ew")

        self.btn_frame = ctk.CTkFrame(self, fg_color="transparent")
        self.btn_frame.grid(row=2, column=0, padx=20, pady=15, sticky="ew")
        self.btn_frame.grid_columnconfigure((0, 1, 2, 3, 4, 5), weight=1, uniform="group1")

        self.scan_btn = ctk.CTkButton(self.btn_frame, text="SCAN CHROME", command=self.get_chrome_url, fg_color="#34495e")
        self.scan_btn.grid(row=0, column=0, padx=5, sticky="ew")

        self.start_btn = ctk.CTkButton(self.btn_frame, text="START / RESUME", command=self.start_download, fg_color="#27ae60")
        self.start_btn.grid(row=0, column=1, padx=5, sticky="ew")

        self.bulk_btn = ctk.CTkButton(self.btn_frame, text="BULK REELS", command=self.start_bulk_download, fg_color="#8e44ad", hover_color="#9b59b6")
        self.bulk_btn.grid(row=0, column=2, padx=5, sticky="ew")

        self.pause_btn = ctk.CTkButton(self.btn_frame, text="PAUSE", command=self.pause_download, fg_color="#f39c12", state="disabled")
        self.pause_btn.grid(row=0, column=3, padx=5, sticky="ew")

        self.stop_btn = ctk.CTkButton(self.btn_frame, text="STOP", command=self.stop_download, fg_color="#c0392b", state="disabled")
        self.stop_btn.grid(row=0, column=4, padx=5, sticky="ew")

        self.folder_btn = ctk.CTkButton(self.btn_frame, text="FOLDER", command=self.open_folder, fg_color="#7f8c8d")
        self.folder_btn.grid(row=0, column=5, padx=5, sticky="ew")

        self.info_label = ctk.CTkLabel(self, text="Status: Khởi tạo hệ thống...", font=ctk.CTkFont(size=12))
        self.info_label.grid(row=3, column=0, padx=20, pady=(10, 0))
        self.progress_bar = ctk.CTkProgressBar(self)
        self.progress_bar.grid(row=4, column=0, padx=20, pady=10, sticky="ew")
        self.progress_bar.set(0)

        self.textbox = ctk.CTkTextbox(self, font=("Consolas" if sys.platform == "win32" else "Menlo", 11))
        self.textbox.grid(row=5, column=0, padx=20, pady=20, sticky="nsew")
        
        self.after(500, self.check_dependencies)

    def log(self, message):
        ts = datetime.now().strftime("%H:%M:%S")
        self.textbox.insert("end", f"[{ts}] {message}\n")
        self.textbox.see("end")

    def check_dependencies(self):
        try:
            missing = []
            if not shutil.which("yt-dlp"): missing.append("yt-dlp")
            if not shutil.which("ffmpeg"): missing.append("ffmpeg")
            
            if missing:
                self.log(f"LỖI: Thiếu thư viện lõi: {', '.join(missing)}. Vui lòng cài đặt (Windows: winget, Mac: brew).")
                self.status_signal.configure(text="● SYS ERROR", text_color="#c0392b")
                self.start_btn.configure(state="disabled")
                self.bulk_btn.configure(state="disabled")
            else:
                self.log(">>> Hệ thống an toàn. Đã sẵn sàng.")
                self.status_signal.configure(text="● READY", text_color="#3498db")
        except Exception as e:
            self.log(f"LỖI HỆ THỐNG MÔI TRƯỜNG: {str(e)}")
            self.status_signal.configure(text="● SYS ERROR", text_color="#c0392b")

    def open_folder(self):
        if sys.platform == "win32":
            os.startfile(self.download_path)
        else:
            subprocess.Popen(["open", self.download_path])

    def get_chrome_url(self):
        if sys.platform == "win32":
            self.log("Lỗi: Tính năng tự động SCAN CHROME chỉ hỗ trợ trên macOS. Hãy copy/paste URL thủ công.")
            return

        script = 'tell application "Google Chrome" to get URL of active tab of first window'
        try:
            url = subprocess.check_output(['osascript', '-e', script]).decode('utf-8').strip()
            url = re.sub(r'[?&]minHeight=\d+', '', url)
            self.url_entry.delete(0, tk.END)
            self.url_entry.insert(0, url)
            self.log(f"Bắt link: {url}")
        except: self.log("Lỗi: Không tìm thấy Chrome.")

    def start_download(self):
        url = self.url_entry.get().strip()
        if not url: return
        if not self.recording:
            self.session_id = datetime.now().strftime("%Y%m%d_%H%M")
            self.part_count = 1
        self.recording = True
        self.paused = False
        self.bulk_mode = False
        
        self.start_btn.configure(state="disabled")
        self.bulk_btn.configure(state="disabled")
        self.pause_btn.configure(state="normal")
        self.stop_btn.configure(state="normal")
        self.status_signal.configure(text="● RECORDING...", text_color="#e74c3c")
        
        threading.Thread(target=self.download_engine, args=(url,), daemon=True).start()

    def start_bulk_download(self):
        self.recording = True
        self.paused = False
        self.bulk_mode = True
        
        self.start_btn.configure(state="disabled")
        self.bulk_btn.configure(state="disabled")
        self.pause_btn.configure(state="disabled") 
        self.stop_btn.configure(state="normal")
        self.status_signal.configure(text="● BULK EXTRACTING...", text_color="#9b59b6")
        
        self.log(f"Khởi động cỗ máy quét DOM (Scraping)...")
        threading.Thread(target=self.bulk_engine, daemon=True).start()

    def safe_kill_process(self):
        if not self.download_process: return
        try:
            if sys.platform == "win32":
                self.download_process.send_signal(signal.CTRL_BREAK_EVENT)
            else:
                os.killpg(os.getpgid(self.download_process.pid), signal.SIGINT)
        except Exception as e:
            self.log(f"Lỗi đóng gói file: {str(e)}")

    def pause_download(self):
        if self.download_process and not self.bulk_mode:
            self.paused = True
            self.status_signal.configure(text="● SAVING PART...", text_color="#f1c40f")
            self.safe_kill_process()
            self.start_btn.configure(state="normal", text="RESUME")
            self.pause_btn.configure(state="disabled")

    def stop_download(self):
        self.recording = False
        self.paused = False
        if self.download_process:
            self.status_signal.configure(text="● STOPPING...", text_color="#f1c40f")
            self.safe_kill_process()
        self.reset_ui()

    def download_engine(self, url):
        while self.recording and not self.paused:
            output_file = os.path.join(self.download_path, f"TDG_{self.session_id}_Part{self.part_count}.mp4")
            cmd = [
                "yt-dlp", "--newline",
                "-S", "ext:mp4:m4a,vcodec:h264", 
                "--downloader", "ffmpeg",
                "--downloader-args", "ffmpeg:-fs 2048M -c copy -copyts -avoid_negative_ts make_non_negative -thread_queue_size 1024",
                "--remux-video", "mp4", "--no-part",
                "-o", output_file, url
            ]

            kwargs = {'stdout': subprocess.PIPE, 'stderr': subprocess.STDOUT, 'text': True}
            if sys.platform == "win32":
                kwargs['creationflags'] = subprocess.CREATE_NEW_PROCESS_GROUP
            else:
                kwargs['preexec_fn'] = os.setsid

            try:
                self.download_process = subprocess.Popen(cmd, **kwargs)
                for line in self.download_process.stdout:
                    if self.paused or not self.recording: break
                    self.update_ui_progress(line.strip())

                self.download_process.wait()
                if self.recording and not self.paused:
                    if "100%" in self.info_label.cget("text"):
                        self.log(">>> Hoàn tất tải file Video ngắn.")
                        break 
                    else: self.part_count += 1
                else: break
            except Exception as e:
                self.log(f"Lỗi Engine: {str(e)}")
                break

        self.reset_ui()

    def extract_reels_from_dom(self):
        if sys.platform == "win32":
            self.log("Lỗi: Tính năng chọc DOM tự động chỉ hỗ trợ trên macOS. Hãy dùng tính năng tải từng link cho Windows.")
            return []

        script = """
        tell application "Google Chrome"
            tell active tab of front window
                execute javascript "Array.from(new Set(Array.from(document.querySelectorAll('a[href*=\\\"/reel/\\\"]')).map(a => a.href))).join(',');"
            end tell
        end tell
        """
        try:
            result = subprocess.check_output(['osascript', '-e', script]).decode('utf-8').strip()
            if result:
                return result.split(',')
            return []
        except Exception as e:
            self.log("Lỗi AppleScript: Hãy chắc chắn bạn đã bật 'Allow JavaScript from Apple Events' trong menu View > Developer của Chrome.")
            return []

    def bulk_engine(self):
        links = self.extract_reels_from_dom()
        
        if not links:
            self.log(">>> Không tìm thấy video nào! Vui lòng cuộn trang FB xuống để hiện video, kiểm tra quyền JS và thử lại.")
            self.reset_ui()
            return
            
        total_videos = len(links)
        self.log(f">>> Đã vét được {total_videos} link Reels. Bắt đầu tải...")
        
        for index, url in enumerate(links, start=1):
            if not self.recording: break
            
            self.status_signal.configure(text=f"● BULK: {index}/{total_videos}", text_color="#9b59b6")
            self.info_label.configure(text=f"Đang xử lý video {index} trên tổng {total_videos}")
            self.log(f"-> Bắt đầu tải: {url}")
            
            output_template = os.path.join(self.download_path, "TDG_Reel_%(id)s.%(ext)s")
            
            cmd = [
                "yt-dlp", "--newline",
                "-S", "ext:mp4:m4a,vcodec:h264", 
                "--downloader", "ffmpeg",
                "--downloader-args", "ffmpeg:-c copy", 
                "--remux-video", "mp4", "--no-part",
                "-o", output_template, url
            ]

            kwargs = {'stdout': subprocess.PIPE, 'stderr': subprocess.STDOUT, 'text': True}
            if sys.platform == "win32":
                kwargs['creationflags'] = subprocess.CREATE_NEW_PROCESS_GROUP
            else:
                kwargs['preexec_fn'] = os.setsid

            try:
                self.download_process = subprocess.Popen(cmd, **kwargs)
                
                for line in self.download_process.stdout:
                    if not self.recording: break
                    if "%" in line:
                        try:
                            p = float(re.search(r'(\d+\.\d+)%', line).group(1)) / 100
                            self.progress_bar.set(p)
                        except: pass

                self.download_process.wait()
            except Exception as e:
                self.log(f"Lỗi tải video {index}: {str(e)}")
            
            if self.recording and index < total_videos:
                self.log("Nghỉ 1.5s chờ Rate Limit...")
                time.sleep(1.5)
                
        if self.recording:
            self.log(">>> HOÀN TẤT BULK DOWNLOAD! Toàn bộ thư mục Reels đã được tải.")
        
        self.reset_ui()

    def update_ui_progress(self, line):
        if "frame=" in line:
            self.info_label.configure(text=f"Live | Part {self.part_count} | {line[:40]}")
            self.progress_bar.configure(mode="indeterminate")
            self.progress_bar.start()
        elif "%" in line:
            try:
                p = float(re.search(r'(\d+\.\d+)%', line).group(1)) / 100
                self.progress_bar.set(p)
                self.info_label.configure(text=f"Đang tải: {int(p*100)}%")
                self.progress_bar.stop()
            except: pass

    def reset_ui(self):
        self.recording = False
        self.paused = False
        self.bulk_mode = False
        self.download_process = None
        
        self.start_btn.configure(state="normal", text="START / RESUME")
        self.bulk_btn.configure(state="normal")
        self.pause_btn.configure(state="disabled")
        self.stop_btn.configure(state="disabled")
        self.status_signal.configure(text="● READY", text_color="#3498db")
        self.progress_bar.stop()
        self.progress_bar.set(0)
        self.info_label.configure(text="Status: Ready")

if __name__ == "__main__":
    app = TDGDownloader()
    app.mainloop()

3Khởi chạy Phần mềm

Mở Terminal / Command Prompt tại thư mục chứa file tdg_downloader.py và gõ lệnh sau để mở giao diện tải Video:

python tdg_downloader.py

* Phần mềm sẽ tự động tạo thư mục TDG_Downloads trên Desktop của bạn để lưu Video. Hệ thống chống sập sẽ tự động chia nhỏ file khi đạt 2GB.