616 lines
21 KiB
TypeScript
616 lines
21 KiB
TypeScript
import { defineStore } from 'pinia'
|
||
import { computed, ref, watch } from 'vue'
|
||
import { invoke } from '@tauri-apps/api/core'
|
||
import { listen, type UnlistenFn } from '@tauri-apps/api/event'
|
||
import { commands, type MusicEnvStatus, type MusicSettings } from '@/lib/bindings'
|
||
import { useProxyStore } from '@/stores/proxyStore'
|
||
import { createLogger } from '@/lib/logger'
|
||
|
||
/** 搜索结果中的单曲(与 bridge.py song_to_dict 字段对应) */
|
||
export interface MusicSong {
|
||
songName: string | null
|
||
singers: string | null
|
||
album: string | null
|
||
duration: string | null
|
||
durationS: number | null
|
||
fileSize: string | null
|
||
fileSizeBytes: number | null
|
||
ext: string | null
|
||
source: string | null
|
||
rootSource: string | null
|
||
downloadUrl: string | null
|
||
valid: boolean
|
||
coverUrl: string | null
|
||
bitrate: number | null
|
||
/** 下载所需:save_path 命名用 */
|
||
identifier?: string | null
|
||
/** 默认下载请求头(rust 引擎直传下载器模块用) */
|
||
defaultDownloadHeaders?: Record<string, string> | null
|
||
defaultDownloadCookies?: Record<string, string> | null
|
||
/** 懒解析:官方搜索 API 的原始结果(resolve/下载前解析真实链接用) */
|
||
rawSearch?: Record<string, unknown> | null
|
||
/** 音质提示:true=具备无损(ext 或官方音质字段推断),null=未知;懒解析歌 ext/fileSize 为空时筛选依据 */
|
||
lossless?: boolean | null
|
||
/** 全音质档位(搜索阶段多档展示:label/ext/bitrate/sizeBytes/size/lossless);懒解析歌为数组,急切解析/无数据为 null */
|
||
qualities?: MusicQuality[] | null
|
||
}
|
||
|
||
/** 单个音质档位(musicdl 官方接口搜索阶段批量取回的格式+大小+无损) */
|
||
export interface MusicQuality {
|
||
label: string | null
|
||
ext: string | null
|
||
bitrate: number | null
|
||
sizeBytes: number | null
|
||
size: string | null
|
||
lossless: boolean | null
|
||
}
|
||
|
||
/** 桥接下载事件(bridge.py 事件行,经 Rust 原样转发) */
|
||
export interface MusicDownloadEvent {
|
||
event: string
|
||
taskId?: string
|
||
type?: 'start' | 'progress' | 'done' | 'error' | 'cancelled' | 'finished' | 'resolving' | 'bridge-stopped'
|
||
key?: string
|
||
songName?: string
|
||
singers?: string
|
||
ext?: string
|
||
/** 实际解析出的音质档位(start 事件回传,如 "无损"/"320K") */
|
||
quality?: string
|
||
downloaded?: number
|
||
total?: number
|
||
done?: number
|
||
message?: string
|
||
}
|
||
|
||
/** 任务内单曲状态(key = "{source}|{idx}") */
|
||
export interface MusicDownloadSongState {
|
||
key: string
|
||
songName: string
|
||
singers: string
|
||
status: 'queued' | 'resolving' | 'downloading' | 'done' | 'error' | 'cancelled'
|
||
downloaded: number
|
||
total: number
|
||
message?: string
|
||
/** 实际解析出的音质档位(start 事件回传);请求档位(如 "最高")在任务级 task.quality */
|
||
quality?: string
|
||
}
|
||
|
||
/** 音乐下载任务(musicdl 引擎) */
|
||
export interface MusicDownloadTask {
|
||
taskId: string
|
||
engine: 'musicdl' | 'rust'
|
||
status: 'downloading' | 'cancelling' | 'done' | 'error' | 'cancelled' | 'interrupted'
|
||
doneCount: number
|
||
songs: MusicDownloadSongState[]
|
||
/** 完整歌曲数据(localStorage 持久化,用于"重新下载") */
|
||
songsData: MusicSong[]
|
||
/** 下载目录(打开文件夹用) */
|
||
savedir?: string
|
||
/** 本次下载的目标音质 label("" 表示最高),用于任务标题展示 */
|
||
quality?: string
|
||
errorMessage?: string
|
||
createdAt: number
|
||
}
|
||
|
||
/** 运行时安装进度事件负载(事件类型非 specta 生成范围,本地定义) */
|
||
export interface MusicInstallProgress {
|
||
stage: string
|
||
percent: number
|
||
downloadedBytes: number
|
||
totalBytes: number | null
|
||
message: string
|
||
}
|
||
|
||
/** 搜索返回(bridge search 结果结构) */
|
||
export interface MusicSearchPayload {
|
||
results: Record<string, MusicSong[]>
|
||
total: number
|
||
sources: string[]
|
||
}
|
||
|
||
/** 桥接 ping 返回 */
|
||
export interface MusicPingPayload {
|
||
version: string
|
||
python: string
|
||
}
|
||
|
||
const logger = createLogger('music')
|
||
|
||
export const useMusicStore = defineStore('music', () => {
|
||
const settings = ref<MusicSettings | null>(null)
|
||
const env = ref<MusicEnvStatus | null>(null)
|
||
/** musicdl 已注册的全部搜索源(客户端名) */
|
||
const availableSources = ref<string[]>([])
|
||
|
||
// 搜索状态
|
||
const searching = ref(false)
|
||
const searchError = ref('')
|
||
const results = ref<Record<string, MusicSong[]>>({})
|
||
const resultTotal = ref(0)
|
||
/** 已执行过一次搜索/歌单解析(用于空结果时展示"未找到"而非初始引导空态) */
|
||
const searched = ref(false)
|
||
|
||
// 运行时安装状态
|
||
const installing = ref(false)
|
||
const installProgress = ref<MusicInstallProgress | null>(null)
|
||
const installError = ref('')
|
||
let progressUnlisten: UnlistenFn | null = null
|
||
|
||
async function init() {
|
||
restoreTasks()
|
||
await Promise.all([loadSettings(), refreshEnv()])
|
||
}
|
||
|
||
async function loadSettings() {
|
||
settings.value = await commands.musicGetSettings()
|
||
}
|
||
|
||
/** 保存设置(调用方先修改 settings 再调用;前端用 400ms debounce) */
|
||
async function saveSettings() {
|
||
if (!settings.value) return
|
||
await commands.musicSaveSettings(settings.value)
|
||
}
|
||
|
||
async function refreshEnv() {
|
||
env.value = await commands.musicEnvStatus()
|
||
}
|
||
|
||
async function loadSources() {
|
||
try {
|
||
const v = (await invoke('music_get_sources')) as { sources: string[] }
|
||
availableSources.value = v.sources ?? []
|
||
} catch (e) {
|
||
logger.error(`加载音乐源失败: ${e}`)
|
||
availableSources.value = []
|
||
}
|
||
}
|
||
|
||
async function pingBridge(): Promise<MusicPingPayload> {
|
||
return (await invoke('music_ping')) as MusicPingPayload
|
||
}
|
||
|
||
async function search(keyword: string, sources: string[]) {
|
||
searching.value = true
|
||
searchError.value = ''
|
||
try {
|
||
const v = (await invoke('music_search', { keyword, sources })) as MusicSearchPayload
|
||
results.value = v.results ?? {}
|
||
resultTotal.value = v.total ?? 0
|
||
searched.value = true
|
||
} catch (e) {
|
||
searchError.value = typeof e === 'string' ? e : JSON.stringify(e)
|
||
throw e
|
||
} finally {
|
||
searching.value = false
|
||
}
|
||
}
|
||
|
||
// ===== 歌单解析 =====
|
||
const parsing = ref(false)
|
||
const parseError = ref('')
|
||
|
||
/** 解析歌单链接,结果以首个歌曲来源为键写入 results(与搜索共用展示/下载) */
|
||
async function parsePlaylist(url: string, sources: string[]) {
|
||
parsing.value = true
|
||
parseError.value = ''
|
||
try {
|
||
const v = (await invoke('music_parse_playlist', { url, sources })) as {
|
||
songs: MusicSong[]
|
||
count: number
|
||
}
|
||
const count = v.count ?? 0
|
||
searched.value = true
|
||
if (count > 0) {
|
||
const srcKey = v.songs[0]?.source ?? 'playlist'
|
||
results.value = { [srcKey]: v.songs }
|
||
resultTotal.value = count
|
||
} else {
|
||
results.value = {}
|
||
resultTotal.value = 0
|
||
}
|
||
return count
|
||
} catch (e) {
|
||
parseError.value = typeof e === 'string' ? e : JSON.stringify(e)
|
||
throw e
|
||
} finally {
|
||
parsing.value = false
|
||
}
|
||
}
|
||
|
||
// ===== 下载任务 =====
|
||
const tasks = ref<MusicDownloadTask[]>([])
|
||
let downloadUnlisten: UnlistenFn | null = null
|
||
|
||
// 任务历史持久化(localStorage,应用重启后保留记录;恢复时不尝试续传)
|
||
const TASKS_KEY = 'thing.music.tasks'
|
||
const MAX_TASKS = 30
|
||
/** 单次持久化的体积上限:超过则剥离 rawSearch(懒解析字段)后重试,避免撑爆 localStorage 配额 */
|
||
const MAX_PERSIST_CHARS = 1_500_000
|
||
let persistTimer: ReturnType<typeof setTimeout> | null = null
|
||
|
||
/** 剥离仅解析期使用的重字段(只影响「重新下载」对懒解析歌曲的能力) */
|
||
function stripHeavy(s: MusicSong): MusicSong {
|
||
const { rawSearch, defaultDownloadHeaders, defaultDownloadCookies, ...rest } = s
|
||
void rawSearch
|
||
void defaultDownloadHeaders
|
||
void defaultDownloadCookies
|
||
return rest
|
||
}
|
||
|
||
function persistTasks() {
|
||
const base = tasks.value.slice(0, MAX_TASKS)
|
||
try {
|
||
let payload = JSON.stringify(base)
|
||
if (payload.length > MAX_PERSIST_CHARS) {
|
||
payload = JSON.stringify(base.map((t) => ({ ...t, songsData: t.songsData.map(stripHeavy) })))
|
||
}
|
||
localStorage.setItem(TASKS_KEY, payload)
|
||
} catch {
|
||
// localStorage 不可用/超限时静默降级(仅影响历史记录)
|
||
}
|
||
}
|
||
function restoreTasks() {
|
||
try {
|
||
const raw = localStorage.getItem(TASKS_KEY)
|
||
if (!raw) return
|
||
const arr = JSON.parse(raw) as MusicDownloadTask[]
|
||
if (!Array.isArray(arr)) return
|
||
for (const t of arr) {
|
||
// 重启后桥接已无下载状态:残留的"下载中"统一标记为中断;
|
||
// "取消中"表明用户已请求取消,落定为已取消
|
||
if (t.status === 'downloading') t.status = 'interrupted'
|
||
else if (t.status === 'cancelling') t.status = 'cancelled'
|
||
if (!Array.isArray(t.songs)) t.songs = []
|
||
if (!Array.isArray(t.songsData)) t.songsData = []
|
||
}
|
||
tasks.value = arr.slice(0, MAX_TASKS)
|
||
} catch {
|
||
// 数据损坏则丢弃历史
|
||
}
|
||
}
|
||
// 仅在结构性变化(任务增删 / 任务状态 / 完成数)时持久化:
|
||
// 用轻量签名代替深度 watch,避免下载进度事件(每 300ms 修改 downloaded)
|
||
// 触发 30 个任务(含 songsData)的全量 JSON 序列化
|
||
const tasksSignature = computed(
|
||
() => tasks.value.map((t) => `${t.taskId}:${t.status}:${t.doneCount}`).join('|')
|
||
)
|
||
watch(
|
||
tasksSignature,
|
||
() => {
|
||
if (persistTimer) clearTimeout(persistTimer)
|
||
persistTimer = setTimeout(persistTasks, 400)
|
||
}
|
||
)
|
||
|
||
async function ensureDownloadListener() {
|
||
if (downloadUnlisten) return
|
||
downloadUnlisten = await listen<MusicDownloadEvent>('music-download-event', (e) => {
|
||
handleDownloadEvent(e.payload)
|
||
})
|
||
}
|
||
|
||
function handleDownloadEvent(ev: MusicDownloadEvent) {
|
||
if (ev.type === 'bridge-stopped') {
|
||
// 桥接进程被停止:活动任务标记为中断;取消中的任务落定为已取消
|
||
for (const t of tasks.value) {
|
||
if (t.status === 'downloading') t.status = 'interrupted'
|
||
else if (t.status === 'cancelling') t.status = 'cancelled'
|
||
}
|
||
return
|
||
}
|
||
if (!ev.taskId) return
|
||
const task = tasks.value.find((t) => t.taskId === ev.taskId)
|
||
if (!task) return
|
||
const type = ev.type
|
||
if (type === 'resolving') {
|
||
// 懒解析:下载 worker 正在解析真实下载链接(第三方 API + 官方音质阶梯)
|
||
const st = task.songs.find((s) => s.key === ev.key)
|
||
if (st) {
|
||
st.status = 'resolving'
|
||
st.songName = ev.songName ?? st.songName
|
||
st.singers = ev.singers ?? st.singers
|
||
}
|
||
if (task.status !== 'cancelling') task.status = 'downloading'
|
||
} else if (type === 'start') {
|
||
const st = task.songs.find((s) => s.key === ev.key)
|
||
if (st) {
|
||
st.status = 'downloading'
|
||
st.songName = ev.songName ?? st.songName
|
||
st.singers = ev.singers ?? st.singers
|
||
st.total = ev.total ?? st.total
|
||
st.quality = ev.quality ?? st.quality
|
||
}
|
||
// 取消请求后桥接仍可能推送在途歌曲的 start 事件,不覆盖「取消中」状态
|
||
if (task.status !== 'cancelling') task.status = 'downloading'
|
||
} else if (type === 'progress') {
|
||
const st = task.songs.find((s) => s.key === ev.key)
|
||
if (st) {
|
||
st.downloaded = ev.downloaded ?? 0
|
||
st.total = ev.total ?? st.total
|
||
}
|
||
} else if (type === 'done') {
|
||
const st = task.songs.find((s) => s.key === ev.key)
|
||
if (st) {
|
||
// 幂等:重复的 done 事件(断点续传/重发)不再累加 doneCount
|
||
if (st.status !== 'done') task.doneCount++
|
||
st.status = 'done'
|
||
st.downloaded = st.total || st.downloaded
|
||
}
|
||
} else if (type === 'error') {
|
||
const st = task.songs.find((s) => s.key === ev.key)
|
||
if (st) {
|
||
st.status = 'error'
|
||
st.message = ev.message
|
||
} else if (ev.key === undefined) {
|
||
// 任务级错误(worker 初始化失败/监督线程异常):桥接不会再发 finished,
|
||
// 此处直接落定最终状态,避免任务永久停留在「下载中」
|
||
task.errorMessage = ev.message
|
||
task.status = task.status === 'cancelling' ? 'cancelled' : 'error'
|
||
}
|
||
} else if (type === 'cancelled') {
|
||
const st = task.songs.find((s) => s.key === ev.key)
|
||
if (st) st.status = 'cancelled'
|
||
} else if (type === 'finished') {
|
||
const anyError = task.songs.some((s) => s.status === 'error')
|
||
const anyCancelled = task.songs.some((s) => s.status === 'cancelled')
|
||
task.status = anyError ? 'error' : anyCancelled ? 'cancelled' : 'done'
|
||
}
|
||
}
|
||
|
||
/** 清洗文件名中的 Windows 非法字符(\ / : * ? " < > | 及控制字符),并去掉结尾空格/点 */
|
||
function sanitizeFilename(name: string): string {
|
||
return (
|
||
name
|
||
.replace(/[\\/:*?"<>|\x00-\x1f]/g, '')
|
||
.replace(/\s+/g, ' ')
|
||
.trim()
|
||
.replace(/[. ]+$/, '') || 'music'
|
||
)
|
||
}
|
||
|
||
/** 懒解析:解析单首歌曲的真实下载链接(试听/上传前调用),成功后原地更新
|
||
* 搜索结果并返回带链接的最新歌曲对象;失败返回 null。
|
||
* `quality` 为音质档位 label("" = 最高),解析时按「≤所选最优档」封顶。 */
|
||
async function resolveSong(source: string, index: number, quality = ''): Promise<MusicSong | null> {
|
||
const song = results.value[source]?.[index]
|
||
if (!song) return null
|
||
if (song.downloadUrl) return song
|
||
if (!song.rawSearch) return null
|
||
const v = (await invoke('music_resolve', { song, quality })) as {
|
||
songs: (MusicSong | null)[]
|
||
}
|
||
const resolved = v.songs?.[0]
|
||
if (!resolved?.downloadUrl) return null
|
||
// 原地更新(索引未变,key 稳定);保留 rawSearch 供后续重新解析
|
||
if (results.value[source]?.[index] === song) {
|
||
results.value[source][index] = { ...song, ...resolved }
|
||
}
|
||
return resolved
|
||
}
|
||
|
||
/** 启动下载。engine='rust' 时交给下载器模块(任务在其列表中管理);
|
||
* 否则走 musicdl 桥接。返回实际使用的引擎与跳过的无链接歌曲数 */
|
||
async function startDownload(
|
||
songs: MusicSong[],
|
||
opts: {
|
||
savedir: string
|
||
lyric: boolean
|
||
cover: boolean
|
||
proxyUrl: string
|
||
engine: string
|
||
maxConcurrent: number
|
||
/** 目标音质 label("" 表示最高);解析下载时按「≤所选最优档」封顶 */
|
||
quality?: string
|
||
}
|
||
): Promise<{ engine: 'musicdl' | 'rust'; skipped: number; taskId?: string }> {
|
||
if (songs.length === 0) return { engine: 'musicdl', skipped: 0 }
|
||
await ensureDownloadListener()
|
||
const quality = opts.quality ?? ''
|
||
|
||
if (opts.engine === 'rust') {
|
||
// 懒解析歌曲先批量解析出真实链接(桥接内并行),再交给下载器模块
|
||
const lazy = songs.filter((s) => !s.downloadUrl && s.rawSearch)
|
||
if (lazy.length > 0) {
|
||
try {
|
||
const v = (await invoke('music_resolve', { songs: lazy, quality })) as {
|
||
songs: (MusicSong | null)[]
|
||
}
|
||
// 返回与输入顺序对齐,失败位为 null
|
||
v.songs?.forEach((r, i) => {
|
||
if (r && lazy[i]) Object.assign(lazy[i], r)
|
||
})
|
||
} catch (e) {
|
||
logger.error(`批量解析下载链接失败: ${e}`)
|
||
}
|
||
}
|
||
let skipped = 0
|
||
for (const song of songs) {
|
||
if (!song.downloadUrl) {
|
||
skipped++
|
||
continue
|
||
}
|
||
const ext = (song.ext ?? '').replace(/[\\/:*?"<>|\x00-\x1f]/g, '')
|
||
const raw = `${song.songName ?? 'music'}${ext ? '.' + ext : ''}`
|
||
try {
|
||
await commands.downloaderAddTask(
|
||
song.downloadUrl,
|
||
sanitizeFilename(raw),
|
||
opts.savedir,
|
||
song.defaultDownloadHeaders ?? null,
|
||
true,
|
||
null
|
||
)
|
||
} catch (e) {
|
||
logger.error(`下载失败 ${song.songName}: ${e}`)
|
||
throw e
|
||
}
|
||
}
|
||
return { engine: 'rust', skipped }
|
||
}
|
||
|
||
const taskId = crypto.randomUUID()
|
||
const task: MusicDownloadTask = {
|
||
taskId,
|
||
engine: 'musicdl',
|
||
status: 'downloading',
|
||
doneCount: 0,
|
||
createdAt: Date.now(),
|
||
songsData: songs,
|
||
savedir: opts.savedir,
|
||
quality,
|
||
songs: songs.map((s, idx) => ({
|
||
key: `${s.source ?? ''}|${idx}`,
|
||
songName: s.songName ?? '',
|
||
singers: s.singers ?? '',
|
||
status: 'queued' as const,
|
||
downloaded: 0,
|
||
total: s.fileSizeBytes ?? 0
|
||
}))
|
||
}
|
||
tasks.value.unshift(task)
|
||
if (tasks.value.length > 30) tasks.value.length = 30
|
||
try {
|
||
await invoke('music_download', {
|
||
taskId,
|
||
// 有 rawSearch 时丢弃搜索阶段的 downloadUrl(CDN 链接有时效,
|
||
// 交给桥接在下载 worker 里重新解析,顺带修复"隔夜链接过期"问题)
|
||
songs: songs.map((s) => (s.rawSearch ? { ...s, downloadUrl: null } : s)),
|
||
savedir: opts.savedir,
|
||
lyric: opts.lyric,
|
||
cover: opts.cover,
|
||
proxyUrl: opts.proxyUrl,
|
||
maxConcurrent: opts.maxConcurrent,
|
||
quality
|
||
})
|
||
} catch (e) {
|
||
task.status = 'error'
|
||
task.errorMessage = String(e)
|
||
throw e
|
||
}
|
||
return { engine: 'musicdl', skipped: 0, taskId }
|
||
}
|
||
|
||
async function cancelDownload(taskId: string) {
|
||
const task = tasks.value.find((t) => t.taskId === taskId)
|
||
if (!task || (task.status !== 'downloading' && task.status !== 'cancelling')) return
|
||
// 队列级取消:当前歌曲会完成,其余标记取消;最终状态由 finished 事件落定,
|
||
// 此处先置「取消中」防止用户在剩余歌曲停止前误点「重新下载」产生重复任务
|
||
task.status = 'cancelling'
|
||
try {
|
||
await invoke('music_download_cancel', { taskId })
|
||
} catch {
|
||
// 桥接不可用(进程已死,无后台下载)→ 直接落定为已取消,避免卡在取消中
|
||
if (task.status === 'cancelling') task.status = 'cancelled'
|
||
}
|
||
}
|
||
|
||
function removeTask(taskId: string) {
|
||
// 下载中/取消中的任务先请求队列级取消,防止移除后后台仍在下载
|
||
const task = tasks.value.find((t) => t.taskId === taskId)
|
||
if (task && (task.status === 'downloading' || task.status === 'cancelling')) {
|
||
invoke('music_download_cancel', { taskId }).catch(() => {})
|
||
}
|
||
tasks.value = tasks.value.filter((t) => t.taskId !== taskId)
|
||
}
|
||
|
||
/** 重新下载:用任务内保存的完整歌曲数据重新发起(error/cancelled/interrupted 状态可用) */
|
||
async function redownloadTask(taskId: string) {
|
||
const task = tasks.value.find((t) => t.taskId === taskId)
|
||
if (
|
||
!task ||
|
||
task.status === 'downloading' ||
|
||
task.status === 'cancelling' ||
|
||
task.songsData.length === 0
|
||
) {
|
||
return
|
||
}
|
||
const s = settings.value
|
||
if (!s) return
|
||
// 代理 URL:惰性读取代理模块设置(避免 store 初始化时跨 store 依赖)
|
||
let proxy = ''
|
||
if (s.useProxy) {
|
||
const port = useProxyStore().settings?.mixedPort
|
||
proxy = port ? `http://127.0.0.1:${port}` : ''
|
||
}
|
||
await startDownload(task.songsData, {
|
||
savedir: s.savedir,
|
||
lyric: s.lyricDownload,
|
||
cover: s.coverDownload,
|
||
proxyUrl: proxy,
|
||
engine: s.downloadEngine,
|
||
maxConcurrent: s.maxConcurrent,
|
||
// 沿用原任务的目标音质("最高" 存为空串),而不是当前默认音质
|
||
quality: task.quality ?? s.defaultDownloadQuality ?? ''
|
||
})
|
||
}
|
||
|
||
async function installRuntime() {
|
||
if (installing.value) return
|
||
installing.value = true
|
||
installError.value = ''
|
||
installProgress.value = null
|
||
if (!progressUnlisten) {
|
||
progressUnlisten = await listen<MusicInstallProgress>(
|
||
'music-runtime-install-progress',
|
||
(e) => {
|
||
installProgress.value = e.payload
|
||
}
|
||
)
|
||
}
|
||
try {
|
||
env.value = await commands.musicInstallRuntime()
|
||
} catch (e) {
|
||
// 保留失败原因供设置页面板展示;进度停在最后事件值,需清掉避免误导
|
||
installError.value = typeof e === 'string' ? e : JSON.stringify(e)
|
||
throw e
|
||
} finally {
|
||
installing.value = false
|
||
if (progressUnlisten) {
|
||
progressUnlisten()
|
||
progressUnlisten = null
|
||
}
|
||
}
|
||
}
|
||
|
||
function cancelInstall() {
|
||
commands.musicCancelRuntimeInstall().catch(() => {})
|
||
}
|
||
|
||
async function stopBridge() {
|
||
await commands.musicStopBridge()
|
||
}
|
||
|
||
return {
|
||
settings,
|
||
env,
|
||
availableSources,
|
||
searching,
|
||
searchError,
|
||
searched,
|
||
parsing,
|
||
parseError,
|
||
results,
|
||
resultTotal,
|
||
installing,
|
||
installProgress,
|
||
installError,
|
||
tasks,
|
||
init,
|
||
loadSettings,
|
||
saveSettings,
|
||
refreshEnv,
|
||
loadSources,
|
||
pingBridge,
|
||
search,
|
||
parsePlaylist,
|
||
resolveSong,
|
||
startDownload,
|
||
cancelDownload,
|
||
removeTask,
|
||
redownloadTask,
|
||
installRuntime,
|
||
cancelInstall,
|
||
stopBridge
|
||
}
|
||
})
|