Skip to content

유튜브 다운로더 #14139

@billier3393

Description

@billier3393

What would you like to share?

import pandas as pd
from yt_dlp import YoutubeDL
import os
import time
import sys
import ctypes
import multiprocessing
import subprocess
import random

==========================================

[0] 사용자 설정 (FFmpeg 경로 유지)

==========================================

FFMPEG_PATH = r'E:\다운로드\ffmpeg-2026-01-05-git-2892815c45-full_build\ffmpeg-2026-01-05-git-2892815c45-full_build\bin\ffmpeg.exe'

DOWNLOAD_DIR = r'C:\YouTubeMusic_Downloads'
FINAL_DIR = os.path.join(DOWNLOAD_DIR, '정리완료_최종')

==========================================

[1] 유틸리티

==========================================

def get_program_path():
if getattr(sys, 'frozen', False):
return os.path.dirname(sys.executable)
else:
return os.path.dirname(os.path.abspath(file))

def is_admin():
try: return ctypes.windll.shell32.IsUserAnAdmin()
except: return False

==========================================

[2] 작업 함수

==========================================

def download_worker(args):
url, file_prefix, ffmpeg_path, download_dir = args

# [핵심 1] 차단 방지를 위해 2~5초 랜덤하게 쉬었다가 다운로드
time.sleep(random.uniform(2.0, 5.0))

ydl_opts = {
    'ffmpeg_location': ffmpeg_path,
    'format': 'bestaudio/best',
    'writethumbnail': True,
    
    # [핵심 2] PC가 아닌 '안드로이드 폰'인 척 위장 (쿠키 없이 403 우회)
    'extractor_args': {
        'youtube': {
            'player_client': ['android', 'ios'],
        }
    },
    
    # 보안 경고 무시 설정
    'nocheckcertificate': True,
    'ignoreerrors': True,
    'no_warnings': True,
    
    'postprocessors': [
        {'key': 'FFmpegExtractAudio', 'preferredcodec': 'mp3', 'preferredquality': '192'},
        {'key': 'EmbedThumbnail'}, # 썸네일 삽입
        {'key': 'FFmpegMetadata', 'add_metadata': True}
    ],
    'outtmpl': f'{download_dir}/{file_prefix}%(title)s.%(ext)s', 
    'restrictfilenames': False,
    'windowsfilenames': True,
    'noplaylist': True,
    'quiet': True,
}

try:
    with YoutubeDL(ydl_opts) as ydl:
        ydl.download([url])
    return f"✅ [다운로드 성공] {file_prefix}"
except Exception as e:
    # 에러 메시지 간소화
    msg = str(e)
    if "403" in msg:
        return f"❌ [다운로드 차단됨] {file_prefix} (잠시 후 다시 시도하세요)"
    return f"❌ [다운로드 실패] {file_prefix}: {msg[:30]}..."

def normalize_worker(args):
ffmpeg_path, input_p, output_p = args

command = [
    ffmpeg_path,
    '-hwaccel', 'cuda',               
    '-i', input_p,
    '-map', '0',           
    '-c:v', 'copy',        
    '-id3v2_version', '3', 
    '-af', 'loudnorm=I=-14:LRA=11:TP=-1', 
    '-c:a', 'libmp3lame',
    '-b:a', '192k',
    '-y', output_p
]

try:
    subprocess.run(command, check=True, capture_output=True)
    return f"✅ [소리조정 완료] {os.path.basename(input_p)}"
except:
    return f"❌ [소리조정 실패] {os.path.basename(input_p)}"

==========================================

[3] 메인 로직

==========================================

def run_main():
current_folder = get_program_path()
excel_file = os.path.join(current_folder, 'links.xlsx')

print(f"\n📂 프로그램 실행 위치: {current_folder}")
if not os.path.exists(DOWNLOAD_DIR): os.makedirs(DOWNLOAD_DIR)
if not os.path.exists(FINAL_DIR): os.makedirs(FINAL_DIR)

# ---------------------------------------------------------
# STEP 1: 다운로드 (안드로이드 모드)
# ---------------------------------------------------------
urls = []
if os.path.exists(excel_file):
    try:
        df = pd.read_excel(excel_file, engine='openpyxl')
        urls = df.iloc[:, 0].dropna().tolist()
        print(f"📋 다운로드 목록: {len(urls)}곡")
    except:
        print("❌ 엑셀 읽기 오류")
else:
    print("⚠️ 엑셀 파일 없음. 다운로드는 건너뜁니다.")

if urls:
    print("\n🚀 [1단계] 스마트폰 모드로 다운로드 시작 (차단 회피)")
    print("ℹ️ 안전을 위해 동시 다운로드를 3개로 제한합니다.")
    
    dl_args = [(url, f"{i:03d}. ", FFMPEG_PATH, DOWNLOAD_DIR) for i, url in enumerate(urls, start=1)]
    
    # [중요] 다운로드는 3개까지만 동시에 (봇 탐지 방지)
    with multiprocessing.Pool(processes=3) as pool:
        results = pool.map(download_worker, dl_args)
        for res in results: print(res)

# ---------------------------------------------------------
# STEP 2: 소리 조정 (GPU 가속)
# ---------------------------------------------------------
print("\n🎚️ [2단계] 소리 평준화 & 썸네일 정리")
files = [f for f in os.listdir(DOWNLOAD_DIR) if f.lower().endswith('.mp3')]

if files:
    print(f"🔥 총 {len(files)}곡 변환 중 (Ryzen 12스레드 풀가동)...")
    norm_args = [(FFMPEG_PATH, os.path.join(DOWNLOAD_DIR, f), os.path.join(FINAL_DIR, f)) for f in files]
    
    # 소리 조정은 유튜브 서버와 상관없으므로 12개 풀가동
    with multiprocessing.Pool(processes=12) as pool:
        results = pool.map(normalize_worker, norm_args)
        for res in results: print(res)
else:
    print("📂 작업할 파일이 없습니다.")

print("\n✨ 작업 완료! '정리완료_최종' 폴더를 확인하세요.")
print(f"📂 위치: {FINAL_DIR}")

if name == 'main':
multiprocessing.freeze_support()
if is_admin():
run_main()
input("\n엔터를 누르면 종료됩니다...")
else:
ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, " ".join(sys.argv), None, 1)

Additional information

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    awaiting triageAwaiting triage from a maintainer

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions