Youtube Channel Scratch – DarkCorners

Date: 16/12/2025

Category: Python

  • Phần mềm hỗ trợ lấy tất cả thông tin từ kênh youtube.
  • Những thông tin sẽ lấy. Toàn bộ danh sách video. Kèm theo thông tin tên, mô tả, urls, ngày đăng.
  • Cần chuẩn bị API Keys tương ứng “Youtube Data APIs”.
  1. Khởi động phần mềm.
  2. Chọn thư mục chứa tệp được xuất ra.
  3. Điền liên kết của kênh youtube cần lấy thông tin.
  4. Chọn loại tệp xuất ra và thời gian.
  5. Nhấn nút [Bắt Đầu].
  6. Theo dõi lịch trình xử lý.
import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext
from tkinter.ttk import Combobox
import threading
import os
import csv
import requests
import isodate
from datetime import datetime
from urllib.parse import urlparse
from googleapiclient.discovery import build

API_KEY = 'AIzaSyA-_OOaPgMbzoou8GLuEn9-a3AWMhe2P8U'

# --- Phân tích đầu vào ---
def resolve_channel_id(youtube, input_value, log_fn=None):
    from selenium import webdriver
    from selenium.webdriver.chrome.options import Options
    import re, time
    from urllib.parse import urlparse
    input_value = input_value.strip()
    if input_value.startswith("http"):
        parsed = urlparse(input_value)
        path = parsed.path
        if '/channel/' in path:
            return path.split('/channel/')[-1]
        elif '/@' in path:
            try:
                options = Options()
                options.add_argument("--headless")
                options.add_argument("--disable-gpu")
                options.add_argument("--log-level=3")
                options.add_argument("--window-size=1920,1080")
                if log_fn:
                    log_fn("🔎 Đang tìm kiếm ID kênh YouTube từ handle...")
                driver = webdriver.Chrome(options=options)
                driver.get(input_value)
                time.sleep(2)
                html = driver.page_source
                driver.quit()
                match = re.search(r'https://www\.youtube\.com/channel/(UC[\w-]+)', html)
                if match:
                    return match.group(1)
                else:
                    raise Exception("Không xác định được Channel ID từ handle.")
            except Exception as e:
                raise Exception(f"Không thể truy cập handle: {e}")
        else:
            raise Exception("❌ Link không hợp lệ.")
    else:
        return input_value

# --- API ---
def get_channel_title(youtube, channel_id):
    res = youtube.channels().list(part='snippet', id=channel_id).execute()
    if 'items' not in res or not res['items']:
        raise Exception("❌ Không tìm thấy thông tin kênh YouTube.")
    return res['items'][0]['snippet']['title']

def get_uploads_playlist_id(youtube, channel_id):
    res = youtube.channels().list(part='contentDetails', id=channel_id).execute()
    return res['items'][0]['contentDetails']['relatedPlaylists']['uploads']

def get_video_details(youtube, video_id):
    res = youtube.videos().list(part='snippet,contentDetails', id=video_id).execute()
    item = res['items'][0]
    snippet = item['snippet']
    raw_duration = item['contentDetails']['duration']
    duration = parse_duration(raw_duration)
    return {
        'title': snippet['title'],
        'description': snippet.get('description', '').replace('\r', '').strip(),
        'publishedAt': snippet['publishedAt'],
        'url': f'https://www.youtube.com/watch?v={video_id}',
        'duration': duration
    }

def get_all_videos(youtube, playlist_id, log_fn, days_limit=None):
    from dateutil import parser
    from datetime import timedelta
    videos = []
    next_page = None
    while True:
        res = youtube.playlistItems().list(
            part='contentDetails',
            playlistId=playlist_id,
            maxResults=50,
            pageToken=next_page
        ).execute()
        for item in res['items']:
            vid = item['contentDetails']['videoId']
            details = get_video_details(youtube, vid)
            if days_limit is not None:
                published_date = parser.parse(details['publishedAt']).date()
                if published_date < (datetime.utcnow().date() - timedelta(days=days_limit)):
                    break
            videos.append(details)
            log_fn(f"✔ {details['title']}")
        next_page = res.get('nextPageToken')
        if not next_page:
            break
    return videos

def parse_duration(duration_str):
    try:
        duration = isodate.parse_duration(duration_str)
        total_seconds = int(duration.total_seconds())
        hours = total_seconds // 3600
        minutes = (total_seconds % 3600) // 60
        seconds = total_seconds % 60
        return f"{hours:02}:{minutes:02}:{seconds:02}"
    except Exception:
        return "00:00:00"

def save_to_file(videos, path, format, channel_title, log_fn):
    safe_title = "".join(c for c in channel_title if c.isalnum() or c in "-_ ").strip().replace(" ", "_")
    date_str = datetime.now().strftime('%Y%m%d')
    filename = f"{safe_title}-{date_str}.{format}"
    file_path = os.path.join(path, filename)
    if format == 'txt':
        with open(file_path, 'w', encoding='utf-8') as f:
            for v in videos:
                pub_date = v['publishedAt'][:10]
                description = v['description'].replace('\r', '').replace('\n', ' ')
                f.write(f"{v['url']}|{v['title']}|{description}|{pub_date}|{v['duration']}\n")
    elif format == 'csv':
        with open(file_path, 'w', encoding='utf-8-sig', newline='') as f:
            writer = csv.writer(f, quoting=csv.QUOTE_ALL)
            writer.writerow(['Link', 'Tiêu đề', 'Mô tả', 'Ngày xuất bản', 'Thời lượng'])
            for v in videos:
                pub_date = v['publishedAt'][:10]
                writer.writerow([v['url'], v['title'], v['description'], pub_date, v['duration']])
    else:
        log_fn(f"❌ Định dạng không hợp lệ: {format}")
        return

    log_fn(f"\n✅ Đã trích xuất được {len(videos)} video theo yêu cầu của bạn.\n\n✅ Tập tin được lưu vào {file_path}")

def start_scrape(input_value, output_dir, format, log_fn, button, days_limit=None):
    def task():
        try:
            log_fn("🔍 Đang kết nối với YouTube API...")
            youtube = build('youtube', 'v3', developerKey=API_KEY)

            channel_id = resolve_channel_id(youtube, input_value, log_fn)
            log_fn(f"🔎 ID Kênh YouTube : {channel_id}\n")

            channel_title = get_channel_title(youtube, channel_id)
            log_fn(f"📺 Tên Kênh YouTube: {channel_title}")

            playlist_id = get_uploads_playlist_id(youtube, channel_id)
            log_fn(f"📁 Playlist ID : {playlist_id}\n")

            videos = get_all_videos(youtube, playlist_id, log_fn, days_limit)
            save_to_file(videos, output_dir, format, channel_title, log_fn)
            log_fn("\n🌝 Đã Hoàn Tất.")
        except Exception as e:
            log_fn(f"❌ Lỗi: {e}")
        finally:
            button.config(state='normal')

    threading.Thread(target=task).start()

def center_window(root, width=900, 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}")

def main_gui():
    root = tk.Tk()
    root.title("Youtube Channel Scratch - DarkCorners")
    root.resizable(False, False)
    center_window(root, 900, 600)

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

    # Hàng 2 : Khung thông tin thư mục
    frame_top = tk.Frame(root)
    frame_top.pack(pady=10, padx=10, fill='x')
    # Hàng 2 Trái : Youtube Channel
    frame_channel_wrap = tk.Frame(frame_top, width=400, height=60)
    frame_channel_wrap.pack_propagate(False)
    frame_channel_wrap.pack(side=tk.LEFT, padx=(0, 5), expand=True, fill='both')
    lf_channel = tk.LabelFrame(frame_channel_wrap, text="Youtube Channel", font=("Arial", 10, "bold"))
    lf_channel.pack(fill='both', expand=True)
    frame_channel = tk.Frame(lf_channel)
    frame_channel.pack(fill='x', pady=5, padx=5)
    tk.Label(frame_channel, text="ID / Link", font=("Arial", 10)).pack(side=tk.LEFT)
    channel_input_var = tk.StringVar()
    tk.Entry(frame_channel, textvariable=channel_input_var).pack(side=tk.LEFT, fill='x', expand=True, padx=(5, 0))
    # Hàng 2 Phải : Thư Mục Lưu Tệp
    frame_folder_wrap = tk.Frame(frame_top, width=400, height=60)
    frame_folder_wrap.pack_propagate(False)
    frame_folder_wrap.pack(side=tk.RIGHT, padx=(5, 0), expand=True, fill='both')
    lf_folder = tk.LabelFrame(frame_folder_wrap, text="Chọn Thư Mục Lưu Tệp", font=("Arial", 10, "bold"))
    lf_folder.pack(fill='both', expand=True)
    frame_folder = tk.Frame(lf_folder)
    frame_folder.pack(fill='x', pady=5, padx=5)
    folder_path = tk.StringVar()
    tk.Entry(frame_folder, textvariable=folder_path).pack(side=tk.LEFT, fill='x', expand=True)
    tk.Button(frame_folder, text="Chọn", command=lambda: folder_path.set(filedialog.askdirectory())).pack(side=tk.LEFT, padx=(5, 0))

    # Hàng 3 : Tùy Chỉnh & Điều Khiển
    frame_middle = tk.Frame(root)
    frame_middle.pack(pady=10, padx=10, fill='x')
    # Hàng 3 Trái : Tùy Chỉnh
    frame_format_wrap = tk.Frame(frame_middle, width=400, height=60)
    frame_format_wrap.pack_propagate(False)
    frame_format_wrap.pack(side=tk.LEFT, padx=(0, 5), expand=True, fill='both')
    lf_format = tk.LabelFrame(frame_format_wrap, text="Tùy Chỉnh", font=("Arial", 10, "bold"))
    lf_format.pack(fill='both', expand=True)
    frame_format = tk.Frame(lf_format)
    frame_format.pack(fill='x', pady=5, padx=5)
    tk.Label(frame_format, text="Định Dạng Tệp", font=("Arial", 10)).pack(side=tk.LEFT)
    format_var = tk.StringVar(value='csv')
    format_combo = Combobox(frame_format, textvariable=format_var, values=['csv', 'txt'], state='readonly', width=8, font=("Arial", 10))
    format_combo.pack(side=tk.LEFT, padx=(10, 20))
    tk.Label(frame_format, text="Giới Hạn Ngày", font=("Arial", 10)).pack(side=tk.LEFT)
    date_limit_var = tk.StringVar(value='Toàn Bộ')
    date_limit_combo = Combobox(frame_format, textvariable=date_limit_var, values=['07 Ngày', '10 Ngày', '20 Ngày', '30 Ngày', 'Toàn Bộ'], state='readonly', width=8, font=("Arial", 10))
    date_limit_combo.pack(side=tk.LEFT, padx=(10, 0))
    # Hàng 3 Phải : Điều Khiển
    frame_control_wrap = tk.Frame(frame_middle, width=400, height=60)
    frame_control_wrap.pack_propagate(False)
    frame_control_wrap.pack(side=tk.RIGHT, padx=(5, 0), expand=True, fill='both')
    lf_control = tk.LabelFrame(frame_control_wrap, text="Điều Khiển", font=("Arial", 10, "bold"))
    lf_control.pack(fill='both', expand=True)
    frame_control = tk.Frame(lf_control)
    frame_control.pack(pady=5, padx=5, fill='x')
    def log(message):
        log_text.insert(tk.END, message + "\n")
        log_text.see(tk.END)
    def on_start():
        input_val = channel_input_var.get().strip()
        out_dir = folder_path.get().strip()
        file_format = format_var.get()
        if not input_val:
            messagebox.showerror("Lỗi", "Vui lòng nhập Channel ID hoặc link.")
            return
        if not out_dir:
            messagebox.showerror("Lỗi", "Vui lòng chọn thư mục lưu file.")
            return
        start_button.config(state='disabled')
        log_text.delete(1.0, tk.END)

        days_str = date_limit_var.get()
        days_map = {
            '07 Ngày': 7,
            '10 Ngày': 10,
            '20 Ngày': 20,
            '30 Ngày': 30
        }
        days_limit = days_map.get(days_str, None)
        start_scrape(input_val, out_dir, file_format, log, start_button, days_limit)
    start_button = tk.Button(frame_control, text="▶ Bắt Đầu", command=on_start, font=("Arial", 10), bg="#4CAF50", fg="white", relief="ridge", width=12)
    start_button.pack(side=tk.LEFT, padx=10)
    tk.Button(frame_control, text="✖ Thoát", command=root.destroy, font=("Arial", 10), bg="#f44336", fg="white", relief="ridge", width=10).pack(side=tk.LEFT, padx=10)

    # Hàng 4 : Lịch Trình Xử Lý
    frame_log = tk.LabelFrame(root, text="Lịch Trình Xử Lý", font=("Arial", 10, "bold"))
    frame_log.pack(padx=10, pady=5, fill='both', expand=True)
    log_text = scrolledtext.ScrolledText(frame_log, font=("Consolas", 10))
    log_text.pack(fill='both', expand=True, padx=5, pady=5)

    root.mainloop()

if __name__ == "__main__":
    main_gui()
pyinstaller --noconsole --onefile --windowed --add-data "1-YoutubeChannelScratch-icon.ico;." --icon=1-YoutubeChannelScratch-icon.ico 1-YoutubeChannelScratch-Source.py

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