quintodrome/ui/src/reducers/playerReducer.js
Alanna a20d56c137
fix(ui): prevent "Play Next" restarting play at top of queue (#5049)
Set playIndex when rebuilding the queue in reducePlayNext so the music
player library knows which track is currently playing. Without this, the
library's loadNewAudioLists defaults playIndex to 0, causing playback to
restart from the top of the queue on rapid "Play Next" actions.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 08:34:24 -05:00

216 lines
4.7 KiB
JavaScript

import { v4 as uuidv4 } from 'uuid'
import subsonic from '../subsonic'
import {
PLAYER_ADD_TRACKS,
PLAYER_CLEAR_QUEUE,
PLAYER_CURRENT,
PLAYER_PLAY_NEXT,
PLAYER_PLAY_TRACKS,
PLAYER_SET_TRACK,
PLAYER_SET_VOLUME,
PLAYER_SYNC_QUEUE,
PLAYER_SET_MODE,
} from '../actions'
import config from '../config'
const initialState = {
queue: [],
current: {},
clear: false,
volume: config.defaultUIVolume / 100,
savedPlayIndex: 0,
}
const pad = (value) => {
const str = value.toString()
if (str.length === 1) {
return `0${str}`
} else {
return str
}
}
const mapToAudioLists = (item) => {
// If item comes from a playlist, trackId is mediaFileId
const trackId = item.mediaFileId || item.id
if (item.isRadio) {
return {
trackId,
uuid: uuidv4(),
name: item.name,
song: item,
musicSrc: item.streamUrl,
cover: item.cover,
isRadio: true,
}
}
const { lyrics } = item
let lyricText = ''
if (lyrics) {
const structured = JSON.parse(lyrics)
for (const structuredLyric of structured) {
if (structuredLyric.synced) {
for (const line of structuredLyric.line) {
let time = Math.floor(line.start / 10)
const ms = time % 100
time = Math.floor(time / 100)
const sec = time % 60
time = Math.floor(time / 60)
const min = time % 60
ms.toString()
lyricText += `[${pad(min)}:${pad(sec)}.${pad(ms)}] ${line.value}\n`
}
}
}
}
return {
trackId,
uuid: uuidv4(),
song: item,
name: item.title,
lyric: lyricText,
singer: item.artist,
duration: item.duration,
musicSrc: subsonic.streamUrl(trackId),
cover: subsonic.getCoverArtUrl(
{
id: trackId,
updatedAt: item.updatedAt,
album: item.album,
},
300,
),
}
}
const reduceClearQueue = () => ({ ...initialState, clear: true })
const reducePlayTracks = (state, { data, id }) => {
let playIndex = 0
const queue = Object.keys(data).map((key, idx) => {
if (key === id) {
playIndex = idx
}
return mapToAudioLists(data[key])
})
return {
...state,
queue,
playIndex,
clear: true,
}
}
const reduceSetTrack = (state, { data }) => {
return {
...state,
queue: [mapToAudioLists(data)],
playIndex: 0,
clear: true,
}
}
const reduceAddTracks = (state, { data }) => {
const queue = state.queue
Object.keys(data).forEach((id) => {
queue.push(mapToAudioLists(data[id]))
})
return { ...state, queue, clear: false }
}
const reducePlayNext = (state, { data }) => {
const newQueue = []
const current = state.current || {}
let foundPos = false
let currentIndex = 0
state.queue.forEach((item) => {
newQueue.push(item)
if (item.uuid === current.uuid) {
foundPos = true
currentIndex = newQueue.length - 1
Object.keys(data).forEach((id) => {
newQueue.push(mapToAudioLists(data[id]))
})
}
})
if (!foundPos) {
Object.keys(data).forEach((id) => {
newQueue.push(mapToAudioLists(data[id]))
})
}
return {
...state,
queue: newQueue,
playIndex: foundPos ? currentIndex : undefined,
clear: true,
}
}
const reduceSetVolume = (state, { data: { volume } }) => {
return {
...state,
volume,
}
}
const reduceSyncQueue = (state, { data: { audioInfo, audioLists } }) => {
return {
...state,
queue: audioLists,
clear: false,
playIndex: undefined,
}
}
const reduceCurrent = (state, { data }) => {
const current = data.ended ? {} : data
const savedPlayIndex = state.queue.findIndex(
(item) => item.uuid === current.uuid,
)
return {
...state,
current,
playIndex: undefined,
savedPlayIndex,
volume: data.volume,
}
}
const reduceMode = (state, { data: { mode } }) => {
return {
...state,
mode,
}
}
export const playerReducer = (previousState = initialState, payload) => {
const { type } = payload
switch (type) {
case PLAYER_CLEAR_QUEUE:
return reduceClearQueue()
case PLAYER_PLAY_TRACKS:
return reducePlayTracks(previousState, payload)
case PLAYER_SET_TRACK:
return reduceSetTrack(previousState, payload)
case PLAYER_ADD_TRACKS:
return reduceAddTracks(previousState, payload)
case PLAYER_PLAY_NEXT:
return reducePlayNext(previousState, payload)
case PLAYER_SET_VOLUME:
return reduceSetVolume(previousState, payload)
case PLAYER_SYNC_QUEUE:
return reduceSyncQueue(previousState, payload)
case PLAYER_CURRENT:
return reduceCurrent(previousState, payload)
case PLAYER_SET_MODE:
return reduceMode(previousState, payload)
default:
return previousState
}
}