构建一个全能音乐服务器:从零到自动同步

在数字音乐时代,我们都曾梦想过一件事:拥有一个属于自己的、统一的、能跨平台同步的音乐库。一个能将我们散落在QQ音乐、网易云音乐的收藏与自己本地的高品质FLAC、Master音源完美融合的“终极媒体库”。

这个项目从一个简单的API脚本开始,最终演变成一个集成了Web界面、AI伴奏处理、元数据自动补全、Nginx安全流媒体和Navidrome歌单自动同步的复杂系统。

项目地址:music-api-server

阶段一:奠基 - 从 Flask 到 FastAPI 的架构演进

我们的起点很明确:需要一个后端的API服务。

1. 最初的选择:Flask 与多线程

我们最初选择了 Flask。轻量、简单,非常适合快速启动项目。我们很快就搭建起了main.py,并实现了几个核心功能:

  • API密钥保护: 通过 @require_api_key 装饰器,确保了服务的私密性。
  • 后台下载: 我们遇到的第一个挑战就是“等待”。当用户请求一首在线歌曲时,API需要时间去下载保存到本地库。我们不能让前端“假死”等待。解决方案是经典的多线程 (threading.Thread)。当一个下载请求(无论是单曲还是整个歌单)被触发时,主线程会立即返回一个 202 Accepted(任务已接受)的响应,同时将真正的下载任务抛给一个新创建的后台线程去执行。

2. 性能瓶颈:为什么选择升级到 FastAPI?

Flask + 多线程的组合在初期运行良好,但我们很快遇到了天花板。多线程的资源开销大,且上下文切换有性能损耗。我们的应用本质上是一个I/O密集型(网络请求、磁盘读写)服务,而不是CPU密集型服务。

在I/O密集的场景下,异步I/O (Asyncio) 是公认的最佳解决方案。

我们决定进行一次彻底的架构升级:**从 Flask (WSGI) 迁移到 FastAPI (ASGI)**。

这次升级带来了巨大的收益:

  • 原生异步: 所有的API路由(async def)现在都运行在同一个事件循环上。当一个请求在等待网络I/O时,服务器会立刻去处理成百上千个其他请求,并发能力呈指数级增长。
  • 依赖注入: FastAPI的 Depends 系统让我们的 verify_api_key 认证逻辑变得更加优雅。
  • 自动API文档: 启动服务后,我们立即免费获得了 /docs 路径下的交互式API文档,这对后续的调试和开发起到了革命性的作用。

3. “异步”与“同步”的桥梁:run_in_threadpool

升级到FastAPI后,我们遇到了一个新问题:api/local.pyapi/navidrome.py 这两个模块中,包含了大量同步的代码(例如 sqlite3 数据库查询、os.remove 文件操作)。

如果我们在异步的FastAPI路由中直接调用这些同步函数,会像“路障”一样堵塞整个异步事件循环,让异步的优势荡然无存。

我们的解决方案是使用FastAPI提供的“桥接工具”:fastapi.concurrency.run_in_threadpool

1
2
3
4
5
6
7
# main.py
@app.get('/api/local/search', ...)
async def handle_local_search(q: str):
# 将同步的数据库查询,扔到FastAPI管理的线程池中去运行
# 主事件循环可以继续处理其他请求,等这个函数执行完后再回来
data = await run_in_threadpool(local_api.search_song, search_key=q)
return data

通过这种方式,我们以最小的改动成本,将同步的“老爷车”安全地开上了异步的“高速公路”,而没有造成交通堵塞。

阶段二:核心功能 - 在线API与本地库

我们的服务器有三个数据源,每个都需要一套独特的解决方案。

1. 网易云音乐:eapi 的加密与“金矿”的发现

  • eapi 加密: 我们通过分析,成功地在 api/netease.py 中复现了 eapi 的加密算法,使用 cryptography 库实现了AES加密,让我们能够模拟客户端请求。
  • “金矿”的发现: 在开发中,我们发现标准API返回的元数据(如作词、作曲)不完整。通过抓包,我们找到了一个未公开的“歌曲百科”接口 (/api/link/page/parent/relation/construct/info)。这是一个“金矿”,返回了构建App页面所需的所有详细元数据,包括作词、作曲、编曲、制作人、混音、母带、BPM甚至更精确的曲风。我们立刻将其整合到了 _get_song_wiki_details 函数中,这成为了我们元数据补全功能的核心。

2. QQ音乐:vkey 的破解与并发优化

  • vkey 机制: 我们实现了 _fetch_single_url 函数,用于模拟客户端获取播放所需的 vkey,从而拼接出真实的下载链接。
  • 并发优化: 在FastAPI架构下,我们将 get_song_urls 函数从“串行请求”(一个一个地尝试 master, flac, 320)升级为 asyncio.gather 并发请求。现在,脚本会同时向服务器询问所有音质的链接,这使得获取URL的速度从几秒钟缩短到了几百毫秒。

3. 本地音乐库:scanner.pylocal_api.py

  • scanner.py (索引器): 我们创建了一个独立的 scanner.py 脚本,负责遍历您在 config.py 中指定的 MUSIC_DIRECTORYFLAC_DIRECTORY 目录,读取所有音乐文件的元数据,并在 music_library.db (SQLite) 数据库中为们建立索引。
  • api/local.py (数据库API): 这个模块封装了所有对 music_library.db 的同步操作,如 search_song (支持音质降级搜索)、get_song_path_by_id 等。

阶段三:部署与运维 - Nginx、Samba 与 Systemd

当代码在本地跑通后,将其部署到生产服务器上,我们遇到了全新的、更严峻的挑战。

1. Nginx:从反向代理到安全流媒体服务器

  • 基础: 我们使用Nginx作为反向代理,将公网的 musicapi.demo.com 域名(443端口)转发到 127.0.0.1:5000 上运行的 uvicorn 服务。
  • 安全流媒体: 我们很快意识到,直接暴露 /api/local/download/<id> 接口是极其危险的。我们转而采用了Nginx的 secure_link 模块。
    1. Flask/FastAPI (generate_stream_url): 负责生成一个临时的、带签名的流媒体链接,签名中包含了过期时间戳和我们的私有密钥。
    2. Nginx (/secure_media/): 负责验证这个链接的签名和时效性。验证通过后,Nginx会直接将文件发送给用户,完全绕开Python后端,性能极高。
  • 流媒体性能优化: 为了解决拖动进度条卡顿的问题,我们在Nginx配置中加入了 slice 1m; 指令,开启了“分片处理”,让Nginx能像专业流媒体服务器一样响应 Range 请求。
  • 调试 bind() 失败: 我们在尝试将Nginx绑定到Docker的 172.17.0.1 地址时,遇到了 bind() to ... failed (99: Unknown error) 的错误。我们通过设置 sudo sysctl net.ipv4.ip_nonlocal_bind=1 解决了这个问题。

2. 存储方案:从 NFS 到高性能 Samba

  • 需求: 我们的存储服务器(Navidrome)和API服务器是两台独立的设备。
  • 方案演进:
    1. NFS: 这是Linux间的首选,但我们的另一台设备不支持。
    2. Samba (匿名): 我们最初配置了 guest ok = yes 的Samba共享,但立刻意识到这在局域网中也极其危险。
    3. Samba (安全版): 我们迅速切换到了“安全版”方案:创建专用的 music_share_user 用户,使用 smbpasswd 设置密码,并在 fstab 中通过 credentials 文件进行安全挂载。
  • 测速: 通过 dd 命令测试,我们最初的读写速度(4-13 MB/s)虽然够用,但在优化网络连接后,我们将其提升到了 60-80 MB/s 的高性能水平,为流畅播放奠定了坚实的基础。

3. systemd:自动化运维

为了让服务在后台稳定运行并开机自启,我们编写了 systemd 单元文件:

  • music_api.service: 使用 uvicorn 启动 main:app,并配置为 Restart=always
  • music_sync.service: 定义了如何运行我们的同步脚本,并从 .env 文件安全地读取密码。
  • music_sync.timer: 一个定时器,用于在每天凌晨3点15分自动触发 music_sync.service

阶段四:AI伴奏与歌单同步

在解决了基础架构问题后,我们开始构建真正让这个项目变得“全能”的高级功能。

1. AI 伴奏工作流

  • 工具: 我们使用 Ultimate Vocal Remover GUI (UVR) 在本地电脑上分离伴奏。
  • 问题: UVR 生成的文件(如 1_周杰伦 - 龙卷风 Jay [M]_(Instrumental).flac)文件名混乱且没有元数据。
  • 我们的解决方案 (一个自动化脚本):
    1. upload_and_tag.sh (本地): 我们在本地创建了这个Shell脚本。首先在本地自动重命名文件,将其清理为 周杰伦 - 龙卷风 Jay (Instrumental).flac;然后使用 scp 将其上传到服务器的 INSTRUMENTAL_DIRECTORY;最后通过 ssh 远程触发服务器上的标记脚本。
    2. tag_instrumentals.py (服务器): 这个Python脚本被远程触发后,会扫描 INSTRUMENTAL_DIRECTORY
    3. 智能匹配: 会解析文件名,并在数据库中找到对应的原版歌曲(例如 ...[M].flac)。
    4. 格式统一 (关键): 会检查原版和伴奏的格式(FLAC/MP3),如果不一致,会调用 ffmpeg 自动将伴奏转换为与原版相同的格式
    5. 完美复制: 因为格式已统一,可以执行一次高保真元数据克隆,复制所有标签,包括专辑封面和 navidrome 分组所需的隐藏ID指纹 (musicbrainz_albumid)。
    6. 添加后缀: 最后,将伴奏文件的“歌曲名”标签修改为 龙卷风 (Instrumental),确保 navidrome 能将和原版区分开,但又归入同一张专辑

2. Navidrome 歌单同步

  1. 建立映射: 我们在 main.py 中创建了 /api/navidrome/import 接口。当用户通过 playlist_importer.html 导入一个在线歌单时,后端会自动在 playlist_mappings 表中记录下“在线ID”和“Navidrome ID”的映射关系。
  2. playlist_sync.py (同步器): 这个强大的脚本是整个项目的核心功能之一。当您通过 systemd 定时器运行时,会:
    • 登录 Navidrome
    • 遍历数据库中的所有映射
    • 获取两端列表: 异步获取最新的“在线歌单列表”和Navidrome中的“当前歌单列表”。
    • 计算差异: 找出需要添加移除的歌曲。
    • 执行同步: 通过Navidrome API执行增删操作。
    • 【新】同步顺序: 基于我们抓包发现的 PUT /api/playlist/.../tracks/<id> 接口,脚本会反向迭代目标列表,逐一移动歌曲,确保Navidrome中的歌单顺序与在线歌单完全一致

阶段五:精细化与拓展 - 搜索、推荐

1. 搜索引擎的进化:从“精准匹配”到“模糊检索”

最初的本地搜索逻辑非常原始,依赖于 scanner.py 生成的一个固定格式的 search_key(通常是 歌手 - 歌名)。

为了提供媲美商业App的搜索体验,我们对 api/local.py 进行了一次修改:

  • 废弃单一键值查询:不再依赖 search_key 的全等匹配。
  • 引入 SQL 动态构建:编写了全新的 search_local_music 函数。支持多种模式(按歌手、按专辑、按歌名)。
  • 智能分词逻辑:最关键的改进在于处理用户输入。
    • 空格即“与”逻辑:输入 周杰伦 七里香,后端会自动切分关键词,构建出 WHERE (artist LIKE '%周杰伦%' OR ...) AND (title LIKE '%七里香%' OR ...) 的查询语句。
    • 正则解析:为了支持更复杂的场景(如包含空格的英文名),我们引入了正则表达式来精准提取查询词。

现在的搜索接口,不仅快,而且“懂”用户的意图。

2. 引入算法推荐:挖掘网易云的“日推”与“风格漫游”

有了庞大的本地库后,如何发现新歌?我们决定“借用”网易云强大的推荐算法。

  • 每日推荐:我们分析了 网易云的接口,成功复现了获取“每日推荐歌曲”的功能。
  • 风格/场景推荐:这是一个更复杂的交互。
    1. 获取标签:通过 /eapi/homepage/daily/song/config/get 获取所有可用的风格(如“R&B”、“伤感”、“驾驶”)。
    2. 两步走策略:获取具体歌单时,必须先调用 tag/save 接口告诉网易云“我偏好这个风格”,然后再调用 song/list 获取实际歌单。我们使用 asyncio 将这两步封装成一个原子的异步操作。

这一功能的加入,让我们的私有服务器拥有了“源源不断”的新歌来源。