Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,677 changes: 1,461 additions & 216 deletions package-lock.json

Large diffs are not rendered by default.

20 changes: 10 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,35 +16,35 @@
},
"devDependencies": {
"@melt-ui/pp": "0.1.4",
"@melt-ui/svelte": "0.63.1",
"@melt-ui/svelte": "0.64.5",
"@playwright/test": "^1.40.1",
"@sveltejs/adapter-auto": "^2.1.1",
"@sveltejs/adapter-netlify": "2.0.8",
"@sveltejs/enhanced-img": "0.1.5",
"@sveltejs/kit": "^1.27.6",
"@types/node": "20.10.1",
"@typescript-eslint/eslint-plugin": "^6.13.1",
"@typescript-eslint/parser": "^6.13.1",
"@sveltejs/kit": "^1.27.7",
"@types/node": "20.10.3",
"@typescript-eslint/eslint-plugin": "^6.13.2",
"@typescript-eslint/parser": "^6.13.2",
"autoprefixer": "10.4.16",
"dotenv": "^16.3.1",
"drizzle-kit": "^0.20.6",
"eslint": "^8.54.0",
"eslint-config-prettier": "^9.0.0",
"eslint": "^8.55.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.35.1",
"lefthook": "1.5.5",
"npm-run-all": "4.1.5",
"postcss": "8.4.31",
"postcss": "8.4.32",
"postcss-jit-props": "1.0.14",
"prettier": "^3.1.0",
"prettier-plugin-svelte": "^3.1.2",
"svelte": "^4.2.8",
"svelte-check": "^3.6.2",
"svelte-sequential-preprocessor": "2.0.1",
"tslib": "^2.6.2",
"tsx": "^4.6.1",
"tsx": "^4.6.2",
"typescript": "^5.3.2",
"vite": "^4.5.0",
"vitest": "^0.34.6"
"vitest": "^1.0.1"
},
"private": true,
"scripts": {
Expand Down
33 changes: 25 additions & 8 deletions src/hooks.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,21 @@ function isProtectedRoute(pathname: string) {
}

export const handle: Handle = async ({ event, resolve }) => {
event.locals.auth = auth.handleRequest(event);
const session = await event.locals.auth.validate();
if (event.url.pathname === "/") {
return resolve(event);
}

let session = null;

try {
event.locals.auth = auth.handleRequest(event);
session = await event.locals.auth.validate();
} catch (error) {
console.error(error);
}

if (session) {
if (
event.url.pathname.startsWith("/login") ||
event.url.pathname === "/app" ||
event.url.pathname === "/"
) {
if (event.url.pathname.startsWith("/login") || event.url.pathname === "/app") {
throw redirect(303, "/app/playlists");
}
} else {
Expand All @@ -26,5 +32,16 @@ export const handle: Handle = async ({ event, resolve }) => {
}
}

return await resolve(event);
return resolve(event);
};

export const handleError = (error: Error) => {
console.error(error);

return new Response(null, {
status: 302,
headers: {
Location: "/",
},
});
};
2 changes: 1 addition & 1 deletion src/lib/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { auth } from "$lib/server/lucia";

export const actions: Actions = {
logout: async ({ locals }) => {
const session = await locals.auth.validate();
const session = await locals.auth?.validate();

if (!session) return fail(401);

Expand Down
7 changes: 3 additions & 4 deletions src/lib/components/app.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
{ icon: "icon-track", href: "/app/tracks", label: "Tracks" },
];

$: isLoggedIn = avatar;
$: isLoggedIn = Boolean(avatar);
$: rootUrl = isLoggedIn ? "/app/playlists" : "/";
$: currentPath = $page.url.pathname;
$: isLinkActive = (href: string) => currentPath.startsWith(href);
Expand All @@ -43,9 +43,8 @@
<div class="app__content">
<nav class="app__rail">
<div class="app__rail__lead">
{#if isLoggedIn}
<RailLinks {links} {isLinkActive} />
{/if}
<RailLinks {links} {isLinkActive} {isLoggedIn} />

<slot name="rail-lead" />
</div>

Expand Down
3 changes: 2 additions & 1 deletion src/lib/components/rail-links.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@

export let links: { icon: string; href: string; label: string }[];
export let isLinkActive: (href: string) => boolean;
export let isLoggedIn: boolean;
</script>

<nav class="rail__links">
<nav class="rail__links" data-sveltekit-preload-data={isLoggedIn}>
{#each links as link}
<a class="rail__link title" class:active={isLinkActive(link.href)} href={link.href}>
<span class="rail__link__icon">
Expand Down
34 changes: 20 additions & 14 deletions src/lib/server/api.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

import type { AuthRequest } from "lucia";
import { getAccessToken } from "./token";

// TODO: Set signal on session?
import { getAccessToken } from "./token";

export async function queryApiFn(authRequest: AuthRequest) {
const session = await authRequest.validate();
const accessToken = await getAccessToken(session);

return async function queryApi<T>(endpoint: string, options: RequestInit = {}) {
const res = await fetch(endpoint, {
...options,
headers: {
"cache-control": "public, max-age=3600",
Authorization: `Bearer ${accessToken}`,
},
});

if (res.ok === false) {
throw new Error("Failed to fetch data", { cause: res });
}
try {
const res = await fetch(endpoint, {
...options,
headers: {
"cache-control": "public, max-age=3600",
Authorization: `Bearer ${accessToken}`,
},
});

if (res.ok === false) {
throw new Error("Failed to fetch data", { cause: res });
}

return res.json() as Promise<T>;
return res.json() as Promise<T>;
} catch (error: any) {
console.error(`queryApi(${endpoint}): ${error.message}`);
return null;
}
};
}

Expand Down
2 changes: 1 addition & 1 deletion src/lib/server/lucia.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const auth = lucia({
spotifyAccessExpiresAt: data.access_expires_at,
};
},
getSessionAttributes: (): { tokenRefreshing?: Promise<string> } => ({}),
getSessionAttributes: (): { tokenRefreshing?: Promise<string | null> } => ({}),
});

export const spotifyAuth = spotify(auth, {
Expand Down
7 changes: 6 additions & 1 deletion src/lib/server/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export async function getAccessToken(session: Session | null) {
throw new Error("No session provided");
}

let accessToken = session.user.spotifyAccessToken;
let accessToken: string | null = session.user.spotifyAccessToken;

if (validateUserAccessToken(session)) {
return accessToken;
Expand All @@ -74,6 +74,11 @@ export async function getAccessToken(session: Session | null) {

session.tokenRefreshing = refreshUserAccessToken(secrets, session);
accessToken = await session.tokenRefreshing;

if (accessToken === null) {
throw new Error("Failed to refresh access token");
}

delete session.tokenRefreshing;

return accessToken;
Expand Down
18 changes: 11 additions & 7 deletions src/lib/stores/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,18 @@ export function createPageStore<T>(endpoint: string) {

const url = new URL(endpoint, window.location.origin);
const res = await fetch(getAppEndpoint(next, url));
const data = (await res.json()) as Page<T>;

update((state) => {
return {
...data,
items: [...state.items, ...(data.items ?? [])],
};
});
if (res.ok) {
const json = await res.json();
const data = json as Page<T>;

update((state) => {
return {
...data,
items: [...state.items, ...(data.items ?? [])],
};
});
}
},
};
}
2 changes: 1 addition & 1 deletion src/lib/typings/app.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { AudioFeatures, SimplifiedTrack } from "$lib/typings/spotify";

export type QueryApi = <T>(endpoint: string, options?: RequestInit) => Promise<T>;
export type QueryApi = <T>(endpoint: string, options?: RequestInit) => Promise<T | null>;

export type TrackAudioFeatures = Pick<AudioFeatures, "mode" | "tempo" | "key">;

Expand Down
8 changes: 4 additions & 4 deletions src/lib/utils/track.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ export async function getAudioFeatures({
const ids = [...uniqueIds].join(",");

const endpoint = getEndpoint("audio-features", { ids });
const { audio_features } = await queryApi<AudioFeaturesCollection>(endpoint);
const res = await queryApi<AudioFeaturesCollection>(endpoint);

const trackAudio: Record<string, TrackAudioFeatures> = {};
if (audio_features?.[0]) {
for (const feature of audio_features) {
if (res?.audio_features?.[0]) {
for (const feature of res?.audio_features ?? []) {
const { id, mode, tempo, key } = feature ?? {};
trackAudio[id] = { mode, tempo: Math.round(tempo), key };
}
Expand Down Expand Up @@ -65,7 +65,7 @@ export async function injectAudio(queryApi: QueryApi, rawTracks: TrackItem[] | A
return playableTracks;
}

export function getTrackLinks(track: Track | SimplifiedTrack | undefined) {
export function getTrackLinks(track: Track | SimplifiedTrack | null) {
if (!track) return;

const bandcampArtist = track.artists[0]?.name;
Expand Down
4 changes: 2 additions & 2 deletions src/routes/+error.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script>
// src/routes/blog/[slug]/+page.svelte

import { page } from "$app/stores";

console.error($page.error);
</script>

<h1>{$page.status}: {$page.error?.message}</h1>
Expand Down
2 changes: 1 addition & 1 deletion src/routes/+layout.server.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { LayoutServerLoad } from "./$types";

export const load: LayoutServerLoad = async ({ locals }) => {
const session = await locals.auth.validate();
const session = await locals.auth?.validate();

if (!session) return;

Expand Down
10 changes: 6 additions & 4 deletions src/routes/api/albums/[id]/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,18 @@ const getDefaultParams = () => ({

// TODO: add `albums/${params.id}/tracks` endpoint
export const GET = async ({ locals, params, url }) => {
const queryApi = await queryApiFn(locals.auth);
const queryApi = await queryApiFn(locals?.auth);

if (!queryApi) return json(null);

const endpoint = getEndpoint(`albums/${params.id}`, mergeParams(getDefaultParams(), url));
const album = await queryApi<Album>(endpoint);

const trackItems = (album?.tracks?.items ?? []) as AudioTrack[];
const audioTracks = await injectAudio(queryApi, trackItems);
album.tracks.items = audioTracks;
if (album) {
const trackItems = (album?.tracks?.items ?? []) as AudioTrack[];
const audioTracks = await injectAudio(queryApi, trackItems);
album.tracks.items = audioTracks;
}

return json(album);
};
2 changes: 1 addition & 1 deletion src/routes/api/artists/[id]/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const GET = async ({ locals, params }) => {

try {
const [artist, topTracks, albums, appearsOn, related] = await Promise.all(requests);
const tracks = await injectAudio(queryApi, topTracks.tracks);
const tracks = await injectAudio(queryApi, topTracks?.tracks ?? []);

return json({
artist,
Expand Down
4 changes: 2 additions & 2 deletions src/routes/api/artists/following/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export async function GET({ locals, url }) {
const urlParams = mergeParams(getDefaultParams(), url);
const endpoint = getEndpoint(`me/following`, urlParams);

const { artists } = await queryApi<{ artists: Page<Artist> }>(endpoint);
const res = await queryApi<{ artists: Page<Artist> }>(endpoint);

return json(artists);
return json(res?.artists);
}
15 changes: 10 additions & 5 deletions src/routes/api/playlists/[id]/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,15 @@ export async function GET({ locals, params, fetch, url }) {
const playlist = await queryApi<Playlist>(endpoint);

const res = await fetch(`/api/playlists/${params.id}/tracks?offset=0`);
const tracks = (await res.json()) as Page<AudioTrack>;

return json({
playlist,
tracks,
});
if (res.ok) {
const tracks = (await res.json()) as Page<AudioTrack>;

return json({
playlist,
tracks,
});
}

return json(null);
}
7 changes: 4 additions & 3 deletions src/routes/api/playlists/[id]/tracks/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@ export async function GET({ params, locals, url }) {
`playlists/${params.id}/tracks`,
mergeParams(getDefaultParams(), url),
);
const { items, ...page } = await queryApi<Page<PlaylistedTrack<TrackItem>>>(endpoint);
const trackItems = items.map(({ track }) => track);
const audioTracks = await injectAudio(queryApi, trackItems);
const res = await queryApi<Page<PlaylistedTrack<TrackItem>>>(endpoint);
const { items, ...page } = res ?? {};
const trackItems = items?.map(({ track }) => track);
const audioTracks = await injectAudio(queryApi, trackItems ?? []);
const tracks = { ...page, items: audioTracks };

return json(tracks);
Expand Down
2 changes: 1 addition & 1 deletion src/routes/api/tracks/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export async function GET({ locals, url }) {
if (!queryApi) return json(null);

const endpoint = getEndpoint("me/top/tracks", mergeParams(getDefaultParams(), url));
const page = await queryApi<Page<AudioTrack>>(endpoint);
const page = (await queryApi<Page<AudioTrack>>(endpoint)) ?? { items: [] };

page.items = await injectAudio(queryApi, page.items);

Expand Down
Loading