From 4c043cdd24515f2c6b7c18441f73f1e9d0f1af95 Mon Sep 17 00:00:00 2001 From: Isaac Hunter Date: Sat, 4 Jan 2025 10:15:00 -0500 Subject: [PATCH] refactor out current playing context, create controls hook --- src/App.tsx | 73 +++++++++----- src/apps/admin/pages/Overview.tsx | 6 +- src/apps/admin/pages/SpotifyPlayer.tsx | 15 +-- src/components/audio-player/AudioPlayer.tsx | 13 ++- src/context/CurrentlyPlayingContext.tsx | 104 -------------------- src/context/SpotifyPlayerContext.tsx | 55 ++++++----- src/hooks/index.ts | 1 + src/hooks/use-player-controls.tsx | 34 +++++++ src/lib/spotify.ts | 23 +++-- src/store/jukebox/jbxActions.ts | 29 +++++- src/store/jukebox/jbxSelectors.ts | 5 + src/store/jukebox/jbxSlice.ts | 13 ++- src/types/spotify.d.ts | 6 +- 13 files changed, 195 insertions(+), 182 deletions(-) delete mode 100644 src/context/CurrentlyPlayingContext.tsx create mode 100644 src/hooks/use-player-controls.tsx diff --git a/src/App.tsx b/src/App.tsx index 423d916..fb70355 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import { useCallback, useContext, useEffect } from 'react' +import { useCallback, useContext, useEffect, useState } from 'react' import { useSelector } from 'react-redux' import { Outlet } from 'react-router-dom' import { SPOTIFY_AUTH_CHECK_MS } from './config' @@ -8,22 +8,29 @@ import { SpotifyPlayerProvider, Theme, } from './context' -import { CurrentlyPlayingProvider } from './context/CurrentlyPlayingContext' import { authenticateLink, checkLinkAuth, + doPlayerAction, fetchCurrentlyPlaying, fetchNextTracks, + incrementLiveProgress, selectCurrentJukebox, + selectPlayerState, selectSpotifyAuth, setNextTracks, setPlayerState, updatePlayerState, } from './store' +import { uniqueId } from './utils' export const App = () => { const spotifyAuth = useSelector(selectSpotifyAuth) const currentJukebox = useSelector(selectCurrentJukebox) + const storePlayerState = useSelector(selectPlayerState) + const [initialized, setInitialized] = useState(false) + + const [timer, setTimer] = useState(null) const { emitMessage, @@ -31,15 +38,39 @@ export const App = () => { isConnected: socketIsConnected, } = useContext(SocketContext) + useEffect(() => { + setTimeout(() => { + setInitialized(true) + }, 60 * 1000) + }, []) + /** * ======================== * * Spotify Track State Sync * * ======================== * */ + useEffect(() => { + if (timer) clearInterval(timer) + if (storePlayerState?.is_playing) { + const t = setInterval(() => { + incrementLiveProgress() + }, 1000) + + setTimer(t) + } + + return () => { + if (timer) clearInterval(timer) + } + }, [ + storePlayerState?.current_track, + storePlayerState?.is_playing, + storePlayerState?.progress, + ]) // Triggers when receive spotify credentials from server useEffect(() => { - if (!spotifyAuth) return + if (!spotifyAuth || !initialized) return const timer = setInterval(async () => { await checkLinkAuth() @@ -64,7 +95,7 @@ export const App = () => { }) onEvent('player-action', (data) => { - updatePlayerState(data) + doPlayerAction(data) }) onEvent('track-queue-update', (data) => { @@ -74,24 +105,20 @@ export const App = () => { // Primary function that runs when Spotify Player changes const handlePlayerTrackChange = useCallback( - (state: { - currentTrack: ITrack - position: number - isPlaying: boolean - nextTracks: ITrack[] - changedTracks: boolean - }) => { - const { currentTrack, position, isPlaying, nextTracks, changedTracks } = - state - - emitMessage('player-aux-update', { - jukebox_id: currentJukebox!.id, - current_track: currentTrack, - progress: position, - is_playing: isPlaying, - default_next_tracks: nextTracks, - changed_tracks: changedTracks, + (state?: IPlayerAuxUpdate) => { + if (!state) { + emitMessage('player-aux-update', {}) + return + } + + updatePlayerState({ + ...state, + current_track: state.current_track && { + ...state.current_track, + queue_id: uniqueId(), + }, }) + emitMessage('player-aux-update', state) }, [currentJukebox], ) @@ -104,9 +131,7 @@ export const App = () => { jukebox={currentJukebox} onPlayerStateChange={handlePlayerTrackChange} > - - - + diff --git a/src/apps/admin/pages/Overview.tsx b/src/apps/admin/pages/Overview.tsx index b0b77d0..d2077c3 100644 --- a/src/apps/admin/pages/Overview.tsx +++ b/src/apps/admin/pages/Overview.tsx @@ -3,16 +3,14 @@ import './Overview.scss' import FallbackImg from 'src/assets/img/jukeboxImage.png' import Disk from 'src/assets/svg/Disk.svg?react' -import { useContext } from 'react' import { useSelector } from 'react-redux' import { AudioPlayer, TrackList } from 'src/components' import { TrackActivity } from 'src/components/track-list/TrackActivity' -import { CurrentlyPlayingContext } from 'src/context/CurrentlyPlayingContext' -import { selectNextTracks } from 'src/store/jukebox' +import { selectCurrentTrack, selectNextTracks } from 'src/store/jukebox' export const Overview = () => { const queuedTracks = useSelector(selectNextTracks) - const { currentTrack } = useContext(CurrentlyPlayingContext) + const currentTrack = useSelector(selectCurrentTrack) return ( <> diff --git a/src/apps/admin/pages/SpotifyPlayer.tsx b/src/apps/admin/pages/SpotifyPlayer.tsx index 165b7ff..74d8102 100644 --- a/src/apps/admin/pages/SpotifyPlayer.tsx +++ b/src/apps/admin/pages/SpotifyPlayer.tsx @@ -3,9 +3,12 @@ import { useSelector } from 'react-redux' import { AudioPlayer, Form, FormSelectGroup, FormSubmit } from 'src/components' import { REACT_ENV } from 'src/config' import { SpotifyPlayerContext } from 'src/context' -import { CurrentlyPlayingContext } from 'src/context/CurrentlyPlayingContext' import { authenticateLink } from 'src/store' -import { selectJukeboxLinks } from 'src/store/jukebox' +import { + selectCurrentTrack, + selectJukeboxLinks, + selectNextTracks, +} from 'src/store/jukebox' import { formatDuration } from 'src/utils' import { SpotifyPlayerAccount } from '../components/SpotifyPlayer/SpotifyPlayerAccount' import { SpotifyPlayerDetail } from '../components/SpotifyPlayer/SpotifyPlayerDetail' @@ -14,10 +17,10 @@ import './SpotifyPlayer.scss' export const SpotifyPlayer = () => { const jukeboxLinks = useSelector(selectJukeboxLinks) - const { currentTrack } = useContext(CurrentlyPlayingContext) + const currentTrack = useSelector(selectCurrentTrack) + const nextTracks = useSelector(selectNextTracks) const { - nextTracks: playerNextTracks, deviceIsActive: isActive, spotifyIsConnected: isConnected, connectDevice, @@ -109,11 +112,11 @@ export const SpotifyPlayer = () => {
- {playerNextTracks.length > 0 && ( + {nextTracks.length > 0 && ( <>

Next Up

    - {playerNextTracks.map((track) => ( + {nextTracks.map((track) => (
  1. {/* TODO: Make different set of styles for track list */} {!track &&

    No track specified.

    } diff --git a/src/components/audio-player/AudioPlayer.tsx b/src/components/audio-player/AudioPlayer.tsx index ea49d80..f4814ea 100644 --- a/src/components/audio-player/AudioPlayer.tsx +++ b/src/components/audio-player/AudioPlayer.tsx @@ -1,8 +1,10 @@ /** * @fileoverview Audio Player Component */ -import { useContext, useEffect, useRef, useState } from 'react' -import { CurrentlyPlayingContext } from 'src/context/CurrentlyPlayingContext' +import { useEffect, useRef, useState } from 'react' +import { useSelector } from 'react-redux' +import { usePlayerControls } from 'src/hooks' +import { selectLiveProgress, selectPlayerState } from 'src/store' import './AudioPlayer.scss' import { Controls } from './Controls' import { ProgressBar } from './ProgressBar' @@ -15,9 +17,10 @@ import './ProgressBar.scss' */ export const AudioPlayer = (props: { disableControls?: boolean }) => { const { disableControls } = props + const playerState = useSelector(selectPlayerState) + const liveProgress = useSelector(selectLiveProgress) + const { - playerState, - liveProgress, play, pause, setProgress, @@ -26,7 +29,7 @@ export const AudioPlayer = (props: { disableControls?: boolean }) => { togglePlay, like, repeat, - } = useContext(CurrentlyPlayingContext) + } = usePlayerControls() // Refs const containerRef = useRef(null) diff --git a/src/context/CurrentlyPlayingContext.tsx b/src/context/CurrentlyPlayingContext.tsx deleted file mode 100644 index 3adf29b..0000000 --- a/src/context/CurrentlyPlayingContext.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import type { ReactNode } from 'react' -import { createContext, useContext, useEffect, useState } from 'react' -import { useSelector } from 'react-redux' -import { REACT_ENV } from 'src/config' -import { selectPlayerState, selectSpotifyAuth } from 'src/store' -import { SpotifyPlayerContext } from './SpotifyPlayerContext' - -export const CurrentlyPlayingContext = createContext({ - currentTrack: null as ITrack | null, - liveProgress: null as number | null, - playerState: null as IPlayerState | null, - play: () => {}, - pause: () => {}, - setProgress: (ms: number) => {}, - nextTrack: () => {}, - prevTrack: () => {}, - togglePlay: () => {}, - like: () => {}, - repeat: () => {}, -}) - -export const CurrentlyPlayingProvider = (props: { children?: ReactNode }) => { - const storePlayerState = useSelector(selectPlayerState) - const spotifyAuth = useSelector(selectSpotifyAuth) - - const [timer, setTimer] = useState(null) - const [playerState, setPlayerState] = useState(null) - const [liveProgress, setLiveProgress] = useState(0) - - const { - playerState: spotifyPlayerState, - play, - pause, - togglePlay, - setProgress, - nextTrack, - prevTrack, - like, - repeat, - } = useContext(SpotifyPlayerContext) - - useEffect(() => { - setLiveProgress(playerState?.progress ?? null) - - if (timer) clearInterval(timer) - - if (playerState?.is_playing) { - const t = setInterval(() => { - setLiveProgress((prev) => (prev && prev + 1000) || 0) - }, 1000) - - setTimer(t) - } - }, [ - playerState?.current_track, - playerState?.is_playing, - playerState?.progress, - ]) - - useEffect(() => { - if (spotifyPlayerState?.current_track) { - setPlayerState(spotifyPlayerState) - } else { - setPlayerState(storePlayerState) - } - }, [ - spotifyPlayerState?.current_track?.id, - spotifyPlayerState?.is_playing, - spotifyPlayerState?.progress, - storePlayerState?.current_track?.id, - storePlayerState?.is_playing, - storePlayerState?.progress, - ]) - - useEffect(() => { - if ( - REACT_ENV === 'dev' && - spotifyAuth && - spotifyPlayerState?.current_track - ) { - setPlayerState(spotifyPlayerState) - } - }, [spotifyAuth, spotifyPlayerState]) - - return ( - - {props.children} - - ) -} diff --git a/src/context/SpotifyPlayerContext.tsx b/src/context/SpotifyPlayerContext.tsx index a9ce2ec..2a3ccb8 100644 --- a/src/context/SpotifyPlayerContext.tsx +++ b/src/context/SpotifyPlayerContext.tsx @@ -15,6 +15,7 @@ import { import { SpotifyPlayer } from 'src/lib' import { Network } from 'src/network' import { setHasAux } from 'src/store' +import { uniqueId } from 'src/utils' import { KeyboardContext } from './KeyboardContext' export const SpotifyPlayerContext = createContext({ @@ -24,8 +25,8 @@ export const SpotifyPlayerContext = createContext({ /** The player has authenticated with Spotify */ spotifyIsConnected: false, - playerState: null as IPlayerState | null, - nextTracks: [] as Spotify.Track[], + playerState: null as IPlayerMetaState | null, + // nextTracks: [] as Spotify.Track[], nextTrack: () => {}, prevTrack: () => {}, play: () => {}, @@ -41,22 +42,15 @@ export const SpotifyPlayerProvider = (props: { children: ReactNode token: Nullable jukebox: IJukebox | null - onPlayerStateChange: (state: { - currentTrack: ITrack - position: number - isPlaying: boolean - nextTracks: ITrack[] - changedTracks: boolean - }) => void + onPlayerStateChange: (state?: IPlayerAuxUpdate) => void }) => { const { children, token, jukebox, onPlayerStateChange } = props const playerRef = useRef(null) const networkRef = useRef() const [initialized, setInitialized] = useState(false) - const [playerState, setPlayerState] = useState(null) + const [playerState, setPlayerState] = useState(null) const [active, setActive] = useState(false) - const [nextTracks, setNextTracks] = useState([]) const [deviceId, setDeviceId] = useState('') const [connected, setConnected] = useState(false) @@ -67,10 +61,10 @@ export const SpotifyPlayerProvider = (props: { }, []) useEffect(() => { - if (token && jukebox) { + if (token && jukebox && !playerRef.current) { SpotifyPlayer.getInstance(token) .getPlayer() - .then(async ({ player, deviceId: resDeviceId }) => { + .then(({ player, deviceId: resDeviceId }) => { playerRef.current = player setDeviceId(resDeviceId) @@ -91,9 +85,11 @@ export const SpotifyPlayerProvider = (props: { // Actions to run when state changes const handlePlayerStateChange = (state?: Spotify.PlaybackState) => { - if (!state) { + if (!state || !jukebox) { // Spotify returns null state if playback transferred to another device setActive(false) + onPlayerStateChange() + return } @@ -105,22 +101,38 @@ export const SpotifyPlayerProvider = (props: { const changedTracks = spotifyTrack.id !== prev?.current_track?.id || state.position === 0 + let currentMetaTrack: ITrackMeta | undefined + + if (changedTracks) { + currentMetaTrack = { + ...spotifyTrack, + queue_id: uniqueId(), + spotify_queued: true, + } + } else if (prev.current_track) { + currentMetaTrack = { ...prev.current_track, ...spotifyTrack } + } else { + currentMetaTrack = undefined + } + onPlayerStateChange({ - currentTrack: spotifyTrack, - position: state.position, - isPlaying: !state.paused, - nextTracks: state.track_window.next_tracks, - changedTracks, + jukebox_id: jukebox?.id, + current_track: spotifyTrack, + progress: state.position, + is_playing: !state.paused, + default_next_tracks: state.track_window.next_tracks, + changed_tracks: changedTracks, }) return { + ...prev, jukebox_id: jukebox!.id, - current_track: spotifyTrack, + current_track: currentMetaTrack, is_playing: !state.paused, progress: state.position, + default_next_tracks: state.track_window.next_tracks, } }) - setNextTracks(state.track_window.next_tracks) setDeviceId(state.playback_id) playerRef.current?.getCurrentState().then((state) => { @@ -238,7 +250,6 @@ export const SpotifyPlayerProvider = (props: { deviceIsActive: active, spotifyIsConnected: connected, playerState, - nextTracks, nextTrack, prevTrack: prevTrack, play, diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 343ab02..6098be3 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1 +1,2 @@ +export * from './use-player-controls' export * from './use-time' diff --git a/src/hooks/use-player-controls.tsx b/src/hooks/use-player-controls.tsx new file mode 100644 index 0000000..3aea843 --- /dev/null +++ b/src/hooks/use-player-controls.tsx @@ -0,0 +1,34 @@ +import { useContext } from 'react' +import { SpotifyPlayerContext } from 'src/context' + +/** + * Access player controls. + * + * If Spotify is connected, it will use spotify's controls, + * otherwise will use the REST api to control the spotify state. + */ +export const usePlayerControls = () => { + const { + play, + pause, + togglePlay, + setProgress, + nextTrack, + prevTrack, + like, + repeat, + } = useContext(SpotifyPlayerContext) + + // TODO: Implement api controls + + return { + play, + pause, + togglePlay, + setProgress, + nextTrack, + prevTrack, + like, + repeat, + } +} diff --git a/src/lib/spotify.ts b/src/lib/spotify.ts index 53f88bb..1326e2c 100644 --- a/src/lib/spotify.ts +++ b/src/lib/spotify.ts @@ -19,9 +19,11 @@ export class SpotifyPlayer { this.resolvePlayer = resolve this.rejectPlayer = reject this.playerPromise = promise + console.log('init spotify') this.token = token this.connect() + // this.setToken(token) } public static getInstance(): SpotifyPlayer | null public static getInstance(token: string): SpotifyPlayer @@ -36,9 +38,17 @@ export class SpotifyPlayer { } private connect() { - if (this.token === 'YOUR-LONG-TOKEN-HERE') return + if (this.token === 'YOUR-LONG-TOKEN-HERE' || !this.token) return - window.onSpotifyWebPlaybackSDKReady = async () => { + if (!window.Spotify) { + console.log('added spotify script') + const scriptTag = document.createElement('script') + scriptTag.src = 'https://sdk.scdn.co/spotify-player.js' + + document.head!.appendChild(scriptTag) + } + + window.onSpotifyWebPlaybackSDKReady = () => { const player = new Spotify.Player({ name: SPOTIFY_PLAYER_NAME, getOAuthToken: (cb) => { @@ -75,13 +85,6 @@ export class SpotifyPlayer { }) player.connect() } - - if (!window.Spotify) { - const scriptTag = document.createElement('script') - scriptTag.src = 'https://sdk.scdn.co/spotify-player.js' - - document.head!.appendChild(scriptTag) - } } public async getPlayer(): Promise { @@ -102,6 +105,6 @@ export class SpotifyPlayer { if (!token) return this.token = token - this.connect() + // this.connect() } } diff --git a/src/store/jukebox/jbxActions.ts b/src/store/jukebox/jbxActions.ts index a30d4fb..5019583 100644 --- a/src/store/jukebox/jbxActions.ts +++ b/src/store/jukebox/jbxActions.ts @@ -5,6 +5,7 @@ import { store } from '../store' import { selectActiveLink, selectCurrentJukebox, + selectPlayerState, selectSpotifyAuth, } from './jbxSelectors' import { @@ -20,17 +21,39 @@ const { setPlayerStateReducer, setNextTracksReducer, setHasAuxReducer, - updatePlayerStateReducer, + performPlayerActionReducer: updatePlayerStateReducer, + setLiveProgressReducer, + incrementLiveProgressReducer, } = jukeboxActions -export const setPlayerState = (currentlyPlaying: IPlayerQueueState) => { +export const setPlayerState = (currentlyPlaying: IPlayerMetaState) => { store.dispatch(setPlayerStateReducer(currentlyPlaying)) } -export const updatePlayerState = (payload: IPlayerAction) => { +export const updatePlayerState = (currentlyPlaying: IPlayerMetaUpdate) => { + const prevState = selectPlayerState(store.getState()) + const payload: IPlayerMetaState = { + ...prevState, + ...currentlyPlaying, + default_next_tracks: currentlyPlaying.default_next_tracks ?? [], + is_playing: currentlyPlaying.is_playing ?? false, + } + setLiveProgress(payload.progress) + store.dispatch(setPlayerStateReducer(payload)) +} + +export const doPlayerAction = (payload: IPlayerAction) => { store.dispatch(updatePlayerStateReducer(payload)) } +export const setLiveProgress = (ms?: number) => { + store.dispatch(setLiveProgressReducer({ ms })) +} + +export const incrementLiveProgress = () => { + store.dispatch(incrementLiveProgressReducer()) +} + export const setNextTracks = (nextTracks: ITrackMeta[]) => { store.dispatch(setNextTracksReducer(nextTracks)) } diff --git a/src/store/jukebox/jbxSelectors.ts b/src/store/jukebox/jbxSelectors.ts index a205a38..cddbc5a 100644 --- a/src/store/jukebox/jbxSelectors.ts +++ b/src/store/jukebox/jbxSelectors.ts @@ -37,6 +37,11 @@ export const selectPlayerState = createSelector( (state) => state.playerState, ) +export const selectLiveProgress = createSelector( + jbxStateSelector, + (state) => state.liveProgress, +) + export const selectNextTracks = createSelector( jbxStateSelector, (state) => state.nextTracks, diff --git a/src/store/jukebox/jbxSlice.ts b/src/store/jukebox/jbxSlice.ts index ab5b9ec..68fb12a 100644 --- a/src/store/jukebox/jbxSlice.ts +++ b/src/store/jukebox/jbxSlice.ts @@ -16,15 +16,16 @@ export const jukeboxSlice = createSlice({ /** User is connected to spotify, and the player is active */ hasAux: false, currentJukebox: null as IJukebox | null, - playerState: null as IPlayerQueueState | null, + playerState: null as IPlayerMetaState | null, nextTracks: [] as ITrackMeta[], spotifyAuth: null as ISpotifyAccount | null, + liveProgress: 0 as number | null, }, reducers: { - setPlayerStateReducer: (state, action: { payload: IPlayerQueueState }) => { + setPlayerStateReducer: (state, action: { payload: IPlayerMetaState }) => { state.playerState = action.payload }, - updatePlayerStateReducer: (state, action: { payload: IPlayerAction }) => { + performPlayerActionReducer: (state, action: { payload: IPlayerAction }) => { if (!state.playerState?.current_track) return state.playerState = { @@ -42,6 +43,12 @@ export const jukeboxSlice = createSlice({ setHasAuxReducer: (state, action: { payload: boolean }) => { state.hasAux = action.payload }, + setLiveProgressReducer: (state, action: { payload: { ms?: number } }) => { + state.liveProgress = action.payload.ms ?? null + }, + incrementLiveProgressReducer: (state) => { + state.liveProgress = (state.liveProgress ?? 0) + 1000 + }, }, extraReducers: (builder) => { builder.addCase(thunkFetchJukeboxes.fulfilled, (state, action) => { diff --git a/src/types/spotify.d.ts b/src/types/spotify.d.ts index 928abfb..993bdaf 100644 --- a/src/types/spotify.d.ts +++ b/src/types/spotify.d.ts @@ -24,7 +24,7 @@ declare interface ISpotifyAccount extends IModel { declare interface IPlayerState { jukebox_id: number current_track?: ITrack - progress: number + progress?: number is_playing: boolean } @@ -49,6 +49,10 @@ declare interface IPlayerAuxUpdate extends IPlayerState { changed_tracks?: boolean default_next_tracks: ITrack[] } + +declare interface IPlayerMetaUpdate extends Partial { + jukebox_id: number +} type IPlayerUpdate = IPlayerQueueState declare interface IPlayerAction extends Partial {