mirror of
https://github.com/ikun0014/lx-music-mobile.git
synced 2025-05-23 22:37:41 +08:00
510 lines
18 KiB
TypeScript
510 lines
18 KiB
TypeScript
import musicSdk, { findMusic } from '@/utils/musicSdk'
|
|
import {
|
|
// getOtherSource as getOtherSourceFromStore,
|
|
// saveOtherSource as saveOtherSourceFromStore,
|
|
getMusicUrl as getStoreMusicUrl,
|
|
getPlayerLyric as getStoreLyric,
|
|
} from '@/utils/data'
|
|
import { langS2T, toNewMusicInfo, toOldMusicInfo } from '@/utils'
|
|
import { assertApiSupport } from '@/utils/tools'
|
|
import settingState from '@/store/setting/state'
|
|
import { requestMsg } from '@/utils/message'
|
|
import BackgroundTimer from 'react-native-background-timer'
|
|
import { apis } from '@/utils/musicSdk/api-source'
|
|
|
|
|
|
const getOtherSourcePromises = new Map()
|
|
export const existTimeExp = /\[\d{1,2}:.*\d{1,4}\]/
|
|
const otherSourceCache = new Map<LX.Music.MusicInfo | LX.Download.ListItem, LX.Music.MusicInfoOnline[]>()
|
|
|
|
export const getOtherSource = async(musicInfo: LX.Music.MusicInfo | LX.Download.ListItem, isRefresh = false): Promise<LX.Music.MusicInfoOnline[]> => {
|
|
// if (!isRefresh) {
|
|
// const cachedInfo = await getOtherSourceFromStore(musicInfo.id)
|
|
// if (cachedInfo.length) return cachedInfo
|
|
// }
|
|
if (otherSourceCache.has(musicInfo)) return otherSourceCache.get(musicInfo)!
|
|
let key: string
|
|
let searchMusicInfo: {
|
|
name: string
|
|
singer: string
|
|
source: string
|
|
albumName: string
|
|
interval: string
|
|
}
|
|
if ('progress' in musicInfo) {
|
|
key = `local_${musicInfo.id}`
|
|
searchMusicInfo = {
|
|
name: musicInfo.metadata.musicInfo.name,
|
|
singer: musicInfo.metadata.musicInfo.singer,
|
|
source: musicInfo.metadata.musicInfo.source,
|
|
albumName: musicInfo.metadata.musicInfo.meta.albumName,
|
|
interval: musicInfo.metadata.musicInfo.interval ?? '',
|
|
}
|
|
} else {
|
|
key = `${musicInfo.source}_${musicInfo.id}`
|
|
searchMusicInfo = {
|
|
name: musicInfo.name,
|
|
singer: musicInfo.singer,
|
|
source: musicInfo.source,
|
|
albumName: musicInfo.meta.albumName,
|
|
interval: musicInfo.interval ?? '',
|
|
}
|
|
}
|
|
if (getOtherSourcePromises.has(key)) return getOtherSourcePromises.get(key)
|
|
|
|
const promise = new Promise<LX.Music.MusicInfoOnline[]>((resolve, reject) => {
|
|
let timeout: null | number = BackgroundTimer.setTimeout(() => {
|
|
timeout = null
|
|
reject(new Error('find music timeout'))
|
|
}, 12_000)
|
|
findMusic(searchMusicInfo).then((otherSource) => {
|
|
if (otherSourceCache.size > 10) otherSourceCache.clear()
|
|
const source = otherSource.map(toNewMusicInfo) as LX.Music.MusicInfoOnline[]
|
|
otherSourceCache.set(musicInfo, source)
|
|
resolve(source)
|
|
}).catch(reject).finally(() => {
|
|
if (timeout) BackgroundTimer.clearTimeout(timeout)
|
|
})
|
|
}).then((otherSource) => {
|
|
// if (otherSource.length) void saveOtherSourceFromStore(musicInfo.id, otherSource)
|
|
return otherSource
|
|
}).finally(() => {
|
|
if (getOtherSourcePromises.has(key)) getOtherSourcePromises.delete(key)
|
|
})
|
|
getOtherSourcePromises.set(key, promise)
|
|
return promise
|
|
}
|
|
|
|
|
|
export const buildLyricInfo = async(lyricInfo: MakeOptional<LX.Player.LyricInfo, 'rawlrcInfo'>): Promise<LX.Player.LyricInfo> => {
|
|
if (!settingState.setting['player.isS2t']) {
|
|
// @ts-expect-error
|
|
if (lyricInfo.rawlrcInfo) return lyricInfo
|
|
return { ...lyricInfo, rawlrcInfo: { ...lyricInfo } }
|
|
}
|
|
|
|
if (settingState.setting['player.isS2t']) {
|
|
const tasks = [
|
|
lyricInfo.lyric ? langS2T(lyricInfo.lyric) : Promise.resolve(''),
|
|
lyricInfo.tlyric ? langS2T(lyricInfo.tlyric) : Promise.resolve(''),
|
|
lyricInfo.rlyric ? langS2T(lyricInfo.rlyric) : Promise.resolve(''),
|
|
lyricInfo.lxlyric ? langS2T(lyricInfo.lxlyric) : Promise.resolve(''),
|
|
]
|
|
if (lyricInfo.rawlrcInfo) {
|
|
tasks.push(lyricInfo.lyric ? langS2T(lyricInfo.lyric) : Promise.resolve(''))
|
|
tasks.push(lyricInfo.tlyric ? langS2T(lyricInfo.tlyric) : Promise.resolve(''))
|
|
tasks.push(lyricInfo.rlyric ? langS2T(lyricInfo.rlyric) : Promise.resolve(''))
|
|
tasks.push(lyricInfo.lxlyric ? langS2T(lyricInfo.lxlyric) : Promise.resolve(''))
|
|
}
|
|
return Promise.all(tasks).then(([lyric, tlyric, rlyric, lxlyric, lyric_raw, tlyric_raw, rlyric_raw, lxlyric_raw]) => {
|
|
const rawlrcInfo = lyric_raw ? {
|
|
lyric: lyric_raw,
|
|
tlyric: tlyric_raw,
|
|
rlyric: rlyric_raw,
|
|
lxlyric: lxlyric_raw,
|
|
} : {
|
|
lyric,
|
|
tlyric,
|
|
rlyric,
|
|
lxlyric,
|
|
}
|
|
return {
|
|
lyric,
|
|
tlyric,
|
|
rlyric,
|
|
lxlyric,
|
|
rawlrcInfo,
|
|
}
|
|
})
|
|
}
|
|
|
|
// @ts-expect-error
|
|
return lyricInfo.rawlrcInfo ? lyricInfo : { ...lyricInfo, rawlrcInfo: { ...lyricInfo } }
|
|
}
|
|
|
|
export const getCachedLyricInfo = async(musicInfo: LX.Music.MusicInfo): Promise<LX.Player.LyricInfo | null> => {
|
|
let lrcInfo = await getStoreLyric(musicInfo)
|
|
// lrcInfo = {}
|
|
if (existTimeExp.test(lrcInfo.lyric) && lrcInfo.tlyric != null) {
|
|
// if (musicInfo.lrc.startsWith('\ufeff[id:$00000000]')) {
|
|
// let str = musicInfo.lrc.replace('\ufeff[id:$00000000]\n', '')
|
|
// commit('setLrc', { musicInfo, lyric: str, tlyric: musicInfo.tlrc, lxlyric: musicInfo.tlrc })
|
|
// } else if (musicInfo.lrc.startsWith('[id:$00000000]')) {
|
|
// let str = musicInfo.lrc.replace('[id:$00000000]\n', '')
|
|
// commit('setLrc', { musicInfo, lyric: str, tlyric: musicInfo.tlrc, lxlyric: musicInfo.tlrc })
|
|
// }
|
|
|
|
// if (lrcInfo.lxlyric == null) {
|
|
// switch (musicInfo.source) {
|
|
// case 'kg':
|
|
// case 'kw':
|
|
// case 'mg':
|
|
// break
|
|
// default:
|
|
// return lrcInfo
|
|
// }
|
|
// } else
|
|
if (lrcInfo.rlyric == null) {
|
|
if (!['wy', 'kg'].includes(musicInfo.source)) return lrcInfo
|
|
} else return lrcInfo
|
|
}
|
|
return null
|
|
}
|
|
|
|
export const getOnlineOtherSourceMusicUrlByLocal = async(musicInfo: LX.Music.MusicInfoLocal, isRefresh: boolean): Promise<{
|
|
url: string
|
|
quality: LX.Quality
|
|
isFromCache: boolean
|
|
}> => {
|
|
if (!await global.lx.apiInitPromise[0]) throw new Error('source init failed')
|
|
|
|
const quality = '128k'
|
|
|
|
const cachedUrl = await getStoreMusicUrl(musicInfo, quality)
|
|
if (cachedUrl && !isRefresh) return { url: cachedUrl, quality, isFromCache: true }
|
|
|
|
let reqPromise
|
|
try {
|
|
reqPromise = apis('local').getMusicUrl(toOldMusicInfo(musicInfo), null).promise
|
|
} catch (err: any) {
|
|
reqPromise = Promise.reject(err)
|
|
}
|
|
|
|
return reqPromise.then(({ url }: { url: string }) => {
|
|
return { url, quality, isFromCache: false }
|
|
})
|
|
}
|
|
|
|
export const getOnlineOtherSourceLyricByLocal = async(musicInfo: LX.Music.MusicInfoLocal, isRefresh: boolean): Promise<{
|
|
lyricInfo: LX.Music.LyricInfo
|
|
isFromCache: boolean
|
|
}> => {
|
|
if (!await global.lx.apiInitPromise[0]) throw new Error('source init failed')
|
|
|
|
const lyricInfo = await getCachedLyricInfo(musicInfo)
|
|
if (lyricInfo && !isRefresh) return { lyricInfo, isFromCache: true }
|
|
|
|
let reqPromise
|
|
try {
|
|
reqPromise = apis('local').getLyric(toOldMusicInfo(musicInfo)).promise
|
|
} catch (err: any) {
|
|
reqPromise = Promise.reject(err)
|
|
}
|
|
|
|
return reqPromise.then((lyricInfo: LX.Music.LyricInfo) => {
|
|
return { lyricInfo, isFromCache: false }
|
|
})
|
|
}
|
|
|
|
export const getOnlineOtherSourcePicByLocal = async(musicInfo: LX.Music.MusicInfoLocal): Promise<{
|
|
url: string
|
|
}> => {
|
|
if (!await global.lx.apiInitPromise[0]) throw new Error('source init failed')
|
|
|
|
let reqPromise
|
|
try {
|
|
reqPromise = apis('local').getPic(toOldMusicInfo(musicInfo)).promise
|
|
} catch (err: any) {
|
|
reqPromise = Promise.reject(err)
|
|
}
|
|
|
|
return reqPromise.then((url: string) => {
|
|
return { url }
|
|
})
|
|
}
|
|
|
|
export const TRY_QUALITYS_LIST = ['master', 'flac24bit', 'flac', '320k', '128k'] as const
|
|
type TryQualityType = typeof TRY_QUALITYS_LIST[number]
|
|
export const getPlayQuality = (highQuality: LX.Quality, musicInfo: LX.Music.MusicInfoOnline): LX.Quality => {
|
|
let type: LX.Quality = '128k'
|
|
if (TRY_QUALITYS_LIST.includes(highQuality as TryQualityType)) {
|
|
let list = global.lx.qualityList[musicInfo.source]
|
|
|
|
let t = TRY_QUALITYS_LIST
|
|
.slice(TRY_QUALITYS_LIST.indexOf(highQuality as TryQualityType))
|
|
.find(q => musicInfo.meta._qualitys[q] && list?.includes(q))
|
|
|
|
if (t) type = t
|
|
}
|
|
return type
|
|
}
|
|
|
|
export const getOnlineOtherSourceMusicUrl = async({ musicInfos, quality, onToggleSource, isRefresh, retryedSource = [] }: {
|
|
musicInfos: LX.Music.MusicInfoOnline[]
|
|
quality?: LX.Quality
|
|
onToggleSource: (musicInfo?: LX.Music.MusicInfoOnline) => void
|
|
isRefresh: boolean
|
|
retryedSource?: LX.OnlineSource[]
|
|
}): Promise<{
|
|
url: string
|
|
musicInfo: LX.Music.MusicInfoOnline
|
|
quality: LX.Quality
|
|
isFromCache: boolean
|
|
}> => {
|
|
if (!await global.lx.apiInitPromise[0]) throw new Error('source init failed')
|
|
|
|
let musicInfo: LX.Music.MusicInfoOnline | null = null
|
|
let itemQuality: LX.Quality | null = null
|
|
// eslint-disable-next-line no-cond-assign
|
|
while (musicInfo = (musicInfos.shift()!)) {
|
|
if (retryedSource.includes(musicInfo.source)) continue
|
|
retryedSource.push(musicInfo.source)
|
|
if (!assertApiSupport(musicInfo.source)) continue
|
|
itemQuality = quality ?? getPlayQuality(settingState.setting['player.playQuality'], musicInfo)
|
|
if (!musicInfo.meta._qualitys[itemQuality]) continue
|
|
|
|
console.log('try toggle to: ', musicInfo.source, musicInfo.name, musicInfo.singer, musicInfo.interval)
|
|
onToggleSource(musicInfo)
|
|
break
|
|
}
|
|
if (!musicInfo || !itemQuality) throw new Error(global.i18n.t('toggle_source_failed'))
|
|
|
|
const cachedUrl = await getStoreMusicUrl(musicInfo, itemQuality)
|
|
if (cachedUrl && !isRefresh) return { url: cachedUrl, musicInfo, quality: itemQuality, isFromCache: true }
|
|
|
|
let reqPromise
|
|
try {
|
|
reqPromise = musicSdk[musicInfo.source].getMusicUrl(toOldMusicInfo(musicInfo), itemQuality).promise
|
|
} catch (err: any) {
|
|
reqPromise = Promise.reject(err)
|
|
}
|
|
// retryedSource.includes(musicInfo.source)
|
|
// eslint-disable-next-line @typescript-eslint/promise-function-async
|
|
return reqPromise.then(({ url, type }: { url: string, type: LX.Quality }) => {
|
|
return { musicInfo, url, quality: type, isFromCache: false }
|
|
// eslint-disable-next-line @typescript-eslint/promise-function-async
|
|
}).catch((err: any) => {
|
|
if (err.message == requestMsg.tooManyRequests) throw err
|
|
console.log(err)
|
|
return getOnlineOtherSourceMusicUrl({ musicInfos, quality, onToggleSource, isRefresh, retryedSource })
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 获取在线音乐URL
|
|
*/
|
|
export const handleGetOnlineMusicUrl = async({ musicInfo, quality, onToggleSource, isRefresh, allowToggleSource }: {
|
|
musicInfo: LX.Music.MusicInfoOnline
|
|
quality?: LX.Quality
|
|
isRefresh: boolean
|
|
allowToggleSource: boolean
|
|
onToggleSource: (musicInfo?: LX.Music.MusicInfoOnline) => void
|
|
}): Promise<{
|
|
url: string
|
|
musicInfo: LX.Music.MusicInfoOnline
|
|
quality: LX.Quality
|
|
isFromCache: boolean
|
|
}> => {
|
|
if (!await global.lx.apiInitPromise[0]) throw new Error('source init failed')
|
|
// console.log(musicInfo.source)
|
|
const targetQuality = quality ?? getPlayQuality(settingState.setting['player.playQuality'], musicInfo)
|
|
|
|
let reqPromise
|
|
try {
|
|
reqPromise = musicSdk[musicInfo.source].getMusicUrl(toOldMusicInfo(musicInfo), targetQuality).promise
|
|
} catch (err: any) {
|
|
reqPromise = Promise.reject(err)
|
|
}
|
|
return reqPromise.then(({ url, type }: { url: string, type: LX.Quality }) => {
|
|
return { musicInfo, url, quality: type, isFromCache: false }
|
|
}).catch(async(err: any) => {
|
|
console.log(err)
|
|
if (!allowToggleSource || err.message == requestMsg.tooManyRequests) throw err
|
|
onToggleSource()
|
|
// eslint-disable-next-line @typescript-eslint/promise-function-async
|
|
return getOtherSource(musicInfo).then(otherSource => {
|
|
// console.log('find otherSource', otherSource.length)
|
|
if (otherSource.length) {
|
|
return getOnlineOtherSourceMusicUrl({
|
|
musicInfos: [...otherSource],
|
|
onToggleSource,
|
|
quality,
|
|
isRefresh,
|
|
retryedSource: [musicInfo.source],
|
|
})
|
|
}
|
|
throw err
|
|
})
|
|
})
|
|
}
|
|
|
|
|
|
export const getOnlineOtherSourcePicUrl = async({ musicInfos, onToggleSource, isRefresh, retryedSource = [] }: {
|
|
musicInfos: LX.Music.MusicInfoOnline[]
|
|
onToggleSource: (musicInfo?: LX.Music.MusicInfoOnline) => void
|
|
isRefresh: boolean
|
|
retryedSource?: LX.OnlineSource[]
|
|
}): Promise<{
|
|
url: string
|
|
musicInfo: LX.Music.MusicInfoOnline
|
|
isFromCache: boolean
|
|
}> => {
|
|
let musicInfo: LX.Music.MusicInfoOnline | null = null
|
|
// eslint-disable-next-line no-cond-assign
|
|
while (musicInfo = (musicInfos.shift()!)) {
|
|
if (retryedSource.includes(musicInfo.source)) continue
|
|
retryedSource.push(musicInfo.source)
|
|
// if (!assertApiSupport(musicInfo.source)) continue
|
|
console.log('try toggle to: ', musicInfo.source, musicInfo.name, musicInfo.singer, musicInfo.interval)
|
|
onToggleSource(musicInfo)
|
|
break
|
|
}
|
|
if (!musicInfo) throw new Error(global.i18n.t('toggle_source_failed'))
|
|
|
|
if (musicInfo.meta.picUrl && !isRefresh) return { musicInfo, url: musicInfo.meta.picUrl, isFromCache: true }
|
|
|
|
let reqPromise
|
|
try {
|
|
reqPromise = musicSdk[musicInfo.source].getPic(toOldMusicInfo(musicInfo))
|
|
} catch (err: any) {
|
|
reqPromise = Promise.reject(err)
|
|
}
|
|
// retryedSource.includes(musicInfo.source)
|
|
return reqPromise.then((url: string) => {
|
|
return { musicInfo, url, isFromCache: false }
|
|
// eslint-disable-next-line @typescript-eslint/promise-function-async
|
|
}).catch((err: any) => {
|
|
console.log(err)
|
|
return getOnlineOtherSourcePicUrl({ musicInfos, onToggleSource, isRefresh, retryedSource })
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 获取在线歌曲封面
|
|
*/
|
|
export const handleGetOnlinePicUrl = async({ musicInfo, isRefresh, onToggleSource, allowToggleSource }: {
|
|
musicInfo: LX.Music.MusicInfoOnline
|
|
onToggleSource: (musicInfo?: LX.Music.MusicInfoOnline) => void
|
|
isRefresh: boolean
|
|
allowToggleSource: boolean
|
|
}): Promise<{
|
|
url: string
|
|
musicInfo: LX.Music.MusicInfoOnline
|
|
isFromCache: boolean
|
|
}> => {
|
|
// console.log(musicInfo.source)
|
|
let reqPromise
|
|
try {
|
|
reqPromise = musicSdk[musicInfo.source].getPic(toOldMusicInfo(musicInfo))
|
|
} catch (err) {
|
|
reqPromise = Promise.reject(err)
|
|
}
|
|
return reqPromise.then((url: string) => {
|
|
return { musicInfo, url, isFromCache: false }
|
|
}).catch(async(err: any) => {
|
|
console.log(err)
|
|
if (!allowToggleSource) throw err
|
|
onToggleSource()
|
|
// eslint-disable-next-line @typescript-eslint/promise-function-async
|
|
return getOtherSource(musicInfo).then(otherSource => {
|
|
// console.log('find otherSource', otherSource.length)
|
|
if (otherSource.length) {
|
|
return getOnlineOtherSourcePicUrl({
|
|
musicInfos: [...otherSource],
|
|
onToggleSource,
|
|
isRefresh,
|
|
retryedSource: [musicInfo.source],
|
|
})
|
|
}
|
|
throw err
|
|
})
|
|
})
|
|
}
|
|
|
|
|
|
export const getOnlineOtherSourceLyricInfo = async({ musicInfos, onToggleSource, isRefresh, retryedSource = [] }: {
|
|
musicInfos: LX.Music.MusicInfoOnline[]
|
|
onToggleSource: (musicInfo?: LX.Music.MusicInfoOnline) => void
|
|
isRefresh: boolean
|
|
retryedSource?: LX.OnlineSource[]
|
|
}): Promise<{
|
|
lyricInfo: LX.Music.LyricInfo | LX.Player.LyricInfo
|
|
musicInfo: LX.Music.MusicInfoOnline
|
|
isFromCache: boolean
|
|
}> => {
|
|
let musicInfo: LX.Music.MusicInfoOnline | null = null
|
|
// eslint-disable-next-line no-cond-assign
|
|
while (musicInfo = (musicInfos.shift()!)) {
|
|
if (retryedSource.includes(musicInfo.source)) continue
|
|
retryedSource.push(musicInfo.source)
|
|
// if (!assertApiSupport(musicInfo.source)) continue
|
|
console.log('try toggle to: ', musicInfo.source, musicInfo.name, musicInfo.singer, musicInfo.interval)
|
|
onToggleSource(musicInfo)
|
|
break
|
|
}
|
|
if (!musicInfo) throw new Error(global.i18n.t('toggle_source_failed'))
|
|
|
|
if (!isRefresh) {
|
|
const lyricInfo = await getCachedLyricInfo(musicInfo)
|
|
if (lyricInfo) return { musicInfo, lyricInfo, isFromCache: true }
|
|
}
|
|
|
|
let reqPromise
|
|
try {
|
|
// TODO: remove any type
|
|
reqPromise = (musicSdk[musicInfo.source].getLyric(toOldMusicInfo(musicInfo)) as any).promise
|
|
} catch (err: any) {
|
|
reqPromise = Promise.reject(err)
|
|
}
|
|
// retryedSource.includes(musicInfo.source)
|
|
return reqPromise.then(async(lyricInfo: LX.Music.LyricInfo) => {
|
|
return existTimeExp.test(lyricInfo.lyric) ? {
|
|
lyricInfo,
|
|
musicInfo,
|
|
isFromCache: false,
|
|
} : Promise.reject(new Error('failed'))
|
|
// eslint-disable-next-line @typescript-eslint/promise-function-async
|
|
}).catch((err: any) => {
|
|
console.log(err)
|
|
return getOnlineOtherSourceLyricInfo({ musicInfos, onToggleSource, isRefresh, retryedSource })
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 获取在线歌词信息
|
|
*/
|
|
export const handleGetOnlineLyricInfo = async({ musicInfo, onToggleSource, isRefresh, allowToggleSource }: {
|
|
musicInfo: LX.Music.MusicInfoOnline
|
|
onToggleSource: (musicInfo?: LX.Music.MusicInfoOnline) => void
|
|
isRefresh: boolean
|
|
allowToggleSource: boolean
|
|
}): Promise<{
|
|
musicInfo: LX.Music.MusicInfoOnline
|
|
lyricInfo: LX.Music.LyricInfo | LX.Player.LyricInfo
|
|
isFromCache: boolean
|
|
}> => {
|
|
// console.log(musicInfo.source)
|
|
let reqPromise
|
|
try {
|
|
// TODO: remove any type
|
|
reqPromise = (musicSdk[musicInfo.source].getLyric(toOldMusicInfo(musicInfo)) as any).promise
|
|
} catch (err) {
|
|
reqPromise = Promise.reject(err)
|
|
}
|
|
return reqPromise.then(async(lyricInfo: LX.Music.LyricInfo) => {
|
|
return existTimeExp.test(lyricInfo.lyric) ? {
|
|
musicInfo,
|
|
lyricInfo,
|
|
isFromCache: false,
|
|
} : Promise.reject(new Error('failed'))
|
|
}).catch(async(err: any) => {
|
|
console.log(err)
|
|
if (!allowToggleSource) throw err
|
|
|
|
onToggleSource()
|
|
// eslint-disable-next-line @typescript-eslint/promise-function-async
|
|
return getOtherSource(musicInfo).then(otherSource => {
|
|
// console.log('find otherSource', otherSource.length)
|
|
if (otherSource.length) {
|
|
return getOnlineOtherSourceLyricInfo({
|
|
musicInfos: [...otherSource],
|
|
onToggleSource,
|
|
isRefresh,
|
|
retryedSource: [musicInfo.source],
|
|
})
|
|
}
|
|
throw err
|
|
})
|
|
})
|
|
}
|