如何在 YouTube Music 下载歌曲

今天学到用 yt-dlp 下载 YouTube Music 歌曲的方法。该方法来自文章「嗯,我还是喜欢下载mp3」。
原文章写得比较简略,针对有一定计算机基础人士。我在 Google Gemini 帮助下,终于搞懂完整过程,怕自己忘记,现记录如下:
首先,安装 yt-dlp。
brew install yt-dlp
接着,需要安装依赖 FFmpeg(第一次因为没有安装这个依赖,导致转换mp3错误)。
brew install ffmpeg
最后,在终端执行如下指令:
yt-dlp -x --audio-format mp3 \
--audio-quality 0 \
--embed-metadata --embed-thumbnail \
--postprocessor-args "ExtractAudio:-q:a 0 -id3v2_version 3" \
--postprocessor-args "FFmpegMetadata:-metadata comment=YouTubeID=%(id)s" \
-o "%(artist,uploader)s-%(track,title)s.%(ext)s" \
'https://music.youtube.com/playlist?list=PLJQHrLUqwS07gskhUSN5akj_2fbs3oIvz'实现原码率下载 mp3,如果有视频则忽略视频只保留音频,并将视频的元数据(如标题、艺术家、发布者等)作为 ID3 标签嵌入到最终的 MP3 文件中,完美。
上面命令第7行 “https://music.youtube.com/playlist?list=PLJQHrLUqwS07gskhUSN5akj_2fbs3oIvz' Music " YouTube Music 播放列表地址,可以替换成自己的播放列表地址,下载相应歌曲。
以上是使用 Mac OS 执行成功,其他如Linux、Windows 等系统方法类似,仅供参考。
原作者还给出如何给已下载歌曲配上歌词的方法:原理是在 mp3 文件里,基本上已包含“不带时间轴”的歌词,但存在 user_text_frames 字段的 description 里,而 Music.app 只认 USLT 这个 frame,于是利用 Python 语言一段代码实现歌词匹配。代码如下:
import sys
import eyed3
from eyed3.id3.frames import LyricsFrame, LYRICS_FID
def process_file(file_path):
"""处理单个 MP3 文件,将 description 字段的内容复制到 USLT 帧"""
import os
# 获取文件名(不含路径)
file_name = os.path.basename(file_path)
try:
audio = eyed3.load(file_path)
if audio is None:
print(f"{file_name}: ❌ 无法加载文件")
return False
tag = audio.tag
if tag is None:
print(f"{file_name}: ⚠️ 没有 ID3 标签,跳过")
return False
# 查找 description 字段
lyrics_text = None
for frame in tag.user_text_frames:
if frame.description == "description":
# frame.text 可能是一个字符串或列表
if isinstance(frame.text, list):
lyrics_text = "\n".join(frame.text)
else:
lyrics_text = str(frame.text)
break
if not lyrics_text:
print(f"{file_name}: ⚠️ 未找到 description 字段,跳过")
return False
# 直接创建并设置 USLT 帧(苹果系播放器只认 USLT)
# 对于 macOS/iOS Music app,使用空的 description 和 'chi' 语言代码(中文)
try:
# 删除所有现有的 USLT 帧
if tag.frame_set:
frames_to_remove = []
for frame_id in list(tag.frame_set.keys()):
if isinstance(frame_id, bytes) and frame_id == LYRICS_FID:
frames_to_remove.append(frame_id)
for frame_id in frames_to_remove:
del tag.frame_set[frame_id]
# 创建新的 USLT 帧,使用构造函数参数
uslt_frame = LyricsFrame(text=lyrics_text, description="", lang=b"chi")
# 直接赋值给 frame_set(frame_set.__setitem__ 会处理成列表)
tag.frame_set[LYRICS_FID] = uslt_frame
# 保存更改,使用 ID3 v2.3 版本以确保兼容性
tag.save(version=eyed3.id3.ID3_V2_3)
print(f"{file_name}: ✅ 成功写入 USLT 帧 ({len(lyrics_text)} 字符)")
return True
except Exception as e:
print(f"{file_name}: ❌ 设置 USLT 帧失败 - {str(e)}")
return False
except Exception as e:
print(f"{file_name}: ❌ 处理失败 - {str(e)}")
return False
# 主程序:遍历所有输入的文件
if len(sys.argv) < 2:
print("用法: python deal.py <文件1> [文件2] [文件3] ...")
sys.exit(1)
files = sys.argv[1:]
success_count = 0
fail_count = 0
for file_path in files:
if process_file(file_path):
success_count += 1
else:
fail_count += 1
print()
print(f"处理完成: 成功 {success_count} 个,失败 {fail_count} 个")