diff --git a/src/renderer/hooks/use-server-authenticated.ts b/src/renderer/hooks/use-server-authenticated.ts new file mode 100644 index 00000000..3696595c --- /dev/null +++ b/src/renderer/hooks/use-server-authenticated.ts @@ -0,0 +1,54 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCurrentServer } from '/@/renderer/store'; +import { AuthState, ServerListItem, ServerType } from '/@/renderer/types'; +import { api } from '/@/renderer/api'; +import { SongListSort, SortOrder } from '/@/renderer/api/types'; +import { debounce } from 'lodash'; + +export const useServerAuthenticated = () => { + const priorServerId = useRef(); + const server = useCurrentServer(); + const [ready, setReady] = useState( + server?.type === ServerType.NAVIDROME ? AuthState.LOADING : AuthState.VALID, + ); + + const authenticateNavidrome = useCallback(async (server: ServerListItem) => { + // This trick works because navidrome-api.ts will internally check for authentication + // failures and try to log in again (where available). So, all that's necessary is + // making one request first + try { + await api.controller.getSongList({ + apiClientProps: { server }, + query: { + limit: 1, + sortBy: SongListSort.NAME, + sortOrder: SortOrder.ASC, + startIndex: 0, + }, + }); + + setReady(AuthState.VALID); + } catch (error) { + setReady(AuthState.INVALID); + } + }, []); + + const debouncedAuth = debounce((server: ServerListItem) => { + authenticateNavidrome(server).catch(console.error); + }, 300); + + useEffect(() => { + if (priorServerId.current !== server?.id) { + priorServerId.current = server?.id || ''; + + if (server?.type === ServerType.NAVIDROME) { + setReady(AuthState.LOADING); + debouncedAuth(server); + } else { + setReady(AuthState.VALID); + } + } + }, [debouncedAuth, server]); + + return ready; +}; diff --git a/src/renderer/router/app-outlet.tsx b/src/renderer/router/app-outlet.tsx index d77a8981..8e1c52a0 100644 --- a/src/renderer/router/app-outlet.tsx +++ b/src/renderer/router/app-outlet.tsx @@ -3,7 +3,9 @@ import isElectron from 'is-electron'; import { Navigate, Outlet } from 'react-router-dom'; import { AppRoute } from '/@/renderer/router/routes'; import { useCurrentServer, useSetPlayerFallback } from '/@/renderer/store'; -import { toast } from '/@/renderer/components'; +import { Spinner, toast } from '/@/renderer/components'; +import { useServerAuthenticated } from '/@/renderer/hooks/use-server-authenticated'; +import { AuthState } from '/@/renderer/types'; const ipc = isElectron() ? window.electron.ipc : null; const utils = isElectron() ? window.electron.utils : null; @@ -12,6 +14,7 @@ const mpvPlayerListener = isElectron() ? window.electron.mpvPlayerListener : nul export const AppOutlet = () => { const currentServer = useCurrentServer(); const setFallback = useSetPlayerFallback(); + const authState = useServerAuthenticated(); const isActionsRequired = useMemo(() => { const isServerRequired = !currentServer; @@ -37,7 +40,11 @@ export const AppOutlet = () => { }; }, [setFallback]); - if (isActionsRequired) { + if (authState === AuthState.LOADING) { + return ; + } + + if (isActionsRequired || authState === AuthState.INVALID) { return (