first attempt at spotify playback
This commit is contained in:
parent
dd02db9011
commit
032e656297
8 changed files with 204 additions and 87 deletions
|
|
@ -38,6 +38,7 @@ export const auth = betterAuth({
|
||||||
clientId: SPOTIFY_CLIENT_ID,
|
clientId: SPOTIFY_CLIENT_ID,
|
||||||
clientSecret: SPOTIFY_CLIENT_SECRET,
|
clientSecret: SPOTIFY_CLIENT_SECRET,
|
||||||
scope: [
|
scope: [
|
||||||
|
"streaming",
|
||||||
"user-read-playback-state",
|
"user-read-playback-state",
|
||||||
"user-read-currently-playing",
|
"user-read-currently-playing",
|
||||||
"user-modify-playback-state",
|
"user-modify-playback-state",
|
||||||
|
|
|
||||||
|
|
@ -61,8 +61,13 @@ export function Question() {
|
||||||
question?.song?.platform === "spotify" && question.song.platform_id
|
question?.song?.platform === "spotify" && question.song.platform_id
|
||||||
? `spotify:track:${question.song.platform_id}`
|
? `spotify:track:${question.song.platform_id}`
|
||||||
: null;
|
: null;
|
||||||
const { enabled: spotifyEnabled, setEnabled: setSpotifyEnabled } =
|
const {
|
||||||
useSpotifyPlayer(spotifyTrackUri);
|
enabled: spotifyEnabled,
|
||||||
|
setEnabled: setSpotifyEnabled,
|
||||||
|
status: spotifyStatus,
|
||||||
|
error: spotifyError,
|
||||||
|
isLoading: spotifyIsLoading,
|
||||||
|
} = useSpotifyPlayer(spotifyTrackUri);
|
||||||
const questionStartTimestamp = question?.startTimestamp ?? null;
|
const questionStartTimestamp = question?.startTimestamp ?? null;
|
||||||
const questionAnnouncement = question
|
const questionAnnouncement = question
|
||||||
? getQuestionAnnouncement(question)
|
? getQuestionAnnouncement(question)
|
||||||
|
|
@ -191,6 +196,7 @@ export function Question() {
|
||||||
<Switch
|
<Switch
|
||||||
id="spotify-playback"
|
id="spotify-playback"
|
||||||
checked={spotifyEnabled}
|
checked={spotifyEnabled}
|
||||||
|
disabled={spotifyIsLoading || !spotifyTrackUri}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
void setSpotifyEnabled(checked === true)
|
void setSpotifyEnabled(checked === true)
|
||||||
}
|
}
|
||||||
|
|
@ -201,6 +207,13 @@ export function Question() {
|
||||||
? "Listen closely and guess the song."
|
? "Listen closely and guess the song."
|
||||||
: (question.song?.name ??
|
: (question.song?.name ??
|
||||||
"This question has no associated song.")}
|
"This question has no associated song.")}
|
||||||
|
{!spotifyTrackUri
|
||||||
|
? " Spotify playback is unavailable for this question."
|
||||||
|
: spotifyError
|
||||||
|
? ` ${spotifyError}`
|
||||||
|
: spotifyStatus === "loading"
|
||||||
|
? " Connecting Spotify..."
|
||||||
|
: null}
|
||||||
</ItemDescription>
|
</ItemDescription>
|
||||||
</ItemContent>
|
</ItemContent>
|
||||||
</Item>
|
</Item>
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ export function useSpotifyPlayer(trackUri: string | null | undefined) {
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const playerRef = useRef<SpotifyPlayer | null>(null);
|
const playerRef = useRef<SpotifyPlayer | null>(null);
|
||||||
|
const playerPromiseRef = useRef<Promise<SpotifyPlayer> | null>(null);
|
||||||
const deviceIdRef = useRef<string | null>(null);
|
const deviceIdRef = useRef<string | null>(null);
|
||||||
const trackUriRef = useRef<string | null | undefined>(trackUri);
|
const trackUriRef = useRef<string | null | undefined>(trackUri);
|
||||||
const enabledRef = useRef(enabled);
|
const enabledRef = useRef(enabled);
|
||||||
|
|
@ -77,6 +78,130 @@ export function useSpotifyPlayer(trackUri: string | null | undefined) {
|
||||||
trackUriRef.current = trackUri;
|
trackUriRef.current = trackUri;
|
||||||
}, [trackUri]);
|
}, [trackUri]);
|
||||||
|
|
||||||
|
const ensurePlayer = useCallback(async (options?: { silent?: boolean }) => {
|
||||||
|
if (playerRef.current) return playerRef.current;
|
||||||
|
if (playerPromiseRef.current) return playerPromiseRef.current;
|
||||||
|
|
||||||
|
const silent = options?.silent === true;
|
||||||
|
if (!silent) {
|
||||||
|
setStatus("loading");
|
||||||
|
setError(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
playerPromiseRef.current = (async () => {
|
||||||
|
await loadSpotifySdk();
|
||||||
|
|
||||||
|
if (!window.Spotify) {
|
||||||
|
throw new Error("Spotify playback SDK did not initialize.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let settled = false;
|
||||||
|
let readyTimeoutId: number | null = null;
|
||||||
|
let resolveReady: ((deviceId: string) => void) | null = null;
|
||||||
|
let rejectReady: ((error: Error) => void) | null = null;
|
||||||
|
const readyPromise = new Promise<string>((resolve, reject) => {
|
||||||
|
resolveReady = resolve;
|
||||||
|
rejectReady = reject;
|
||||||
|
readyTimeoutId = window.setTimeout(() => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
reject(new Error("Timed out waiting for Spotify playback device."));
|
||||||
|
}, 15_000);
|
||||||
|
});
|
||||||
|
|
||||||
|
const fail = (message: string) => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
if (readyTimeoutId) window.clearTimeout(readyTimeoutId);
|
||||||
|
rejectReady?.(new Error(message));
|
||||||
|
};
|
||||||
|
|
||||||
|
const player = new window.Spotify.Player({
|
||||||
|
name: "ITPDP Quiz",
|
||||||
|
volume: 0.8,
|
||||||
|
getOAuthToken: async (cb) => {
|
||||||
|
try {
|
||||||
|
cb(await fetchSpotifyAccessToken());
|
||||||
|
} catch (error_) {
|
||||||
|
const message = parseSpotifyError(error_);
|
||||||
|
fail(message);
|
||||||
|
setError(message);
|
||||||
|
setStatus("error");
|
||||||
|
cb("");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
player.addListener("ready", ({ device_id }) => {
|
||||||
|
deviceIdRef.current = device_id;
|
||||||
|
if (readyTimeoutId) window.clearTimeout(readyTimeoutId);
|
||||||
|
settled = true;
|
||||||
|
resolveReady?.(device_id);
|
||||||
|
setStatus("ready");
|
||||||
|
});
|
||||||
|
player.addListener("not_ready", ({ device_id }) => {
|
||||||
|
if (deviceIdRef.current === device_id) {
|
||||||
|
deviceIdRef.current = null;
|
||||||
|
}
|
||||||
|
setStatus(enabledRef.current ? "loading" : "paused");
|
||||||
|
});
|
||||||
|
player.addListener("initialization_error", ({ message }) => {
|
||||||
|
fail(message);
|
||||||
|
setError(message);
|
||||||
|
setStatus("error");
|
||||||
|
});
|
||||||
|
player.addListener("authentication_error", ({ message }) => {
|
||||||
|
fail(message);
|
||||||
|
setError(message);
|
||||||
|
setStatus("error");
|
||||||
|
});
|
||||||
|
player.addListener("account_error", ({ message }) => {
|
||||||
|
fail(message);
|
||||||
|
setError(message);
|
||||||
|
setStatus("error");
|
||||||
|
});
|
||||||
|
player.addListener("playback_error", ({ message }) => {
|
||||||
|
fail(message);
|
||||||
|
setError(message);
|
||||||
|
setStatus("error");
|
||||||
|
});
|
||||||
|
player.addListener("autoplay_failed", () => {
|
||||||
|
setError(
|
||||||
|
"Spotify playback was blocked by the browser autoplay policy.",
|
||||||
|
);
|
||||||
|
setStatus("error");
|
||||||
|
});
|
||||||
|
|
||||||
|
const connected = await player.connect();
|
||||||
|
if (!connected) {
|
||||||
|
throw new Error("Unable to connect to Spotify playback.");
|
||||||
|
}
|
||||||
|
|
||||||
|
playerRef.current = player;
|
||||||
|
const deviceId = await readyPromise;
|
||||||
|
deviceIdRef.current = deviceId;
|
||||||
|
return player;
|
||||||
|
})()
|
||||||
|
.catch((error_: unknown) => {
|
||||||
|
playerRef.current?.disconnect();
|
||||||
|
playerRef.current = null;
|
||||||
|
deviceIdRef.current = null;
|
||||||
|
throw error_;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
playerPromiseRef.current = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return playerPromiseRef.current;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!trackUri || playerRef.current || playerPromiseRef.current) return;
|
||||||
|
void ensurePlayer({ silent: true }).catch(() => {
|
||||||
|
// Ignore preload failures; explicit enable will surface them.
|
||||||
|
});
|
||||||
|
}, [trackUri, ensurePlayer]);
|
||||||
|
|
||||||
const pause = useCallback(async () => {
|
const pause = useCallback(async () => {
|
||||||
const player = playerRef.current;
|
const player = playerRef.current;
|
||||||
if (!player) return;
|
if (!player) return;
|
||||||
|
|
@ -87,83 +212,17 @@ export function useSpotifyPlayer(trackUri: string | null | undefined) {
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const ensurePlayer = useCallback(async () => {
|
|
||||||
if (playerRef.current) return playerRef.current;
|
|
||||||
|
|
||||||
setStatus("loading");
|
|
||||||
setError(null);
|
|
||||||
await loadSpotifySdk();
|
|
||||||
|
|
||||||
if (!window.Spotify) {
|
|
||||||
throw new Error("Spotify playback SDK did not initialize.");
|
|
||||||
}
|
|
||||||
|
|
||||||
let resolveReady: ((deviceId: string) => void) | null = null;
|
|
||||||
const readyPromise = new Promise<string>((resolve) => {
|
|
||||||
resolveReady = resolve;
|
|
||||||
});
|
|
||||||
|
|
||||||
const player = new window.Spotify.Player({
|
|
||||||
name: "ITPDP Quiz",
|
|
||||||
volume: 0.8,
|
|
||||||
getOAuthToken: async (cb) => {
|
|
||||||
try {
|
|
||||||
cb(await fetchSpotifyAccessToken());
|
|
||||||
} catch (error_) {
|
|
||||||
setError(parseSpotifyError(error_));
|
|
||||||
setStatus("error");
|
|
||||||
cb("");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
player.addListener("ready", ({ device_id }) => {
|
|
||||||
deviceIdRef.current = device_id;
|
|
||||||
resolveReady?.(device_id);
|
|
||||||
setStatus("ready");
|
|
||||||
});
|
|
||||||
player.addListener("not_ready", ({ device_id }) => {
|
|
||||||
if (deviceIdRef.current === device_id) {
|
|
||||||
deviceIdRef.current = null;
|
|
||||||
}
|
|
||||||
setStatus(enabledRef.current ? "loading" : "paused");
|
|
||||||
});
|
|
||||||
player.addListener("initialization_error", ({ message }) => {
|
|
||||||
setError(message);
|
|
||||||
setStatus("error");
|
|
||||||
});
|
|
||||||
player.addListener("authentication_error", ({ message }) => {
|
|
||||||
setError(message);
|
|
||||||
setStatus("error");
|
|
||||||
});
|
|
||||||
player.addListener("account_error", ({ message }) => {
|
|
||||||
setError(message);
|
|
||||||
setStatus("error");
|
|
||||||
});
|
|
||||||
player.addListener("playback_error", ({ message }) => {
|
|
||||||
setError(message);
|
|
||||||
setStatus("error");
|
|
||||||
});
|
|
||||||
|
|
||||||
const connected = await player.connect();
|
|
||||||
if (!connected) {
|
|
||||||
throw new Error("Unable to connect to Spotify playback.");
|
|
||||||
}
|
|
||||||
|
|
||||||
playerRef.current = player;
|
|
||||||
await readyPromise;
|
|
||||||
return player;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const playTrack = useCallback(
|
const playTrack = useCallback(
|
||||||
async (uri: string) => {
|
async (uri: string) => {
|
||||||
if (!uri) return;
|
if (!uri) return;
|
||||||
const player = await ensurePlayer();
|
const player = playerRef.current ?? (await ensurePlayer());
|
||||||
const deviceId = deviceIdRef.current;
|
const deviceId = deviceIdRef.current;
|
||||||
if (!player || !deviceId) {
|
if (!player || !deviceId) {
|
||||||
throw new Error("Spotify playback device is not ready.");
|
throw new Error("Spotify playback device is not ready.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await player.activateElement();
|
||||||
|
|
||||||
const accessToken = await fetchSpotifyAccessToken();
|
const accessToken = await fetchSpotifyAccessToken();
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`https://api.spotify.com/v1/me/player/play?device_id=${encodeURIComponent(deviceId)}`,
|
`https://api.spotify.com/v1/me/player/play?device_id=${encodeURIComponent(deviceId)}`,
|
||||||
|
|
@ -201,7 +260,9 @@ export function useSpotifyPlayer(trackUri: string | null | undefined) {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ensurePlayer();
|
if (!playerRef.current) {
|
||||||
|
await ensurePlayer();
|
||||||
|
}
|
||||||
if (trackUriRef.current) {
|
if (trackUriRef.current) {
|
||||||
await playTrack(trackUriRef.current);
|
await playTrack(trackUriRef.current);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { Link } from "@tanstack/react-router";
|
|
||||||
import { authClient } from "#/lib/auth-client";
|
import { authClient } from "#/lib/auth-client";
|
||||||
|
|
||||||
export default function BetterAuthHeader() {
|
export default function BetterAuthHeader() {
|
||||||
|
|
@ -22,24 +21,22 @@ export default function BetterAuthHeader() {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<button
|
<a
|
||||||
onClick={() => {
|
href="/logout"
|
||||||
void authClient.signOut();
|
className="flex-1 inline-flex h-9 items-center justify-center px-4 text-sm font-medium bg-white dark:bg-neutral-900 text-neutral-900 dark:text-neutral-50 border border-neutral-300 dark:border-neutral-700 hover:bg-neutral-50 dark:hover:bg-neutral-800 transition-colors"
|
||||||
}}
|
|
||||||
className="flex-1 h-9 px-4 text-sm font-medium bg-white dark:bg-neutral-900 text-neutral-900 dark:text-neutral-50 border border-neutral-300 dark:border-neutral-700 hover:bg-neutral-50 dark:hover:bg-neutral-800 transition-colors"
|
|
||||||
>
|
>
|
||||||
Sign out
|
Sign out
|
||||||
</button>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<a
|
||||||
to="/demo/better-auth"
|
href="/demo/better-auth"
|
||||||
className="h-9 px-4 text-sm font-medium bg-white dark:bg-neutral-900 text-neutral-900 dark:text-neutral-50 border border-neutral-300 dark:border-neutral-700 hover:bg-neutral-50 dark:hover:bg-neutral-800 transition-colors inline-flex items-center"
|
className="h-9 px-4 text-sm font-medium bg-white dark:bg-neutral-900 text-neutral-900 dark:text-neutral-50 border border-neutral-300 dark:border-neutral-700 hover:bg-neutral-50 dark:hover:bg-neutral-800 transition-colors inline-flex items-center"
|
||||||
>
|
>
|
||||||
Sign in
|
Sign in
|
||||||
</Link>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,15 @@
|
||||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||||
|
|
||||||
import { Route as rootRouteImport } from './routes/__root'
|
import { Route as rootRouteImport } from './routes/__root'
|
||||||
|
import { Route as LogoutRouteImport } from './routes/logout'
|
||||||
import { Route as LoginRouteImport } from './routes/login'
|
import { Route as LoginRouteImport } from './routes/login'
|
||||||
import { Route as IndexRouteImport } from './routes/index'
|
import { Route as IndexRouteImport } from './routes/index'
|
||||||
|
|
||||||
|
const LogoutRoute = LogoutRouteImport.update({
|
||||||
|
id: '/logout',
|
||||||
|
path: '/logout',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
const LoginRoute = LoginRouteImport.update({
|
const LoginRoute = LoginRouteImport.update({
|
||||||
id: '/login',
|
id: '/login',
|
||||||
path: '/login',
|
path: '/login',
|
||||||
|
|
@ -26,31 +32,42 @@ const IndexRoute = IndexRouteImport.update({
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
|
'/logout': typeof LogoutRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
|
'/logout': typeof LogoutRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
|
'/logout': typeof LogoutRoute
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
fullPaths: '/' | '/login'
|
fullPaths: '/' | '/login' | '/logout'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to: '/' | '/login'
|
to: '/' | '/login' | '/logout'
|
||||||
id: '__root__' | '/' | '/login'
|
id: '__root__' | '/' | '/login' | '/logout'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
IndexRoute: typeof IndexRoute
|
IndexRoute: typeof IndexRoute
|
||||||
LoginRoute: typeof LoginRoute
|
LoginRoute: typeof LoginRoute
|
||||||
|
LogoutRoute: typeof LogoutRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@tanstack/react-router' {
|
declare module '@tanstack/react-router' {
|
||||||
interface FileRoutesByPath {
|
interface FileRoutesByPath {
|
||||||
|
'/logout': {
|
||||||
|
id: '/logout'
|
||||||
|
path: '/logout'
|
||||||
|
fullPath: '/logout'
|
||||||
|
preLoaderRoute: typeof LogoutRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
'/login': {
|
'/login': {
|
||||||
id: '/login'
|
id: '/login'
|
||||||
path: '/login'
|
path: '/login'
|
||||||
|
|
@ -71,6 +88,7 @@ declare module '@tanstack/react-router' {
|
||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexRoute: IndexRoute,
|
IndexRoute: IndexRoute,
|
||||||
LoginRoute: LoginRoute,
|
LoginRoute: LoginRoute,
|
||||||
|
LogoutRoute: LogoutRoute,
|
||||||
}
|
}
|
||||||
export const routeTree = rootRouteImport
|
export const routeTree = rootRouteImport
|
||||||
._addFileChildren(rootRouteChildren)
|
._addFileChildren(rootRouteChildren)
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ export const Route = createRootRouteWithContext<MyRouterContext>()({
|
||||||
}),
|
}),
|
||||||
shellComponent: RootDocument,
|
shellComponent: RootDocument,
|
||||||
beforeLoad: async ({ context, location }) => {
|
beforeLoad: async ({ context, location }) => {
|
||||||
const authPublicPaths = new Set(["/login"]);
|
const authPublicPaths = new Set(["/login", "/logout"]);
|
||||||
const isAuthPublicPath = authPublicPaths.has(location.pathname);
|
const isAuthPublicPath = authPublicPaths.has(location.pathname);
|
||||||
let session: AuthSession | null;
|
let session: AuthSession | null;
|
||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
|
|
|
||||||
24
web/src/routes/logout.tsx
Normal file
24
web/src/routes/logout.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { createFileRoute, useRouter } from "@tanstack/react-router";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import { signOutAndClearQueryCache } from "#/lib/auth-client";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/logout")({
|
||||||
|
component: LogoutRoute,
|
||||||
|
});
|
||||||
|
|
||||||
|
function LogoutRoute() {
|
||||||
|
const { queryClient } = Route.useRouteContext();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void signOutAndClearQueryCache({
|
||||||
|
queryClient,
|
||||||
|
navigateToLogin: () => router.navigate({ to: "/login", replace: true }),
|
||||||
|
});
|
||||||
|
}, [queryClient, router]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6 text-sm text-muted-foreground">Signing out...</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
web/src/types/spotify.d.ts
vendored
5
web/src/types/spotify.d.ts
vendored
|
|
@ -39,6 +39,8 @@ declare global {
|
||||||
| "playback_error",
|
| "playback_error",
|
||||||
callback: (payload: { message: string }) => void,
|
callback: (payload: { message: string }) => void,
|
||||||
): void;
|
): void;
|
||||||
|
addListener(event: "autoplay_failed", callback: () => void): void;
|
||||||
|
activateElement(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type SpotifyPlayerEvent =
|
type SpotifyPlayerEvent =
|
||||||
|
|
@ -47,5 +49,6 @@ declare global {
|
||||||
| "initialization_error"
|
| "initialization_error"
|
||||||
| "authentication_error"
|
| "authentication_error"
|
||||||
| "account_error"
|
| "account_error"
|
||||||
| "playback_error";
|
| "playback_error"
|
||||||
|
| "autoplay_failed";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue