用 golang 写了,一套面向个人音乐资产的本地优先音乐系统 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
爱意满满的作品展示区。
vincentchyu

用 golang 写了,一套面向个人音乐资产的本地优先音乐系统

  •  2
     
  •   vincentchyu 1 天前 1238 次点击

    SonicLens:一套面向个人音乐资产的本地优先音乐系统

    很多开发者都做过“记录自己在听什么”的小工具,但真正把这件事做成一个可以长期运行、可持续扩展、还能支撑多端体验的完整工程,其实并不容易。

    我做 SonicLens,并不是为了再造一个播放器,也不是为了做一个简单的 scrobble 脚本,而是想回答一个更具体的问题:

    如果一个人多年分散在 Apple Music 、Roon 、Audirvana 、Last.fm 里的听歌行为,最终都要沉淀为属于自己的音乐资产,那么这套系统应该长什么样?

    项目: https://github.com/vincentchyu/sonic-lens.git

    这就是 SonicLens 的起点。

    它的目标很明确:

    • 统一接入多个播放来源,持续监听和记录播放行为
    • 把听歌历史沉淀到本地可控的数据模型中,而不是依赖单一平台
    • 在此基础上构建统计、资料库、歌词、AI 解析、收藏同步和多端浏览能力
    • 让这些能力不只是“能跑”,而是能成为一个长期维护的产品

    如果用一句话概括这个项目,我会这样描述:

    SonicLens 是一套以“个人音乐资产”为中心构建的本地优先音乐基础设施,它把听歌记录、资料整理、AI 洞察和多端消费体验连接成一个完整闭环。

    SonicLens Web

    一、这个项目真正想解决的,不是“听了什么”,而是“如何拥有自己的音乐历史”

    大多数音乐平台都能告诉你“最近听了什么”,但它们很少真正为用户提供一套稳定、可迁移、可扩展的数据资产模型。

    一旦平台策略变化、账号迁移、播放器更换,过去的听歌行为往往就会被切碎,最终只剩一堆零散记录。对我来说,这件事最大的问题不是统计缺失,而是:

    你的听歌历史没有真正属于你。

    SonicLens 的设计从一开始就不是围绕某个单独播放器展开,而是围绕“个人音乐资产沉淀”展开。也正因为如此,它在架构上天然不是一个脚本型项目,而更像一个围绕领域模型搭建的系统:

    • 后端长期运行,负责监听、同步、聚合、广播和任务调度
    • Web 管理端负责运维入口、数据看板和后台治理
    • soniclens-bridge 负责 macOS 、iPadOS 、iPhone 三端原生消费体验
    • AI 解析、歌词、收藏状态、资料库同步都围绕统一的数据事实源工作

    这意味着它的工程重点不在“把接口调通”,而在于让多个异步链路、多个数据来源和多个客户端之间保持一致性。

    二、从工程视角看,SonicLens 其实是四层系统

    从当前仓库实现看,SonicLens 可以拆成四个核心层次。

    1. 持续运行的 Go 后端

    后端入口在 main.go。应用启动后会初始化配置、日志、OpenTelemetry 、Redis 、数据库、MusicBrainz 、对象存储,然后启动:

    • HTTP API 服务
    • WebSocket 实时推送
    • 播放器监听链路
    • Dashboard 统计调度
    • D1 云侧镜像同步
    • 播放记录 replay 补偿任务
    • Bonjour 局域网发现广播

    这类启动方式的重点,是把系统当作一个长期运行的服务,而不是一组临时执行的命令。

    2. 明确分层的业务结构

    这个项目后端不是把所有逻辑塞进 handler ,而是做了比较清晰的职责分层:

    • api/ 负责 Gin 路由、参数绑定、缓存中间件和响应
    • internal/logic/ 负责业务编排,例如资料库、音眸、流派、MusicBrainz 、封面等服务
    • internal/model/ 负责所有数据库 CRUD 和事务入口
    • internal/scrobbler/ 负责播放器监听与当前播放状态处理
    • internal/sync/ 负责后台同步、D1 镜像和调度任务
    • core/ 负责 Redis 、Telemetry 、对象存储、AI 、歌词、WebSocket 、Bonjour 等基础能力

    这种结构的价值不是“看起来规范”,而是当项目开始变复杂之后,数据访问边界、事务边界和业务边界依然可控。

    GEMINI.md 里对这一点有非常明确的约束:数据库 CRUD 必须收口在 internal/model/,Logic 层只负责编排,不允许把原始 SQL 和 GORM 细节散落到业务代码里。

    这是一个非常重要的工程判断,因为它直接决定了系统后期是否还能继续演化。

    3. 产品化的原生三端客户端

    很多个人项目做到后端和网页就结束了,但 SonicLens 还继续往前走了一层:我为它单独做了一套原生 Bridge 客户端。

    soniclens-bridge 目前包含四个 target:

    • SoniclensBridgeMac
    • SoniclensBridgePad
    • SoniclensBridgePhone
    • SoniclensActivities

    也就是说,这不是一个“顺带做了个移动端壳”的项目,而是一套有明确模块边界的多端产品:

    • 共享层 SoniclensCore 负责网络、模型、资料库同步、连接恢复、WebSocket 、播放态与收藏态
    • ViewModels 负责各类业务页的数据协调
    • Views 层根据 macOS 、iPadOS 、iPhone 形态做容器和交互差异
    • iPhone 侧还包含 Live Activity 和分享海报相关能力

    project.yml 可以看到,整个 Xcode 工程本身也是生成式管理的,target 、scheme 和 extension 嵌入关系统一由配置驱动,而不是手工在 .xcodeproj 里维护这一点在多人协作和长期迭代时非常关键。

    SonicLens iPhone

    4. 以 AI 为能力层,而不是以 AI 为项目本体

    SonicLens 里我最看重的一点,是 AI 在这里是增强层,不是伪需求的中心

    项目里的“音眸”能力并不是简单给歌曲丢一个 prompt ,而是围绕真实业务对象构建的:

    • 曲目 insight 有独立 schema 、版本、评分和反馈
    • 专辑 insight 不是重复逐曲分析,而是聚合曲目 insight 之后再进行二次生成
    • 解析结果支持历史版本、推荐版本和反馈回灌
    • 长耗时任务通过 insight_job 做异步调度,并用 WebSocket 推送状态
    • LLM 调用日志被单独记录到 llm_call_logs,用于审计、回放和排障

    这意味着 AI 不是一个“调用成功就结束”的黑盒,而是被纳入了工程系统本身。

    三、这个项目最有技术含量的部分,不是接口数量,而是几个闭环

    如果只看 API 数量,SonicLens 当前已经有几十个接口;但真正体现专业性的,不是接口多,而是几个关键闭环是否成立。

    1. 播放监听闭环:从播放器状态到统一播放事实

    SonicLens 当前支持接入 Apple Music 、Audirvana 、Roon 等来源。播放器监听不是单纯轮询标题字符串,而是围绕统一的播放事实在工作:

    • 识别当前播放器运行状态和播放状态
    • 读取歌曲元信息,包括曲目号、盘号、专辑副标题、采样率等细节
    • 生成统一 TrackMetadata
    • 判断播放阈值,决定何时落库、何时 scrobble
    • 推送 WebSocket now_playing
    • 同步收藏态并维护 favorite projection

    这部分实现里还有几个我自己非常在意的细节:

    • Apple Music 元数据不是一视同仁处理,而是根据来源质量给出不同置信度
    • 当前播放链路有独立 trace 设计,不会每次轮询都制造噪声 span
    • 停止播放时会判断是否还有其他播放器处于活跃状态,避免误广播 stop

    这些处理看起来不“炫技”,但恰恰决定了系统在长期运行时是不是稳定。

    2. 资料库同步闭环:不是简单拉接口,而是本地索引系统

    SonicLens 的客户端资料库不是“每次打开页面重新请求远端分页”这种常见方案,而是采用了一套更像本地应用的设计:

    • 服务端通过 library_change_log 记录专辑和曲目的增删改
    • /api/library/sync 提供基于版本号的增量同步
    • WebSocket 广播 library_updated(version) 作为刷新触发器
    • Bridge 客户端本地维护 SQLite 轻量索引和 FTS5 搜索
    • LibrarySyncService 先尝试增量 apply ,失败后自动回退全量重建

    这套设计有两个明显好处。

    第一,列表浏览和搜索体验不会严重依赖网络往返,客户端更接近原生 app 的使用感受。

    第二,服务端和客户端之间的边界非常清楚:服务端提供变更事实,客户端维护查询性能。

    这也是为什么 GEMINI.md 里会专门把“本地 SQLite 轻量索引 + FTS5 + 增量同步 + WebSocket 推送 + 详情页懒加载”列为长期红线。因为这不是一个实现细节,而是整个三端体验成立的基础。

    3. 音眸异步任务闭环:把 AI 长任务做成产品能力

    很多项目接入 AI 时最容易忽略的一点,是长耗时任务的状态管理。

    SonicLens 里我专门为 AI 解析设计了 insight_job 这条链路,用它承载:

    • 任务创建
    • 幂等复用
    • 运行中状态
    • 终态结果
    • 失败原因
    • 客户端恢复
    • Live Activity 联动

    对应的接口链路也很完整:

    • POST /api/insight-jobs 创建任务
    • GET /api/insight-jobs/:id 查询任务和调用流水
    • POST /api/insight-jobs/:id/cancel 取消任务
    • POST /api/insight-jobs/:id/retry 重试任务
    • WebSocket insight_job_updated 广播状态变化

    这背后体现的是一个产品判断:

    AI 解析不是“点一下等结果”,而是要适配真实客户端环境,包括前后台切换、网络中断、长耗时等待和终态回流。

    如果未来我把这个项目继续做大,这条链路依然能继续承载更复杂的模型能力,而不是推倒重来。

    4. 数据治理闭环:系统不是只会“记”,还要会“修”

    这是我很喜欢 SonicLens 的一个点。

    音乐数据不是天然干净的。专辑名不一致、版本名混杂、曲目归属错误、第三方元信息缺失,这些问题只要你真的做过音乐资料系统,就一定会遇到。

    所以我没有把系统停留在“记录下来”,而是继续做了治理侧能力:

    • Pending Albums 待归因工作台
    • MusicBrainz 候选搜索与绑定
    • Deep maintenance 深度维护
    • Replay 播放记录补偿
    • 收藏状态补写

    这意味着 SonicLens 不是一个被动收集器,而是一个可以持续整理自己数据的系统。

    对我来说,这类能力的价值非常高,因为它说明项目已经开始从“功能实现”走向“数据运营”。

    四、我在这个项目里特别重视的工程质量点

    如果把 SonicLens 当成一份作品来看,我最希望别人看到的不是“功能很多”,而是我对工程质量的判断标准。

    1. 不是只写功能,而是写长期可维护的结构

    GEMINI.md 里保留了大量长期架构约束,这件事本身就说明这个项目不是靠短期记忆在推进,而是在持续沉淀工程规则。

    例如:

    • DAO 必须收口,事务入口必须由 internal/model/ 提供
    • 异步逻辑不允许直接写裸 go func,必须走安全封装
    • OpenTelemetry 、Redis 、GORM 、database/sql 的观测链路要统一
    • API 变更要同步维护文档
    • 客户端 target 变更必须先改 project.yml

    这些约束看起来“麻烦”,但正是这些约束让系统不会在迭代三个月后失控。

    2. 对可观测性有明确投入

    这个项目不是出了问题靠猜。

    从实现上看,Telemetry 已经接入到:

    • Gin 入站链路
    • Redis
    • GORM
    • D1 database/sql
    • 出站 HTTP client

    而且不仅有 trace ,还有 meter 、db stats metrics 和启动自检逻辑。这对一个个人项目来说其实投入不小,但我认为非常值得,因为系统一旦有多个后台任务、多个外部依赖和多条异步链路,没有观测能力几乎不可能稳定演进。

    3. 对客户端性能边界有明确设计

    在三端客户端这块,我没有把所有状态都堆进一个全局 store ,而是明确区分了:

    • AppStore 负责低频全局状态
    • PlaybackStore 负责高频播放态
    • FavoriteStore 负责收藏态
    • LibraryViewModel 做 single-flight 、页优先加载和过期请求丢弃

    这一套拆分说明我在做的不是“能显示出来就行”的 SwiftUI 页面,而是在认真处理高频状态更新对列表、详情和播放条的影响范围。

    4. 把文档当成系统的一部分

    SonicLens 不是写完代码才补 README 的项目。

    除了 README ,本仓库还维护了:

    • GEMINI.md 作为长期架构记忆
    • api/api.md 作为接口盘点
    • Bridge 客户端边界清单
    • 按日期归档的 memory 清单

    我越来越认同一件事:真正复杂的个人项目,必须有自己的“工程记忆系统”。

    因为当系统开始跨越后端、客户端、异步任务、数据同步和 AI 能力时,光靠代码本身已经不足以承载完整上下文。

    五、为什么我觉得 SonicLens 是一份值得拿出来展示的作品

    如果从面试或者技术交流的角度看,我认为 SonicLens 最有说服力的地方,不是它“用了多少技术栈”,而是它体现了一种完整的工程能力组合。

    它至少覆盖了下面这些维度:

    • Go 后端分层架构设计
    • 长期运行服务的任务调度与资源治理
    • WebSocket 实时推送与状态同步
    • 本地优先的数据建模与资料库增量同步
    • 多端原生客户端的共享层设计
    • AI 能力的工程化接入,而不是 demo 式接入
    • 数据治理与外部元信息修复链路
    • 可观测性、文档化和长期演化约束

    更重要的是,这些能力不是彼此孤立的,而是组成了一个完整系统。

    换句话说,SonicLens 不是“我会做后端”“我也会写一点 SwiftUI”“我还能接个大模型”的拼盘式展示,而是一份能体现系统设计意识、产品意识和工程落地能力的综合作品。

    六、接下来它还会继续往前走

    SonicLens 现在已经不是一个原型,但我也不把它视作完成态。

    接下来我仍然会继续打磨几个方向:

    • 更稳的资料治理和专辑归因能力
    • 更完整的 AI 反馈回灌与质量优化
    • 更成熟的分享和输出链路
    • 更统一的多端体验细节
    • 更长期可维护的系统观测与运维工具

    对我来说,做 SonicLens 的意义从来不只是“做一个能用的项目”。

    它更像是一块长期打磨的工程试验田:我把自己对后端架构、客户端设计、数据治理、AI 工程化和产品实现的理解,都持续沉淀在这里。

    如果未来有人问我,什么项目最能代表我真正的技术表达,我想 SonicLens 一定会是其中之一。


    如果你也对“个人音乐资产”“本地优先产品”或者“AI 与传统软件系统的结合方式”感兴趣,欢迎交流。

    对我来说,SonicLens 不是结束,而是一个还会继续生长的开始。

    第 1 条附言    1 天前
    专辑赏析《 24’相见恨晚》
    https://mp.weixin.qq.com/s/AoMfb5dsB5l8YDDwgbBLDw
    赏析《揪心的玩笑与漫长的白日梦》
    https://mp.weixin.qq.com/s/QLlGvqe7QrdV9FOyS0OXDw
    16 条回复    2026-04-23 16:37:59 +08:00
    chochox
        1
    chochox  
       1 天前
    有想法,给你点个赞
    YanSeven
        2
    YanSeven  
       1 天前
    感觉这种“集成”类的软件,很大的风险是不是在于“统一接入多个播放来源,持续监听和记录播放行为”。

    那些平台有官方可靠的接口不
    innocent245
        3
    innocent245  
       1 天前   1
    不错的想法, 已 star
    vincentchyu
        4
    vincentchyu  
    OP
       1 天前 via iPhone
    @YanSeven 依靠 apple script 和 medianowing (这个是私有库)全部都不依赖音乐软件的 api 属于系统支持的采集
    vincentchyu
        5
    vincentchyu  
    OP
       1 天前 via iPhone
    @chochox 感谢
    crime1024
        6
    crime1024  
       1 天前
    一键三连
    zdf3
        7
    zdf3  
       1 天前
    为什么总要重复造轮子 就觉得自己与众不同?
    JYii
        8
    JYii  
       1 天前   1
    “这个项目最有技术含量的部分,不是接口数量,而是几个闭环”

    AI 润色完能不能自己再看看
    xinjiawei
        9
    xinjiawei  
       1 天前
    @YanSeven 太对了,就算是官方的 api ,有时候也会失效
    ZenOfAI
        10
    ZenOfAI  
       1 天前   1
    用 GPT 写的文章,真的很难看懂
    vincentchyu
        11
    vincentchyu  
    OP
       1 天前 via iPhone
    @zdf3 不爱请尊重,不要随便评价别人与众不同,这样显得你很 Low 。另外重复造什么轮子?
    wait9yan
        12
    wait9yan  
       1 天前   1
    问,这个文章里有几个“不是...而是...”
    zdf3
        13
    zdf3  
       23 小时 28 分钟前
    @vincentchyu #11 你是真的下头 你发帖不就是让人评价的 咋了 只能说好听的 说你重复造轮子就是 low?
    actopas
        14
    actopas  
       23 小时 20 分钟前
    感觉心智负担超重,从此听歌像是做任务
    vincentchyu
        15
    vincentchyu  
    OP
       23 小时 2 分钟前
    @zdf3 发帖反馈没问题,但评价的前提是‘看过内容’。你连对方造了什么轮子都没讲,直接贴标签,这不叫指出问题,叫情绪输出。你说‘只能说好听的’,“随便评价别人要与众不同”,那你现在这段算好听的吗?接受不了反问却要求别人闭嘴,这自由确实挺双向的。不展开争论了,祝你下次发帖能收到你期待的反馈。
    vincentchyu
        16
    vincentchyu  
    OP
       21 小时 44 分钟前
    @JYii #7
    @ZenOfAI #9
    @wait9yan #11

    感谢提出文章的建议,发之前我看下来没觉得有啥你们一说确实很机械化,我的 prompt 是基于 README.md 让他自动写文章的。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     5610 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 104ms UTC 06:22 PVG 14:22 LAX 23:22 JFK 02:22
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86