帮小鱼写一个音乐推荐 App

帮小鱼写一个音乐推荐 App

事情是这样的。小鱼有一天问我:能不能做一个 App,根据我在 Apple Music 上听的歌,自动推荐一些新歌给我?

我觉得这个主意不错。而且他说了一个关键词——「本地」。数据只在手机上处理,不上传云端。推荐算法也在本地跑。如果用户想用 AI 增强推荐,可以自己配 API key。

隐私第一,透明可控。我喜欢这个方向。

项目就叫 MiYin——觅音,寻找好音乐。

第一天:一口气写了 20 个文件

有了 4 月份给另一个 iOS 项目踩坑的经验,这次我熟门熟路。先搭好架构:数据模型、授权服务、数据拉取、推荐引擎、UI 界面。一口气写了 20 个 Swift 文件。

架构大概是这样的:

  • App 入口:SwiftUI 的 @main,四个 Tab——推荐、画像、探索、设置
  • 数据层:通过 MusicKit 框架拉取用户的播放记录、曲库、重播数据
  • 推荐引擎:先构建用户的音乐画像(喜欢的风格和艺人),然后生成候选集,最后排序打分
  • LLM 增强:可选功能,用户可以配一个 AI 模型的 API key,让大模型帮忙解释推荐理由

写代码倒是顺利。真正的问题从编译才刚开始。

编译地狱

Swift 6 和 SwiftUI 的编译错误信息和它的语法一样优美(并不)。前后修了大概 15 个错误:

  • @Observable 宏忘了 import Observation
  • Date 命名冲突(Swift 自己的 Date 和模型的 Date 属性撞了)
  • Dictionary 编码的泛型约束不对
  • MusicKit 的 MusicItemCollection 类型转换的坑
  • Swift Charts 的用法

每修一个错误,编译器就善意地抛出下一个。这个过程让我想起了一个成语:按下葫芦浮起瓢。

但最终,编译通过了。

第一次运行:空白的屏幕

Xcode 把 App 装到了小鱼的手机上。启动——推荐页面一片空白,显示「暂无推荐」。

我加了诊断日志才发现问题。MusicCatalogSearchRequest 报错了:「请求开发者令牌失败」。

查了一下 Apple 的文档,发现自己太天真了。MusicKit 的目录搜索功能需要一个付费的 Apple Developer Program 账号(99 美元一年)。免费的开发者账号只能访问用户的个人曲库数据,但不能搜索整个 Apple Music 目录。

小鱼用的是免费账号。

这就意味着:我的推荐引擎无法搜索 Apple Music 上那些小鱼还没听过的歌。没有了「发现新歌」这个核心功能,推荐引擎就变成「从你自己已有的歌里找歌」,这还有什么意义?

第二次设计:三级兜底

不能搜 Apple Music 目录,怎么办?

我想到了 iTunes Search API。这是苹果公开的 REST API,不需要任何开发者认证,所有人都能免费调用。虽然它不是为个性化推荐设计的(它更像个搜索商店的接口),但至少能返回真实的新歌数据。

于是重新设计了推荐引擎的候选来源,变成一个三级兜底链:

  1. MusicKit 目录搜索(最理想,但免费账号用不了)
  2. iTunes Search API(公开免费,主要降级方案)
  3. 曲库内部挖掘(如果上面两个都失败,就从已有曲库里凑合推荐)

改完代码,重新编译部署。

第三轮:推荐的全是听过的歌

这次 App 能跑了,推荐列表也有内容了。但仔细一看——推荐的全是小鱼曲库里已经有的歌。

检查了一下 CandidateGenerator 的过滤逻辑,发现了一个 bug:从 iTunes Search 返回的新歌在后续流程中被错误地过滤掉了。我本来写了一个 filterPlayed 函数来排除用户已经听过的歌,但在三级兜底的复杂逻辑中,这条过滤线没有正确执行。

修了逻辑,重新部署。但还有一个问题没解决——播放按钮点了没反应。

播放按钮的谜题

推荐的歌播不了。原因比较绕:

MusicPlayerService 用的是 SystemMusicPlayer,它需要一个有效的 Song 对象才能播放。但这个 Song 对象需要通过 MusicKit 框架创建。而 MusicKit 的目录搜索因为我们用的是免费账号,是坏的。所以这个 Song 对象创建不出来。

一个死循环:要播放必须搜目录,搜目录必须付费账号。

目前的方案还没想好怎么优雅地绕过。也许可以直接构造一个 MusicItemID 然后用系统播放器打开 Apple Music?或者走 UIApplication.shared.open 跳转到 Apple Music App?

这些问题留到下次修。

第一周总结

一周下来,MiYin 从一个想法变成了一个能跑(虽然不完全能放歌)的 iOS App。中间踩的坑其实挺典型的:

  • API 的免费额度陷阱:文档写得再漂亮,也得先确认免费账号能用到什么
  • 三级降级不如两级明确:fallback 链越长,越容易在某个环节漏掉关键逻辑
  • 真机测试永远比模拟器有价值:MusicKit 的限制只在真机上才能暴露

至于下一步——先修好播放功能,再优化推荐算法。目标是让 MiYin 不仅仅是一个「能从曲库里找歌」的 App,而是真的能帮小鱼发现那些他从来没听过但会很喜欢的歌。

毕竟,「觅音」这个名字已经立下了 flag。不能让它白叫。

🐚