Skip to content

Commit

Permalink
Add dimension selectors and auth refresh
Browse files Browse the repository at this point in the history
  • Loading branch information
cephalization committed Sep 19, 2024
1 parent cd08110 commit 17db224
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 25 deletions.
2 changes: 1 addition & 1 deletion app/components/ui/label.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as React from "react";
import { cn } from "~/lib/utils";

const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
"flex flex-col gap-2 text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
);

const Label = React.forwardRef<
Expand Down
4 changes: 4 additions & 0 deletions app/entry.server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import { renderToPipeableStream } from "react-dom/server";

const ABORT_DELAY = 5_000;

process.on("unhandledRejection", (reason) => {
console.error("Unhandled rejection:", reason);
});

export default function handleRequest(
request: Request,
responseStatusCode: number,
Expand Down
19 changes: 14 additions & 5 deletions app/lib/auth.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,23 @@ export const getAuth = async (
try {
const session = await authSession.getSession(request.headers.get("Cookie"));
const auth = authSchema.parse(session.get("auth"));
if (auth.expires_at < Date.now()) {
// TODO: refresh token
}
const refresh = async () => {
const r = await refreshAuth(auth.refresh_token);
session.set("auth", r);
throw redirect(request.url, { status: 302 });
};
return {
auth,
spotifyClient: new SpotifyClient({ accessToken: auth.access_token }),
spotifyClient: new SpotifyClient({
accessToken: auth.access_token,
onRefresh: refresh,
}),
};
} catch (e) {
if (e instanceof Response && e.status === 302) {
throw e;
}
if (e instanceof ZodError) {
console.error("Auth error:\n", e.message);
} else {
console.error("Auth error:\n", e);
}
Expand Down Expand Up @@ -90,6 +97,7 @@ export const setAuth = async (
};

export const refreshAuth = async (refreshToken: string): Promise<Auth> => {
console.log("refreshingAuth");
const response = await fetch(`https://accounts.spotify.com/api/token`, {
method: "POST",
headers: {
Expand All @@ -106,6 +114,7 @@ export const refreshAuth = async (refreshToken: string): Promise<Auth> => {
});

const data = await response.json();
data.refresh_token = refreshToken;
const auth = authResponseSchema.parse(data);
const spotifyClient = new SpotifyClient({ accessToken: auth.access_token });
const userData = await spotifyClient.getUser();
Expand Down
10 changes: 3 additions & 7 deletions app/lib/spotify.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,9 @@ export class SpotifyClient {
...options,
});
if (response.status === 401 && retry && this.onRefresh) {
const newToken = await this.onRefresh();
if (newToken) {
this.accessToken = newToken;
return this.request(url, options, false);
} else {
throw new Error("Failed to refresh access token");
}
console.log("refreshing token in client");
// this should throw a redirect
this.onRefresh();
} else if (response.status === 401) {
throw new Error("Unauthorized");
}
Expand Down
92 changes: 88 additions & 4 deletions app/routes/_app.playlist.$id.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ import throttle from "just-throttle";
import { useEffect, useMemo, useState } from "react";
import invariant from "tiny-invariant";
import { Card, CardDescription } from "~/components/ui/card";
import { Label } from "~/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "~/components/ui/select";
import { Tooltip, TooltipContent } from "~/components/ui/tooltip";
import { getAuth } from "~/lib/auth.server";
import { PlaylistTrackResponse, TracksFeaturesResponse } from "~/lib/schemas";
Expand Down Expand Up @@ -45,9 +53,64 @@ const getStyleFromCoordinates = (x: number, y: number) => {
};
};

const dimensions: (keyof TracksFeaturesResponse["audio_features"][number])[] = [
"acousticness",
"danceability",
"energy",
"valence",
"instrumentalness",
"liveness",
"speechiness",
];

const DimensionSelector = ({
value,
onChange,
label,
}: {
value: keyof TracksFeaturesResponse["audio_features"][number];
onChange: (
value: keyof TracksFeaturesResponse["audio_features"][number],
) => void;
label: string;
}) => {
return (
<Label>
{label}
<Select
value={value}
onValueChange={(e) =>
onChange(e as keyof TracksFeaturesResponse["audio_features"][number])
}
>
<SelectTrigger>
<SelectValue placeholder="Select a dimension" />
</SelectTrigger>
<SelectContent>
{dimensions.map((dimension) => (
<SelectItem key={dimension} value={dimension}>
{dimension}
</SelectItem>
))}
</SelectContent>
</Select>
</Label>
);
};

export default function Playlist() {
const { playlist, tracks, features } = useLoaderData<typeof loader>();
const [hoveredPoint, setHoveredPoint] = useState<PointBaseProps | null>(null);
const [x, setX] =
useState<keyof TracksFeaturesResponse["audio_features"][number]>(
"acousticness",
);
const [y, setY] =
useState<keyof TracksFeaturesResponse["audio_features"][number]>(
"danceability",
);
const [z, setZ] =
useState<keyof TracksFeaturesResponse["audio_features"][number]>("energy");
const [mousePosition, setMousePosition] = useState<{
x: number;
y: number;
Expand Down Expand Up @@ -89,7 +152,11 @@ export default function Playlist() {
}, [features, tracks]);
const data = useMemo(() => {
return features.audio_features.map((feature) => ({
position: [feature.acousticness, feature.danceability, feature.energy],
position: [feature[x], feature[y], feature[z]] as [
number,
number,
number,
],
metaData: {
uuid: feature.id,
actualLabel: `${featuresAndTracksByTrackId.get(feature.id)?.track.name ?? feature.id}\n${featuresAndTracksByTrackId
Expand All @@ -98,10 +165,10 @@ export default function Playlist() {
.join(", ")}`,
},
})) satisfies PointBaseProps[];
}, [features, featuresAndTracksByTrackId]);
}, [features, featuresAndTracksByTrackId, x, y, z]);

return (
<>
<div className="h-full w-full relative">
<ThreeDimensionalCanvas camera={{ position: [5, 5, 5], zoom: 10 }}>
<pointLight position={[10, 10, 10]} />
<ThreeDimensionalControls
Expand Down Expand Up @@ -139,6 +206,23 @@ export default function Playlist() {
</CardDescription>
</Card>
)}
</>
<div className="absolute top-2 left-2 flex flex-col gap-2">
<DimensionSelector
value={x}
onChange={(value) => setX(value)}
label="X"
/>
<DimensionSelector
value={y}
onChange={(value) => setY(value)}
label="Y"
/>
<DimensionSelector
value={z}
onChange={(value) => setZ(value)}
label="Z"
/>
</div>
</div>
);
}
17 changes: 9 additions & 8 deletions app/routes/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,17 @@ export const loader = async (args: LoaderFunctionArgs) => {
invariant(authenticated, "Not authenticated");
const { auth, spotifyClient } = authenticated;
const playlists = await spotifyClient.getPlaylists();
return json(
{ authenticated: auth, playlists },
{ headers: { "Cache-Control": "private, max-age=300" } },
);
return json({
expiresAt: auth.expires_at,
userId: auth.user_id,
playlists,
});
};

export default function Index() {
const panelRef = useRef<ImperativePanelGroupHandle>(null);
const { state } = useNavigation();
const { authenticated, playlists } = useLoaderData<typeof loader>();
const { expiresAt, userId, playlists } = useLoaderData<typeof loader>();
const isDesktop = useBreakpoint("sm");
const [date, setDate] = useState<Date | null>(null);
const togglePanel = () => {
Expand All @@ -55,9 +56,9 @@ export default function Index() {
}
};
useEffect(() => {
const date = new Date(authenticated.expires_at);
const date = new Date(expiresAt);
setDate(date);
}, [authenticated]);
}, [expiresAt]);
useEffect(() => {
if (!isDesktop) {
openPlaylists();
Expand All @@ -75,7 +76,7 @@ export default function Index() {
>
Playlist Cloud
</Link>
Hi, {authenticated.user_id}
Hi, {userId}
{date ? (
<pre className="max-w-full overflow-auto">
Session Expires at: {date.toLocaleTimeString()}{" "}
Expand Down
28 changes: 28 additions & 0 deletions app/routes/spotify.refresh.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {
ActionFunctionArgs,
LoaderFunctionArgs,
json,
redirect,
} from "@remix-run/node";
import invariant from "tiny-invariant";
import { authSession, refreshAuth, setAuth } from "~/lib/auth.server";

export const action = async (args: ActionFunctionArgs) => {
try {
const { request } = args;
const { refreshToken } = await request.json();
invariant(typeof refreshToken === "string", "refreshToken is required");

const r = await refreshAuth(refreshToken);
const session = await authSession.getSession(request.headers.get("Cookie"));

return json(r, {
headers: {
"Set-Cookie": await authSession.commitSession(session),
},
});
} catch (e) {
console.error(e);
throw redirect("/login");
}
};

0 comments on commit 17db224

Please sign in to comment.