Skip to content

Commit

Permalink
feat(providers): Handle player registration and state mgmt
Browse files Browse the repository at this point in the history
  • Loading branch information
busybox11 committed Nov 22, 2024
1 parent 936bf64 commit 88efbbd
Show file tree
Hide file tree
Showing 8 changed files with 106 additions and 8 deletions.
30 changes: 27 additions & 3 deletions app/components/contexts/PlayerProviders.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { createContext, useContext, useCallback, useMemo } from "react";
import { createContext, useContext, useCallback, useMemo, useRef } from "react";
import type { ReactNode } from "@tanstack/react-router";

import providers from "@/providers";
import { IProviderClient } from "@/types/providers/client";
import { useLocalStorage } from "usehooks-ts";
import { PlayerState } from "@/types/player";
import { useAtomValue, useSetAtom } from "jotai";
import { activePlayerAtom, playerStateAtom } from "@/state/player";

const PlayerProvidersContext = createContext<{
[key: string]: IProviderClient;
Expand All @@ -21,22 +24,41 @@ export function PlayerProvidersProvider({
}>) {
const [_lastUsed, setLastUsed, _removeLastUsed] =
useLocalStorage<LastUsedProvider>("lastUsedProvider", null);
const setActivePlayer = useSetAtom(activePlayerAtom);
const setPlayerState = useSetAtom(playerStateAtom);

const providerInstancesRef = useRef<{ [key: string]: IProviderClient }>({});

const handleAuth = useCallback((provider: string) => {
setActivePlayer(provider);
setLastUsed({ id: provider, date: Date.now() });

// Get the specific provider instance
const providerInstance = providerInstancesRef.current[provider];
if (providerInstance) {
providerInstance.registerPlayer();
}
}, []);

const value = useMemo(() => {
return Object.fromEntries(
const instances = Object.fromEntries(
providers.map(([id, provider]) => {
return [
id,
new provider({
onAuth: () => handleAuth(id),
sendPlayerState: (playerObj: PlayerState) => {
setPlayerState(playerObj);
console.log(playerObj);
},
}),
];
})
}),
);

// Store references to provider instances
providerInstancesRef.current = instances;
return instances;
}, []);

return (
Expand All @@ -48,9 +70,11 @@ export function PlayerProvidersProvider({

export function usePlayerProviders() {
const [lastUsed] = useLocalStorage<LastUsedProvider>("lastUsed", null);
const activePlayer = useAtomValue(activePlayerAtom);

return {
lastUsedProvider: lastUsed,
providers: useContext(PlayerProvidersContext),
activePlayer,
};
}
46 changes: 42 additions & 4 deletions app/providers/spotify/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { SPOTIFY_OAUTH_SCOPES } from "./constants";
import {
SpotifyApi,
AuthorizationCodeWithPKCEStrategy,
PlaybackState,
} from "@spotify/web-api-ts-sdk";

import { providerConfig } from "./config";
Expand All @@ -10,6 +11,7 @@ import {
IProviderClientConstructor,
} from "@/types/providers/client";
import spotifyProviderMeta from "@/providers/spotify";
import { PlayerState } from "@/types/player";

const { VITE_SPOTIFY_CLIENT_ID, VITE_SPOTIFY_REDIRECT_URI } = providerConfig;

Expand All @@ -19,24 +21,45 @@ export default class SpotifyProvider implements IProviderClient {

// API event handlers
private onAuth: () => void;
private sendPlayerState: (playerObj: PlayerState) => void;

constructor({ onAuth }: IProviderClientConstructor) {
constructor({ onAuth, sendPlayerState }: IProviderClientConstructor) {
this.onAuth = onAuth;
this.sendPlayerState = sendPlayerState;
}

// Private properties and methods
private _client: SpotifyApi | null = null;
private _playerLoopInstance: number = NaN;
private _lastPlaybackState: PlaybackState | null = null;

private async _getPlayerState() {
if (!this._client) return null;

const playerState = await this._client.player.getPlaybackState(
undefined,
"episode"
);
this._lastPlaybackState = playerState;

return playerState;
}

// Public implemented methods

async authenticate() {
const auth = new AuthorizationCodeWithPKCEStrategy(
VITE_SPOTIFY_CLIENT_ID,
VITE_SPOTIFY_REDIRECT_URI,
[...SPOTIFY_OAUTH_SCOPES]
);
const client = new SpotifyApi(auth);
this._client = new SpotifyApi(auth);

try {
const { authenticated } = await client.authenticate();
const { authenticated } = await this._client.authenticate();
this.isAuthenticated = authenticated;

if (authenticated) {
this.isAuthenticated = true;
this.onAuth();
}
} catch (e) {
Expand All @@ -47,4 +70,19 @@ export default class SpotifyProvider implements IProviderClient {
async callback() {
await this.authenticate();
}

async registerPlayer() {
this._playerLoopInstance = window.setInterval(async () => {
const playerState = await this._getPlayerState();

this.sendPlayerState(playerState);
}, 1000);
}

async unregisterPlayer() {
if (this._playerLoopInstance) {
clearInterval(this._playerLoopInstance);
this._playerLoopInstance = NaN;
}
}
}
4 changes: 3 additions & 1 deletion app/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const Route = createFileRoute("/")({
});

function Home() {
const { providers } = usePlayerProviders();
const { providers, activePlayer } = usePlayerProviders();

return (
<main className="flex flex-col items-center justify-center h-screen gap-12">
Expand Down Expand Up @@ -37,6 +37,8 @@ function Home() {
</button>
);
})}

<span>currently active player {activePlayer}</span>
</div>
</main>
);
Expand Down
5 changes: 5 additions & 0 deletions app/state/player.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { PlayerState } from "@/types/player";
import { atom } from "jotai";

export const activePlayerAtom = atom<null | string>(null);
export const playerStateAtom = atom<PlayerState>(null);
3 changes: 3 additions & 0 deletions app/types/player/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { PlaybackState } from "@spotify/web-api-ts-sdk";

export type PlayerState = PlaybackState | null;
5 changes: 5 additions & 0 deletions app/types/providers/client.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { PlayerState } from "@/types/player";
import { ProviderMeta } from "@/types/providers/meta";

export interface IProviderClientConstructor {
onAuth: () => void;
sendPlayerState: (playerObj: PlayerState) => void;
}

export interface IProviderClient {
Expand All @@ -11,4 +13,7 @@ export interface IProviderClient {

authenticate(): Promise<void>;
callback(): Promise<void>;

registerPlayer(): Promise<void>;
unregisterPlayer(): Promise<void>;
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@tanstack/react-router": "^1.81.5",
"@tanstack/start": "^1.81.5",
"dotenv": "^16.4.5",
"jotai": "^2.10.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-icons": "^5.3.0",
Expand Down
20 changes: 20 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 88efbbd

Please sign in to comment.