반응형
유튜브 영상을 직접 다운로드해서 저장하고 싶다면, 파이썬을 이용한 간단한 앱으로 손쉽게 해결할 수 있다.
⌨️ 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 기준)
- https://www.gyan.dev/ffmpeg/builds/ 에서 ffmpeg-release-essentials.zip 다운로드
- 압축 해제 후 bin 폴더 경로 복사 (예: C:\dev\tools\ffmpeg-7.1.1\bin)
- 시스템 환경 변수(Path)에 복사한 경로 추가
- 터미널(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
반응형
'개발노트' 카테고리의 다른 글
게임 서버·NAS 연결을 위한 포트포워딩 설정법 (윈도우 & 맥) (0) | 2025.06.24 |
---|---|
파이썬 정규표현식 사용법과 예제 (0) | 2025.06.21 |
파이썬으로 CSV 파일 가공하기 (1) | 2025.06.20 |
파이썬 웹 크롤링 – BeautifulSoup 사용법 (0) | 2025.06.19 |
TIL - S3 Presigned URL을 통해 이미지 파일 업로드 (0) | 2024.08.26 |