import { qualityList } from '@renderer/store' import { assertApiSupport } from '@renderer/store/utils' import musicSdk from '@renderer/utils/musicSdk' import { getOtherSource as getOtherSourceFromStore, saveOtherSource as saveOtherSourceFromStore, getMusicUrl as getStoreMusicUrl, getPlayerLyric as getStoreLyric, } from '@renderer/utils/ipc' import { appSetting } from '@renderer/store/setting' import { langS2T, toNewMusicInfo, toOldMusicInfo } from '@renderer/utils' import { requestMsg } from '@renderer/utils/message' const getOtherSourcePromises = new Map() export const existTimeExp = /\[\d{1,2}:.*\d{1,4}\]/ 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 } 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 | NodeJS.Timeout = setTimeout(() => { timeout = null reject(new Error('find music timeout')) }, 20_000) musicSdk.findMusic(searchMusicInfo).then((otherSource) => { resolve(otherSource.map(toNewMusicInfo) as LX.Music.MusicInfoOnline[]) }).catch(reject).finally(() => { if (timeout) 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 (!appSetting['player.isS2t']) { // @ts-expect-error if (lyricInfo.rawlrcInfo) return lyricInfo return { ...lyricInfo, rawlrcInfo: { ...lyricInfo } } } if (appSetting['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) { // 以下源支持lxlyric 重新获取 case 'kg': case 'kw': case 'mg': case 'wy': case 'tx': break default: return lrcInfo } } else if (lrcInfo.rlyric == null) { // 以下源支持 rlyric 重新获取 if (!['wy', 'kg', 'tx'].includes(musicInfo.source)) return lrcInfo } else return lrcInfo } return null } export const getPlayQuality = (highQuality: boolean, musicInfo: LX.Music.MusicInfoOnline): LX.Quality => { let type: LX.Quality = '128k' let list = qualityList.value[musicInfo.source] if (highQuality && musicInfo.meta._qualitys['320k'] && list?.includes('320k')) type = '320k' 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 }> => { let musicInfo: LX.Music.MusicInfoOnline | null = null let itemQuality: LX.Quality | null = null // eslint-disable-next-line no-cond-assign while (musicInfo = (musicInfos.shift() as LX.Music.MusicInfoOnline)) { if (retryedSource.includes(musicInfo.source)) continue retryedSource.push(musicInfo.source) if (!assertApiSupport(musicInfo.source)) continue itemQuality = quality ?? getPlayQuality(appSetting['player.highQuality'], 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(window.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 }> => { // console.log(musicInfo.source) const targetQuality = quality ?? getPlayQuality(appSetting['player.highQuality'], 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) 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() as LX.Music.MusicInfoOnline)) { 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(window.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) 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() as LX.Music.MusicInfoOnline)) { 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(window.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) // eslint-disable-next-line @typescript-eslint/promise-function-async return reqPromise.then((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) } // eslint-disable-next-line @typescript-eslint/promise-function-async return reqPromise.then((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) if (otherSource.length) { return getOnlineOtherSourceLyricInfo({ musicInfos: [...otherSource], onToggleSource, isRefresh, retryedSource: [musicInfo.source], }) } throw err }) }) }