import { v4 as uuidv4 } from 'uuid' import subsonic from '../subsonic' import { decisionService } from '../transcode' 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, PLAYER_REFRESH_QUEUE, } 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 makeMusicSrc = (trackId) => decisionService.getProfile() ? () => decisionService .resolveStreamUrl(trackId) .catch(() => subsonic.streamUrl(trackId)) : subsonic.streamUrl(trackId) 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: makeMusicSrc(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, // Keep clear and playIndex alive so the music player can still // pick up a pending track selection set by PLAYER_PLAY_TRACKS. // They will be consumed by the next PLAYER_CURRENT dispatch. clear: state.playIndex != null ? state.clear : false, playIndex: state.playIndex != null ? state.playIndex : undefined, } } const reduceCurrent = (state, { data }) => { const current = data.ended ? {} : data const savedPlayIndex = state.queue.findIndex( (item) => item.uuid === current.uuid, ) // When a track selection is pending (playIndex is set), keep it alive // until the music player confirms it actually switched to the requested // track. Without this, a premature onAudioPlay callback for the // still-playing old track would overwrite the pending selection. const pending = state.playIndex != null && savedPlayIndex !== state.playIndex return { ...state, current, playIndex: pending ? state.playIndex : undefined, clear: pending ? state.clear : false, savedPlayIndex: pending ? state.savedPlayIndex : 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) case PLAYER_REFRESH_QUEUE: { const resolvedUrls = payload.data || {} return { ...previousState, queue: previousState.queue.map((item) => ({ ...item, musicSrc: item.isRadio ? item.musicSrc : resolvedUrls[item.trackId] || subsonic.streamUrl(item.trackId), })), clear: true, autoPlay: false, playIndex: previousState.savedPlayIndex >= 0 ? previousState.savedPlayIndex : 0, } } default: return previousState } }