샥즈 오픈스윔 프로를 구매하면서 mp3 파일이 필요하게 되어
유튜브에서 노래 다운받을 수 있는 사이트를 만들겠다고 했었습니다.

아래와 같은 형태로 만들 예정이었고, 어느 정도 완성되어서 도메인을 알아보면서
chatgpt와 클로드랑 대화는 해보니 불법적인 사이트라고 하더라고요.

“예시”

인터넷에 찾아보면 비슷한 유형의 사이트들이 있는데, 합법적인 건 아니고 서버를 이상한 데서 운영하면서 주소를 계속 바꾸는 경우라고 하네요

그래서 그냥 간단하게나마 노래를 다운받는 파이썬 스크립트를 공유하려고 합니다.
이미 관련 웹사이트들이 있고, 스크립트 만드는 것도 너무 쉬워진 요즘이지만
일을 끝낸다는 마음으로 공유를 하면서 마침표를 찍으려고 합니다.

파이썬 패키지 1개, 시스템 패키지 1개만 설치하면 실행할 수 있습니다.
음질을 위한 비트레이트, 그리고 볼륨을 적정 수준으로 하는 있는 음량 정규화 옵션을 선택할 수 있습니다.

1
2
3
4
5
6
7
8
9
# YouTube MP3 다운로드 스크립트 전용 패키지
# 설치: pip install -r requirements_script.txt
#
# 시스템 패키지 (pip으로 설치 불가, 별도 설치 필요):
#   Ubuntu/Debian: sudo apt install ffmpeg
#   macOS:         brew install ffmpeg
#   Windows:       https://ffmpeg.org/download.html 에서 다운로드 후 PATH 등록

yt-dlp>=2024.12.0
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
#!/usr/bin/env python3
"""YouTube URL을 입력받아 MP3로 다운로드하는 스탠드얼론 스크립트.

사용법:
    python download_mp3.py <URL>
    python download_mp3.py <URL> -o ~/Music
"""
from __future__ import annotations

import re
import sys
from pathlib import Path

import yt_dlp

PROGRESS_RE = re.compile(r"(\d+(?:\.\d+)?)%")
MP3_QUALITY = "320"


def _parse_percent(data: dict) -> float:
    percent_str = data.get("_percent_str")
    if isinstance(percent_str, str):
        match = PROGRESS_RE.search(percent_str)
        if match:
            return float(match.group(1))

    downloaded = data.get("downloaded_bytes")
    total = data.get("total_bytes") or data.get("total_bytes_estimate")
    if isinstance(downloaded, int) and isinstance(total, int) and total > 0:
        return (downloaded / total) * 100

    return 0.0


def download_mp3(url: str, output_dir: Path, quality: str = MP3_QUALITY, normalize: bool = True) -> Path:
    output_dir.mkdir(parents=True, exist_ok=True)

    def hook(data: dict) -> None:
        status = data.get("status")
        if status == "downloading":
            percent = _parse_percent(data)
            print(f"\r다운로드 중... {percent:.1f}%", end="", flush=True)
        elif status == "finished":
            print("\r변환 중...              ", flush=True)
        elif status == "postprocess":
            print("MP3 변환 중...", flush=True)

    ydl_opts = {
        "format": "bestaudio/best",
        "outtmpl": str(output_dir / "%(title)s.%(ext)s"),
        "postprocessors": [
            {
                "key": "FFmpegExtractAudio",
                "preferredcodec": "mp3",
                "preferredquality": quality,
            }
        ],
        "progress_hooks": [hook],
        "quiet": True,
        "no_warnings": True,
    }

    if normalize:
        ydl_opts["postprocessor_args"] = ["-af", "loudnorm=I=-14:LRA=11:TP=-1.5"]

    with yt_dlp.YoutubeDL(ydl_opts) as ydl:
        info = ydl.extract_info(url, download=False)
        title = info.get("title", "알 수 없음") if isinstance(info, dict) else "알 수 없음"
        print(f"제목: {title}")

        info = ydl.extract_info(url, download=True)

    output_path: str | None = None
    if isinstance(info, dict):
        downloads = info.get("requested_downloads")
        if isinstance(downloads, list) and downloads:
            output_path = downloads[0].get("filepath") or downloads[0].get("_filename")
        if not output_path:
            output_path = info.get("_filename")

    if not output_path:
        raise RuntimeError("다운로드된 파일을 찾을 수 없습니다.")

    mp3_path = Path(output_path).with_suffix(".mp3")
    print(f"\n완료: {mp3_path}")
    return mp3_path


QUALITY_OPTIONS = ["320", "256", "192", "128"]


def _ask_url() -> str:
    while True:
        url = input("YouTube URL: ").strip()
        if url.startswith("http://") or url.startswith("https://"):
            return url
        print("올바른 URL을 입력하세요 (http:// 또는 https://로 시작).")


def _ask_quality() -> str:
    print("\n비트레이트를 선택하세요:")
    for i, q in enumerate(QUALITY_OPTIONS, 1):
        default_mark = " (기본값)" if q == MP3_QUALITY else ""
        print(f"  {i}. {q}kbps{default_mark}")
    while True:
        choice = input(f"선택 [1-{len(QUALITY_OPTIONS)}] (기본값: {len(QUALITY_OPTIONS)}): ").strip()
        if choice == "":
            return MP3_QUALITY
        if choice.isdigit() and 1 <= int(choice) <= len(QUALITY_OPTIONS):
            return QUALITY_OPTIONS[int(choice) - 1]
        print(f"1에서 {len(QUALITY_OPTIONS)} 사이의 숫자를 입력하세요.")


def _ask_normalize() -> bool:
    while True:
        choice = input("\n음량 정규화를 적용하시겠습니까? [Y/n]: ").strip().lower()
        if choice in ("", "y", "yes"):
            return True
        if choice in ("n", "no"):
            return False
        print("y 또는 n을 입력하세요.")


def main() -> None:
    print("=== YouTube MP3 다운로더 ===\n")

    try:
        url = _ask_url()
        quality = _ask_quality()
        normalize = _ask_normalize()

        output_dir = Path(".").resolve()
        print()
        download_mp3(url, output_dir, quality=quality, normalize=normalize)
    except KeyboardInterrupt:
        print("\n취소되었습니다.")
        sys.exit(1)
    except Exception as e:
        print(f"\n오류: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == "__main__":
    main()

실행예시)

“예시2”

겸사겸사 노래도 하나 추천합니다. AI로 만들어진 부분인데 초반 부분이 좋아요 초반만..
이상한 링크 아닙니다..

https://youtu.be/BKkK17d3mHU?si=veh3UoeFCVHhd2wB