議原理)
MusicFree 完整源碼解析一首歌的插件化播放旅程與背后的協(xié)議原理【免費(fèi)下載鏈接】MusicFree插件化、定制化、無(wú)廣告的免費(fèi)音樂(lè)播放器項(xiàng)目地址: https://gitcode.com/maotoumao/MusicFreeMusicFree 是一款插件化、無(wú)廣告的免費(fèi)音樂(lè)播放器支持 Android 與 HarmonyOS。它解決的核心問(wèn)題只有一個(gè)播放器本體不內(nèi)置任何音源搜索、解析、播放地址獲取全部由第三方插件按統(tǒng)一協(xié)議注入。下面以一首歌的完整播放旅程為主線把官方 README 的用法介紹和 src/core/pluginManager 的源碼實(shí)現(xiàn)串起來(lái)講。它到底是什么定位與能力速覽一句話概括它的設(shè)計(jì)哲學(xué)本軟件僅僅是一個(gè)播放器本身不集成任何平臺(tái)的任何音源音源相關(guān)的一切邏輯都解耦給插件播放器本體只專注把播這件事做好。這意味著只要某個(gè)音源在互聯(lián)網(wǎng)上能被搜索到并且有人為它寫了對(duì)應(yīng)插件你就可以用它完成搜索、播放、歌詞、歌單導(dǎo)入。主要特性一覽特性說(shuō)明插件化搜索音樂(lè)、專輯、作者、播放、專輯/作者詳情、導(dǎo)入單曲/歌單、歌詞獲取等全部基于插件定制化、無(wú)廣告淺色/深色模式支持自定義背景基于 AGPL 3.0 開(kāi)源并保持免費(fèi)隱私所有數(shù)據(jù)存儲(chǔ)在本地不收集任何個(gè)人信息歌詞關(guān)聯(lián)把歌曲 A 的歌詞關(guān)聯(lián)到 B甚至 A→B→C 形成鏈條A、B、C 均顯示 C 的歌詞平臺(tái)支持Android 與 HarmonyOS桌面版在獨(dú)立倉(cāng)庫(kù)維護(hù)從 package.json 可以看到當(dāng)前版本為0.6.2技術(shù)棧是 React Native 0.76.5 React 18.3.1 TypeScript狀態(tài)管理用 jotai本地存儲(chǔ)用 MMKV原生播放走 react-native-track-player。憑什么做到核心設(shè)計(jì)與關(guān)鍵協(xié)議插件的本質(zhì)一個(gè)被沙箱執(zhí)行的 CommonJS 模塊按 README 的定義插件本質(zhì)上是一個(gè)滿足插件協(xié)議的 CommonJS 模塊插件開(kāi)發(fā)者只關(guān)心輸入輸出邏輯分頁(yè)、緩存全部交給 MusicFree 控制。從源碼看加載動(dòng)作發(fā)生在 plugin.ts 的mountPlugin插件源碼被包裹進(jìn)一個(gè)Function(...)內(nèi)執(zhí)行并注入以下運(yùn)行時(shí)環(huán)境注入項(xiàng)內(nèi)容require/__musicfree_require內(nèi)置cheerio、crypto-js、axios、dayjs、big-integer、qs、he、webdav等常用庫(kù)見(jiàn) plugin.ts插件可直接require而無(wú)需自帶envgetUserVariables()/userVariables用戶在設(shè)置頁(yè)填寫的自定義變量、appVersion、os、langprocessplatform、version、envconsole被重定向到應(yīng)用日志系統(tǒng)devLog便于排查插件問(wèn)題URL來(lái)自 react-native-url-polyfill執(zhí)行結(jié)果取自module.exports.default ?? module.exports即插件只需導(dǎo)出一個(gè)對(duì)象。插件元信息字段IPluginDefine插件導(dǎo)出對(duì)象需滿足IPluginDefine接口定義見(jiàn) plugin.d.ts字段說(shuō)明platform來(lái)源名插件名必填同時(shí)作為該插件產(chǎn)出媒體項(xiàng)的歸屬標(biāo)識(shí)appVersion匹配的應(yīng)用版本號(hào)不滿足時(shí)插件進(jìn)入VersionNotMatch錯(cuò)誤態(tài)version插件版本號(hào)用于安裝/更新時(shí)的版本比較srcUrl遠(yuǎn)程更新地址供更新插件功能使用primaryKey主鍵字段列表會(huì)存入 media metadefaultSearchType/supportedSearchType默認(rèn)搜索類型 / 支持的搜索類型cacheControlcache \| no-cache \| no-store插件級(jí)緩存策略author/description作者 / 描述支持 MarkdownuserVariables用戶自定義輸入項(xiàng)形如{ key, name?, hint? }能力方法一覽所有能力方法都是可選的插件按需實(shí)現(xiàn)播放器通過(guò)supportedMethods集合判斷某個(gè)能力是否存在方法作用關(guān)鍵入?yún)?/ 返回search搜索音樂(lè)、專輯、作者query、page、type→{ data, isEnd }getMediaSource根據(jù)歌曲信息解析真實(shí)播放地址musicItem、quality→{ url, headers?, userAgent?, quality? }getMusicInfo按主鍵查詢歌曲信息補(bǔ)全musicBasegetLyric獲取歌詞musicItem→{ rawLrc, translation? }getAlbumInfo/getMusicSheetInfo專輯 / 歌單信息分頁(yè)對(duì)應(yīng) item、pagegetArtistWorks作者作品分頁(yè)artistItem、page、typeimportMusicSheet/importMusicItem導(dǎo)入歌單 / 單曲形如 URL 的輸入串getTopLists/getTopListDetail榜單 / 榜單詳情分頁(yè)后者需topListItem、pagegetRecommendSheetTags/getRecommendSheetsByTag推薦歌單 tag / 按 tag 取歌單后者需tag、pagegetMusicComments評(píng)論分頁(yè)musicItem、page音質(zhì)由IQualityKey定義low | standard | high | super見(jiàn) music.d.tsgetMediaSource未指定時(shí)默認(rèn)按standard解析。插件生命周期一個(gè)四態(tài)狀態(tài)機(jī)每個(gè)插件實(shí)例都有狀態(tài)機(jī)plugin.tsInitializing初始化中→Loading加載中→Mounted已掛載出錯(cuò)則進(jìn)入Error錯(cuò)誤原因分兩類VersionNotMatchappVersion與當(dāng)前應(yīng)用版本不滿足由satisfies校驗(yàn)觸發(fā)與CannotParse源碼無(wú)法解析。插件的唯一 ID 是hash sha256(funcCode)plugin.ts用于去重、卸載、緩存管理——同名但 hash 不同的插件被視為舊版本。跟著一首播放走完全程接下來(lái)跟著某首歌走一遍完整鏈路搜索 → 取播放地址 → 歌詞 → 緩存/兜底。第一站搜索——所有插件并行應(yīng)召你在搜索框輸入關(guān)鍵詞時(shí)useSearch.ts 會(huì)調(diào)用PluginManager.getSearchablePlugins()拿到所有已啟用且實(shí)現(xiàn)了search的插件然后對(duì)每個(gè)插件發(fā)起plugin.methods.search(query, page, searchType)。幾個(gè)值得注意的細(xì)節(jié)分頁(yè)完全由播放器控制插件只返回{ data, isEnd }isEnd false且本頁(yè)有數(shù)據(jù)時(shí)狀態(tài)置為PARTLY_DONE上拉加載自動(dòng)請(qǐng)求page 1每首歌都會(huì)被resetMediaItem(_, this.plugin.name)打上歸屬platform標(biāo)記——這就是之后按平臺(tái)找插件的依據(jù)每個(gè)插件的搜索結(jié)果按searchType → plugin.hash獨(dú)立維護(hù)一份狀態(tài)RequestStateCode狀態(tài)機(jī)某個(gè)插件失敗不影響其他插件的結(jié)果展示。第二站取播放地址——getMediaSource 的五級(jí)解析點(diǎn)擊播放后TrackPlayer會(huì)按音質(zhì)順序qualityOrder由basic.defaultPlayQuality與basic.playQualityOrder配置決定逐檔嘗試核心調(diào)用是PluginMethodsWrapper.getMediaSource(musicItem, quality)plugin.ts。它按以下順序解析真實(shí)音源本地文件優(yōu)先歌曲帶本地路徑且文件存在時(shí)直接返回file://地址緩存播放讀取MediaCache僅在cacheControl cache或no-cache且當(dāng)前離線Network.isOffline時(shí)直接命中緩存的{ url, headers, userAgent }替代插件若你為插件 A 配置了替代插件 BalternativePlugin見(jiàn) meta.ts且 B 實(shí)現(xiàn)了getMediaSource則用 B 解析 A 的歌插件解析調(diào)用getMediaSource(musicItem, quality)失敗時(shí)以 150ms 間隔重試一次拋NOT RETRY標(biāo)記的錯(cuò)誤則不重試結(jié)果緩存除非cacheControl no-store否則把該音質(zhì)的{ headers, userAgent, url }寫回MediaCache的source[quality]。兜底路徑同樣清晰若插件未實(shí)現(xiàn)getMediaSource直接取musicItem.qualities[quality].url ?? musicItem.url若 URL 里內(nèi)嵌了賬號(hào)密碼formatAuthUrl會(huì)自動(dòng)剝掉并轉(zhuǎn)成Authorization: Basic ...請(qǐng)求頭。此外還有兩層更外層的兜底trackPlayer按音質(zhì)從高到低降檔重試全部失敗且開(kāi)啟basic.tryChangeSourceWhenPlayFail時(shí)自動(dòng)搜索同名同歌手的相似歌曲、換插件再試。URL 是.m3u8后綴時(shí)會(huì)被標(biāo)記為hls類型交給播放器。第三站歌詞——五級(jí)優(yōu)先級(jí)與歌詞關(guān)聯(lián)歌詞獲取在getLyricplugin.ts中按優(yōu)先級(jí)鏈執(zhí)行關(guān)聯(lián)歌詞讀取歌曲擴(kuò)展屬性associatedLrc若存在則改去拉被關(guān)聯(lián)歌曲的歌詞——這就是歌詞關(guān)聯(lián)特性的實(shí)現(xiàn)基礎(chǔ)A→B→C 的鏈條也完全靠這個(gè)屬性遞歸傳遞與音源插件徹底解耦本地手動(dòng)設(shè)置按MD5(platform)/MD5(id).lrc及.tran.lrc翻譯文件檢查本地歌詞目錄命中直接返回緩存歌詞優(yōu)先用MediaCache中的遠(yuǎn)程歌詞或本地歌詞文件路徑插件拉取調(diào)用插件getLyric成功后把歌詞寫入lrcCachePath/{nanoid}.lrc并回寫緩存下次免網(wǎng)絡(luò)本地文件兜底若是本地音樂(lè)用內(nèi)置的localFilePlugin讀取內(nèi)嵌歌詞或同名.lrc文件。拿到歌詞文本后交給 lrcParser.ts用timeReg /\[[\d:.]\]/g提取時(shí)間戳、metaReg提取[offset:...]等元信息并支持疊加用戶在播放頁(yè)設(shè)置的偏移量。原文與翻譯同時(shí)存在時(shí)解析器用雙指針?biāo)惴ò磿r(shí)間軸把翻譯逐句對(duì)齊合并到每句歌詞上lrcParser.ts渲染層則通過(guò)getPosition(position)隨播放進(jìn)度定位當(dāng)前句。第四站緩存與淘汰——MediaCache 的容量控制MediaCachemediaCache.ts基于 MMKV以platform id生成的唯一鍵存儲(chǔ)每首歌的緩存音源、歌詞路徑等上限800 條寫滿時(shí)隨機(jī)刪除一半并聯(lián)動(dòng)清理被刪條目對(duì)應(yīng)的本地歌詞文件。配合插件側(cè)的cacheControl形成三級(jí)緩存語(yǔ)義cacheControl行為cache正常讀寫緩存命中即用緩存 URLno-cache默認(rèn)每次重新解析但離線時(shí)允許用緩存兜底no-store解析結(jié)果永不寫入緩存走完這四級(jí)一首歌的旅程就閉環(huán)了搜索定位歌是誰(shuí)家的 →getMediaSource解析從哪播 →getLyric按優(yōu)先級(jí)鏈拿詞從哪來(lái) → 緩存層決定下次還要不要走網(wǎng)絡(luò)。上手實(shí)戰(zhàn)安裝、配置與日常管理安裝入口與三種方式 入口在側(cè)邊欄 → 設(shè)置 → 插件設(shè)置頁(yè)面實(shí)現(xiàn)見(jiàn) pluginList.tsx支持三種安裝方式從本地安裝通過(guò)系統(tǒng)文件選擇器選擇.js插件文件支持多選內(nèi)部調(diào)用PluginManager.installPluginFromLocalFile從網(wǎng)絡(luò)安裝輸入一個(gè) URL。若以.json結(jié)尾先 GET 該文件并解析其中的plugins[].url列表逐個(gè)安裝否則把輸入本身當(dāng)作插件.js地址{ plugins: [ { version: 1.0.0, url: https://example.com/plugin.js } ] }更新訂閱 / 更新全部插件訂閱地址來(lái)自plugin.subscribeUrl配置單個(gè)插件的更新源則是其srcUrl字段updatePlugin內(nèi)部直接復(fù)用installPluginFromUrl。安裝過(guò)程中的版本處理 安裝邏輯集中在 index.ts行為可以照表預(yù)期場(chǎng)景結(jié)果新插件 hash 已存在靜默返回插件已安裝同名插件已存在且已裝版本更高compare-versions比較version返回已安裝更新版本的插件除非開(kāi)啟basic.notCheckPluginVersion安裝成功文件以nanoid隨機(jī)名復(fù)制進(jìn)插件目錄舊版本同名片被移除并替換日常管理排序、啟停、卸載與懶加載排序setPluginOrder把順序存入插件 meta 的order字段列表與搜索結(jié)果均按該順序展示啟用/禁用setPluginEnabled維護(hù)disabledPlugins集合卸載uninstallPlugin(hash)刪除插件文件且當(dāng)無(wú)其他同名插件時(shí)清理該插件產(chǎn)生的 media extra 數(shù)據(jù)卸載全部會(huì)清空插件目錄懶加載開(kāi)啟basic.lazyLoadPlugin后插件元信息名稱、hash、支持方法列表緩存在 MMKV啟動(dòng)時(shí)不解析源碼首次調(diào)用能力方法時(shí)才由ensureMounted加載執(zhí)行啟動(dòng) 10 秒后還會(huì)異步把剩余插件掛載完成用戶變量插件聲明userVariables后可在插件設(shè)置頁(yè)填寫運(yùn)行時(shí)經(jīng)env.getUserVariables()讀回典型用途是填 API key。如果你是二次開(kāi)發(fā)者可以這樣開(kāi)始git clone https://gitcode.com/maotoumao/MusicFree深挖源碼值得借鑒的實(shí)現(xiàn)細(xì)節(jié)沙箱掛載用 Function 構(gòu)造器造一個(gè)夠用的運(yùn)行時(shí)mountPlugin不引入完整 Node 運(yùn)行時(shí)而是用Function構(gòu)造器包一層只注入 8 個(gè)參數(shù)require、module、exports、console、env、URL、process等和 8 個(gè)白名單內(nèi)置庫(kù)plugin.ts。這是移動(dòng)端插件沙箱的一個(gè)務(wù)實(shí)做法能力面小、攻擊面小、依賴零體積。console被綁定到devLog后插件作者的調(diào)試日志能直接落到應(yīng)用日志里。hash 去重 同名替換無(wú)狀態(tài)存儲(chǔ)下的版本管理插件文件用隨機(jī)名存儲(chǔ){nanoid}.js身份完全靠sha256(funcCode)判定同 hash 判重、同platform判舊版本替換index.ts。卸載也是按 hash 定位。這意味著即使插件被改名分發(fā)只要源碼一字不差就會(huì)被識(shí)別為已安裝。緩存容量控制簡(jiǎn)單粗暴但有效MediaCache的淘汰策略不是 LRU而是寫滿 800 條就隨機(jī)刪一半并順帶刪除關(guān)聯(lián)的本地歌詞文件mediaCache.ts。對(duì)一個(gè)移動(dòng)端應(yīng)用來(lái)說(shuō)這種 O(1) 判斷 批量清理比精確 LRU 便宜得多代碼里也留了 TODO 說(shuō)明作者對(duì)它的定位。雙指針?lè)g合并把兩個(gè) LRC 對(duì)齊成一份LrcParser 處理原文與翻譯時(shí)間軸不一致的經(jīng)典問(wèn)題兩個(gè)指針?lè)謩e掃原文與翻譯數(shù)組翻譯指針只在時(shí)間更早時(shí)前進(jìn)命中同時(shí)間點(diǎn)就掛上翻譯否則置空。沒(méi)有用二分、沒(méi)有建索引兩句 while 循環(huán)解決邊界處理翻譯比原文短、時(shí)間點(diǎn)錯(cuò)位都自然覆蓋。邊界、合規(guī)與避坑下載渠道安裝包請(qǐng)以發(fā)布頁(yè)為準(zhǔn)若打不開(kāi)可將 github 域名替換為 gitcode或關(guān)注官方渠道獲取其他平臺(tái)的收費(fèi)版/無(wú)廣告版/破解版均為假的本來(lái)就是開(kāi)源項(xiàng)目遇到收費(fèi)版請(qǐng)直接舉報(bào)。開(kāi)源協(xié)議項(xiàng)目基于AGPL 3.0開(kāi)源另有三條附加約定——打包/二次分發(fā)需保留代碼出處、不得用于商業(yè)用途、協(xié)議變更僅在倉(cāng)庫(kù)更新不另行通知。安全提醒使用第三方插件請(qǐng)自行鑒別安全性重點(diǎn)看是否有異常網(wǎng)絡(luò)請(qǐng)求自己寫的插件最安全不要安裝來(lái)路不明的插件第三方惡意插件造成的損失與本軟件無(wú)關(guān)。版權(quán)合規(guī)插件使用過(guò)程中可能產(chǎn)生與本軟件無(wú)關(guān)的版權(quán)數(shù)據(jù)請(qǐng)及時(shí)刪除本軟件不提倡也不會(huì)提供破解行為示例插件倉(cāng)庫(kù)基于互聯(lián)網(wǎng)公開(kāi)接口封裝并過(guò)濾掉所有 VIP、試聽(tīng)、付費(fèi)歌曲后續(xù)也不會(huì)提供具備破解功能的插件。假冒警示小米/華為/vivo 等應(yīng)用市場(chǎng)上名為 MusicFree 的 App 與本軟件無(wú)關(guān)是套用名稱和 Logo 的廣告軟件速悅音樂(lè)基于本軟件二次開(kāi)發(fā)但未遵守開(kāi)源協(xié)議請(qǐng)謹(jǐn)慎對(duì)待。成熟度預(yù)期README 明示這是業(yè)余維護(hù)項(xiàng)目、長(zhǎng)時(shí)間處于不穩(wěn)定測(cè)試版本、更新頻率不定請(qǐng)謹(jǐn)慎用于唯一聽(tīng)歌工具常見(jiàn)問(wèn)題以官方 QA 文檔為準(zhǔn)新需求走 issue。一句話帶走M(jìn)usicFree 給出的可遷移結(jié)論是把內(nèi)容從哪來(lái)和內(nèi)容怎么消費(fèi)拆成協(xié)議兩端——播放器只實(shí)現(xiàn)消費(fèi)側(cè)播放、分頁(yè)、緩存、歌詞渲染、狀態(tài)機(jī)內(nèi)容側(cè)用一個(gè)帶沙箱的 CommonJS 協(xié)議完全開(kāi)放。任何需要多音源/多后端/多供應(yīng)商的客戶端產(chǎn)品都可以照這條旅程去設(shè)計(jì)自己的插件協(xié)議定義最小必選字段platform 全可選能力方法 明確的生命周期與緩存語(yǔ)義?!久赓M(fèi)下載鏈接】MusicFree插件化、定制化、無(wú)廣告的免費(fèi)音樂(lè)播放器項(xiàng)目地址: https://gitcode.com/maotoumao/MusicFree創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考