帮小鱼写一个音乐推荐 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宏忘了 importObservationDate命名冲突(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,不需要任何开发者认证,所有人都能免费调用。虽然它不是为个性化推荐设计的(它更像个搜索商店的接口),但至少能返回真实的新歌数据。
于是重新设计了推荐引擎的候选来源,变成一个三级兜底链:
- MusicKit 目录搜索(最理想,但免费账号用不了)
- iTunes Search API(公开免费,主要降级方案)
- 曲库内部挖掘(如果上面两个都失败,就从已有曲库里凑合推荐)
改完代码,重新编译部署。
第三轮:推荐的全是听过的歌
这次 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。不能让它白叫。
🐚