Files
lx-music-mobile-mod/src/core/player/player.ts

523 lines
16 KiB
TypeScript

import { isInitialized, initial as playerInitial, isEmpty, setPause, setPlay, setResource, setStop } from '@/plugins/player'
import {
setStatusText,
} from '@/core/player/playStatus'
import playerState from '@/store/player/state'
import settingState from '@/store/setting/state'
import {
getList,
setPlayMusicInfo,
setMusicInfo,
setPlayListId,
} from '@/core/player/playInfo'
import {
clearPlayedList,
addPlayedList,
removePlayedList,
} from '@/core/player/playedList'
import {
clearTempPlayeList,
removeTempPlayList,
} from '@/core/player/tempPlayList'
import { getMusicUrl, getPicPath, getLyricInfo } from '@/core/music'
import { requestMsg } from '@/utils/message'
import { getRandom } from '@/utils/common'
import { filterList } from './utils'
import BackgroundTimer from 'react-native-background-timer'
import { checkIgnoringBatteryOptimization, checkNotificationPermission, debounceBackgroundTimer } from '@/utils/tools'
// import { checkMusicFileAvailable } from '@renderer/utils/music'
const createDelayNextTimeout = (delay: number) => {
let timeout: number | null
const clearDelayNextTimeout = () => {
// console.log(this.timeout)
if (timeout) {
BackgroundTimer.clearTimeout(timeout)
timeout = null
}
}
const addDelayNextTimeout = () => {
clearDelayNextTimeout()
timeout = BackgroundTimer.setTimeout(() => {
timeout = null
if (global.lx.isPlayedStop) return
console.log('delay next timeout timeout', delay)
void playNext(true)
}, delay)
}
return {
clearDelayNextTimeout,
addDelayNextTimeout,
}
}
const { addDelayNextTimeout, clearDelayNextTimeout } = createDelayNextTimeout(5000)
const { addDelayNextTimeout: addLoadTimeout, clearDelayNextTimeout: clearLoadTimeout } = createDelayNextTimeout(100000)
const createGettingUrlId = (musicInfo: LX.Music.MusicInfo | LX.Download.ListItem) => {
const tInfo = 'progress' in musicInfo ? musicInfo.metadata.musicInfo.meta.toggleMusicInfo : musicInfo.meta.toggleMusicInfo
return `${musicInfo.id}_${tInfo?.id ?? ''}`
}
/**
* 检查音乐信息是否已更改
*/
const diffCurrentMusicInfo = (curMusicInfo: LX.Music.MusicInfo | LX.Download.ListItem): boolean => {
// return curMusicInfo !== playerState.playMusicInfo.musicInfo || playerState.isPlay
return createGettingUrlId(curMusicInfo) != global.lx.gettingUrlId || curMusicInfo.id != playerState.playMusicInfo.musicInfo?.id || playerState.isPlay
}
let cancelDelayRetry: (() => void) | null = null
const delayRetry = async(musicInfo: LX.Music.MusicInfo | LX.Download.ListItem, isRefresh = false): Promise<string | null> => {
// if (cancelDelayRetry) cancelDelayRetry()
return new Promise<string | null>((resolve, reject) => {
const time = getRandom(2, 6)
setStatusText(global.i18n.t('player__geting_url_delay_retry', { time }))
const tiemout = setTimeout(() => {
getMusicPlayUrl(musicInfo, isRefresh, true).then((result) => {
cancelDelayRetry = null
resolve(result)
}).catch(async(err: any) => {
cancelDelayRetry = null
reject(err)
})
}, time * 1000)
cancelDelayRetry = () => {
clearTimeout(tiemout)
cancelDelayRetry = null
resolve(null)
}
})
}
const getMusicPlayUrl = async(musicInfo: LX.Music.MusicInfo | LX.Download.ListItem, isRefresh = false, isRetryed = false): Promise<string | null> => {
// this.musicInfo.url = await getMusicPlayUrl(targetSong, type)
setStatusText(global.i18n.t('player__geting_url'))
addLoadTimeout()
// const type = getPlayType(settingState.setting['player.isPlayHighQuality'], musicInfo)
let toggleMusicInfo = ('progress' in musicInfo ? musicInfo.metadata.musicInfo : musicInfo).meta.toggleMusicInfo
return (toggleMusicInfo ? getMusicUrl({
musicInfo: toggleMusicInfo,
isRefresh,
allowToggleSource: false,
}) : Promise.reject(new Error('not found'))).catch(async() => {
return getMusicUrl({
musicInfo,
isRefresh,
onToggleSource(mInfo) {
if (diffCurrentMusicInfo(musicInfo)) return
setStatusText(global.i18n.t('toggle_source_try'))
},
})
}).then(url => {
if (global.lx.isPlayedStop || diffCurrentMusicInfo(musicInfo)) return null
return url
}).catch(async err => {
// console.log('err', err.message)
if (global.lx.isPlayedStop ||
diffCurrentMusicInfo(musicInfo) ||
err.message == requestMsg.cancelRequest) return null
if (err.message == requestMsg.tooManyRequests) return delayRetry(musicInfo, isRefresh)
if (!isRetryed) return getMusicPlayUrl(musicInfo, isRefresh, true)
throw err
})
}
export const setMusicUrl = (musicInfo: LX.Music.MusicInfo | LX.Download.ListItem, isRefresh?: boolean) => {
// addLoadTimeout()
if (!diffCurrentMusicInfo(musicInfo)) return
if (cancelDelayRetry) cancelDelayRetry()
global.lx.gettingUrlId = createGettingUrlId(musicInfo)
void getMusicPlayUrl(musicInfo, isRefresh).then((url) => {
if (!url) return
setResource(musicInfo, url, playerState.progress.nowPlayTime)
}).catch((err: any) => {
console.log(err)
setStatusText(err.message as string)
global.app_event.error()
addDelayNextTimeout()
}).finally(() => {
if (musicInfo === playerState.playMusicInfo.musicInfo) {
global.lx.gettingUrlId = ''
clearLoadTimeout()
}
})
}
// 恢复上次播放的状态
const handleRestorePlay = async(restorePlayInfo: LX.Player.SavedPlayInfo) => {
const musicInfo = playerState.playMusicInfo.musicInfo
if (!musicInfo) return
setTimeout(() => {
global.app_event.setProgress(settingState.setting['player.isSavePlayTime'] ? restorePlayInfo.time : 0, restorePlayInfo.maxTime)
})
const playMusicInfo = playerState.playMusicInfo
void getPicPath({ musicInfo, listId: playMusicInfo.listId }).then((url: string) => {
if (
musicInfo.id != playMusicInfo.musicInfo?.id ||
playerState.musicInfo.pic == url ||
playerState.loadErrorPicUrl == url
) return
setMusicInfo({ pic: url })
global.app_event.picUpdated()
})
void getLyricInfo({ musicInfo }).then((lyricInfo) => {
if (musicInfo.id != playMusicInfo.musicInfo?.id) return
setMusicInfo({
lrc: lyricInfo.lyric,
tlrc: lyricInfo.tlyric,
lxlrc: lyricInfo.lxlyric,
rlrc: lyricInfo.rlyric,
rawlrc: lyricInfo.rawlrcInfo.lyric,
})
global.app_event.lyricUpdated()
}).catch((err) => {
console.log(err)
if (musicInfo.id != playMusicInfo.musicInfo?.id) return
setStatusText(global.i18n.t('lyric__load_error'))
})
if (settingState.setting['player.togglePlayMethod'] == 'random' && !playMusicInfo.isTempPlay) addPlayedList(playMusicInfo as LX.Player.PlayMusicInfo)
}
const debouncePlay = debounceBackgroundTimer((musicInfo: LX.Player.PlayMusic) => {
setMusicUrl(musicInfo)
void getPicPath({ musicInfo, listId: playerState.playMusicInfo.listId }).then((url: string) => {
if (
musicInfo.id != playerState.playMusicInfo.musicInfo?.id ||
playerState.musicInfo.pic == url ||
playerState.loadErrorPicUrl == url) return
setMusicInfo({ pic: url })
global.app_event.picUpdated()
})
void getLyricInfo({ musicInfo }).then((lyricInfo) => {
if (musicInfo.id != playerState.playMusicInfo.musicInfo?.id) return
setMusicInfo({
lrc: lyricInfo.lyric,
tlrc: lyricInfo.tlyric,
lxlrc: lyricInfo.lxlyric,
rlrc: lyricInfo.rlyric,
rawlrc: lyricInfo.rawlrcInfo.lyric,
})
global.app_event.lyricUpdated()
}).catch((err) => {
console.log(err)
if (musicInfo.id != playerState.playMusicInfo.musicInfo?.id) return
setStatusText(global.i18n.t('lyric__load_error'))
})
}, 200)
// 处理音乐播放
const handlePlay = async() => {
if (!isInitialized()) {
await checkNotificationPermission()
void checkIgnoringBatteryOptimization()
await playerInitial({
volume: settingState.setting['player.volume'],
playRate: settingState.setting['player.playbackRate'],
cacheSize: settingState.setting['player.cacheSize'] ? parseInt(settingState.setting['player.cacheSize']) : 0,
isHandleAudioFocus: settingState.setting['player.isHandleAudioFocus'],
isEnableAudioOffload: settingState.setting['player.isEnableAudioOffload'],
})
}
global.lx.isPlayedStop &&= false
if (global.lx.restorePlayInfo) {
void handleRestorePlay(global.lx.restorePlayInfo)
global.lx.restorePlayInfo = null
return
}
const playMusicInfo = playerState.playMusicInfo
const musicInfo = playMusicInfo.musicInfo
if (!musicInfo) return
await setStop()
global.app_event.pause()
clearDelayNextTimeout()
clearLoadTimeout()
if (settingState.setting['player.togglePlayMethod'] == 'random' && !playMusicInfo.isTempPlay) addPlayedList(playMusicInfo as LX.Player.PlayMusicInfo)
debouncePlay(musicInfo)
}
/**
* 播放列表内歌曲
* @param listId 列表id
* @param index 播放的歌曲位置
*/
export const playList = async(listId: string, index: number) => {
await pause()
const prevListId = playerState.playInfo.playerListId
setPlayListId(listId)
setPlayMusicInfo(listId, getList(listId)[index])
if (settingState.setting['player.isAutoCleanPlayedList'] || prevListId != listId) clearPlayedList()
clearTempPlayeList()
await handlePlay()
}
const handleToggleStop = async() => {
await stop()
setTimeout(() => {
setPlayMusicInfo(null, null)
})
}
/**
* 下一曲
* @param isAutoToggle 是否自动切换
* @returns
*/
export const playNext = async(isAutoToggle = false): Promise<void> => {
if (playerState.tempPlayList.length) { // 如果稍后播放列表存在歌曲则直接播放改列表的歌曲
const playMusicInfo = playerState.tempPlayList[0]
removeTempPlayList(0)
await pause()
setPlayMusicInfo(playMusicInfo.listId, playMusicInfo.musicInfo, playMusicInfo.isTempPlay)
await handlePlay()
return
}
const playMusicInfo = playerState.playMusicInfo
const playInfo = playerState.playInfo
if (playMusicInfo.musicInfo == null) return handleToggleStop()
// console.log(playInfo.playerListId)
const currentListId = playInfo.playerListId
if (!currentListId) return handleToggleStop()
const currentList = getList(currentListId)
const playedList = playerState.playedList
if (playedList.length) { // 移除已播放列表内不存在原列表的歌曲
let currentId: string
if (playMusicInfo.isTempPlay) {
const musicInfo = currentList[playInfo.playerPlayIndex]
if (musicInfo) currentId = musicInfo.id
} else {
currentId = playMusicInfo.musicInfo.id
}
// 从已播放列表移除播放列表已删除的歌曲
let index
for (index = playedList.findIndex(m => m.musicInfo.id === currentId) + 1; index < playedList.length; index++) {
const playMusicInfo = playedList[index]
const currentId = playMusicInfo.musicInfo.id
if (playMusicInfo.listId == currentListId && !currentList.some(m => m.id === currentId)) {
removePlayedList(index)
continue
}
break
}
if (index < playedList.length) {
const playMusicInfo = playedList[index]
await pause()
setPlayMusicInfo(playMusicInfo.listId, playMusicInfo.musicInfo, playMusicInfo.isTempPlay)
await handlePlay()
return
}
}
// const isCheckFile = findNum > 2 // 针对下载列表,如果超过两次都碰到无效歌曲,则过滤整个列表内的无效歌曲
let { filteredList, playerIndex } = await filterList({ // 过滤已播放歌曲
listId: currentListId,
list: currentList,
playedList,
playerMusicInfo: currentList[playInfo.playerPlayIndex],
isNext: true,
})
if (!filteredList.length) return handleToggleStop()
// let currentIndex: number = filteredList.indexOf(currentList[playInfo.playerPlayIndex])
if (playerIndex == -1 && filteredList.length) playerIndex = 0
let nextIndex = playerIndex
let togglePlayMethod = settingState.setting['player.togglePlayMethod']
if (!isAutoToggle) {
switch (togglePlayMethod) {
case 'list':
case 'singleLoop':
case 'none':
togglePlayMethod = 'listLoop'
}
}
switch (togglePlayMethod) {
case 'listLoop':
nextIndex = playerIndex === filteredList.length - 1 ? 0 : playerIndex + 1
break
case 'random':
nextIndex = getRandom(0, filteredList.length)
break
case 'list':
nextIndex = playerIndex === filteredList.length - 1 ? -1 : playerIndex + 1
break
case 'singleLoop':
break
default:
nextIndex = -1
return
}
if (nextIndex < 0) return
const nextPlayMusicInfo = {
musicInfo: filteredList[nextIndex],
listId: currentListId,
isTempPlay: false,
}
await pause()
setPlayMusicInfo(nextPlayMusicInfo.listId, nextPlayMusicInfo.musicInfo)
await handlePlay()
}
/**
* 上一曲
*/
export const playPrev = async(isAutoToggle = false): Promise<void> => {
const playMusicInfo = playerState.playMusicInfo
if (playMusicInfo.musicInfo == null) return handleToggleStop()
const playInfo = playerState.playInfo
const currentListId = playInfo.playerListId
if (!currentListId) return handleToggleStop()
const currentList = getList(currentListId)
const playedList = playerState.playedList
if (playedList.length) {
let currentId: string
if (playMusicInfo.isTempPlay) {
const musicInfo = currentList[playInfo.playerPlayIndex]
if (musicInfo) currentId = musicInfo.id
} else {
currentId = playMusicInfo.musicInfo.id
}
// 从已播放列表移除播放列表已删除的歌曲
let index
for (index = playedList.findIndex(m => m.musicInfo.id === currentId) - 1; index > -1; index--) {
const playMusicInfo = playedList[index]
const currentId = playMusicInfo.musicInfo.id
if (playMusicInfo.listId == currentListId && !currentList.some(m => m.id === currentId)) {
removePlayedList(index)
continue
}
break
}
if (index > -1) {
const playMusicInfo = playedList[index]
await pause()
setPlayMusicInfo(playMusicInfo.listId, playMusicInfo.musicInfo, playMusicInfo.isTempPlay)
await handlePlay()
return
}
}
// const isCheckFile = findNum > 2
let { filteredList, playerIndex } = await filterList({ // 过滤已播放歌曲
listId: currentListId,
list: currentList,
playedList,
playerMusicInfo: currentList[playInfo.playerPlayIndex],
isNext: false,
})
if (!filteredList.length) return handleToggleStop()
// let currentIndex = filteredList.indexOf(currentList[playInfo.playerPlayIndex])
if (playerIndex == -1 && filteredList.length) playerIndex = 0
let nextIndex = playerIndex
if (!playMusicInfo.isTempPlay) {
let togglePlayMethod = settingState.setting['player.togglePlayMethod']
if (!isAutoToggle) {
switch (togglePlayMethod) {
case 'list':
case 'singleLoop':
case 'none':
togglePlayMethod = 'listLoop'
}
}
switch (togglePlayMethod) {
case 'random':
nextIndex = getRandom(0, filteredList.length)
break
case 'listLoop':
case 'list':
nextIndex = playerIndex === 0 ? filteredList.length - 1 : playerIndex - 1
break
case 'singleLoop':
break
default:
nextIndex = -1
return
}
if (nextIndex < 0) return
}
const nextPlayMusicInfo = {
musicInfo: filteredList[nextIndex],
listId: currentListId,
isTempPlay: false,
}
await pause()
setPlayMusicInfo(nextPlayMusicInfo.listId, nextPlayMusicInfo.musicInfo)
await handlePlay()
}
/**
* 恢复播放
*/
export const play = () => {
if (playerState.playMusicInfo.musicInfo == null) return
if (isEmpty()) {
if (createGettingUrlId(playerState.playMusicInfo.musicInfo) != global.lx.gettingUrlId) setMusicUrl(playerState.playMusicInfo.musicInfo)
return
}
void setPlay()
}
/**
* 暂停播放
*/
export const pause = async() => {
await setPause()
}
/**
* 停止播放
*/
export const stop = async() => {
await setStop()
setTimeout(() => {
global.app_event.stop()
})
}
/**
* 播放、暂停播放切换
*/
export const togglePlay = () => {
global.lx.isPlayedStop &&= false
if (playerState.isPlay) {
void pause()
} else {
play()
}
}