mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-29 01:19:28 +01:00
queue actions for player so they don't overlap
This commit is contained in:
parent
eb4199de37
commit
b2d6840901
@ -2,8 +2,14 @@ import { useAppState } from '@react-native-community/hooks'
|
|||||||
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
|
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import { View } from 'react-native'
|
import { View } from 'react-native'
|
||||||
import TrackPlayer, { Event, useTrackPlayerEvents } from 'react-native-track-player'
|
import TrackPlayer, { Event, State, useTrackPlayerEvents } from 'react-native-track-player'
|
||||||
import { currentTrackAtom, getQueue, getTrack, playerStateAtom, queueWriteAtom } from '../state/trackplayer'
|
import {
|
||||||
|
currentTrackAtom,
|
||||||
|
playerStateAtom,
|
||||||
|
queueWriteAtom,
|
||||||
|
useRefreshCurrentTrack,
|
||||||
|
useRefreshQueue,
|
||||||
|
} from '../state/trackplayer'
|
||||||
|
|
||||||
const AppActiveResponder: React.FC<{
|
const AppActiveResponder: React.FC<{
|
||||||
update: () => void
|
update: () => void
|
||||||
@ -32,23 +38,16 @@ const TrackPlayerEventResponder: React.FC<{
|
|||||||
|
|
||||||
const CurrentTrackState = () => {
|
const CurrentTrackState = () => {
|
||||||
const setCurrentTrack = useUpdateAtom(currentTrackAtom)
|
const setCurrentTrack = useUpdateAtom(currentTrackAtom)
|
||||||
|
const refreshCurrentTrack = useRefreshCurrentTrack()
|
||||||
|
|
||||||
const update = async (payload?: Payload) => {
|
const update = async (payload?: Payload) => {
|
||||||
if (payload?.type === Event.PlaybackQueueEnded && 'track' in payload) {
|
const queueEnded = payload?.type === Event.PlaybackQueueEnded && 'track' in payload
|
||||||
|
const remoteStop = payload?.type === Event.RemoteStop
|
||||||
|
if (queueEnded || remoteStop) {
|
||||||
setCurrentTrack(undefined)
|
setCurrentTrack(undefined)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
await refreshCurrentTrack()
|
||||||
const index = await TrackPlayer.getCurrentTrack()
|
|
||||||
if (index !== null && index >= 0) {
|
|
||||||
const track = await getTrack(index)
|
|
||||||
if (track !== null) {
|
|
||||||
setCurrentTrack(track)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setCurrentTrack(undefined)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -70,31 +69,36 @@ const PlayerState = () => {
|
|||||||
const setPlayerState = useUpdateAtom(playerStateAtom)
|
const setPlayerState = useUpdateAtom(playerStateAtom)
|
||||||
|
|
||||||
const update = async (payload?: Payload) => {
|
const update = async (payload?: Payload) => {
|
||||||
|
if (payload?.type === Event.RemoteStop) {
|
||||||
|
setPlayerState(State.None)
|
||||||
|
return
|
||||||
|
}
|
||||||
setPlayerState(payload?.state || (await TrackPlayer.getState()))
|
setPlayerState(payload?.state || (await TrackPlayer.getState()))
|
||||||
}
|
}
|
||||||
|
|
||||||
return <TrackPlayerEventResponder events={[Event.PlaybackState]} update={update} />
|
return <TrackPlayerEventResponder events={[Event.PlaybackState, Event.RemoteStop]} update={update} />
|
||||||
}
|
}
|
||||||
|
|
||||||
const QueueState = () => {
|
const QueueState = () => {
|
||||||
const setQueue = useUpdateAtom(queueWriteAtom)
|
const setQueue = useUpdateAtom(queueWriteAtom)
|
||||||
|
const refreshQueue = useRefreshQueue()
|
||||||
|
|
||||||
const update = async (payload?: Payload) => {
|
const update = async (payload?: Payload) => {
|
||||||
if (payload) {
|
if (payload) {
|
||||||
setQueue([])
|
setQueue([])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setQueue(await getQueue())
|
await refreshQueue()
|
||||||
}
|
}
|
||||||
|
|
||||||
return <TrackPlayerEventResponder events={[Event.RemoteStop]} update={update} />
|
return <TrackPlayerEventResponder events={[Event.RemoteStop]} update={update} />
|
||||||
}
|
}
|
||||||
|
|
||||||
const Debug = () => {
|
const Debug = () => {
|
||||||
const value = useAtomValue(queueWriteAtom)
|
const value = useAtomValue(currentTrackAtom)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log(value.map(t => t.title))
|
// ToastAndroid.show(value?.title || 'undefined', 1)
|
||||||
}, [value])
|
}, [value])
|
||||||
|
|
||||||
return <></>
|
return <></>
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import TrackPlayer, { State, Track } from 'react-native-track-player'
|
|||||||
import equal from 'fast-deep-equal'
|
import equal from 'fast-deep-equal'
|
||||||
import { useUpdateAtom } from 'jotai/utils'
|
import { useUpdateAtom } from 'jotai/utils'
|
||||||
import { Song } from '../models/music'
|
import { Song } from '../models/music'
|
||||||
|
import { PromiseQueue } from '../util'
|
||||||
|
|
||||||
type TrackExt = Track & {
|
type TrackExt = Track & {
|
||||||
id: string
|
id: string
|
||||||
@ -36,7 +37,7 @@ export const queueAtom = atom<TrackExt[]>(get => get(_queue))
|
|||||||
export const queueWriteAtom = atom<TrackExt[], TrackExt[]>(
|
export const queueWriteAtom = atom<TrackExt[], TrackExt[]>(
|
||||||
get => get(_queue),
|
get => get(_queue),
|
||||||
(get, set, update) => {
|
(get, set, update) => {
|
||||||
if (get(_queue) !== update) {
|
if (!equal(get(_queue), update)) {
|
||||||
set(_queue, update)
|
set(_queue, update)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -50,19 +51,44 @@ export const queueNameAtom = atom<string | undefined>(get => {
|
|||||||
return undefined
|
return undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getQueue = async (): Promise<TrackExt[]> => {
|
const trackPlayerCommands = new PromiseQueue(1)
|
||||||
|
|
||||||
|
const getQueue = async (): Promise<TrackExt[]> => {
|
||||||
return ((await TrackPlayer.getQueue()) as TrackExt[]) || []
|
return ((await TrackPlayer.getQueue()) as TrackExt[]) || []
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getTrack = async (index: number): Promise<TrackExt> => {
|
const getTrack = async (index: number): Promise<TrackExt> => {
|
||||||
return ((await TrackPlayer.getTrack(index)) as TrackExt) || undefined
|
return ((await TrackPlayer.getTrack(index)) as TrackExt) || undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useRefreshQueue = () => {
|
||||||
|
const setQueue = useUpdateAtom(queueWriteAtom)
|
||||||
|
|
||||||
|
return () =>
|
||||||
|
trackPlayerCommands.enqueue(async () => {
|
||||||
|
setQueue(await getQueue())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useRefreshCurrentTrack = () => {
|
||||||
|
const setCurrentTrack = useUpdateAtom(currentTrackAtom)
|
||||||
|
|
||||||
|
return () =>
|
||||||
|
trackPlayerCommands.enqueue(async () => {
|
||||||
|
const index = await TrackPlayer.getCurrentTrack()
|
||||||
|
if (typeof index === 'number' && index >= 0) {
|
||||||
|
setCurrentTrack(await getTrack(index))
|
||||||
|
} else {
|
||||||
|
setCurrentTrack(undefined)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export const usePrevious = () => {
|
export const usePrevious = () => {
|
||||||
const setCurrentTrack = useUpdateAtom(currentTrackAtom)
|
const setCurrentTrack = useUpdateAtom(currentTrackAtom)
|
||||||
|
|
||||||
return async () => {
|
return () =>
|
||||||
try {
|
trackPlayerCommands.enqueue(async () => {
|
||||||
const [current, queue] = await Promise.all([await TrackPlayer.getCurrentTrack(), await getQueue()])
|
const [current, queue] = await Promise.all([await TrackPlayer.getCurrentTrack(), await getQueue()])
|
||||||
if (current > 0) {
|
if (current > 0) {
|
||||||
await TrackPlayer.skipToPrevious()
|
await TrackPlayer.skipToPrevious()
|
||||||
@ -71,15 +97,14 @@ export const usePrevious = () => {
|
|||||||
await TrackPlayer.seekTo(0)
|
await TrackPlayer.seekTo(0)
|
||||||
}
|
}
|
||||||
await TrackPlayer.play()
|
await TrackPlayer.play()
|
||||||
} catch {}
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useNext = () => {
|
export const useNext = () => {
|
||||||
const setCurrentTrack = useUpdateAtom(currentTrackAtom)
|
const setCurrentTrack = useUpdateAtom(currentTrackAtom)
|
||||||
|
|
||||||
return async () => {
|
return () =>
|
||||||
try {
|
trackPlayerCommands.enqueue(async () => {
|
||||||
const [current, queue] = await Promise.all([await TrackPlayer.getCurrentTrack(), await getQueue()])
|
const [current, queue] = await Promise.all([await TrackPlayer.getCurrentTrack(), await getQueue()])
|
||||||
if (current >= queue.length - 1) {
|
if (current >= queue.length - 1) {
|
||||||
await TrackPlayer.skip(0)
|
await TrackPlayer.skip(0)
|
||||||
@ -90,64 +115,68 @@ export const useNext = () => {
|
|||||||
setCurrentTrack(queue[current + 1])
|
setCurrentTrack(queue[current + 1])
|
||||||
await TrackPlayer.play()
|
await TrackPlayer.play()
|
||||||
}
|
}
|
||||||
} catch {}
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAdd = () => {
|
export const useAdd = () => {
|
||||||
const setQueue = useUpdateAtom(queueWriteAtom)
|
const setQueue = useUpdateAtom(queueWriteAtom)
|
||||||
const setCurrentTrack = useUpdateAtom(currentTrackAtom)
|
const setCurrentTrack = useUpdateAtom(currentTrackAtom)
|
||||||
|
|
||||||
return async (tracks: TrackExt | TrackExt[], insertBeforeindex?: number) => {
|
return (tracks: TrackExt | TrackExt[], insertBeforeindex?: number) =>
|
||||||
await TrackPlayer.add(tracks, insertBeforeindex)
|
trackPlayerCommands.enqueue(async () => {
|
||||||
|
await TrackPlayer.add(tracks, insertBeforeindex)
|
||||||
|
|
||||||
const queue = await getQueue()
|
const queue = await getQueue()
|
||||||
setQueue(queue)
|
setQueue(queue)
|
||||||
setCurrentTrack(queue.length > 0 ? queue[await TrackPlayer.getCurrentTrack()] : undefined)
|
setCurrentTrack(queue.length > 0 ? queue[await TrackPlayer.getCurrentTrack()] : undefined)
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useReset = () => {
|
export const useReset = (enqueue = true) => {
|
||||||
const setQueue = useUpdateAtom(queueWriteAtom)
|
const setQueue = useUpdateAtom(queueWriteAtom)
|
||||||
const setCurrentTrack = useUpdateAtom(currentTrackAtom)
|
const setCurrentTrack = useUpdateAtom(currentTrackAtom)
|
||||||
|
|
||||||
return async () => {
|
const reset = async () => {
|
||||||
await TrackPlayer.reset()
|
await TrackPlayer.reset()
|
||||||
setQueue([])
|
setQueue([])
|
||||||
setCurrentTrack(undefined)
|
setCurrentTrack(undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return enqueue ? () => trackPlayerCommands.enqueue(reset) : reset
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useSetQueue = () => {
|
export const useSetQueue = () => {
|
||||||
const setCurrentTrack = useUpdateAtom(currentTrackAtom)
|
const setCurrentTrack = useUpdateAtom(currentTrackAtom)
|
||||||
const setQueue = useUpdateAtom(queueWriteAtom)
|
const setQueue = useUpdateAtom(queueWriteAtom)
|
||||||
|
const reset = useReset(false)
|
||||||
|
|
||||||
return async (songs: Song[], name: string, playId?: string) => {
|
return async (songs: Song[], name: string, playId?: string) =>
|
||||||
await TrackPlayer.reset()
|
trackPlayerCommands.enqueue(async () => {
|
||||||
const tracks = songs.map(s => mapSongToTrack(s, name))
|
await reset()
|
||||||
|
const tracks = songs.map(s => mapSongToTrack(s, name))
|
||||||
|
|
||||||
if (playId) {
|
if (playId) {
|
||||||
setCurrentTrack(tracks.find(t => t.id === playId))
|
setCurrentTrack(tracks.find(t => t.id === playId))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!playId) {
|
if (!playId) {
|
||||||
await TrackPlayer.add(tracks)
|
await TrackPlayer.add(tracks)
|
||||||
} else if (playId === tracks[0].id) {
|
} else if (playId === tracks[0].id) {
|
||||||
await TrackPlayer.add(tracks)
|
await TrackPlayer.add(tracks)
|
||||||
await TrackPlayer.play()
|
await TrackPlayer.play()
|
||||||
} else {
|
} else {
|
||||||
const playIndex = tracks.findIndex(t => t.id === playId)
|
const playIndex = tracks.findIndex(t => t.id === playId)
|
||||||
const tracks1 = tracks.slice(0, playIndex)
|
const tracks1 = tracks.slice(0, playIndex)
|
||||||
const tracks2 = tracks.slice(playIndex)
|
const tracks2 = tracks.slice(playIndex)
|
||||||
|
|
||||||
await TrackPlayer.add(tracks2)
|
await TrackPlayer.add(tracks2)
|
||||||
await TrackPlayer.play()
|
await TrackPlayer.play()
|
||||||
|
|
||||||
await TrackPlayer.add(tracks1, 0)
|
await TrackPlayer.add(tracks1, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
setQueue(await getQueue())
|
setQueue(await getQueue())
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapSongToTrack(song: Song, queueName: string): TrackExt {
|
function mapSongToTrack(song: Song, queueName: string): TrackExt {
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import {
|
|||||||
} from './responses'
|
} from './responses'
|
||||||
import { Server } from '../models/settings'
|
import { Server } from '../models/settings'
|
||||||
import paths from '../paths'
|
import paths from '../paths'
|
||||||
|
import { PromiseQueue } from '../util'
|
||||||
|
|
||||||
export class SubsonicApiError extends Error {
|
export class SubsonicApiError extends Error {
|
||||||
method: string
|
method: string
|
||||||
@ -42,37 +43,7 @@ export class SubsonicApiError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type QueuePromise = () => Promise<any>
|
const downloadQueue = new PromiseQueue(1)
|
||||||
|
|
||||||
class Queue {
|
|
||||||
maxSimultaneously: number
|
|
||||||
|
|
||||||
private active = 0
|
|
||||||
private queue: QueuePromise[] = []
|
|
||||||
|
|
||||||
constructor(maxSimultaneously = 1) {
|
|
||||||
this.maxSimultaneously = maxSimultaneously
|
|
||||||
}
|
|
||||||
|
|
||||||
async enqueue(func: QueuePromise) {
|
|
||||||
if (++this.active > this.maxSimultaneously) {
|
|
||||||
await new Promise(resolve => this.queue.push(resolve as QueuePromise))
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await func()
|
|
||||||
} catch (err) {
|
|
||||||
throw err
|
|
||||||
} finally {
|
|
||||||
this.active--
|
|
||||||
if (this.queue.length) {
|
|
||||||
this.queue.shift()?.()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const downloadQueue = new Queue(1)
|
|
||||||
|
|
||||||
export class SubsonicApiClient {
|
export class SubsonicApiClient {
|
||||||
address: string
|
address: string
|
||||||
|
|||||||
30
src/util.ts
30
src/util.ts
@ -9,3 +9,33 @@ export function formatDuration(seconds: number): string {
|
|||||||
}
|
}
|
||||||
return time
|
return time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type QueuedPromise = () => Promise<any>
|
||||||
|
|
||||||
|
export class PromiseQueue {
|
||||||
|
maxSimultaneously: number
|
||||||
|
|
||||||
|
private active = 0
|
||||||
|
private queue: QueuedPromise[] = []
|
||||||
|
|
||||||
|
constructor(maxSimultaneously = 1) {
|
||||||
|
this.maxSimultaneously = maxSimultaneously
|
||||||
|
}
|
||||||
|
|
||||||
|
async enqueue<T>(func: () => Promise<T>) {
|
||||||
|
if (++this.active > this.maxSimultaneously) {
|
||||||
|
await new Promise(resolve => this.queue.push(resolve as QueuedPromise))
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await func()
|
||||||
|
} catch (err) {
|
||||||
|
throw err
|
||||||
|
} finally {
|
||||||
|
this.active--
|
||||||
|
if (this.queue.length) {
|
||||||
|
this.queue.shift()?.()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user