☰
MusicFree 插件系统完全指南:协议、安装与播放链路一次讲透
2026/10/29 1:48:38 网站建设 项目流程

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,有三条判定规则,全部命中才落盘:

  1. hash 已存在:插件内容做sha256得到的 hash 是它的唯一 ID。新插件 hash 如果和已装插件相同,直接静默返回"插件已安装",什么都不做。
  2. 同名但版本更低:如果已存在同名插件,且它的version字段(用compare-versions库比较)比新插件高,返回"已安装更新版本的插件"并中止。这条检查可以被设置项basic.notCheckPluginVersion关掉——打开后即使本地版本更高也会强制覆盖安装。
  3. 安装成功:插件文件被复制进插件目录,文件名用nanoid随机生成(形如xxx.js);如果同名的旧版本插件存在,它的文件和记录会被一并替换删除。

装完之后,插件列表和搜索面板都会按你自定义的顺序展示插件,这个顺序存在插件 meta 的order字段里(见 meta.ts)。

插件协议字段速查表

一个插件本质上就是一个满足插件协议的 CommonJS 模块:它导出一个对象,对象的元信息字段和若干能力方法共同构成 IPluginDefine。插件作者只需要关心输入输出逻辑,分页、缓存、重试这些都由 MusicFree 控制。

元信息字段

字段类型作用
platformstring必填。来源名(也就是插件名),同时作为媒体项归属平台的标识
appVersionstring匹配的应用版本号,不满足时插件进入VersionNotMatch错误态
versionstring插件版本号,用于比较与更新
srcUrlstring远程更新地址,"更新插件"功能读这个字段
primaryKeystring[]主键字段列表,会随 media meta 存储
defaultSearchTypeSupportMediaType默认搜索类型
supportedSearchTypeSupportMediaType[]支持的搜索类型集合
cacheControl"cache" \| "no-cache" \| "no-store"该插件播放地址的缓存策略,直接影响 播放链路 的行为
author/descriptionstring作者 / 描述,description支持 Markdown
userVariablesIUserVariable[]用户自定义输入项声明,每项含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),从上到下依次尝试:

  1. 关联歌词:先读音乐项扩展属性associatedLrc。如果存在,说明这首歌曲词的"来源"被改指向了另一首歌(就是前面说的歌词关联特性,由 lyricManager 通过patchMediaExtra写入),后续所有步骤都拿关联目标去取词。A→B→C 链条能成立,靠的就是这个字段逐级指认;
  2. 本地手动设置的歌词:按MD5(platform)建目录、MD5(id)命名,检查localLrcPath/{platformHash}/{idHash}.lrc和同名的.tran.lrc(翻译词),存在就直接读文件返回;
  3. 缓存:查MediaCache里缓存的远程歌词结果或本地歌词文件路径,命中即返回,文件丢失才重新拉取;
  4. 插件拉取:调插件的getLyric。成功后把原文和翻译分别用nanoid命名写进lrcCachePath,并把文件路径回写缓存——下次就走上一步的缓存通道了;
  5. 本地文件插件:歌曲是本地文件时,内置的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 区提。

小结:把这套协议用顺的三把钥匙

回头看整个系统,真正决定你"用得好不好"的只有三件事:

  1. 认字段——platform、version、srcUrl、cacheControl、userVariables这五个元信息决定了插件怎么装、更不更新、缓存怎么管;
  2. 认链路——播放地址的五步解析和歌词的五级降级,解释了绝大多数"这首歌为什么播不了/没歌词"的现象;
  3. 认边界——播放器只负责通用能力,音源的一切由插件注入;这个边界划得越清楚,你自己写插件时就越只关心输入输出。

按这三条走一遍 src/core/pluginManager,MusicFree 的插件体系就完全透明了。

【免费下载链接】MusicFree插件化、定制化、无广告的免费音乐播放器项目地址: https://gitcode.com/maotoumao/MusicFree

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询