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
})
})
}