프로그램 제작 동기
어머니께서 강의 참고 자료로 유튜브 영상을 사용하시려고 하는데 유튜브 영상 다운로드가 필요했습니다.
그런데 유튜브 영상을 다운로드 받으려고 검색을 해 보니 링크된 사이트에서 다운받는 것이 안전해 보이지 않아서
전에 파이썬으로 유튜브 영상 만들었던 것처럼 영상도 다운받을 수 있는 프로그램을 만들 수 있지 않을까?라는 생각이 들어서 프로그램을 제작해 보게되었습니다.
기능
유튜브 영상 링크 입력 시, 영상 및 오디오 다운로드
프로그램 제작 과정
1. pytube라이브러리(실패)
구글에서 검색하여 pytube 라이브러리를 이용한 방법을 찾았습니다.
이 링크가 처음 사용한 코드는 아니었지만 이런 내용이었습니다.
파이썬 코드로 유튜브 영상 자동으로 다운받는 방법 | 테크버킷 블로그
파이썬 코드를 사용하여 유튜브 영상을 다운받는 방법을 소개합니다. youtube api 및 pytube 라이브러리를 이용합니다. 영상 한개를 다운받는 방법과 채널의 영상을 다운받는 방법으로 나누어 코드
techbukket.com
하지만 HTTP Error 400: Bad Request 가 발생하였습니다.
검색해 보았지만 특별히 문제가 해결되지 않아 다른 라이브러리를 이용해 보기로 하였습니다.
2. yt-dlp 라이브러리(실패)
Gemini에게 다른 라이브러리를 이용할 수 있는지 물어봤는데
yt-dlp는 ffmpeg를 다운로드 및 경로 설정을 해 주어야 한다고 해서 번거로워 보여서 다른 방법을 찾아보기로 하였습니다.
일단 저녁에 피곤해서 하룻밤 자고 다음날 아침 다시 프로그래밍을 진행하였습니다.
3. pytubefix 라이브러리(성공)
pytube로 작성한 코드를 업로드한 블로그 글의 댓글 목록에서 pytube가 작동하지 않고 pytubefix로 바뀌었다는 글을 보게되었습니다.
그래서 Gemini에게 부탁해 코드를 수정하고 GUI도 만들도록 코드를 부탁했습니다.
- 다운받을 유튜브 URL입력
- 저장 경로 설정
- 다운받을 파일 설정
- video, 해상도(1080p, 720p, 420p, 360p, ...)
- audio
- 다운로드 진행상황 정보 메세지창
으로 GUI가 구성되어 있도록 코드를 완성하였습니다.
4. 해상도 선택 문제 발생
그런데 해상도를 선택해도 항상 비디오가 360p로 다운로드 되는 문제가 있었습니다.
해결해 보려했지만 현재 사용하는 라이브러리에서 코드를 수정할 수 없을 것 같아 해상도 선택 옵션을 없애고 해상도를 360p로 고정하여 다운받도록 하였습니다.
완성한 코드
import tkinter as tk
from tkinter import messagebox, ttk, filedialog
from pytubefix import YouTube
import threading
from pathlib import Path
import re
# --- 설정 및 변수 ---
DEFAULT_DOWNLOAD_PATH = Path("./Youtube_Downloads")
# --- 유틸리티 함수 ---
def sanitize_filename(title):
"""파일 이름에 사용할 수 없는 문자를 제거하고 정리합니다."""
sanitized = re.sub(r'[\\/:*?"<>|]', '_', title)
return sanitized
# --- GUI 액션 함수 ---
def toggle_resolution_state():
"""다운로드 타입에 따라 해상도 콤보박스의 활성화 상태를 변경합니다."""
selected_type = type_var.get()
def browse_path():
"""파일 탐색기를 열어 저장할 폴더를 선택하고 경로를 업데이트합니다."""
folder_selected = filedialog.askdirectory(initialdir=Path.cwd().as_posix())
if folder_selected:
path_var.set(Path(folder_selected).as_posix())
def show_silent_info(title, message):
"""알림음 없이 메시지를 표시합니다."""
root = tk.Toplevel()
root.withdraw() # 메인 창은 숨김
# 알림음 없이 메시지만 표시
try:
messagebox.showinfo(title, message, parent=root)
finally:
root.destroy()
def start_download():
"""메인 다운로드 함수를 별도의 스레드에서 실행합니다."""
download_button.config(state=tk.DISABLED)
download_thread = threading.Thread(target=download_process)
download_thread.start()
def download_process():
"""pytubefix를 사용하여 다운로드 옵션에 따라 영상을 다운로드합니다."""
url = url_entry.get()
download_type = type_var.get()
save_path = Path(path_var.get())
user_filename_input = filename_entry.get()
if not url or not save_path:
status_label.config(text="⚠️ URL 또는 저장 경로를 입력/선택해주세요.", fg="orange")
download_button.config(state=tk.NORMAL)
return
save_path.mkdir(parents=True, exist_ok=True)
status_label.config(text="⏳ 다운로드 준비 중...", fg="blue")
try:
yt = YouTube(url)
# 1. 파일 이름 설정 (생략)
if user_filename_input:
base_filename = sanitize_filename(user_filename_input)
else:
base_filename = sanitize_filename(yt.title)
# 2. 다운로드 유형에 따른 스트림 선택 및 다운로드
if download_type == "Video":
final_filename = f"{base_filename}.mp4"
final_filepath = save_path / final_filename
status_label.config(text=f"⬇️ '{base_filename}' 비디오 다운로드 시작...", fg="blue")
# Progressive stream 중 가장 높은 해상도로 선택
stream = yt.streams.filter(progressive=True, file_extension='mp4').order_by('resolution').desc().first()
if stream is None:
# 최종적으로도 찾지 못하면 에러 발생
raise Exception("다운로드 가능한 통합 스트림(progressive stream)이 없습니다. 다른 URL을 시도해 보세요.")
# 파일명 지정하여 다운로드
stream.download(output_path=save_path, filename=final_filename)
final_message = f"✅ 비디오(MP4) 다운로드 완료! 실제 해상도: {stream.resolution} (저장 위치: {final_filepath})"
elif download_type == "Audio":
# 오디오 다운로드 로직은 이전과 동일 (생략)
audio_stream = yt.streams.get_audio_only()
native_extension = '.' + audio_stream.mime_type.split('/')[1]
final_filename = f"{base_filename}{native_extension}"
final_filepath = save_path / final_filename
status_label.config(text=f"🎶 '{base_filename}' 오디오 다운로드 시작...", fg="blue")
audio_stream.download(output_path=save_path, filename=final_filename)
final_message = f"✅ 오디오({native_extension.upper()}) 다운로드 완료! 저장 위치: {final_filepath}"
status_label.config(text=final_message, fg="green")
show_silent_info("완료", final_message) # 🔔 알림음 없이 메시지 표시
except Exception as e:
error_message = f"❌ 다운로드 오류가 발생했습니다: {e}"
status_label.config(text=error_message, fg="red")
show_silent_info("오류", error_message) # 🔔 알림음 없이 메시지 표시
finally:
download_button.config(state=tk.NORMAL)
# --- GUI 설정 ---
app = tk.Tk()
app.title("유튜브 다운로더 (pytubefix)")
app.geometry("550x450")
app.resizable(False, False)
# 1. URL 섹션
url_label = tk.Label(app, text="1. 유튜브 영상 URL", font=("맑은 고딕", 10, "bold"))
url_label.pack(pady=(10, 0))
url_entry = tk.Entry(app, width=60, font=("맑은 고딕", 10))
url_entry.pack(pady=5, padx=20)
# 2. 파일 이름 섹션
filename_label = tk.Label(app, text="2. 파일 이름 (선택 사항: 미입력 시 유튜브 제목 사용)", font=("맑은 고딕", 10, "bold"))
filename_label.pack(pady=(5, 0))
filename_entry = tk.Entry(app, width=60, font=("맑은 고딕", 10))
filename_entry.pack(pady=5, padx=20)
# 3. 저장 경로 섹션
path_label = tk.Label(app, text="3. 저장 경로", font=("맑은 고딕", 10, "bold"))
path_label.pack(pady=(5, 0))
path_frame = tk.Frame(app)
path_frame.pack(pady=5, padx=20)
path_var = tk.StringVar(value=DEFAULT_DOWNLOAD_PATH.resolve().as_posix())
path_entry = tk.Entry(path_frame, textvariable=path_var, width=50, font=("맑은 고딕", 10), state="readonly")
path_entry.pack(side=tk.LEFT, padx=(0, 5))
browse_button = tk.Button(path_frame, text="폴더 선택", command=browse_path, font=("맑은 고딕", 9))
browse_button.pack(side=tk.LEFT)
# 4. 옵션 섹션 (해상도 및 타입)
options_label = tk.Label(app, text="4. 다운로드 옵션", font=("맑은 고딕", 10, "bold"))
options_label.pack(pady=(10, 0))
options_frame = tk.Frame(app)
options_frame.pack(pady=5)
### 4-1. 다운로드 타입 (라디오 버튼)
type_label = tk.Label(options_frame, text="타입:", font=("맑은 고딕", 10))
type_label.pack(side=tk.LEFT, padx=(0, 5))
type_var = tk.StringVar(value="Video")
video_radio = tk.Radiobutton(options_frame, text="비디오 (MP4, 360p)", variable=type_var, value="Video",
command=toggle_resolution_state, font=("맑은 고딕", 10))
audio_radio = tk.Radiobutton(options_frame, text="오디오 (MP3)", variable=type_var, value="Audio",
command=toggle_resolution_state, font=("맑은 고딕", 10))
video_radio.pack(side=tk.LEFT, padx=5)
audio_radio.pack(side=tk.LEFT, padx=5)
# 5. 다운로드 버튼
download_button = tk.Button(app, text="🚀 다운로드 시작", command=start_download,
bg="#ff0000", fg="white", font=("맑은 고딕", 12, "bold"))
download_button.pack(pady=15)
# 6. 상태 표시 레이블
status_label = tk.Label(app, text="준비됨", fg="gray", font=("맑은 고딕", 10))
status_label.pack(pady=5)
# GUI 실행
app.mainloop()