diff --git a/src/remote/store/index.ts b/src/remote/store/index.ts index 02caf63bd..f1cbc901d 100644 --- a/src/remote/store/index.ts +++ b/src/remote/store/index.ts @@ -3,6 +3,8 @@ import { devtools, persist } from 'zustand/middleware'; import { immer } from 'zustand/middleware/immer'; import { createWithEqualityFn } from 'zustand/traditional'; +import { LogCategory, logFn } from '/@/renderer/utils/logger'; +import { logMsg } from '/@/renderer/utils/logger-message'; import { toast } from '/@/shared/components/toast/toast'; import { ClientEvent, ServerEvent, SongUpdateSocket } from '/@/shared/types/remote-types'; @@ -40,6 +42,9 @@ export const useRemoteStore = createWithEqualityFn()( immer((set, get) => ({ actions: { reconnect: async () => { + logFn.debug(logMsg[LogCategory.REMOTE].reconnectInitiated, { + category: LogCategory.REMOTE, + }); const existing = get().socket; if (existing) { @@ -47,6 +52,10 @@ export const useRemoteStore = createWithEqualityFn()( existing.readyState === WebSocket.OPEN || existing.readyState === WebSocket.CONNECTING ) { + logFn.debug(logMsg[LogCategory.REMOTE].closingExistingSocket, { + category: LogCategory.REMOTE, + meta: { readyState: existing.readyState }, + }); existing.natural = true; existing.close(4001); } @@ -55,28 +64,63 @@ export const useRemoteStore = createWithEqualityFn()( let authHeader: string | undefined; try { + logFn.debug(logMsg[LogCategory.REMOTE].fetchingCredentials, { + category: LogCategory.REMOTE, + }); const credentials = await fetch('/credentials'); authHeader = await credentials.text(); + logFn.debug(logMsg[LogCategory.REMOTE].credentialsFetched, { + category: LogCategory.REMOTE, + meta: { hasAuthHeader: !!authHeader }, + }); } catch (error) { - console.error('Failed to get credentials', error); + logFn.error(logMsg[LogCategory.REMOTE].failedToGetCredentials, { + category: LogCategory.REMOTE, + meta: { error }, + }); } set((state) => { - const socket = new WebSocket( - location.href.replace('http', 'ws'), - ) as StatefulWebSocket; + const wsUrl = location.href.replace('http', 'ws'); + logFn.debug(logMsg[LogCategory.REMOTE].creatingWebSocket, { + category: LogCategory.REMOTE, + meta: { url: wsUrl }, + }); + const socket = new WebSocket(wsUrl) as StatefulWebSocket; socket.natural = false; socket.addEventListener('message', (message) => { const { data, event } = JSON.parse(message.data) as ServerEvent; + logFn.debug(logMsg[LogCategory.REMOTE].webSocketMessageReceived, { + category: LogCategory.REMOTE, + meta: { data, event }, + }); + switch (event) { case 'error': { + logFn.error( + logMsg[LogCategory.REMOTE].webSocketErrorEvent, + { + category: LogCategory.REMOTE, + meta: { data }, + }, + ); toast.error({ message: data, title: 'Socket error' }); break; } case 'favorite': { + logFn.debug( + logMsg[LogCategory.REMOTE].favoriteEventReceived, + { + category: LogCategory.REMOTE, + meta: { + favorite: data.favorite, + id: data.id, + }, + }, + ); set((state) => { if (state.info.song?.id === data.id) { state.info.song.userFavorite = data.favorite; @@ -85,18 +129,39 @@ export const useRemoteStore = createWithEqualityFn()( break; } case 'playback': { + logFn.debug( + logMsg[LogCategory.REMOTE].playbackEventReceived, + { + category: LogCategory.REMOTE, + meta: { status: data }, + }, + ); set((state) => { state.info.status = data; }); break; } case 'position': { + logFn.debug( + logMsg[LogCategory.REMOTE].positionEventReceived, + { + category: LogCategory.REMOTE, + meta: { position: data }, + }, + ); set((state) => { state.info.position = data; }); break; } case 'proxy': { + logFn.debug(logMsg[LogCategory.REMOTE].proxyEventReceived, { + category: LogCategory.REMOTE, + meta: { + dataLength: data?.length, + hasData: !!data, + }, + }); set((state) => { if (state.info.song) { state.info.song.imageUrl = `data:image/jpeg;base64,${data}`; @@ -105,6 +170,16 @@ export const useRemoteStore = createWithEqualityFn()( break; } case 'rating': { + logFn.debug( + logMsg[LogCategory.REMOTE].ratingEventReceived, + { + category: LogCategory.REMOTE, + meta: { + id: data.id, + rating: data.rating, + }, + }, + ); set((state) => { if (state.info.song?.id === data.id) { state.info.song.userRating = data.rating; @@ -113,30 +188,68 @@ export const useRemoteStore = createWithEqualityFn()( break; } case 'repeat': { + logFn.debug( + logMsg[LogCategory.REMOTE].repeatEventReceived, + { + category: LogCategory.REMOTE, + meta: { repeat: data }, + }, + ); set((state) => { state.info.repeat = data; }); break; } case 'shuffle': { + logFn.debug( + logMsg[LogCategory.REMOTE].shuffleEventReceived, + { + category: LogCategory.REMOTE, + meta: { shuffle: data }, + }, + ); set((state) => { state.info.shuffle = data; }); break; } case 'song': { + logFn.debug(logMsg[LogCategory.REMOTE].songEventReceived, { + category: LogCategory.REMOTE, + meta: { + artistName: data?.artistName, + id: data?.id, + name: data?.name, + }, + }); set((state) => { state.info.song = data; }); break; } case 'state': { + logFn.debug(logMsg[LogCategory.REMOTE].stateEventReceived, { + category: LogCategory.REMOTE, + meta: { + hasSong: !!data.song, + position: data.position, + status: data.status, + volume: data.volume, + }, + }); set((state) => { state.info = data; }); break; } case 'volume': { + logFn.debug( + logMsg[LogCategory.REMOTE].volumeEventReceived, + { + category: LogCategory.REMOTE, + meta: { volume: data }, + }, + ); set((state) => { state.info.volume = data; }); @@ -145,7 +258,17 @@ export const useRemoteStore = createWithEqualityFn()( }); socket.addEventListener('open', () => { + logFn.debug(logMsg[LogCategory.REMOTE].webSocketOpened, { + category: LogCategory.REMOTE, + meta: { + hasAuthHeader: !!authHeader, + readyState: socket.readyState, + }, + }); if (authHeader) { + logFn.debug(logMsg[LogCategory.REMOTE].sendingAuthentication, { + category: LogCategory.REMOTE, + }); socket.send( JSON.stringify({ event: 'authenticate', @@ -157,14 +280,40 @@ export const useRemoteStore = createWithEqualityFn()( }); socket.addEventListener('close', (reason) => { + logFn.debug(logMsg[LogCategory.REMOTE].webSocketClosed, { + category: LogCategory.REMOTE, + meta: { + code: reason.code, + natural: socket.natural, + reason: reason.reason, + wasClean: reason.wasClean, + }, + }); if (reason.code === 4002 || reason.code === 4003) { + logFn.debug(logMsg[LogCategory.REMOTE].reloadingPage, { + category: LogCategory.REMOTE, + meta: { code: reason.code }, + }); location.reload(); } else if (reason.code === 4000) { + logFn.warn(logMsg[LogCategory.REMOTE].serverIsDown, { + category: LogCategory.REMOTE, + }); toast.warn({ message: 'Feishin remote server is down', title: 'Connection closed', }); } else if (reason.code !== 4001 && !socket.natural) { + logFn.error( + logMsg[LogCategory.REMOTE].socketClosedUnexpectedly, + { + category: LogCategory.REMOTE, + meta: { + code: reason.code, + reason: reason.reason, + }, + }, + ); toast.error({ message: 'Socket closed for unexpected reason', title: 'Connection closed', @@ -180,7 +329,23 @@ export const useRemoteStore = createWithEqualityFn()( }); }, send: (data: ClientEvent) => { - get().socket?.send(JSON.stringify(data)); + const socket = get().socket; + if (socket) { + logFn.debug(logMsg[LogCategory.REMOTE].sendingEventToServer, { + category: LogCategory.REMOTE, + meta: { + data: data, + event: data.event, + readyState: socket.readyState, + }, + }); + socket.send(JSON.stringify(data)); + } else { + logFn.warn(logMsg[LogCategory.REMOTE].cannotSendEvent, { + category: LogCategory.REMOTE, + meta: { event: data.event }, + }); + } }, toggleIsDark: () => { set((state) => { diff --git a/src/renderer/features/remote/hooks/use-remote.tsx b/src/renderer/features/remote/hooks/use-remote.tsx index fc0325d3f..3a73aa5f0 100644 --- a/src/renderer/features/remote/hooks/use-remote.tsx +++ b/src/renderer/features/remote/hooks/use-remote.tsx @@ -1,11 +1,13 @@ import isElectron from 'is-electron'; -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events'; import { useCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation'; import { useDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation'; import { useSetRating } from '/@/renderer/features/shared/mutations/set-rating-mutation'; -import { usePlayerActions, useRemoteSettings } from '/@/renderer/store'; +import { usePlayerActions, usePlayerStore, useRemoteSettings } from '/@/renderer/store'; +import { LogCategory, logFn } from '/@/renderer/utils/logger'; +import { logMsg } from '/@/renderer/utils/logger-message'; import { toast } from '/@/shared/components/toast/toast'; import { LibraryItem } from '/@/shared/types/domain-types'; import { PlayerShuffle } from '/@/shared/types/types'; @@ -14,19 +16,31 @@ const remote = isElectron() ? window.api.remote : null; const ipc = isElectron() ? window.api.ipc : null; export const useRemote = () => { - const { mediaSkipForward, setTimestamp, setVolume } = usePlayerActions(); + const { mediaSkipForward, setVolume } = usePlayerActions(); + const player = usePlayerStore(); const remoteSettings = useRemoteSettings(); const updateRatingMutation = useSetRating({}); const addToFavoritesMutation = useCreateFavorite({}); const removeFromFavoritesMutation = useDeleteFavorite({}); + const isRemoteEnabled = remoteSettings.enabled; + // Initialize the remote useEffect(() => { - if (!remote) { + if (!isRemoteEnabled) { return; } + logFn.debug(logMsg[LogCategory.REMOTE].initializingRemoteSettings, { + category: LogCategory.REMOTE, + meta: { + enabled: remoteSettings.enabled, + port: remoteSettings.port, + username: remoteSettings.username, + }, + }); + remote ?.updateSetting( remoteSettings.enabled, @@ -35,6 +49,10 @@ export const useRemote = () => { remoteSettings.password, ) .catch((error) => { + logFn.error(logMsg[LogCategory.REMOTE].failedToEnableRemote, { + category: LogCategory.REMOTE, + meta: { error }, + }); toast.warn({ message: error, title: 'Failed to enable remote' }); }); // We only want to fire this once @@ -42,21 +60,33 @@ export const useRemote = () => { }, []); useEffect(() => { - if (!remote) { + if (!isRemoteEnabled || !remote) { return; } remote.requestPosition((_e: unknown, data: { position: number }) => { + logFn.debug(logMsg[LogCategory.REMOTE].requestPositionReceived, { + category: LogCategory.REMOTE, + meta: { position: data.position }, + }); const newTime = data.position; - setTimestamp(newTime); + player.mediaSeekToTimestamp(newTime); }); remote.requestSeek((_e: unknown, data: { offset: number }) => { + logFn.debug(logMsg[LogCategory.REMOTE].requestSeekReceived, { + category: LogCategory.REMOTE, + meta: { offset: data.offset }, + }); mediaSkipForward(data.offset); }); remote.requestRating( (_e: unknown, data: { id: string; rating: number; serverId: string }) => { + logFn.debug(logMsg[LogCategory.REMOTE].requestRatingReceived, { + category: LogCategory.REMOTE, + meta: { id: data.id, rating: data.rating, serverId: data.serverId }, + }); updateRatingMutation.mutate({ apiClientProps: { serverId: data.serverId }, query: { @@ -69,11 +99,19 @@ export const useRemote = () => { ); remote.requestVolume((_e: unknown, data: { volume: number }) => { + logFn.debug(logMsg[LogCategory.REMOTE].requestVolumeReceived, { + category: LogCategory.REMOTE, + meta: { volume: data.volume }, + }); setVolume(data.volume); }); remote.requestFavorite( (_e: unknown, data: { favorite: boolean; id: string; serverId: string }) => { + logFn.debug(logMsg[LogCategory.REMOTE].requestFavoriteReceived, { + category: LogCategory.REMOTE, + meta: { favorite: data.favorite, id: data.id, serverId: data.serverId }, + }); const mutator = data.favorite ? addToFavoritesMutation : removeFromFavoritesMutation; @@ -96,63 +134,140 @@ export const useRemote = () => { }; }, [ addToFavoritesMutation, + isRemoteEnabled, mediaSkipForward, + player, removeFromFavoritesMutation, - setTimestamp, setVolume, updateRatingMutation, ]); + // Send initial song if one is already playing + const isInitializedRef = useRef(false); + useEffect(() => { + if (isInitializedRef.current || !isRemoteEnabled || !remote) { + return; + } + + isInitializedRef.current = true; + + const currentSong = player.getCurrentSong(); + + if (currentSong) { + logFn.debug(logMsg[LogCategory.REMOTE].sendingInitialSong, { + category: LogCategory.REMOTE, + meta: { + artistName: currentSong.artistName, + id: currentSong.id, + name: currentSong.name, + }, + }); + remote.updateSong(currentSong); + } + }, [isRemoteEnabled, player]); + usePlayerEvents( { - onPlayerProgress: (properties) => { - if (!remote) { + onCurrentSongChange: (properties) => { + if (!isRemoteEnabled || !remote) { return; } + logFn.debug(logMsg[LogCategory.REMOTE].updateSongSent, { + category: LogCategory.REMOTE, + meta: { + artistName: properties.song?.artistName, + id: properties.song?.id, + index: properties.index, + name: properties.song?.name, + }, + }); + remote.updateSong(properties.song); + }, + onPlayerProgress: (properties) => { + if (!isRemoteEnabled || !remote) { + return; + } + + logFn.debug(logMsg[LogCategory.REMOTE].updatePositionSent, { + category: LogCategory.REMOTE, + meta: { timestamp: properties.timestamp }, + }); remote.updatePosition(properties.timestamp); }, onPlayerRepeat: (properties) => { - if (!remote) { + if (!isRemoteEnabled || !remote) { return; } + logFn.debug(logMsg[LogCategory.REMOTE].updateRepeatSent, { + category: LogCategory.REMOTE, + meta: { repeat: properties.repeat }, + }); remote.updateRepeat(properties.repeat); }, onPlayerShuffle: (properties) => { - if (!remote) { + if (!isRemoteEnabled || !remote) { return; } const isShuffleEnabled = properties.shuffle !== PlayerShuffle.NONE; + logFn.debug(logMsg[LogCategory.REMOTE].updateShuffleSent, { + category: LogCategory.REMOTE, + meta: { isShuffleEnabled, shuffle: properties.shuffle }, + }); remote.updateShuffle(isShuffleEnabled); }, onPlayerStatus: (properties) => { - if (!remote) { + if (!isRemoteEnabled || !remote) { return; } + logFn.debug(logMsg[LogCategory.REMOTE].updatePlaybackSent, { + category: LogCategory.REMOTE, + meta: { status: properties.status }, + }); remote.updatePlayback(properties.status); }, onPlayerVolume: (properties) => { - if (!remote) { + if (!isRemoteEnabled || !remote) { return; } + logFn.debug(logMsg[LogCategory.REMOTE].updateVolumeSent, { + category: LogCategory.REMOTE, + meta: { volume: properties.volume }, + }); remote.updateVolume(properties.volume); }, onUserFavorite: (properties) => { - if (!remote) { + if (!isRemoteEnabled || !remote) { return; } + logFn.debug(logMsg[LogCategory.REMOTE].updateFavoriteSent, { + category: LogCategory.REMOTE, + meta: { + favorite: properties.favorite, + id: properties.id, + serverId: properties.serverId, + }, + }); remote.updateFavorite(properties.favorite, properties.serverId, properties.id); }, onUserRating: (properties) => { - if (!remote) { + if (!isRemoteEnabled || !remote) { return; } + logFn.debug(logMsg[LogCategory.REMOTE].updateRatingSent, { + category: LogCategory.REMOTE, + meta: { + id: properties.id, + rating: properties.rating || 0, + serverId: properties.serverId, + }, + }); remote.updateRating(properties.rating || 0, properties.serverId, properties.id); }, }, diff --git a/src/renderer/utils/logger-message.ts b/src/renderer/utils/logger-message.ts index 947aba719..5b4c0a932 100644 --- a/src/renderer/utils/logger-message.ts +++ b/src/renderer/utils/logger-message.ts @@ -60,6 +60,50 @@ export const logMsg = { toggleRepeat: 'Toggle repeat', toggleShuffle: 'Toggle shuffle', }, + [LogCategory.REMOTE]: { + cannotSendEvent: 'Cannot send event - socket not available', + closingExistingSocket: 'Closing existing socket', + creatingWebSocket: 'Creating new WebSocket', + credentialsFetched: 'Credentials fetched', + failedToEnableRemote: 'Failed to enable remote', + failedToGetCredentials: 'Failed to get credentials', + favoriteEventReceived: 'Favorite event received', + fetchingCredentials: 'Fetching credentials', + initializingRemoteSettings: 'Initializing remote settings', + playbackEventReceived: 'Playback event received', + positionEventReceived: 'Position event received', + proxyEventReceived: 'Proxy event received (image update)', + ratingEventReceived: 'Rating event received', + reconnectInitiated: 'Reconnect initiated', + reloadingPage: 'Reloading page due to close code', + repeatEventReceived: 'Repeat event received', + requestFavoriteReceived: 'Request favorite received', + requestPositionReceived: 'Request position received', + requestRatingReceived: 'Request rating received', + requestSeekReceived: 'Request seek received', + requestVolumeReceived: 'Request volume received', + sendingAuthentication: 'Sending authentication', + sendingEventToServer: 'Sending event to server', + sendingInitialSong: 'Sending initial song', + serverIsDown: 'Server is down', + shuffleEventReceived: 'Shuffle event received', + socketClosedUnexpectedly: 'Socket closed unexpectedly', + songEventReceived: 'Song event received', + stateEventReceived: 'State event received (full state update)', + updateFavoriteSent: 'Update favorite sent', + updatePlaybackSent: 'Update playback sent', + updatePositionSent: 'Update position sent', + updateRatingSent: 'Update rating sent', + updateRepeatSent: 'Update repeat sent', + updateShuffleSent: 'Update shuffle sent', + updateSongSent: 'Update song sent', + updateVolumeSent: 'Update volume sent', + volumeEventReceived: 'Volume event received', + webSocketClosed: 'WebSocket closed', + webSocketErrorEvent: 'WebSocket error event', + webSocketMessageReceived: 'WebSocket message received', + webSocketOpened: 'WebSocket opened', + }, [LogCategory.SCROBBLE]: { scrobbledPause: 'Scrobbled a pause event', scrobbledStart: 'Scrobbled a start event', diff --git a/src/renderer/utils/logger.ts b/src/renderer/utils/logger.ts index 6f0aa7091..84f4049fc 100644 --- a/src/renderer/utils/logger.ts +++ b/src/renderer/utils/logger.ts @@ -7,6 +7,7 @@ export enum LogCategory { GENERAL = 'general', OTHER = 'other', PLAYER = 'player', + REMOTE = 'remote', SCROBBLE = 'scrobble', SYSTEM = 'system', }