【免费下载链接】myspoti
Native Android client for Spotify, built with Jetpack Compose
Myspoti 是一款原生 Android 的 Spotify 客户端,通过 Jetpack Compose 构建,支持免广告播放、离线下载和逐字歌词。它的扩展系统允许开发者用一个 JavaScript 文件加一个manifest.json接入歌词、音源、下载、首页分区、菜单按钮甚至登录自己的服务器。本文面向新手,逐项详解 Myspoti 扩展 API 中的 Actions(权限)、Hooks(钩子)与myspoti对象全部能力,帮你快速上手 Myspoti 扩展开发。
一、Myspoti 扩展机制总览 🧩
一个扩展就是两个文件:
manifest.json:声明扩展的身份信息和它能做什么(actions列表);main.js:实际逻辑,运行在沙箱里。
Myspoti 在沙箱中执行脚本,能做的事完全由一份封闭的 Actions 目录决定。每个 action 都有固定图标、简短标签、安全级别(normal / critical)和它解锁的 API,用户在安装前会看到每个 action 及其"理由(why)"。沙箱严格执行清单声明:调用未声明的能力会直接抛错。
官方完整文档:docs/extensions/API.md
最小可运行的扩展长这样(示例来自官方 lrclib 歌词样例):
myspoti.provide('lyrics', function (track) { var r = myspoti.http.get('https://lrclib.net/api/get?' + myspoti.query({ track_name: track.title, artist_name: track.artist })); if (!r.ok) return null; // 返回 null = "我没有答案",App 继续问其他来源 return { provider: 'LRCLIB', lines: parseLrc(r.json().syncedLyrics) }; });二、manifest.json 字段逐项详解 📋
清单放在扩展包根目录,未知字段会被忽略。核心字段如下(完整规则见 API.md 第 2 节):
| 字段 | 必填 | 说明 |
|---|---|---|
id | ✅ | 反向域名式小写 id(如io.github.you.lyrics),发布后永不改变 |
name/version/versionCode | ✅ | 展示名;版本号。versionCode越大越新,是升级比较的依据 |
author/description | 作者必填 | 最多 60 / 500 字符 |
actions | ✅ | 从第 4 节目录里选,每项{ "name", "why", "hosts"? },why 必填(1~80 字符,展示给用户) |
settings | ❌ | 用户可调的选项,类型支持text、secret、toggle、number、choice、button,需要settings.pageaction |
category/tags | ❌ | 商店分类(lyrics、playback、library、discovery、stats、look、social、tools、other)与搜索标签 |
minAppVersion | ❌ | 兼容的最低 Myspoti 版本,旧版本会显示"需要 X 或更新" |
replaces/conflictsWith | ❌ | 替代 / 与谁互斥的扩展 id(各最多 10 个) |
changelog/welcome/homepage/icon | ❌ | 更新说明、首次安装引导、主页、图标(≤256px 方形 PNG) |
打包约束:zip ≤ 2 MB、≤ 30 个文件、脚本 ≤ 1 MB、图标 ≤ 256 KB,且每次构建结果一致(固定顺序与日期),任何人可重建并比对 sha256。
两个官方样例可直接参考:
- docs/extensions/samples/lrclib-lyrics/manifest.json(歌词 + 网络 + 设置页)
- docs/extensions/samples/blocks-demo/manifest.json(全部 UI 块类型演示)
三、Actions 权限目录逐项详解 🔐
Actions 是一份封闭目录,用户安装时看到图标、标签和你的why;critical 级别会用警示色标出。逐项列表(图标与级别以 API.md 第 4 节 为准):
| Action | 级别 | 解锁的能力 |
|---|---|---|
playback.control | normal | myspoti.player.*播放控制;Spotify 播放/暂停/快进/随机/音量/倍速/切设备 |
playback.queue | normal | 编辑 Spotify 播放队列(加入、下一首播放、移动、移除、清空) |
playback.events | normal | myspoti.player.current()、track/playback事件、播放器状态读取 |
lyrics.provide | normal | lyrics钩子:提供歌词 |
trackinfo.provide | normal | trackinfo钩子:在歌曲演职员表下方加原生块 |
audio.source | critical | audio-source钩子:从外部地址播放音频(顺序询问) |
download.source | critical | download-source钩子:提供可下载文件(顺序询问) |
home.sections | normal | home钩子:在首页末尾加分区 |
search.provide | normal | search钩子:在搜索结果下方加内容 |
menu.items | normal | menu钩子:往歌曲/专辑/歌单/艺人 ⋯ 菜单加条目 |
player.buttons | normal | player钩子:在播放器控制区下加按钮 |
settings.page | normal | 独立设置页、myspoti.settings、setting事件 |
notify | normal | myspoti.notify()屏幕短消息 |
library.read | normal | 读资料库、 liked songs、用户、最近、设备列表、Blends 读取 |
library.write | critical | 资料库增删、关注、隐私会话、口味屏蔽等写操作 |
playlists.write | critical | 歌单编辑、Blends 邀请/加入/改名/退出 |
spotify.catalog | normal | Spotify 搜索、目录、公开歌单、首页、播客等浏览 |
spotify.raw | critical | raw.gql/raw.service/raw.request等完整访问,可声明固定写操作 |
network | normal(*为 critical) | myspoti.http访问清单里列出的主机;whenOnline与online事件 |
webview.page | critical | 扩展自己的锁定 WebView 页面(page/message钩子) |
webview.auth | critical | myspoti.auth.open()网站登录(独立浏览器 profile) |
storage | normal | myspoti.storage本地存储(共 512 KB) |
attest | normal | myspoti.attest硬件密钥证明(向自己的服务器证明"这真是 Myspoti") |
关键规则:
- 不能单独取消勾选:要么授予扩展列出的全部 action,要么不装;
- Additive(叠加型):歌词、详情、首页、搜索、菜单、按钮——每个扩展都问,答案按用户排序合并;
- Ordered(顺序型):
audio.source与download.source——按用户在设置里排的顺序逐个询问,直到有人回答; - 破坏性 Spotify 写操作(删除、清空等)运行时逐次弹确认框,可给
raw.request的写操作选"Always allow"。
四、Hooks 钩子逐项详解 🪝
钩子是 App 主动询问扩展的固定时点,用myspoti.provide(name, fn)注册。fn同步接收一个对象并同步返回答案(不能返回 Promise);返回null/undefined表示"没有答案",抛错或超时记为该扩展的最后错误。完整定义见 API.md 第 3 节。
| 钩子 | 所需 Action | 输入 | 回答 |
|---|---|---|---|
lyrics | lyrics.provide | track | 歌词对象(带时间戳行 / 纯文本) |
audio-source | audio.source | track(含ahead预取标记) | { url, headers?, expiresInMs?, codec?, bitDepth?, sampleRate?, … }或 DASH manifest |
download-source | download.source | track +quality(hi-res/cd/small) | 文件列表(最多 10 个,按优劣排序),支持flac、dash-flac、m4a、mp3 |
trackinfo | trackinfo.provide | track | 原生块列表,显示在歌曲演职员表下方 |
home | home.sections | {} | 首页末尾的分区块(每扩展最多 2 个分区) |
search | search.provide | { query } | 搜索结果下方的块 |
menu | menu.items | { kind, uri, title, subtitle } | menu块(每扩展 2 条) |
player | player.buttons | track | button块(每扩展 1 个) |
page/message | webview.page | {}/ 页面 post 的消息 | { title?, html }/ 回传页面的回复 |
tap | 任一 blocks action | { action, surface, context } | 无(点击回调,处理完可再刷新界面) |
status | 无需 action | {} | { live: true/false, detail? },5 分钟一次的存活探测;live: false时音源/下载被跳过 |
歌词(lyrics)要点
- 同步歌词:
{ provider, synced: true, lines: [{ time, end?, text, words? }] },每行 ≤500 字符,总行数 ≤3000,词级时间戳每行最多 200 个词; - 非同步歌词:
{ plain: '一行一句的文本' }; - 时间单位是毫秒;没有内容的歌词等同"无答案"。
音源(audio-source)要点
- 返回
url后,歌曲在Myspoti 自己的播放器中播放(不走 Spotify 播放器); - 也支持Lossless DASH:返回 base64 的 MPEG-DASH manifest(≤1 MB),Myspoti 按段顺序播放、可拖动;先检查
myspoti.features.indexOf('audio.manifest') >= 0; ahead: true表示"提前 10 秒预取下一首";expiresInMs声明地址有效期(复用最长 10 分钟,401/403/404/410 会自动重新询问一次);- 声明
codec/bitDepth/sampleRate会在播放时显示音质行(如 "FLAC · 24-bit · 96 kHz")。
下载(download-source)要点
- 输入含用户所选
quality;返回的文件按你的顺序逐一校验:FLAC 必须是真 FLAC,m4a/mp3 必须含手机可解码的音轨,且时长与 Spotify 记录相差不超过 -5s/+15s; dash-flac用 base64 manifest 代替url,Myspoti 会抓取分段并合成完整 FLAC;isrcMatched: true仅当你确认"这个文件就是这首歌(按 ISRC 找到的)"时声明——Myspoti 本身不与参考录音比对。
原生块(Blocks):扩展不画 UI,只描述 JSON
扩展永远不自己绘制界面,而是返回原生块的 JSON,由 App 用自己的字体、配色和图标绘制(标注"From X")。块类型(blocks-demo 样例 每种都演示了一个):
| 类型 | 关键字段 | 出现位置 |
|---|---|---|
section | title、blocks(≤12,不可嵌套) | 首页 / 搜索 / 歌曲详情 |
card | title、subtitle?、image?+open或action | 同上 |
tracks/albums/playlists/artists | items: [{ uri, title, subtitle?, image? }](≤50) | 歌曲可播放,其余打开对应页面 |
button | label、icon、action | 含播放器 |
menu | label、icon、action | 仅 ⋯ 菜单 |
text | text(≤1000)、style:title/body/caption | 首页 / 搜索 / 详情 |
image | url(https 公网)、ratio?(0.5~3)、open或action | 首页 / 搜索 / 详情 |
open只能打开spotify:uri(track/album/playlist/artist/show/episode),不能打开网页;action是 1~64 字符的自定义 id,点击时触发你的tap钩子;- 图标从固定名称表选(
add、download、favorite、shuffle等),不认识的按extension显示; myspoti.ui.refresh('home'|'search'|'menu'|'player'|'trackinfo')可请求 App 重新询问。
网页(page / message)与网站登录(auth)
webview.page给扩展一个免费页面(≤512 KB HTML),在锁定的 WebView 中展示:无导航、无 cookie、无存储,资源只能 GET 你network里列的主机;页面与脚本之间用MyspotiPage.postMessage()与message钩子通信;webview.auth是独立登录页:myspoti.auth.open({ url, callback })立刻打开并返回 id,到达回调(自定义 scheme 或 https 地址)后myspoti.on('auth')收到{ id, url };独立 profile 的 cookie 在多次登录间保留,扩展拿不到 cookie;hidden: true可先静默加载,8 秒无人介入才全屏展示——适合"通常无需人工"的校验;- 错误回调:
"closed"、"timeout"(3 分钟)、"network: …"。
五、事件逐项详解 📡
| 事件 | 所需 Action | 说明 |
|---|---|---|
track | playback.events | 每首新歌开始时收到歌曲字段 +origin/providerId,用于区分音频来源 |
playback | playback.events | 播放全程事件流:start/progress(每 10s)/pause/resume/seek/skip/end,含positionMs、playedMs、source(local/spotify)、reason等 |
online | network | 网络恢复时触发,配合myspoti.work.whenOnline()补发离线队列(30s 时限) |
setting | settings.page | 用户改设置时收到{ key };button类型设置收到{ key, pressed: true } |
auth | webview.auth | 网站登录完成或出错 |
每次投递限时 3 秒(setting为 30 秒);监听器内报错只记日志、不抛出;同一事件的后续监听器仍会执行。
六、myspoti 对象逐项详解 🧰
脚本跑在 Rhino 1.8(ES6 语法可用)的独立线程上,只能看到标准 JS 对象、myspoti和console:没有require、fetch、定时器、DOM,eval与从字符串构造函数一律被禁("code from strings is not allowed in extensions")。一切调用都是同步的。
基础信息与生命周期
| 调用 | 说明 |
|---|---|
myspoti.apiVersion | 当前为1 |
myspoti.extension | 清单里的{ id, name, version } |
myspoti.features | 本机支持的增强能力列表(audio.manifest、audio.expires、audio.quality、audio.ahead、http.all、status、attest等);旧版本没有此属性,检测写法:myspoti.features && myspoti.features.indexOf(name) >= 0 |
myspoti.provide(hook, fn)/myspoti.on(event, fn) | 注册钩子 / 事件(重复注册同名钩子会覆盖) |
myspoti.work.whenOnline() | 请求一次online事件(需要network) |
网络:myspoti.http
get/post/request返回{ status, headers, body, ok, json() };4xx/5xx 不抛错,网络失败、主机被拒或超限才抛Error("http: ...")。规则(由 App 强制):
- 只能访问清单里列出的主机(
*.domain匹配子域),https 优先,明文 http 仅限清单精确命名的主机; - 本地地址一律拒绝(localhost、内网段、IPv6 字面量);无 cookie、无缓存、不自动跟随重定向;
- 默认
User-Agent为Myspoti-Extension/1 (<id>);超时默认 15s(钳制在 1~20s);响应体 ≤ 5 MB;120 次/分钟(滑动窗口); myspoti.http.all([...]):一次并发最多4 个请求,脚本仍保持同步,失败项以{ error }占位(需要http.all特性)。
存储、设置与工具
| 调用 | 说明 |
|---|---|
myspoti.storage.get/set/remove(key) | 自己的 JSON 存储,键值合计 512 KB,写满抛"storage full (512 KB)" |
myspoti.settings.get(key) | 用户值 → 清单默认值 →undefined;button类型无值 |
myspoti.notify(text) | 屏幕短消息(≤200 字符,带扩展名),需要notify |
myspoti.log(...)/console.log/info/warn/error | 同一日志(≤2000 字符) |
myspoti.query(obj) | 生成查询串,null/undefined自动省略 |
myspoti.util.base64Encode/Decode、md5/sha1/sha256、hmacSha256 | UTF-8 / 十六进制摘要 |
myspoti.ui.refresh(place) | 请求刷新指定界面 |
myspoti.page.post(data) | 向自己打开的页面发数据 |
本机播放器:myspoti.player
current()(当前歌曲或null)、play()/pause()/next()/previous()/seek(ms),全部经"播放仲裁器"执行:两个扩展 2 秒内发出同类命令时,Playback 区排序靠前者胜出;30 秒内互相矛盾 3 次,后者会被暂停其播放命令并通知用户;每扩展每分钟最多 20 条命令。
Spotify 账号访问:myspoti.spotify
myspoti.spotify.<area>.<method>(...)直接操作 Myspoti 中登录的账号。共 13 个区域(API.md 第 6 节 有完整方法表):
| 区域 | 典型方法 | 所需权限 |
|---|---|---|
player | state、queue、play、pause、seek、next、setShuffle、setRepeat、addToQueue、setVolume、setSpeed、media | 读 =playback.events;写 =playback.control/playback.queue |
search | all、tracks、artists、albums、genres、recent、clearRecent | spotify.catalog |
catalog | track、album、albumTracks、artist、artistTopTracks、discography、appearsOn、similarAlbums | spotify.catalog |
library | contains、items、likedTracks、add、remove、pin、文件夹增删改移 | 读 =library.read,写 =library.write |
playlists | playlist、tracks、create、rename、addTracks、removeTracks、setCover等 26 个方法 | catalog/library.read/playlists.write |
home | feed、categories、category、whatsNew | spotify.catalog |
podcasts | show、episodes、episode、audioUrl、audiobook、savedEpisodes | 读catalog,写library.write |
users | me、profile、followers、top、friendActivity、history、follow | 读library.read,写library.write |
extras | lyrics、canvas、credits、radio、口味/封禁管理 | spotify.catalog/library.write |
devices | list、transfer、pullHere | library.read/playback.control |
recents | recentEntries | library.read |
blends | invitation、group、story、invite、join、rename、leave | 读library.read,写playlists.write |
raw | operations、gql、service、get、request(携带账号凭据访问*.spotify.com/*.spotifycdn.com) | spotify.raw |
使用规则:
- 参数按位置传递;URI 为字符串(列表 ≤1000 项);枚举按名称传、结果为大写名称;总参数 ≤256 KB;
- 错误以
Error抛出,消息以spotify.<area>.<method>:开头;用户拒绝或 120 秒未应答会抛the user refused,记得 catch; - 限制:60 次调用/分钟、响应 ≤2 MB、必须在钩子时限内完成;
- 需确认的调用:所有"删除/清空"类方法、
raw.gql的 mutation、raw.service(总是)、非 GET/HEAD 的raw.request(可用 Always allow 按 method+host+路径模式授权); - 结果统一为普通 JSON:
Page<T>是{ items, offset, total, nextOffset },枚举为大写名,深度截断到 12 层。
硬件证明:myspoti.attest
用于和你的服务器通信时证明"请求来自一台真的在跑该扩展的 Myspoti"(zip 里没有秘密可言,密钥只能由硬件 keystore 生成):
| 调用 | 作用 |
|---|---|
enroll(challenge) | 在硬件 keystore(StrongBox/TEE)中为本扩展创建 EC P-256 签名密钥,返回{ publicKey, certChain };每小时最多 5 次 |
publicKey() | 当前公钥(未注册为null) |
sign(text) | 对 ≤64 KB 文本做 ECDSA-SHA256 签名;120 次/分钟 |
reset() | 删除密钥(卸载扩展也会删) |
它证明密钥在硬件中、由 App 包创建、且由你的服务器发起 challenge;但不能证明 App 未被篡改(root 场景),也不证明用户身份——建议配合按密钥限频。桌面调试器用的是软件测试密钥,服务端按正式流程校验时应拒绝。
七、限制一览表 📏
| 项目 | 限制 |
|---|---|
| 脚本顶层加载 | 3 秒 |
| 钩子调用 | 按类型:lyrics8s、audio-source15s、download-source30s |
track/playback事件 | 3 秒/次 |
myspoti.http | 120 次/分、5 MB/响应、20s/请求、并发 4 |
myspoti.spotify | 60 次/分、256 KB 参数、2 MB/响应 |
storage | 512 KB |
| 歌词 | 3000 行、每行 500 字符、每行 200 词 |
| 下载 | 10 个文件/次、DASH manifest 1 MB |
notify/log | 200 / 2000 字符 |
| 包体 | 2 MB zip、30 文件、1 MB 脚本、256 KB 图标 |
八、开发、测试与发布流程 🚀
官方命令行工具myspoti-ext在电脑上按 App 相同的方式运行扩展(构建与用法见 docs/cli.md,入口 tools/myspoti-cli):
| 命令 | 作用 |
|---|---|
check <folder> | 打印清单问题、以用户视角列出每个 action,并用样例触发一遍 blocks 钩子校验块合法性 |
run <folder\|zip> lyrics --title T --artist A | 调用指定钩子并打印 App 解析后的答案(audio-source、download-source、home、menu、status、track、playback等同理) |
pack <folder> | 校验并构建确定性 zip,打印 sha256 与索引条目 |
index <zip目录> <名称> | 为扩展仓库生成index.json |
要点:
- 不加
--cdp时所有myspoti.spotify调用会抛错(适合测歌词/音源代码);加上--cdp PORT后走你本机 Chrome 里已登录的 Spotify Web Player 做真实调用,需确认的调用会在终端问y/N; check只跑顶层代码、不触发钩子、无账号无网络,run才能暴露钩子内部问题;- 发布:在自己的仓库加
myspoti-extension话题,商店会自动发现(6 小时内);升级只有在不新增 action/主机/why 时才自动生效,否则显示"Update needs your OK"卡片让用户逐项确认; - 扩展源码实现在 base/extensions/(如 Actions.kt、Hooks.kt)。
九、安全模型:新手最容易踩的边界 🛡️
- 隔离:每个扩展独立线程、存储、设置、日志与登录 profile;API 中不存在"别的扩展",块点击与页面消息只回到画它的扩展;
- 沙箱白名单:清单没声明的能力调用即抛错(
"x is not in this extension's manifest (actions)"); - 确认机制:破坏性写操作逐次询问用户,120 秒未答即拒绝;
raw.request写操作可被"Always allow"授权并可在扩展页撤销; - 网络面收窄:
network主机清单之外的地址直接拒绝,本地地址永不放行,无 cookie; - 受保护界面:扩展无法遮挡、延迟或观测 App 的捐赠提醒、新版本卡片、安装确认框、报告按钮与关于页。
总结 🎯
Myspoti 扩展 API 的设计核心可以概括为三句话:Actions 声明能力边界,Hooks 定义触发时机,myspoti对象提供全部同步工具。想写一个歌词扩展?从 lrclib 样例 复制起步,只需lyrics.provide+network两个 action;想做 UI 增强?参考 blocks-demo 样例 掌握 7 种原生块;要读写 Spotify 账号,则逐节对照第 6 节方法表选择最小权限。完整规范始终请以 docs/extensions/API.md 为准——本文覆盖的是 API version 1 的全部内容。
【免费下载链接】myspoti
Native Android client for Spotify, built with Jetpack Compose
相关推荐
Starlight 前端配置(Frontmatter)完全参考:内置字段逐项详解与 Schema 扩展实战
Starlight 前端配置(Frontmatter)完全参考:内置字段逐项详解与 Schema 扩展实战 Starlight 借助内容集合(content c
文档前端开发工具Lepton API 参考:扩展接口详解
Lepton API 参考:扩展接口详解 概述 Lepton 提供了一套完整的 API(Application Programming Interface,应用
桌面应用开发工具Myspoti 扩展开发快速入门:5 分钟写出第一个扩展,打包发布到 GitHub 商店
Myspoti 扩展开发快速入门:5 分钟写出第一个扩展,打包发布到 GitHub 商店 Myspoti 是一款基于 Jetpack Compose 打造的原生
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考