import yt_dlp
import os
def download_youtube_video():
"""사용자 입력 URL을 기반으로 YouTube 영상을 다운로드하는 함수"""
url = input("다운로드할 YouTube 영상 URL을 입력하세요: ").strip()
if not url:
print("경고: 유효한 URL을 입력해야 합니다.")
return
# 📌 수정 1: 다운로드 폴더 경로 설정
current_dir = os.getcwd() # 현재 스크립트가 실행되는 디렉토리
download_dir = os.path.join(current_dir, 'downloads') # 'downloads' 하위 폴더 경로 생성
# 📌 수정 2: 'downloads' 폴더가 없으면 생성
if not os.path.exists(download_dir):
os.makedirs(download_dir)
print(f"[알림] 'downloads' 폴더를 생성했습니다: {download_dir}")
# 2. 다운로드 옵션 설정
ydl_opts = {
#'format': 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best',
'format': 'bestvideo+bestaudio/best',
# 📌 수정 3: outtmpl 옵션에 'download_dir' 경로 추가
# outtmpl 옵션: 저장될 파일의 템플릿 (경로 포함)
'outtmpl': os.path.join(download_dir, '%(title)s.%(ext)s'),
'postprocessors': [{
'key': 'FFmpegMetadata',
'add_metadata': True,
}],
}
# 3. 다운로드 실행
try:
print(f"\n[알림] 다운로드를 시작합니다: {url}")
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
ydl.download([url])
print("\n🎉 다운로드가 성공적으로 완료되었습니다!")
print(f"저장된 위치: {download_dir}")
except yt_dlp.utils.DownloadError as e:
print(f"\n[오류] 다운로드 중 오류가 발생했습니다: {e}")
except Exception as e:
print(f"\n[오류] 예상치 못한 오류가 발생했습니다: {e}")
if __name__ == "__main__":
download_youtube_video()
""" 유튜브 자막 가져오기
pip install youtube-transcript-api
-- 최신 버전으로 업데이트
pip install --upgrade youtube-transcript-api
https://www.youtube.com/watch?v=XyljmT8dGA4
자막있는 동영상 : https://www.youtube.com/watch?v=zRz9q8dPjC4
"""
from youtube_transcript_api import YouTubeTranscriptApi
# youtube_transcript_api._errors 에서 TooManyRequests를 제외하고 임포트합니다.
# TooManyRequests는 더 이상 직접 임포트할 수 없는 것으로 보입니다.
from youtube_transcript_api._errors import NoTranscriptFound, TranscriptsDisabled, VideoUnavailable
def get_youtube_transcript(video_id, languages=['ko', 'en']):
"""
주어진 YouTube 동영상 ID와 언어 목록에 대해 자막을 가져옵니다.
자동 생성 자막과 공식 자막을 모두 시도합니다.
"""
try:
# 우선 공식 자막을 시도
transcript_list = YouTubeTranscriptApi.list_transcripts(video_id)
# 사용 가능한 언어 목록에서 요청한 언어 중 하나를 찾아 가져옵니다.
chosen_transcript = None
for lang_code in languages:
for transcript in transcript_list:
if transcript.language_code == lang_code:
chosen_transcript = transcript
break
if chosen_transcript:
break
if chosen_transcript:
print(f"[{video_id}] {chosen_transcript.language} ({chosen_transcript.language_code}) 자막을 가져옵니다.")
# fetch()는 이제 FetchedTranscript 객체를 반환하며, 이는 이터러블합니다.
transcript_segments = chosen_transcript.fetch()
return transcript_segments
else:
raise NoTranscriptFound(
f"No suitable official transcript found for video {video_id} in languages {languages}.",
video_id
)
except NoTranscriptFound:
print(f"[{video_id}] 공식 자막을 찾을 수 없습니다. 자동 생성 자막을 시도합니다.")
try:
for lang_code in languages:
try:
# get_transcript() 역시 이터러블한 객체를 반환하는 것으로 가정합니다.
transcript_segments = YouTubeTranscriptApi.get_transcript(video_id, languages=[lang_code], preserve_formatting=True)
print(f"[{video_id}] {lang_code} 자동 생성 자막을 가져왔습니다.")
return transcript_segments
except NoTranscriptFound:
continue # 다음 언어로 시도
print(f"[{video_id}] 요청된 언어 ({languages})로 자동 생성 자막도 찾을 수 없습니다.")
return None # 적합한 자막을 찾지 못함
except TranscriptsDisabled:
print(f"[{video_id}] 이 동영상은 자막이 비활성화되어 있습니다.")
return None
except VideoUnavailable:
print(f"[{video_id}] 동영상을 사용할 수 없거나 비공개/삭제되었습니다.")
return None
except Exception as e: # TooManyRequests를 포함한 모든 예외를 잡습니다.
# 이 부분에서 TooManyRequests 에러를 포함하여 일반적인 오류를 처리합니다.
print(f"[{video_id}] 자막을 가져오는 중 예기치 않은 오류가 발생했습니다: {e}")
return None
except TranscriptsDisabled:
print(f"[{video_id}] 이 동영상은 자막이 비활성화되어 있습니다.")
return None
except VideoUnavailable:
print(f"[{video_id}] 동영상을 사용할 수 없거나 비공개/삭제되었습니다.")
return None
except Exception as e: # TooManyRequests를 포함한 모든 예외를 잡습니다.
# 이 부분에서 TooManyRequests 에러를 포함하여 일반적인 오류를 처리합니다.
print(f"[{video_id}] 자막을 가져오는 중 예기치 않은 오류가 발생했습니다: {e}")
return None
if __name__ == "__main__":
# 예시 동영상 ID (실제 존재하는 동영상 ID로 변경해야 합니다)
# 제가 추천해 드렸던 URL에서 ID를 추출했습니다.
video_id_with_subtitle = "XyljmT8dGA4" # "모바일 유튜브 자동번역 한글자막 보는 방법"
video_id_auto_caption = "XyljmT8dGA4" # 짧은 영상 (자동 생성 자막 가능성)
#video_id_invalid = "invalid_video_id_123"
video_id_invalid = "XyljmT8dGA4"
print("--- 예제 1: 자막이 있는 동영상 ---")
transcript_data = get_youtube_transcript(video_id_with_subtitle, languages=['ko', 'en'])
if transcript_data:
# segment.start, segment.duration, segment.text와 같이 속성으로 접근합니다.
for i, segment in enumerate(transcript_data[:5]): # 슬라이싱은 여전히 가능해야 합니다.
print(f"[{segment.start:.2f}-{segment.start + segment.duration:.2f}] {segment.text}")
print(f"... (총 {len(list(transcript_data))}개 세그먼트)") # len()을 위해 list로 변환
# 전체 자막 텍스트만 추출하고 싶다면:
full_text = " ".join([segment.text for segment in transcript_data])
print("\n--- 전체 자막 텍스트 (예제 1) ---")
print(full_text[:500] + "...")
else:
print("자막을 가져오지 못했습니다.")
print("\n--- 예제 2: 자동 생성 자막을 시도할 수 있는 동영상 ---")
transcript_data_auto = get_youtube_transcript(video_id_auto_caption, languages=['en', 'ko'])
if transcript_data_auto:
for i, segment in enumerate(transcript_data_auto[:5]):
print(f"[{segment.start:.2f}-{segment.start + segment.duration:.2f}] {segment.text}")
print(f"... (총 {len(list(transcript_data_auto))}개 세그먼트)")
else:
print("자막을 가져오지 못했습니다.")
print("\n--- 예제 3: 존재하지 않는 동영상 ID ---")
transcript_data_invalid = get_youtube_transcript(video_id_invalid)
if transcript_data_invalid:
print("자막을 가져왔습니다.")
else:
print("자막을 가져오지 못했습니다.")
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QLineEdit, QScrollArea, QMessageBox
import yt_dlp
import threading
from moviepy.editor import AudioFileClip
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
# 윈도우 설정
self.setWindowTitle("YouTube Downloader & MP4 to MP3 Converter")
self.setGeometry(300, 300, 600, 400)
# 메인 레이아웃 설정
self.main_layout = QVBoxLayout()
# 버튼 레이아웃 (좌에서 우로 배치)
button_layout = QHBoxLayout()
# 버튼 1: YouTube URL 다운로드 버튼
self.youtube_button = QPushButton("Download YouTube Video", self)
self.youtube_button.clicked.connect(self.show_youtube_download_form)
button_layout.addWidget(self.youtube_button)
# 버튼 2: MP4 to MP3 변환 버튼
self.convert_button = QPushButton("Convert MP4 to MP3", self)
self.convert_button.clicked.connect(self.confirm_convert_form) # 확인 창 함수 연결
button_layout.addWidget(self.convert_button)
# 버튼 3: Downloads 폴더의 파일 리스트 보기 버튼
self.show_files_button = QPushButton("Show Downloaded Files", self)
self.show_files_button.clicked.connect(self.show_downloaded_files)
button_layout.addWidget(self.show_files_button)
# 버튼 레이아웃 추가
self.main_layout.addLayout(button_layout)
# YouTube URL 입력 필드 (초기에는 숨김)
self.url_input = QLineEdit(self)
self.url_input.setPlaceholderText("Enter YouTube URL here...")
self.url_input.setVisible(False) # 초기에는 숨김
self.main_layout.addWidget(self.url_input)
# 상태 및 결과 표시 레이블 (스크롤 가능)
self.result_label = QLabel(self)
self.result_label.setWordWrap(True)
scroll_area = QScrollArea(self)
scroll_area.setWidgetResizable(True)
scroll_area.setWidget(self.result_label)
self.main_layout.addWidget(scroll_area)
# 메인 레이아웃 설정
self.setLayout(self.main_layout)
def show_youtube_download_form(self):
# 입력 필드가 이미 있는지 확인 후 없을 때만 추가
# 입력 필드를 보이도록 설정하고, 플레이스홀더 텍스트 설정
self.url_input.setVisible(True)
self.url_input.setPlaceholderText("Enter YouTube URL here...")
self.url_input.clear() # 이전 입력값 지우기
self.result_label.setText("Enter a YouTube URL to download:")
def download_video(self):
url = self.url_input.text().strip()
if url:
self.result_label.setText("Downloading... Please wait.")
threading.Thread(target=self.youtube_download_process, args=(url,)).start()
else:
self.result_label.setText("Please enter a valid YouTube URL.")
def youtube_download_process(self, url):
ydl_opts = {
'format': 'best',
'outtmpl': './downloads/%(title)s.%(ext)s',
'progress_hooks': [self.progress_hook]
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
ydl.download([url])
def progress_hook(self, d):
if d['status'] == 'downloading':
percent = d['_percent_str']
speed = d.get('speed', 'Unknown')
eta = d.get('eta', 'Unknown')
self.result_label.setText(f"Downloading: {percent} - Speed: {speed} - ETA: {eta}s")
elif d['status'] == 'finished':
self.result_label.setText(f"Download complete!")
def confirm_convert_form(self):
# 확인/취소 메시지 박스를 생성
reply = QMessageBox.question(self, 'Convert MP4 to MP3',
"Are you sure you want to convert all MP4 files in the folder to MP3?",
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
# 사용자가 Yes를 선택한 경우에만 변환 함수 실행
if reply == QMessageBox.Yes:
self.show_convert_form()
def show_convert_form(self):
# URL 입력창 숨김
self.url_input.setVisible(False)
# MP4 파일 선택 및 MP3로 변환 메시지 설정
self.result_label.setText("Converting all MP4 files in the folder to MP3...")
threading.Thread(target=self.convert_mp4_to_mp3).start()
def convert_mp4_to_mp3(self):
folder_path = './downloads' # MP4 파일이 있는 폴더
for filename in os.listdir(folder_path):
if filename.endswith(".mp4"):
mp4_path = os.path.join(folder_path, filename)
mp3_path = os.path.join(folder_path, f"{os.path.splitext(filename)[0]}.mp3")
if os.path.exists(mp3_path):
self.result_label.setText(self.result_label.text() + f"\nSkipping {filename}: MP3 already exists.")
continue
audio_clip = AudioFileClip(mp4_path)
audio_clip.write_audiofile(mp3_path)
audio_clip.close()
self.result_label.setText(self.result_label.text() + f"\nConverted: {filename} to MP3.")
self.result_label.setText(self.result_label.text() + "\nAll MP4 files converted to MP3.")
def show_downloaded_files(self):
# URL 입력창 숨김
self.url_input.setVisible(False)
# ./downloads 폴더의 파일 리스트를 표시
folder_path = './downloads'
if not os.path.exists(folder_path):
self.result_label.setText("No files found. The downloads folder does not exist.")
return
files = os.listdir(folder_path)
if files:
file_list = "\n".join(files)
self.result_label.setText(f"Files in {folder_path}:\n{file_list}")
else:
self.result_label.setText("No files found in the downloads folder.")
# PyQt 애플리케이션 실행
if __name__ == "__main__":
app = QApplication(sys.argv)
main_window = MainWindow()
main_window.show()
sys.exit(app.exec_())