MusicFree 插件系统完全指南:协议、安装与播放链路一次讲透
【免费下载链接】MusicFree插件化、定制化、无广告的免费音乐播放器项目地址: https://gitcode.com/maotoumao/MusicFree
MusicFree 是一款跑在 Android 和 Harmony OS 上的插件化音乐播放器(当前版本 0.6.2,基于 React Native 0.76.5 与 React 18.3.1 构建),应用本体不携带任何平台音源:搜索、播放、歌单导入这些能力,全部靠你安装的插件提供。这篇指南从安装实操入手,再拆到 插件管理器源码 里,把插件协议字段、播放地址解析链路、歌词优先级机制逐层讲清楚。
没有插件的 MusicFree 只能干两件事
先说结论:MusicFree 本体是一个"空壳播放器",音源相关逻辑全部外置给插件。这样切分带来两个好处——
- 播放器只做它擅长的事:播放控制、缓存、分页、歌词渲染,这些通用能力可以打磨得足够稳;
- 音源能力可以无限扩展:只要某个音源能在互联网上搜到,且有人写了符合协议的插件,你就能在 MusicFree 里搜到并播放它。
插件覆盖的能力面很宽:搜索(音乐、专辑、作者)、播放、查看专辑与作者详情、导入单曲、导入歌单、获取歌词、榜单、推荐歌单、评论等。桌面版在独立仓库维护,与移动端仓库分离。
除播放能力外,本体还提供这些与音源无关的功能:
- 浅色/深色模式,支持自定义背景;
- 所有数据存在本地,不收集任何个人信息;
- 歌词关联:把歌曲 A 的歌词关联到歌曲 B,支持 A→B→C 的多级链条,链条上每首歌都显示终点歌曲 C 的歌词。
插件协议、安装、原理的完整定义都在仓库里,读源码时可以直接定位到 IPluginDefine 接口 和 插件类实现。
三步完成插件安装
安装入口在侧边栏的设置 → 插件设置,页面实现在 pluginList.tsx。支持三种装法,按使用频率排序:
1. 从网络安装插件(最常用)
点"从网络安装插件",输入一个 URL。这里有个判断逻辑值得注意:如果 URL 以.json结尾,应用会先请求这个文件,把它当成描述文件解析,取出plugins数组里每一项的url逐个安装;否则直接把输入当成一个插件.js地址安装。描述文件长这样:
{ "plugins": [ { "version": "1.0.0", "url": "https://example.com/plugin.js" } ] }所以如果你拿到的是一个官方示例插件集(基于互联网公开接口封装,并过滤掉所有 VIP、试听、付费歌曲)的plugins.json地址,粘贴进去确认,一次就能装好整套示例插件。
2. 从本地安装插件
走系统文件选择器挑.js文件,支持多选,内部调用PluginManager.installPluginFromLocalFile。适合你拿到插件作者直接发的文件、或者自己写了插件要调试的场景。
3. 更新订阅 / 更新全部
插件自身的更新源来自它导出对象里的srcUrl字段(插件项菜单里点"更新"即触发updatePlugin);订阅地址则来自plugin.subscribeUrl配置,用于批量跟进一批插件的更新。
安装时的版本冲突怎么处理
安装逻辑集中在 PluginManager,有三条判定规则,全部命中才落盘:
- hash 已存在:插件内容做
sha256得到的 hash 是它的唯一 ID。新插件 hash 如果和已装插件相同,直接静默返回"插件已安装",什么都不做。 - 同名但版本更低:如果已存在同名插件,且它的
version字段(用compare-versions库比较)比新插件高,返回"已安装更新版本的插件"并中止。这条检查可以被设置项basic.notCheckPluginVersion关掉——打开后即使本地版本更高也会强制覆盖安装。 - 安装成功:插件文件被复制进插件目录,文件名用
nanoid随机生成(形如xxx.js);如果同名的旧版本插件存在,它的文件和记录会被一并替换删除。
装完之后,插件列表和搜索面板都会按你自定义的顺序展示插件,这个顺序存在插件 meta 的order字段里(见 meta.ts)。
插件协议字段速查表
一个插件本质上就是一个满足插件协议的 CommonJS 模块:它导出一个对象,对象的元信息字段和若干能力方法共同构成 IPluginDefine。插件作者只需要关心输入输出逻辑,分页、缓存、重试这些都由 MusicFree 控制。
元信息字段
| 字段 | 类型 | 作用 |
|---|---|---|
platform | string | 必填。来源名(也就是插件名),同时作为媒体项归属平台的标识 |
appVersion | string | 匹配的应用版本号,不满足时插件进入VersionNotMatch错误态 |
version | string | 插件版本号,用于比较与更新 |
srcUrl | string | 远程更新地址,"更新插件"功能读这个字段 |
primaryKey | string[] | 主键字段列表,会随 media meta 存储 |
defaultSearchType | SupportMediaType | 默认搜索类型 |
supportedSearchType | SupportMediaType[] | 支持的搜索类型集合 |
cacheControl | "cache" \| "no-cache" \| "no-store" | 该插件播放地址的缓存策略,直接影响 播放链路 的行为 |
author/description | string | 作者 / 描述,description支持 Markdown |
userVariables | IUserVariable[] | 用户自定义输入项声明,每项含key、name、hint,应用会据此在设置页生成填写入口 |
能力方法
能力方法全部可选,插件实现了哪些,应用就会在对应界面上启用哪些功能(应用通过supportedMethods集合做能力检测):
| 方法 | 干什么 | 关键入参 |
|---|---|---|
search | 搜索音乐/专辑/作者 | query、page、type,返回{ data, isEnd } |
getMediaSource | 换真实播放地址 | musicItem、quality,返回{ url, headers?, userAgent?, quality? } |
getMusicInfo | 按主键补全歌曲信息 | musicBase |
getLyric | 取歌词 | musicItem,返回{ rawLrc, translation? } |
getAlbumInfo | 专辑信息(含分页曲目) | albumItem、page |
getMusicSheetInfo | 歌单信息(含分页曲目) | sheetItem、page |
getArtistWorks | 作者作品(分页) | artistItem、page、type |
importMusicSheet | 导入歌单 | 形如 URL 的字符串 |
importMusicItem | 导入单曲 | 形如 URL 的字符串 |
getTopLists/getTopListDetail | 榜单 / 榜单详情(分页) | 后者需要topListItem、page |
getRecommendSheetTags/getRecommendSheetsByTag | 推荐歌单 tag / 按 tag 取歌单 | 后者需要tag、page |
getMusicComments | 评论(分页) | musicItem、page |
音质档位由IMusic.IQualityKey定义,共四档:"low" | "standard" | "high" | "super"。调用getMediaSource时若未指定,默认取standard(见 plugin.ts 第 195 行附近)。
插件运行环境:插件代码能 require 到什么
很多人不知道的一点是:插件代码不是裸跑的。加载发生在Plugin类的mountPlugin方法里,插件源码会被包进一个Function(...)闭包执行,运行时注入以下依赖:
require/__musicfree_require:内置了一组常用库,插件直接require即可,不用自己打包。清单包括cheerio、crypto-js、axios、dayjs、big-integer、qs、he、webdav(定义在 plugin.ts 的 packages 表)。注意axios默认超时被统一设为 2000ms;env:含getUserVariables()函数和userVariablesgetter(读取你在设置页为这个插件填写的自定义变量,通常放 Cookie、API key 之类)、appVersion、os(固定"android")、lang(固定"zh-CN");process:携带platform、version、env;console:输出被重定向到应用日志(devLog),所以调试时看应用日志就能看到插件里的console.log;URL:来自react-native-url-polyfill,补齐移动端缺失的 URL 解析能力。
userVariables的流转路径:插件声明 → 应用生成输入框 → 值存进插件 meta(meta.ts 的${platform}.userVariables)→ 运行时通过env.userVariables读回。
插件生命周期与懒加载
每个插件实例都带一个状态机,定义在 plugin.ts:
Initializing → Loading → Mounted ↘ Error(原因:VersionNotMatch / CannotParse)appVersion与当前应用版本不满足satisfies校验时,抛VersionNotMatch,插件进Error态,界面上会标记为不可用;- 源码解析失败则记
CannotParse; - hash 的算法是
sha256(插件源码),它同时承担去重、卸载、缓存管理的唯一标识职责。
懒加载是个容易忽略的启动优化。开启设置项basic.lazyLoadPlugin后,插件的元信息(名称、hash、支持的方法列表、导出对象)会被缓存进 MMKV(plugin.cache存储)。启动时只读缓存、不解析源码,state停在Initializing;直到你真正调用某个能力方法,ensureMounted才会去读文件、执行mountPlugin。另外启动流程里还有一个兜底动作:setup完成 10 秒后异步把仍处于Initializing的插件补挂载一遍(见 index.ts 的 setup)。
播放链路拆解:一首歌如何拿到真实 URL
这是整个系统最值得看的一段代码,入口是PluginMethodsWrapper.getMediaSource(plugin.ts)。它按固定顺序做五步判断,前面命中后面就短路:
第 1 步:本地文件优先。如果音乐项带本地路径(导入的本地音乐)且文件还在,直接返回file://地址。顺带会做一次数据修正:本地扩展属性里的localPath与实际不一致时立即回写。
第 2 步:缓存播放。读MediaCache中该歌曲对应音质的source[quality].url。但能不能用,取决于插件的cacheControl:只有"cache"时在线也走缓存;"no-cache"(默认值)时仅当Network.isOffline断网才允许用旧缓存。
第 3 步:替代插件。你可以给某个插件配置alternativePlugin(插件项菜单里的"替代插件",存于插件 meta)。配置后,解析地址这件事改由替代插件执行——原插件提供的搜索结果照样能播,实际取址逻辑换掉了。这是"一个源挂了用另一个源顶着"的官方做法。
第 4 步:插件解析。调用getMediaSource(musicItem, quality)。两个细节:
- 失败会重试一次,重试前间隔 150ms;
- 如果错误消息是
"NOT RETRY",则不重试直接返回 null。
第 5 步:结果缓存。只要cacheControl不是"no-store",就把它解析出的{ url, headers, userAgent }写回MediaCache的source[quality],下次命中第 2 步。
还有两个兜底分支:插件根本没实现getMediaSource时,直接用音乐项自带的qualities[quality].url ?? musicItem.url;如果 URL 里嵌了账号密码(user:pass@host形式),formatAuthUrl会把它们剥出来转成Authorization: Basic ...请求头再交给播放器。
歌词从哪里来:五级优先级 + 关联歌词
getLyric的实现同样是一条清晰的降级链(plugin.ts),从上到下依次尝试:
- 关联歌词:先读音乐项扩展属性
associatedLrc。如果存在,说明这首歌曲词的"来源"被改指向了另一首歌(就是前面说的歌词关联特性,由 lyricManager 通过patchMediaExtra写入),后续所有步骤都拿关联目标去取词。A→B→C 链条能成立,靠的就是这个字段逐级指认; - 本地手动设置的歌词:按
MD5(platform)建目录、MD5(id)命名,检查localLrcPath/{platformHash}/{idHash}.lrc和同名的.tran.lrc(翻译词),存在就直接读文件返回; - 缓存:查
MediaCache里缓存的远程歌词结果或本地歌词文件路径,命中即返回,文件丢失才重新拉取; - 插件拉取:调插件的
getLyric。成功后把原文和翻译分别用nanoid命名写进lrcCachePath,并把文件路径回写缓存——下次就走上一步的缓存通道了; - 本地文件插件:歌曲是本地文件时,内置的
localFilePlugin会先读 MP3 内嵌歌词,读不到再找同名.lrc文件。
解析环节在 lrcParser.ts:timeReg(/\[[\d:.]+\]/g)抠出时间戳,metaReg抠出[offset:...]这类元信息;getPosition(position)定位当前句时会先减去 offset 偏移,这就是播放页"歌词偏移"设置生效的位置。原文和翻译同时存在时,解析器用双指针沿时间轴把翻译逐句对齐合并,渲染层只管按播放进度取当前句。
插件管理操作:排序、启停、卸载
插件列表页(pluginSetting)能做的管理动作,底层都对应 PluginManager 的明确方法:
- 排序:
setPluginOrder把顺序写进插件 meta 的order字段,列表与搜索面板共用这份顺序; - 启用/禁用:
setPluginEnabled维护disabledPlugins集合,被禁用的插件不会出现在搜索结果和能力探测里; - 卸载:
uninstallPlugin(hash)删除插件文件;只有当不存在其他同名插件时,才顺带清掉该插件名下的 media extra 数据(避免误删还活着的数据); - 卸载全部:
uninstallAllPlugins清空插件目录,并逐个清理各插件的扩展数据。
安全与合规须知 ⚠️
这部分官方 README 写得很直白,值得逐条对照:
- 第三方插件风险自负:装来路不明的插件前先自查它有没有异常网络请求;自己写的插件最安全。第三方恶意插件造成的损失与 MusicFree 无关;
- 警惕假冒 App:小米/华为/vivo 等应用市场上叫 MusicFree 的应用与本软件无关,是套用名称和 Logo 的广告软件;"速悦音乐"基于本软件二开但未遵守开源协议,需谨慎对待;
- 版权数据:插件使用中可能产生与本软件无关的版权数据,请及时删除;本软件不提供任何破解行为,示例插件集也过滤掉了全部 VIP、试听、付费歌曲;
- 开源协议:项目基于AGPL 3.0开源(见 package.json 的
license字段与 README 的项目使用约定),附加三条:打包/二次分发需保留代码出处;不得用于商业用途;协议变更只在仓库更新、不另行通知。
常见使用问题可以看 changelog.md 追踪版本变化,新需求直接去 issue 区提。
小结:把这套协议用顺的三把钥匙
回头看整个系统,真正决定你"用得好不好"的只有三件事:
- 认字段——
platform、version、srcUrl、cacheControl、userVariables这五个元信息决定了插件怎么装、更不更新、缓存怎么管; - 认链路——播放地址的五步解析和歌词的五级降级,解释了绝大多数"这首歌为什么播不了/没歌词"的现象;
- 认边界——播放器只负责通用能力,音源的一切由插件注入;这个边界划得越清楚,你自己写插件时就越只关心输入输出。
按这三条走一遍 src/core/pluginManager,MusicFree 的插件体系就完全透明了。
【免费下载链接】MusicFree插件化、定制化、无广告的免费音乐播放器项目地址: https://gitcode.com/maotoumao/MusicFree
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考