import { NoClientError } from '@app/models/error' import { Song } from '@app/models/library' import { Progress, QueueContextType, TrackExt } from '@app/models/trackplayer' import PromiseQueue from '@app/util/PromiseQueue' import produce from 'immer' import TrackPlayer, { PlayerOptions, RepeatMode, State } from 'react-native-track-player' import { GetStore, SetStore } from './store' export type TrackPlayerSlice = { queueName?: string setQueueName: (name?: string) => void queueContextType?: QueueContextType setQueueContextType: (queueContextType?: QueueContextType) => void queueContextId?: string setQueueContextId: (queueContextId?: string) => void shuffleOrder?: number[] toggleShuffle: () => Promise repeatMode: RepeatMode toggleRepeatMode: () => Promise playerState: State setPlayerState: (playerState: State) => void duckPaused: boolean setDuckPaused: (duckPaused: boolean) => void currentTrack?: TrackExt currentTrackIdx?: number setCurrentTrackIdx: (idx?: number) => void queue: TrackExt[] setQueue: ( songs: Song[], name: string, contextType: QueueContextType, contextId: string, playTrack?: number, shuffle?: boolean, ) => Promise progress: Progress setProgress: (progress: Progress) => void scrobbleTrack: (id: string) => Promise netState: 'mobile' | 'wifi' setNetState: (netState: 'mobile' | 'wifi') => Promise rebuildQueue: (forcePlay?: boolean) => Promise buildStreamUri: (id: string) => string resetTrackPlayerState: () => void getPlayerOptions: () => PlayerOptions } export const trackPlayerCommands = new PromiseQueue(1) export const createTrackPlayerSlice = (set: SetStore, get: GetStore): TrackPlayerSlice => ({ queueName: undefined, setQueueName: name => set(state => { state.queueName = name }), queueContextType: undefined, setQueueContextType: queueContextType => set(state => { state.queueContextType = queueContextType }), queueContextId: undefined, setQueueContextId: queueContextId => set(state => { state.queueContextId = queueContextId }), shuffleOrder: undefined, toggleShuffle: async () => { return trackPlayerCommands.enqueue(async () => { const queue = await getQueue() const current = await getCurrentTrack() const queueShuffleOrder = get().shuffleOrder await TrackPlayer.remove(queue.map((_t, i) => i).filter(i => i !== current)) if (queueShuffleOrder === undefined) { let { tracks, shuffleOrder } = shuffleTracks(queue, current) if (tracks.length > 0) { tracks = tracks.slice(1) } await TrackPlayer.add(tracks) set(state => { state.shuffleOrder = shuffleOrder }) } else { const tracks = unshuffleTracks(queue, queueShuffleOrder) if (current !== undefined) { const shuffledCurrent = queueShuffleOrder[current] const tracks1 = tracks.slice(0, shuffledCurrent) const tracks2 = tracks.slice(shuffledCurrent + 1) await TrackPlayer.add(tracks2) await TrackPlayer.add(tracks1, 0) } else { await TrackPlayer.add(tracks) } set(state => { state.shuffleOrder = undefined }) } const newQueue = await getQueue() const newCurrentTrackIdx = await getCurrentTrack() set(state => { state.queue = newQueue }) get().setCurrentTrackIdx(newCurrentTrackIdx) }) }, repeatMode: RepeatMode.Off, toggleRepeatMode: async () => { return trackPlayerCommands.enqueue(async () => { const repeatMode = await getRepeatMode() let nextMode = RepeatMode.Off switch (repeatMode) { case RepeatMode.Off: nextMode = RepeatMode.Queue break case RepeatMode.Queue: nextMode = RepeatMode.Track break default: nextMode = RepeatMode.Off break } await TrackPlayer.setRepeatMode(nextMode) set(state => { state.repeatMode = nextMode }) }) }, playerState: State.None, setPlayerState: playerState => set(state => { state.playerState = playerState }), currentTrack: undefined, currentTrackIdx: undefined, setCurrentTrackIdx: idx => { set( produce(state => { state.currentTrackIdx = idx state.currentTrack = idx !== undefined ? state.queue[idx] : undefined }), ) }, duckPaused: false, setDuckPaused: duckPaused => set(state => { state.duckPaused = duckPaused }), queue: [], setQueue: async (songs, name, contextType, contextId, playTrack, shuffle) => { return trackPlayerCommands.enqueue(async () => { const shuffled = shuffle !== undefined ? shuffle : !!get().shuffleOrder await TrackPlayer.setupPlayer(get().getPlayerOptions()) await TrackPlayer.reset() if (songs.length === 0) { return } let queue = await get().mapSongstoTrackExts(songs) if (shuffled) { const { tracks, shuffleOrder } = shuffleTracks(queue, playTrack) set(state => { state.shuffleOrder = shuffleOrder }) queue = tracks playTrack = 0 } else { set(state => { state.shuffleOrder = undefined }) } playTrack = playTrack || 0 try { set(state => { state.queue = queue state.queueName = name state.queueContextType = contextType state.queueContextId = contextId }) get().setCurrentTrackIdx(playTrack) if (playTrack === 0) { await TrackPlayer.add(queue) } else { const tracks1 = queue.slice(0, playTrack) const tracks2 = queue.slice(playTrack) await TrackPlayer.add(tracks2) await TrackPlayer.add(tracks1, 0) } await TrackPlayer.play() } catch { get().resetTrackPlayerState() await TrackPlayer.reset() } }) }, progress: { position: 0, duration: 0, buffered: 0 }, setProgress: progress => set(state => { state.progress = progress }), scrobbleTrack: async id => { const client = get().client if (!client) { return } if (!get().settings.scrobble) { return } try { await client.scrobble({ id }) } catch {} }, netState: 'mobile', setNetState: async netState => { if (netState === get().netState) { return } set(state => { state.netState = netState }) get().rebuildQueue() }, rebuildQueue: async forcePlay => { return trackPlayerCommands.enqueue(async () => { const queue = await getQueue() if (!queue.length) { return } const currentTrack = await getCurrentTrack() const playerState = await getPlayerState() const position = (await TrackPlayer.getPosition()) || 0 const queueName = get().queueName const queueContextId = get().queueContextId const queueContextType = get().queueContextType await TrackPlayer.reset() await TrackPlayer.setupPlayer(get().getPlayerOptions()) try { for (const track of queue) { track.url = get().buildStreamUri(track.id) } } catch { return } set(state => { state.queue = queue state.queueName = queueName state.queueContextType = queueContextType state.queueContextId = queueContextId }) get().setCurrentTrackIdx(currentTrack) await TrackPlayer.add(queue) if (currentTrack) { await TrackPlayer.skip(currentTrack) } await TrackPlayer.seekTo(position) if (playerState === State.Playing || forcePlay) { await TrackPlayer.play() } }) }, buildStreamUri: id => { const client = get().client if (!client) { throw new NoClientError() } return client.streamUri({ id, estimateContentLength: true, maxBitRate: get().netState === 'mobile' ? get().settings.maxBitrateMobile : get().settings.maxBitrateWifi, }) }, resetTrackPlayerState: () => { set(state => { state.queueName = undefined state.queueContextType = undefined state.queueContextId = undefined state.shuffleOrder = undefined state.repeatMode = RepeatMode.Off state.playerState = State.None state.currentTrack = undefined state.currentTrackIdx = undefined state.queue = [] state.progress = { position: 0, duration: 0, buffered: 0 } }) }, getPlayerOptions: () => { return { minBuffer: get().settings.minBuffer, playBuffer: get().settings.minBuffer / 2, maxBuffer: get().settings.maxBuffer, } }, }) export const getQueue = async (): Promise => { return ((await TrackPlayer.getQueue()) as TrackExt[]) || [] } export const getCurrentTrack = async (): Promise => { const current = await TrackPlayer.getCurrentTrack() return typeof current === 'number' ? current : undefined } export const getPlayerState = async (): Promise => { return (await TrackPlayer.getState()) || State.None } export const getRepeatMode = async (): Promise => { return (await TrackPlayer.getRepeatMode()) || RepeatMode.Off } function shuffleTracks(tracks: TrackExt[], firstTrack?: number) { if (tracks.length === 0) { return { tracks, shuffleOrder: [] } } const trackIndexes = tracks.map((_t, i) => i) let shuffleOrder: number[] = [] for (let i = trackIndexes.length; i--; i > 0) { const randi = Math.floor(Math.random() * (i + 1)) shuffleOrder.push(trackIndexes.splice(randi, 1)[0]) } if (firstTrack !== undefined) { shuffleOrder.splice(shuffleOrder.indexOf(firstTrack), 1) shuffleOrder = [firstTrack, ...shuffleOrder] } tracks = shuffleOrder.map(i => tracks[i]) return { tracks, shuffleOrder } } function unshuffleTracks(tracks: TrackExt[], shuffleOrder: number[]): TrackExt[] { if (tracks.length === 0 || shuffleOrder.length === 0) { return tracks } return shuffleOrder.map((_v, i) => tracks[shuffleOrder.indexOf(i)]) }