完整示例
Such 插件完整代码示例:NCM 文件解析器与歌单导入
完整示例
本章提供两个完整的 Such 插件示例,覆盖常见使用场景。
示例 1:NCM 文件解析器
一个通过调用外部解密器解析网易云音乐 .ncm 文件的插件,使用 JSON UI Schema 定义界面。
/**
* Such 插件 — 网易云音乐 NCM 解析器
* 基于 ncmdump-go 批量解析 .ncm 文件并输出到 Such 音乐文件夹
*/
window.source = {
id: 'SUCH_NETEASE_NCM_WIDGET',
name: '网易云音乐 NCM 解析器',
version: '1.0.0',
author: 'enzymeym',
icon: 'https://example.com/ncm-icon.svg',
description: '基于 ncmdump-go 批量解析 .ncm 文件并输出到 Such 音乐文件夹',
isUIWidget: true,
permissions: ['local_program', 'file_system', 'app_info', 'fetch', 'playlist'],
// ==================== JSON UI Schema ====================
uiSchema: {
sections: [
{
title: '解密器设置',
fields: [
{
type: 'input',
key: 'decryptorPath',
label: '解密器路径 (ncmdump-go)',
placeholder: '例如: D:\\tools\\ncmdump-go.exe',
action: 'browse'
},
{
type: 'input',
key: 'ncmDir',
label: 'NCM 文件目录',
placeholder: '选择包含 .ncm 文件的目录',
readonly: true,
action: 'browseDir'
},
{ type: 'text', key: 'musicDir', label: '音乐输出目录' }
]
},
{
title: '状态',
fields: [
{ type: 'tag', key: 'decryptorStatusText' },
{ type: 'tag', key: 'statusText' }
]
},
{
title: '操作',
actions: [
{
type: 'button',
label: '下载解密器',
method: 'downloadDecryptor',
variant: 'default'
},
{ type: 'button', label: '检测解密器', method: 'checkDecryptorStatus' },
{
type: 'button',
label: '开始解密',
method: 'startDecrypt',
variant: 'primary'
}
]
},
{
title: '输出日志',
fields: [{ type: 'log', key: 'outputLog' }]
}
]
},
// ==================== 初始化 ====================
initialization: function () {
var musicFolder = suchmusic.getSetting('musicFolder')
this.setProps({
decryptorPath: '',
ncmDir: '',
musicDir: musicFolder || '',
decryptorStatusText: '未检测',
statusText: '就绪',
outputLog: ''
})
},
// ==================== 检测解密器状态 ====================
checkDecryptorStatus: async function () {
if (!this._decryptorPath) {
window.notify('请先配置 ncmdump-go 解密器路径', 'warning')
return { code: false, message: '未配置路径' }
}
try {
var result = await suchmusic.execProgram(this._decryptorPath, ['--version'])
if (result.code === 0) {
var ver = (result.stdout || '').trim()
this._decryptorStatus = true
this.setProps({ decryptorStatusText: '有效: ' + ver })
window.notify('ncmdump-go 解密器就绪 (' + ver + ')', 'success')
return { code: true, version: ver }
} else {
this._decryptorStatus = false
this.setProps({ decryptorStatusText: '无效' })
window.notify('ncmdump-go 解密器无效,请检查路径', 'error')
return { code: false, message: '解密器无响应' }
}
} catch (err) {
this._decryptorStatus = false
this.setProps({ decryptorStatusText: '未找到' })
window.notify('未找到 ncmdump-go 解密器', 'warning')
return { code: false, message: String(err.message || err) }
}
},
// ==================== 下载解密器 ====================
downloadDecryptor: async function () {
var self = this
self.setProps({ statusText: '下载中...', outputLog: '正在获取最新版本信息...\n' })
window.notify('正在获取 ncmdump-go 最新版本...', 'info')
try {
// 获取最新 release 信息
var apiUrl = 'https://api.example.com/releases/latest'
var response = await suchmusic.fetch(apiUrl)
var release = await response.json()
// 查找 Windows 版本下载链接
var downloadUrl = ''
var assets = release.assets || []
for (var i = 0; i < assets.length; i++) {
if (assets[i].name && assets[i].name.indexOf('windows_amd64') >= 0) {
downloadUrl = assets[i].browser_download_url
break
}
}
if (!downloadUrl) {
window.notify('未找到 Windows 版本', 'error')
return { code: false, message: '未找到 Windows 版本' }
}
// 下载到音乐目录
var musicDir = self.musicDir
var exePath = musicDir + '\\ncmdump-go.exe'
var downloadResult = await suchmusic.downloadFile(downloadUrl, exePath)
if (!downloadResult || !downloadResult.success) {
window.notify('下载失败', 'error')
return { code: false, message: '下载失败' }
}
self._decryptorPath = exePath
self.setProps({
decryptorPath: exePath,
statusText: '下载完成',
outputLog: '下载完成: ' + exePath,
decryptorStatusText: '已下载,请检测'
})
window.notify('ncmdump-go 下载完成,请点击"检测解密器"验证', 'success')
return { code: true, filePath: exePath }
} catch (err) {
var errMsg = String(err.message || err)
self.setProps({ statusText: '下载失败', outputLog: '下载出错:\n' + errMsg })
window.notify('下载失败: ' + errMsg, 'error')
return { code: false, message: errMsg }
}
},
// ==================== NCM 批量解析 ====================
startDecrypt: async function () {
if (!this._decryptorPath) {
window.notify('请先配置 ncmdump-go 解密器路径', 'error')
return { code: false, message: '未配置解密器路径' }
}
if (!this._ncmDir) {
window.notify('请先选择 NCM 文件目录', 'error')
return { code: false, message: '未选择 NCM 目录' }
}
this.setProps({ statusText: '解密中...' })
window.notify('开始解密 NCM 文件...', 'info')
try {
var command = '"' + this._decryptorPath + '" -d "' + this._ncmDir + '" -o "' + this.musicDir + '" -r'
var output = await suchmusic.execTerminal(command)
this.setProps({ statusText: '完成', outputLog: output })
window.notify('NCM 文件解析完成,已输出到音乐文件夹', 'success')
return { code: true, message: '解密完成' }
} catch (err) {
var errMsg = String(err.message || err)
this.setProps({ statusText: '失败', outputLog: '错误:\n' + errMsg })
window.notify('解密失败: ' + errMsg, 'error')
return { code: false, message: errMsg }
}
}
}关键知识点:
- JSON UI Schema 声明式 UI(4 个 sections,含 input / tag / log / button)
suchmusic.execProgram检测外部工具suchmusic.execTerminal执行批量解密命令suchmusic.downloadFile下载可执行文件this.setProps驱动 UI 状态更新window.notify推送用户通知- 完整的错误处理和状态反馈
示例 2:歌单导入
通过远程 API 获取歌单数据,下载音乐文件并导入到 Such。
/**
* Such 插件 — 网易云歌单导入
* 通过歌单 ID 和 Cookie 导入歌单到 Such
*/
window.source = {
id: 'SUCH_NETEASE_IMPORT',
name: '网易云歌单导入',
version: '1.0.0',
author: 'enzymeym',
description: '输入网易云歌单 ID 和 Cookie,一键导入歌单到 Such',
isUIWidget: true,
permissions: ['fetch', 'playlist', 'file_system', 'app_info'],
// ==================== JSON UI Schema ====================
uiSchema: {
sections: [
{
title: '导入设置',
fields: [
{
type: 'input',
key: 'playlistId',
label: '歌单 ID',
placeholder: '输入网易云歌单 ID,例如 18034007303'
},
{
type: 'input',
key: 'cookie',
label: 'Cookie',
placeholder: '登录网页版后获取 Cookie'
}
],
actions: [
{
type: 'button',
label: '开始导入',
method: 'startImport',
variant: 'primary'
}
]
},
{ title: '状态', fields: [{ type: 'tag', key: 'statusText' }] },
{ title: '导入日志', fields: [{ type: 'log', key: 'importLog' }] }
]
},
// ==================== 初始化 ====================
initialization: function () {
this.setProps({ playlistId: '', cookie: '', statusText: '就绪', importLog: '' })
},
// ==================== 开始导入 ====================
startImport: async function () {
var self = this
if (!self._playlistId) {
window.notify('请输入歌单 ID', 'warning')
return { code: false, message: '未输入歌单 ID' }
}
self._logLines = []
self._setStatus('导入中...')
try {
// 1. 获取歌单详情
self._log('正在获取歌单详情...')
var detailUrl = 'https://api.example.com/playlist/detail?id=' + self._playlistId
var detail = await self._fetchWithCookie(detailUrl)
var playlistName = detail.playlist.name
self._log('歌单名称: ' + playlistName)
// 2. 获取歌曲列表
self._log('正在获取歌曲列表...')
var tracks = await self._fetchAllTracks(self._playlistId, detail.playlist.trackCount)
// 3. 构建曲目并导入
self._log('正在导入到 Such...')
var trackEntries = tracks.map(function (t) {
return {
id: 'custom-' + t.id,
title: t.name,
artist: t.ar.map(function (a) { return a.name }).join('/'),
album: t.al ? t.al.name : '',
cover: t.al ? t.al.picUrl : '',
durationMs: t.dt || 0,
source: 'import',
sourceSongId: String(t.id)
}
})
var newPlaylist = await suchmusic.createPlaylist(playlistName, trackEntries)
self._log('歌单创建成功!ID: ' + newPlaylist.id)
self._setStatus('导入完成: ' + playlistName)
window.notify('歌单 "' + playlistName + '" 导入成功!', 'success')
return { code: true, playlistId: newPlaylist.id, trackCount: trackEntries.length }
} catch (err) {
var errMsg = String(err.message || err)
self._log('错误: ' + errMsg)
self._setStatus('导入失败')
window.notify('导入失败: ' + errMsg, 'error')
return { code: false, message: errMsg }
}
},
// ==================== 内部方法 ====================
_logLines: [],
_playlistId: '',
_cookie: '',
_log: function (msg) {
this._logLines.push('[' + new Date().toLocaleTimeString() + '] ' + msg)
this.setProps({ importLog: this._logLines.join('\n') })
},
_setStatus: function (text) {
this.setProps({ statusText: text })
},
_fetchWithCookie: async function (url) {
var options = this._cookie ? { headers: { 'Cookie': this._cookie } } : {}
var resp = await suchmusic.fetch(url, options)
if (!resp.ok) throw new Error('HTTP ' + resp.status)
return resp.json()
},
_fetchAllTracks: async function (playlistId, totalCount) {
var self = this
var allTracks = []
var offset = 0
var limit = 100
while (offset < totalCount) {
var pageNum = Math.floor(offset / limit) + 1
var totalPages = Math.ceil(totalCount / limit)
self._log('获取歌曲: 第 ' + pageNum + '/' + totalPages + ' 页...')
var url = 'https://api.example.com/playlist/track/all?id=' + playlistId + '&offset=' + offset + '&limit=' + limit
var resp = await self._fetchWithCookie(url)
var tracks = resp.songs || []
allTracks = allTracks.concat(tracks)
offset += limit
if (tracks.length < limit) break
}
return allTracks
}
}关键知识点:
suchmusic.fetch+ Cookie 做需要认证的 HTTP 请求suchmusic.createPlaylist创建歌单并将远程数据导入- 分页获取大量数据(while 循环 + offset/limit)
- 日志系统的实现模式(
_log+setProps) - JSON UI Schema 定义简洁的输入+展示界面