如何在 YouTube Music 下载歌曲

youtube-music-1681710563

今天学到用 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} 个")