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() export const getOtherSource = async(musicInfo: LX.Music.MusicInfo | LX.Download.ListItem, isRefresh = false): Promise => { // 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((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): Promise => { 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 => { 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 }) }) }