개발노트

파이썬으로 유튜브 영상 다운로드 앱 만들기

Happy._. 2025. 6. 23. 14:58
반응형

유튜브 영상을 직접 다운로드해서 저장하고 싶다면, 파이썬을 이용한 간단한 앱으로 손쉽게 해결할 수 있다.

 

⌨️ GUI 없이 콘솔에서 실행하는 앱

📦 필요한 라이브러리

유튜브 영상 다운로드 앱을 만들기 위해 필요한 라이브러리는 다음과 같다.

  • yt-dlp: 유튜브 포함 다양한 온라인 동영상 플랫폼에서 영상, 오디오 등을 다운로드할 수 있는 라이브러리

 

🔧 yt-dlp 설치

pip install yt-dlp

 

 

🎬 영상 다운로드 기본 코드

다음은 yt-dlp를 사용하여 유튜브 영상을 다운로드하는 기본 코드이다.

import yt_dlp


# 다운로드할 유튜브 영상 URL
url = "https://www.youtube.com/watch?v=영상ID"

# 저장할 폴더 경로
save_folder = "C:/Users/<사용자명>/Downloads"

# 다운로드 옵션 설정
ydl_opts = {
    'format': 'bestvideo+bestaudio/best',  # 영상+오디오 최고 화질 선택
    'outtmpl': f'{save_folder}/%(title)s.%(ext)s',  # 저장 파일명: 영상 제목.확장자
    'merge_output_format': 'mp4'  # 병합 후 저장할 파일 확장자
}

# 영상 다운로드 실행
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
    ydl.download([url])

 

 

⚠️ 병합 오류 해결: ffmpeg 설치 필요

위 코드를 실행할 때 다음과 같은 에러가 발생할 수 있다.

ERROR: You have requested merging of multiple formats but ffmpeg is not installed. Aborting due to --abort-on-error

 

이 오류는 yt-dlp가 영상과 오디오를 병합(merge) 하려 하는데, 시스템에 ffmpeg가 설치되어 있지 않기 때문이다.

 

 

✅ ffmpeg 설치 방법 (Windows 기준)

  1. https://www.gyan.dev/ffmpeg/builds/ 에서 ffmpeg-release-essentials.zip 다운로드
  2. 압축 해제 후 bin 폴더 경로 복사 (예: C:\dev\tools\ffmpeg-7.1.1\bin)
  3. 시스템 환경 변수(Path)에 복사한 경로 추가
  4. 터미널(cmd)에서 ffmpeg 입력 → 동작 여부 확인

 

GUI가 필요하지 않은 경우 위 내용만으로 영상을 다운로드할 수 있다.

GUI가 필요한 경우 계속 다음 내용을 이어가면 된다.

 

 

📱 GUI를 제공하는 앱

 

📦 필요한 라이브러리

파이썬으로 GUI 앱을 만들기 위해 필요한 라이브러리는 다음과 같다.

  • tkinter: 파이썬에서 GUI를 만들 수 있도록 도와주는 표준 내장 라이브러리로 별도 설치가 필요 없다.

 

🎬 GUI 포함 영상 다운로드 코드

import os
import threading
import tkinter as tk
from tkinter import filedialog, messagebox
from tkinter.ttk import Progressbar
import yt_dlp

# GUI 생성
root = tk.Tk()
root.title("YouTube 영상 다운로더")
root.geometry("600x220")

# 상태 텍스트 및 진행률 바: 전역
# Progressbar: tkinter.ttk 모듈의 진행률 표시 바 생성
# orient: 진행 바의 방향(horizontal: 수평)
# length: 바 길이 설정(픽셀 단위)
# mode: 진행 표시
# determinate: 정량적 진행 표시, 전체 작업량이 명확히 정의되어 있고, 현재 진행률(%)를 알 수 있을 때 사용(예: 파일 다운로드, 압축 해제 등)
# indeterminate: 불확실한 진행 표시(애니메이션만 반복), 얼마나 진행되었는지 알 수 없을 때 사용(예: 서버 응답 대기, 로딩 중 등)
progress = Progressbar(root, orient="horizontal", length=400, mode="determinate")
status_label = tk.Label(root, text="대기 중...")

# 폴더 선택
def browse_folder():
    # 사용자가 폴더를 선택하게 하는 폴더 탐색 창 띄우기
    folder_selected = filedialog.askdirectory() 
    if folder_selected: # 사용자가 폴더를 실제로 선택했는지 확인, 폴더를 선택했을 때 실행됨(취소했으면 실행 X)
        # folder_path느 Tkinter의 StringVar() 객체
        # set() 메서드는 해당 값을 GUI와 연동된 Entity 위젯에 표시하거나 업데이트(사용자가 선택한 폴더 경로를 입력창에 자동으로 보여줌)
        folder_path.set(folder_selected)

# 진행률 갱신 함수 (yt-dlp의 hook에서 호출)
def progress_hook(d): # d: yt-dlp가 전달하는 상태 정보 딕셔너리(다운로드 상태, 퍼센트, 속도 등 다양한 정보가 담김김)
    if d['status'] == 'downloading':
        # print(f"Downloading: {d['_percent_str']} at {d['_speed_str']} ETA: {d['_eta_str']}")
        percent = d.get('_percent_str', '0.0%').strip().replace('%', '')
        try:
            progress['value'] = float(percent)
            # config: 위젯의 설정값(속성)을 실행 중에 동적으로 변경할 때 사용, .configure()도 동일 기능능
            status_label.config(text=f"다운로드 중... {percent}%")
            root.update()  # UI 즉시(강제) 갱신
        except:
            pass
    elif d['status'] == 'finished':
        progress['value'] = 100
        status_label.config(text="다운로드 완료!")
        root.update()


########## 3. 음원 다운로드 수행(스레드로 실행) ##########

def run_download(url, save_path): # GUI 멈춤 방지를 위해 백그라운드 스레드에서 실행됨
    try:
        status_label.config(text="다운로드 시작 중...")
        progress['value'] = 0
        root.update()

        ydl_opts = {
            'format': 'bestvideo+bestaudio/best', # 최적의 비디오+오디오 스트림만 다운로드
            'outtmpl': os.path.join(save_path, '%(title)s.%(ext)s'), # 저장 경로 및 파일명 템플릿 설정
            # 'merge_output_format': 'mp4',  # 확실한 병합 설정
            'progress_hooks': [progress_hook], # 다운로드 진행률 갱신용 hook 등록
        }

        with yt_dlp.YoutubeDL(ydl_opts) as ydl:
            # 다운로드 + 선택된 포맷 정보 추출
            info_dict = ydl.extract_info(url, download=True)

            # 실제 선택된 포맷 출력(디버깅용용)
            print("\n✅ 다운로드된 포맷 정보:")

            if 'requested_formats' in info_dict:
                for f in info_dict['requested_formats']:
                    print(f"  - ID: {f['format_id']} | ext: {f['ext']} | resolution: {f.get('height', 'audio')}p | acodec: {f['acodec']} | vcodec: {f['vcodec']}")
            else:
                f = info_dict['format_id']
                print(f"  - ID: {f} | ext: {info_dict['ext']} | resolution: {info_dict.get('height', 'audio')}p")

            # 정보 추출 없이 다운로드만 하는 경우 사용(반환값 X), 내부적으로 extract_info 실행
            # [url]: 리스트로 받는 이유는 한 번에 여러 URL도 지원하기 때문
            # ydl.download([url]) 

        # messagebox.showinfo("성공", "영상 다운로드가 완료되었습니다!")
    except Exception as e:
        messagebox.showerror("다운로드 실패", f"오류 발생: {str(e)}")
        status_label.config(text="오류 발생")
        progress['value'] = 0
        root.update()


########## 2. 사용자 입력 및 음원 다운로드 함수 실행 ##########

def download_video(): # 다운로드 버튼 누르면 호출되는 함수

    # 사용자 입력 받기기
    url = url_entry.get() # 유튜브 URL
    save_path = folder_path.get() # 저장경로

    if not url: # URL을 입력하지 않은 경우
        messagebox.showerror("오류", "유튜브 영상 URL을 입력하세요.")
        return

    if not save_path: # 저장 폴더를 입력하지 않은 경우
        messagebox.showerror("오류", "저장할 폴더를 선택하세요.")
        return

    # 다운로드 실행(별도 스레드에서 수행)
    threading.Thread(target=run_download, args=(url, save_path), daemon=True).start()


########## 1. GUI 구성 및 시작 ##########

# URL 입력
tk.Label(root, text="YouTube URL:").grid(row=0, column=0, padx=5, pady=10)
url_entry = tk.Entry(root, width=60)
url_entry.grid(row=0, column=1, columnspan=2, padx=5, pady=5)

# 폴더 선택
tk.Label(root, text="저장 폴더:").grid(row=1, column=0, padx=5, pady=5)
# StringVar: Tk의 문자열 변수 객체(Tkinter 위젯과 양방향으로 데이터 동기화)
folder_path = tk.StringVar()
# tk.Entry: 텍스트 입력창을 생성하는 위젯
# root: Entity가 소속될 부모 윈도우(전체 앱 창)
# textvariable: Entity와 연결할 객체로 Tkinter의 Variable 클래스 인스턴스여야 함(예: StringVar, IntVar, 등), .set()으로 Entity에 자동 반영, .get()으로 값 읽기
# width: 입력창 너비 설정(문자 크기로 설정, 픽셀 X)
folder_entry = tk.Entry(root, textvariable=folder_path, width=45)
# grid: 위젯을 윈도우 안의 그리드 레이아웃에 배치
# row: n번째 행에 배치(0부터 시작), column: n번째 열에 배치(0부터 시작), padx: 양옆 여백(픽셀 단위), pady: 위아래 여백(픽셀 단위)
folder_entry.grid(row=1, column=1, padx=5, pady=5)
# Button: 버튼 생성, text: 버튼에 표시될 텍스트, command: 버튼 클릭 시 실행할 함수
tk.Button(root, text="찾아보기", command=browse_folder).grid(row=1, column=2, padx=5, pady=5)

# 진행률 바
tk.Label(root, text="진행 상태:").grid(row=2, column=0, padx=5, pady=5)
progress.grid(row=2, column=1, padx=5, pady=5, columnspan=2)

# 상태 텍스트
status_label.grid(row=3, column=0, columnspan=3, pady=5)

# 다운로드 버튼
# bg: 배경색, fg: 텍스트 색상
tk.Button(root, text="영상 다운로드", command=download_video,
          bg="#4CAF50", fg="white", width=20).grid(row=4, column=1, pady=15)

# Tkinter GUI 이벤트 루프 시작(내부적으로 계속 실행), 없으면 창이 바로 닫히거나 아무것도 뜨지 않음음
# 창 열림 및 사용자 상호작용을 기다림(버튼 클릭, 입력 등)
root.mainloop()

 

위와 같이 코드 작성 후 실행하면 GUI 앱이 실행된다.

단점은 매번 콘솔을 통해 GUI앱을 실행한다는 점이다.

앱을 exe로 만들어서 간단하게 클릭만으로 실행할 수 있게 하려면 추가 라이브러리 설치가 필요하다.

 

 

💽 GUI 앱을 exe 파일로 만들기

📦 필요한 라이브러리

  • pyinstaller: 파이썬으로 만든 프로그램을 실행 파일(exe 등)로 만들어주는 도구
pip install pyinstaller

 

 

▶️ exe 파일을 만들기 위한 명령어

다음 명령어를 실행하면 빌드 완료 후 dist/ 폴더 안에 exe 파일이 생성된다.

pyinstaller --onefile --windowed <파일명>.py
  • --onefile: 단일 .exe 파일로 패킹
  • --windowed 또는 -w: 콘솔 창 없이 GUI만 실행(--windowed가 없으면 실행 시 콘솔 창이 같이 열림)

 

exe 파일에 아이콘 추가하기

pyinstaller --onefile --windowed --icon=icon.ico youtube_downloader.py
  • --icon: 실행 파일에 아이콘 추가(반드시 .ico 포맷)

 

⚠️ exe 파일 생성 후 배포 시 주의할 점

  • yt_dlp가 ffmpeg를 사용하는데, 사용자의 PC에 ffmpeg가 없으면 병합이 실패한다.
  • 이 경우 사용자에게 ffmpeg 설치를 안내하거나 ffmpeg 바이너리를 함께 배포해야 한다.

 

📁 ffmpeg 바이너리와 배포하기

  • yt_dlp는 자동으로 같은 폴더에 있는 ffmpeg.exe를 인식하므로 dist/ 폴더에 ffmpeg.exe, youtube_downloader.exe 함께 넣어 zip파일로 배포한다.
  • yt_dlp는 ffmpeg를 내부적으로 실행해서 오디오 + 비디오를 병합할 때 사용한다.
  • 시스템에 ffmpeg가 PATH에 없더라도, youtube_downloader.exe와 같은 폴더에 ffmpeg.exe가 있으면 자동으로 인식한다.

 

 

⚠️ Windows Media Player에서 다운로드한 mp4 파일의 음원이 재생되지 않는 이유

  • Windows Media Player는 Opus 코덱을 기본적으로 지원하지 않는데 Youtube에서 종종 AAC 대신 Opus 코덱으로 오디오가 인코딩된다.
  • webm 형식은 음원이 재생된다.(webm파일은 Opus 오디오가 들어있음)
  • 다음과 같이 format을 지정하면 AAC 코덱으로 다운로드 받는다.
ydl_opts = {
    'format': '**bestvideo[ext=mp4]+bestaudio[acodec=aac]/best[ext=mp4]**',
}
  • AAC 필터를 사용하는 경우 적절한 오디오가 없을 때 fallback 되어 /best 조건으로 넘어가 결과적으로 낮은 해상도(단일 포맷)가 선택되는 경우가 있을 수 있다.
  • 이 경우 acodec=acc 조건을 삭제하거나 ext=m4a(고품질 오디오) 조건을 추가한다.

 

 

📄 코덱 정보 확인 방법

# 다운로드한 파일의 코덱 정보 확인
ffprobe "파일이름.mp4"

# 다운로드할 파일의 코덱 정보 확인(다운로드 가능 화질 확인)
# acodec, vcodec, ext 확인
yt-dlp -F <https://www.youtube.com/watch?v=abc123>

 

 

🎵 가장 많이 쓰이는 오디오 코덱들 (2025 기준)

코덱 용도/설명 장점 단점
AAC 유튜브, 앱, 스트리밍 고음질, 범용성 좋음 일부 오래된 장비 미지원
Opus 실시간 통신, YouTube WEBM 저지연, 효율성 최고 일부 플레이어 미지원
MP3 전통적 음악 포맷 호환성 매우 좋음 압축 효율 낮음
FLAC 무손실 음원 음질 최고 크기 큼, 실시간 재생 부적합
  • 영상 다운로드 후 대부분의 기기에서 재생하고 싶다 → AAC
  • 음성 통화 품질이 중요하다 → Opus
  • 무손실 음악 아카이브 → FLAC
반응형