Skip to content

Commit

Permalink
Added spotify integration
Browse files Browse the repository at this point in the history
  • Loading branch information
s1lvax committed Oct 23, 2024
1 parent a842d8c commit a2c202f
Show file tree
Hide file tree
Showing 16 changed files with 390 additions and 21 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/deploy-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ jobs:
echo "CLIENT_SECRET=${{ secrets.CLIENT_SECRET_TESTING }}" >> .env
echo "DATABASE_URL=${{ secrets.DATABASE_URL_TESTING }}" >> .env
echo "AUTH_TRUST_HOST=${{ secrets.AUTH_TRUST_HOST }}" >> .env
echo "SPOTIFY_ID=${{ secrets.SPOTIFY_ID }}" >> .env
echo "SPOTIFY_SECRET=${{ secrets.SPOTIFY_SECRET }}" >> .env
echo "SPOTIFY_REDIRECT=${{ secrets.SPOTIFY_REDIRECT_TESTING }}" >> .env
#Prisma
npx prisma generate
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ jobs:
echo "CLIENT_SECRET=${{ secrets.CLIENT_SECRET }}" >> .env
echo "DATABASE_URL=${{ secrets.DATABASE_URL }}" >> .env
echo "AUTH_TRUST_HOST=${{ secrets.AUTH_TRUST_HOST }}" >> .env
echo "SPOTIFY_ID=${{ secrets.SPOTIFY_ID }}" >> .env
echo "SPOTIFY_SECRET=${{ secrets.SPOTIFY_SECRET }}" >> .env
echo "SPOTIFY_REDIRECT=${{ secrets.SPOTIFY_REDIRECT }}" >> .env
#Prisma
npx prisma generate
Expand Down
26 changes: 19 additions & 7 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,17 @@ datasource db {
}

model User {
id Int @id @default(autoincrement())
githubId Int @unique
githubUsername String @unique
views Int @default(0)
openToCollaborating Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
id Int @id @default(autoincrement())
githubId Int @unique
githubUsername String @unique
views Int @default(0)
openToCollaborating Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
Link Link[]
Skill Skill[]
Hobby Hobby[]
spotifyToken SpotifyToken?
}

model Link {
Expand Down Expand Up @@ -54,5 +55,16 @@ model Hobby {
hobby String
createdAt DateTime @default(now())
postedBy User @relation(fields: [userId], references: [githubId])
}

// prisma/schema.prisma
model SpotifyToken {
id Int @id @default(autoincrement())
usedBy User @relation(fields: [userId], references: [githubId])
userId Int @unique
accessToken String
refreshToken String
expiresAt DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
7 changes: 7 additions & 0 deletions src/app.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@
href="https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"
rel="stylesheet"
/>
<!-- Cloudflare Web Analytics -->
<script
defer
src="https://static.cloudflareinsights.com/beacon.min.js"
data-cf-beacon='{"token": "c0d92e4dcb43400c89c094b727fe5026"}'
></script>
<!-- End Cloudflare Web Analytics -->
<title>Route - Connect and Share Your Profile</title>
%sveltekit.head%
</head>
Expand Down
13 changes: 11 additions & 2 deletions src/lib/components/MyProfile/UserSettings.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import { Button } from '$lib/components/ui/button';
import { copyToClipboard } from '$lib/utils/copyToClipboard';
import { confirmDelete } from '$lib/utils/confirmDelete';
import { ArrowUpRight, Trash2, CircleChevronDown, Github, Copy } from 'lucide-svelte';
import { ArrowUpRight, Trash2, CircleChevronDown, Github, Copy, AudioLines } from 'lucide-svelte';
import { enhance } from '$app/forms';
import type { PageData } from '../../../routes/profile/$types';
Expand All @@ -14,7 +14,16 @@
</script>

<div class="flex w-full justify-end space-x-2">
<div class="flex space-x-2">
<div class="flex flex-row space-x-2">
{#if data.spotifyToken}
<form action="?/unlinkSpotify" method="POST" use:enhance>
<Button variant="destructive"><AudioLines class="mr-2" /> Unlink Spotify</Button>
</form>
{:else}
<Button href="/api/spotify/login"
><AudioLines class="mr-2 text-green-700" /> Link Spotify</Button
>
{/if}
<form action="?/updateOpenToCollaborating" method="POST" use:enhance>
<div class="flex flex-row items-center">
<Button type="submit" variant="ghost">
Expand Down
14 changes: 8 additions & 6 deletions src/lib/components/PublicProfile/UserInfo.svelte
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
<script lang="ts">
import * as Avatar from '$lib/components/ui/avatar';
import { Button } from '$lib/components/ui/button';
import { IdCard, Twitter } from 'lucide-svelte';
import { IdCard, Twitter, AudioLines } from 'lucide-svelte';
import GitHub from 'lucide-svelte/icons/github';
import { Skeleton } from '$lib/components/ui/skeleton';
import { Badge } from '$lib/components/ui/badge';
import type { GithubData } from '$lib/types/GithubData';
import type { PublicProfile } from '$lib/types/PublicProfile';
import * as Card from '$lib/components/ui/card';
import * as Table from '$lib/components/ui/table';
import type { GithubData } from '$lib/types/GithubData';
import type { PublicProfile } from '$lib/types/PublicProfile';
import MusicPlayer from '$lib/components/Shared/MusicPlayer.svelte';
export let githubData: GithubData | null;
export let userData: PublicProfile;
</script>
Expand Down Expand Up @@ -64,11 +65,11 @@
</div>
</div>
</div>
<div class="information">
<div class="information flex flex-col space-y-4">
<Table.Root>
<Table.Header>
<Table.Row>
<Table.Head>Platform</Table.Head>
<Table.Head>Socials</Table.Head>
<Table.Head></Table.Head>
</Table.Row>
</Table.Header>
Expand Down Expand Up @@ -109,6 +110,7 @@
<!-- Socials will be here when we implement it -->
</Table.Body>
</Table.Root>
<MusicPlayer githubUsername={userData.username} />
</div>
</div></Card.Content
>
Expand Down
84 changes: 84 additions & 0 deletions src/lib/components/Shared/MusicPlayer.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<script lang="ts">
import { AudioLines } from 'lucide-svelte'; // For the music icon
import { onMount } from 'svelte';
import * as Card from '$lib/components/ui/card';
import type { Artist, CurrentlyPlaying } from '$lib/types/Spotify';
export let githubUsername;
let currentlyPlaying: CurrentlyPlaying | null = null;
let isListening = false;
// Fetch the currently playing song on mount
onMount(async () => {
try {
const response = await fetch(`/api/spotify/currently-listening/${githubUsername}`);
if (response.ok) {
const data = await response.json();
if (data?.item) {
isListening = true;
currentlyPlaying = {
song: data.item.name,
artist: data.item.artists.map((artist: Artist) => artist.name).join(', '),
songUrl: data.item.external_urls.spotify, // Link to song on Spotify
artistUrl: data.item.artists[0].external_urls.spotify, // Link to the first artist on Spotify
albumImageUrl: data.item.album.images[0]?.url ?? '' // Album image URL
};
} else {
isListening = false;
}
}
} catch (error) {
console.error('Error fetching currently playing song:', error);
}
});
</script>

<Card.Root>
<Card.Header>
<Card.Title>
<div class="flex flex-row items-center">
<AudioLines class="mr-2 text-green-700" /> Music
</div>
</Card.Title>
<Card.Description>The developer is currently listening to</Card.Description>
</Card.Header>

<Card.Content>
{#if isListening}
<div class="flex flex-row items-center">
<!-- Album Image -->
{#if currentlyPlaying?.albumImageUrl}
<img
src={currentlyPlaying.albumImageUrl}
alt="Album Art"
class="mr-4 h-12 w-12 rounded-lg"
/>
{/if}

<div class="flex flex-col">
<!-- Song and Artist links -->
<a
href={currentlyPlaying?.songUrl}
target="_blank"
class="font-semibold hover:cursor-pointer hover:underline"
>
{currentlyPlaying?.song}
</a>
<a
href={currentlyPlaying?.artistUrl}
target="_blank"
class=" text-sm hover:cursor-pointer hover:underline"
>
by {currentlyPlaying?.artist}
</a>
</div>
</div>
{:else}
<div class="flex items-center">
<p>Not listening to anything currently</p>
</div>
{/if}
</Card.Content>
</Card.Root>
14 changes: 14 additions & 0 deletions src/lib/types/Spotify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export interface CurrentlyPlaying {
song: string;
artist: string;
songUrl: string;
artistUrl: string;
albumImageUrl: string;
}

export interface Artist {
name: string;
external_urls: {
spotify: string;
};
}
18 changes: 18 additions & 0 deletions src/lib/utils/spotify/fetchSpotifyCurrentlyPlaying.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// src/lib/utils/spotify/fetchSpotifyCurrentlyPlaying.ts
export const SPOTIFY_API_URL = 'https://api.spotify.com/v1';

export const fetchSpotifyCurrentlyPlaying = async (accessToken: string) => {
const response = await fetch(`${SPOTIFY_API_URL}/me/player/currently-playing`, {
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json'
}
});

if (response.status === 204 || !response.ok) {
// Return null if no content (204) or if there's an error
return null;
}

return await response.json();
};
7 changes: 7 additions & 0 deletions src/lib/utils/spotify/generateRandomString.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { randomBytes } from 'crypto';

export const generateRandomString = (length: number): string => {
return randomBytes(Math.ceil(length / 2))
.toString('hex')
.slice(0, length);
};
23 changes: 23 additions & 0 deletions src/lib/utils/spotify/refreshSpotifyToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// src/lib/utils/spotify/refreshSpotifyToken.ts
export const refreshSpotifyToken = async (refreshToken: string) => {
const response = await fetch('https://accounts.spotify.com/api/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Basic ${Buffer.from(
`${process.env.SPOTIFY_CLIENT_ID}:${process.env.SPOTIFY_CLIENT_SECRET}`
).toString('base64')}`
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken
})
});

if (!response.ok) {
console.error('Failed to refresh Spotify token:', response.statusText);
return null;
}

return await response.json();
};
22 changes: 22 additions & 0 deletions src/lib/utils/spotify/unlinkSpotify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { prisma } from '$lib/server/prisma';

export const unlinkSpotify = async (githubId: number) => {
try {
// fetch current state
const user = await prisma.user.findUnique({
where: { githubId: githubId }
});

if (!user) {
throw new Error('User not found');
}

// delete token from db
await prisma.spotifyToken.delete({
where: { userId: githubId }
});
} catch (error) {
console.error(error);
throw new Error('Failed to delete Spotify');
}
};
72 changes: 72 additions & 0 deletions src/routes/api/spotify/callback/+server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// src/routes/auth/callback/+server.ts
import type { RequestHandler } from './$types';
import { json, redirect } from '@sveltejs/kit';
import { prisma } from '$lib/server/prisma'; // Import Prisma client

import { SPOTIFY_ID, SPOTIFY_REDIRECT, SPOTIFY_SECRET } from '$env/static/private';
import { getGitHubUserIdFromImageUrl } from '$lib/utils/getGithubIDFromImage';

//Token URL
const TOKEN_URL = 'https://accounts.spotify.com/api/token';

export const GET: RequestHandler = async ({ url, locals }) => {
const code = url.searchParams.get('code');
const state = url.searchParams.get('state');

// error if no code returns (spotify docs)
if (!code) {
return json({ error: 'Missing authorization code' }, { status: 400 });
}

//make sure an user exists
const session = await locals.auth();
if (!session?.user) throw redirect(303, '/');

//use our image strategy to get the user Id
const userId = getGitHubUserIdFromImageUrl(session.user.image);

// exchange the authorization code for an access token and refresh token
const body = new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: SPOTIFY_REDIRECT,
client_id: SPOTIFY_ID,
client_secret: SPOTIFY_SECRET
});

const response = await fetch(TOKEN_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body
});

const tokenData = await response.json();

if (tokenData.access_token && tokenData.refresh_token) {
const accessToken = tokenData.access_token;
const refreshToken = tokenData.refresh_token;
const expiresIn = tokenData.expires_in;

const expiresAt = new Date(Date.now() + expiresIn * 1000); // Calculate token expiration time

// Upsert tokens into the Prisma database (insert if not exists, otherwise update)
await prisma.spotifyToken.upsert({
where: { userId: userId },
update: {
accessToken,
refreshToken,
expiresAt
},
create: {
userId,
accessToken,
refreshToken,
expiresAt
}
});

throw redirect(302, '/profile');
} else {
return json({ error: 'Failed to get tokens from Spotify' }, { status: 400 });
}
};
Loading

0 comments on commit a2c202f

Please sign in to comment.