ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Nuclear 插件开发:Playlists API(api.Playlists)与 PlaylistProvider 歌单系统全解析

Nuclear 插件开发:Playlists API(api.Playlists)与 PlaylistProvider 歌单系统全解析 Nuclear 插件开发Playlists APIapi.Playlists与 PlaylistProvider 歌单系统全解析【免费下载链接】nuclearStreaming music player that finds free music for you项目地址: https://gitcode.com/GitHub_Trending/nu/nuclear本文基于 Nuclear 官方插件文档 Playlists结合plugin-sdk、model与player三个包的源码实现系统讲解 Nuclear 插件如何读取、创建、修改、导入歌单以及如何注册一个能从 URL如 Spotify 链接抓取歌单的PlaylistProvider。读完本文你可以独立完成一个可管理歌单、可订阅歌单变化、可处理外部歌单链接的 Nuclear 插件并理解底层索引/全文档双层存储、JSON 持久化与 Provider 匹配调度等机制。一、Playlists API 的两个侧面Nuclear 的 Playlists API 分为两个侧面消费者 APIapi.Playlists.*让插件创建、读取、修改和导入歌单Provider 类型PlaylistProvider让插件注册一个能从 URLSpotify 链接、SoundCloud 页面等抓取歌单的处理程序。所有api.Playlists.*操作均在插件生命周期钩子中调用全部为异步方法、返回 Promise。SDK 侧的实现是一个轻量的门面类 PlaylistsAPI每个方法都通过私有的#withHost守卫委托给宿主注入的PlaylistsHost见 types/playlists.ts 中PlaylistsHost接口定义宿主侧实现 playlistsHost 则把每个操作转发到玩家前端的 zustand store usePlaylistStore。也就是说插件代码、SDK、宿主 store 三层严格对齐同一份方法签名// packages/plugin-sdk/src/types/playlists.ts export type PlaylistsListener (index: PlaylistIndexEntry[]) void; export type PlaylistProvider ProviderDescriptorplaylists { matchesUrl: (url: string) boolean; fetchPlaylistByUrl: (url: string) PromisePlaylist; }; export type PlaylistsHost { getIndex: () PromisePlaylistIndexEntry[]; getPlaylist: (id: string) PromisePlaylist | null; createPlaylist: (name: string) Promisestring; deletePlaylist: (id: string) Promisevoid; addTracks: (playlistId: string, tracks: Track[]) PromisePlaylistItem[]; removeTracks: (playlistId: string, itemIds: string[]) Promisevoid; reorderTracks: ( playlistId: string, from: number, to: number, ) Promisevoid; importPlaylist: (playlist: Playlist) Promisestring; saveQueueAsPlaylist: (name: string) Promisestring; subscribe: (listener: PlaylistsListener) () void; };二、核心概念2.1 索引index与完整歌单的分工Nuclear 维护一份所有歌单的轻量索引完整歌单数据则按需加载。这一设计有两点实际意义getIndex()返回PlaylistIndexEntry[]包含名称、时间戳、封面图和聚合统计曲目数、总时长但不含实际曲目列表getPlaylist(id)才返回带items数组的完整Playlist。因此列表展示场景应使用索引只有真正需要曲目数据时才加载完整歌单。索引与完整歌单的 TypeScript 定义见 model/src/playlists.ts并配套 Zod 校验 schemamodel/src/schemas/playlists.ts其中playlistIndexEntrySchema对thumbnails字段提供了空数组默认值export const playlistIndexEntrySchema z.object({ id: z.string(), name: z.string(), createdAtIso: z.string(), lastModifiedIso: z.string(), isReadOnly: z.boolean(), artwork: artworkSetSchema.optional(), itemCount: z.number(), totalDurationMs: z.number(), thumbnails: z.array(z.string()).default([]), });2.2 歌单条目PlaylistItem歌单中的每首曲目都被包装为一个PlaylistItemtype PlaylistItem { id: string; // 该条目的唯一 ID不是曲目 ID track: Track; // 完整曲目元数据 note?: string; // 可选的用户备注 addedAtIso: string; // ISO 时间戳 };同一首曲目可以多次出现在同一个歌单中每次出现都是一个拥有独立id的PlaylistItem。这一点在调用removeTracks时尤为关键——删除是按条目 ID而非曲目 ID 进行的。2.3 持久化每个歌单一个 JSON 文件文档明确说明每个歌单以独立 JSON 文件存储于磁盘所有 API 变更自动持久化。源码印证了这一机制PlaylistFileStore 使用 Tauri 的LazyStore将每个歌单写入playlists/{id}.json读取时经 loadValidated 按playlistSchema做 Zod 校验文件句带 LRU 缓存MAX_CACHED_STORES 20超出时按访问顺序淘汰并关闭最久未用的 store删除歌单时先清空并保存 store再通过tauri-apps/plugin-fs从BaseDirectory.AppData移除物理文件文件已不存在时仅记录 warning 而不抛错全局索引单独存放在playlists/index.json见 PlaylistIndexStore。这意味着插件写入一次后无需关心落盘——playlistStore中每个变更方法末尾都会调用playlistFileService.savePlaylist(updated)并同步刷新索引。三、使用 api.Playlists完整示例以下四组示例完整继承自官方文档可直接复制到插件的onEnable中使用。3.1 读取歌单import type { NuclearPluginAPI } from nuclearplayer/plugin-sdk; export default { async onEnable(api: NuclearPluginAPI) { // 列出所有歌单轻量不含曲目数据 const index await api.Playlists.getIndex(); for (const entry of index) { api.Logger.info(${entry.name}: ${entry.itemCount} tracks, ${entry.totalDurationMs}ms); } // 需要曲目数据时再加载完整歌单 const playlist await api.Playlists.getPlaylist(index[0].id); if (playlist) { for (const item of playlist.items) { api.Logger.debug( ${item.track.title}); } } }, };getPlaylist的宿主实现playlistsHost.ts转发到 store 的loadPlaylist后者带内存缓存命中playlistsMap 直接返回否则从文件加载并缓存。因此同一歌单反复读取不会产生重复 IO。3.2 创建与修改import type { NuclearPluginAPI, Track } from nuclearplayer/plugin-sdk; export default { async onEnable(api: NuclearPluginAPI) { // 创建新歌单 const playlistId await api.Playlists.createPlaylist(Late Night Jazz); // 添加曲目 const tracks: Track[] [ // ... 你的曲目对象 ]; const newItems await api.Playlists.addTracks(playlistId, tracks); api.Logger.info(Added ${newItems.length} items); // 重新排序把第 0 位的曲目移到第 3 位 // 注意必须先加载歌单 await api.Playlists.getPlaylist(playlistId); await api.Playlists.reorderTracks(playlistId, 0, 3); // 按条目 ID不是曲目 ID删除指定条目 await api.Playlists.removeTracks(playlistId, [newItems[0].id]); // 删除整个歌单 await api.Playlists.deletePlaylist(playlistId); }, };两个值得注意的源码级细节“reorder 前必须先加载”不是文档的口头约定而是实现约束。playlistStore.ts 中reorderTracks直接读内存缓存get().playlists.get(playlistId)未加载则静默返回不抛错、不落盘先调getPlaylist保证歌单进入缓存后才能生效createPlaylist生成的歌单使用uuidv4()作为 ID、isReadOnly: falseaddTracks为每首新曲目生成独立的条目 ID 并统一打addedAtIso时间戳返回新增的PlaylistItem[]供后续removeTracks/reorderTracks使用。3.3 导入歌单import type { NuclearPluginAPI, Playlist } from nuclearplayer/plugin-sdk; export default { async onEnable(api: NuclearPluginAPI) { const externalPlaylist: Playlist { id: ignored-original-id, name: Imported Playlist, createdAtIso: new Date().toISOString(), lastModifiedIso: new Date().toISOString(), isReadOnly: true, items: [ // ... 歌单条目 ], }; // importPlaylist 总是生成全新 ID const newId await api.Playlists.importPlaylist(externalPlaylist); api.Logger.info(Imported as ${newId}); // 原始 ID 被丢弃。再次导入同一对象 // 会创建一个独立的第二份歌单。 // 把当前播放队列保存为新歌单 const queuePlaylistId await api.Playlists.saveQueueAsPlaylist(Queue Snapshot); }, };从源码可补充两点行为importPlaylist会强制覆盖id新uuidv4()、两个时间戳并强制isReadOnly: false见 playlistStore.ts 中importPlaylist实现即传入对象的id与只读标记均被忽略导入后本地可以自由编辑saveQueueAsPlaylist读取useQueueStore当前队列并对每首曲目先执行stripResolutionState剥离流媒体解析状态后再包装为PlaylistItem避免把一次性解析结果固化进歌单文件。此外Nuclear 自身还内置了一套 JSON 文件导入逻辑 playlistImport.ts通过detectPlaylistFormat依次尝试新版导出格式playlistExportSchemaPLAYLIST_EXPORT_VERSION 1、旧版配置文件格式nuclear-legacy-config和旧版单曲单格式nuclear-legacy旧格式曲目会自动换算时长秒 → 毫秒、生成新条目 ID 并打上nuclear-legacyprovider 标记。插件开发者若涉及文件格式互通可参考这套检测顺序。3.4 订阅歌单变化import type { NuclearPluginAPI } from nuclearplayer/plugin-sdk; export default { async onEnable(api: NuclearPluginAPI) { const unsubscribe api.Playlists.subscribe((index) { api.Logger.info(Playlists changed: ${index.length} playlists); }); // 务必清理 return () { unsubscribe(); }; }, };宿主实现playlistsHost.ts直接桥接 zustand 的 store 级subscribe任何导致index变化的操作创建、删除、增删改曲目等都会触发监听器回调参数为最新的完整索引数组。插件应在onEnable返回值中注册清理函数插件禁用时解除订阅。四、PlaylistProvider基于 URL 的歌单抓取4.1 调度机制插件可以注册一个PlaylistProvider来处理 URL 型歌单导入。当用户在 Nuclear 的导入对话框中粘贴 URL 时播放器会依次询问每个已注册的 playlist provider 是否能处理该 URL第一个匹配成功的 provider 被调用去抓取歌单。该匹配逻辑在 useNavigateToPlaylist.ts 与 PlaylistsContext.tsx 中体现对注册表中的 provider 逐个调用provider.matchesUrl(url)取首个为true者。一个 provider 需要实现两个方法matchesUrl(url)同步返回该 provider 是否能处理给定 URL。同步调用且必须快——只做正则测试或主机名判断不要发起网络请求fetchPlaylistByUrl(url)从 URL 抓取并返回完整Playlist。4.2 注册与注销像其他 provider 一样通过api.Providers.register()注册kind: playlistsimport type { NuclearPlugin, NuclearPluginAPI, PlaylistProvider, Playlist, } from nuclearplayer/plugin-sdk; const provider: PlaylistProvider { id: acme-playlists, kind: playlists, name: Acme Playlists, matchesUrl(url: string): boolean { return url.includes(acme.music/playlist/); }, async fetchPlaylistByUrl(url: string): PromisePlaylist { const response await fetch(https://api.acme.music/resolve?url${encodeURIComponent(url)}); const data await response.json(); return { id: data.id, name: data.title, createdAtIso: new Date().toISOString(), lastModifiedIso: new Date().toISOString(), isReadOnly: false, items: data.tracks.map((track: any) ({ id: crypto.randomUUID(), track: { title: track.name, artists: [{ name: track.artist, roles: [main] }], source: { provider: acme, id: track.id }, }, addedAtIso: new Date().toISOString(), })), }; }, }; const plugin: NuclearPlugin { onEnable(api: NuclearPluginAPI) { api.Providers.register(provider); }, onDisable(api: NuclearPluginAPI) { api.Providers.unregister(acme-playlists); }, }; export default plugin;警告务必在onDisable中注销 provider。否则插件禁用后 Nuclear 仍会继续调用它。抓取流程的宿主侧实现在 usePlaylistFromProvider.tsURL 先decodeURIComponent解码经providersHost.get(providerId, playlists)取出 provider 后调用fetchPlaylistByUrl返回的Playlist会被播放器统一覆写isReadOnly: true并填充origin: { provider, id, url }记录来源 provider 名称、ID 与原始 URL随后走导入流程。成功/失败分别以 toast 与reportError反馈。测试侧可用 PlaylistProviderBuilderwithMatchesUrl/withUrlPattern等构建器快速构造 provider 用例。五、类型参考以下类型定义完整继承自官方文档并与 model/src/playlists.ts 的源码一致Playlisttype Playlist { id: string; name: string; description?: string; artwork?: ArtworkSet; tags?: string[]; createdAtIso: string; lastModifiedIso: string; origin?: ProviderRef; // 该歌单导入自哪里 isReadOnly: boolean; parentId?: string; items: PlaylistItem[]; };PlaylistIndexEntrytype PlaylistIndexEntry { id: string; name: string; createdAtIso: string; lastModifiedIso: string; isReadOnly: boolean; artwork?: ArtworkSet; itemCount: number; totalDurationMs: number; thumbnails: string[]; };PlaylistItemtype PlaylistItem { id: string; track: Track; note?: string; addedAtIso: string; };六、API 签名速查// 读取 api.Playlists.getIndex(): PromisePlaylistIndexEntry[] api.Playlists.getPlaylist(id: string): PromisePlaylist | null // 创建 api.Playlists.createPlaylist(name: string): Promisestring api.Playlists.importPlaylist(playlist: Playlist): Promisestring api.Playlists.saveQueueAsPlaylist(name: string): Promisestring // 修改 api.Playlists.addTracks(playlistId: string, tracks: Track[]): PromisePlaylistItem[] api.Playlists.removeTracks(playlistId: string, itemIds: string[]): Promisevoid api.Playlists.reorderTracks(playlistId: string, from: number, to: number): Promisevoid // 删除 api.Playlists.deletePlaylist(id: string): Promisevoid // 订阅 api.Playlists.subscribe(listener: (index: PlaylistIndexEntry[]) void): () voidProvider 类型type PlaylistProvider ProviderDescriptorplaylists { matchesUrl: (url: string) boolean; fetchPlaylistByUrl: (url: string) PromisePlaylist; };七、实践清单综合文档与源码开发歌单类插件时可记住这几条硬性规则列表展示走getIndex()按需getPlaylist()不要为渲染列表加载全部完整歌单删除/定位曲目用PlaylistItem.idaddTracks的返回值正是这些 ID 的载体reorderTracks之前必须先getPlaylist把歌单载入缓存否则调用被静默忽略importPlaylist会丢弃传入的id并重置时间戳与只读标记重复导入同一对象得到两份独立歌单PlaylistProvider.matchesUrl保持同步且轻量正则/主机名判断网络抓取只放在fetchPlaylistByUrl中onDisable中必须api.Providers.unregister并在subscribe清理函数中调用unsubscribe。相关源码入口SDK 门面 packages/plugin-sdk/src/api/playlists.ts、宿主桥接 packages/player/src/services/playlistsHost.ts、状态与持久化 packages/player/src/stores/playlistStore.ts、文件层 packages/player/src/services/playlistFileService/、Zod schema packages/model/src/schemas/playlists.ts以及本文开头的官方文档 packages/docs/plugins/playlists.md。【免费下载链接】nuclearStreaming music player that finds free music for you项目地址: https://gitcode.com/GitHub_Trending/nu/nuclear创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表