Skip to content

Commit

Permalink
feat(api): Implement playlist endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
Likqez committed Oct 31, 2024
1 parent 9396b44 commit 4f14f2f
Show file tree
Hide file tree
Showing 7 changed files with 189 additions and 3 deletions.
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
SUPABASE_URL="https://yourhost.supabase.co"
SUPABASE_KEY="your-secret-key"
SUPABASE_SERVICE_KEY="your-service-key"
NUXT_SPOTIFY_CLIENT_ID="your-client-id"
NUXT_SPOTIFY_CLIENT_SECRET="your-client-secret"
21 changes: 18 additions & 3 deletions server/api/v1/playlist/[uid].get.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import {serverSupabaseUser} from "#supabase/server";
import {serverSupabaseServiceRole, serverSupabaseUser} from "#supabase/server";


/**
* Endpoint to get a playlist by id
* @returns {Object} - Playlist object
* @throws {400} - Invalid playlistId
* @throws {401} - Unauthenticated
* @throws {500} - Internal Server Error
*/
export default defineEventHandler(async (event) => {
const playlistId = getRouterParam(event, 'uid')

// check regex playlistId
if (!isValidSpotifyID(playlistId)) {
if (!playlistId || !isValidSpotifyID(playlistId!)) {
setResponseStatus(event, 400);
return {error: 'invalid playlistId'};
}
Expand All @@ -16,6 +24,13 @@ export default defineEventHandler(async (event) => {
return {error: 'unauthenticated'};
}

const client = serverSupabaseServiceRole(event);
const {data, error} = await client.from('playlists').select('*').eq('id', playlistId).single();

if (error) {
setResponseStatus(event, 500);
return {error: error.message};
}

return user;
return data;
})
27 changes: 27 additions & 0 deletions server/api/v1/playlist/index.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {serverSupabaseServiceRole, serverSupabaseUser} from "#supabase/server";

/**
* Endpoint to get all playlists
* @throws {401} - Unauthenticated
* @throws {500} - Internal Server Error
* @returns {Array} - Array of playlists
*/
export default defineEventHandler(async (event) => {

// Require user to be authenticated
const user = await serverSupabaseUser(event);
if (!user?.id) {
setResponseStatus(event, 401);
return {error: 'unauthenticated'};
}

const client = serverSupabaseServiceRole(event);
const {data, error} = await client.from('playlists').select();

if (error) {
setResponseStatus(event, 500);
return {error: error.message};
}

return data;
})
55 changes: 55 additions & 0 deletions server/api/v1/playlist/index.post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import {z} from 'zod'
import {spotifyIDRegex} from "~/server/utils/data-validation";
import {serverSupabaseServiceRole} from "#supabase/server";
import {UNIQUE_VIOLATION} from "~/server/utils/postgres-errors";
import {getPlaylistCover, getSpotifyToken} from "~/server/utils/spotify";

const schema = z.object({
id: z.string().regex(spotifyIDRegex),
name: z.string(),
spotifyId: z.string().regex(spotifyIDRegex),
categories: z.array(z.string()),
enabled: z.boolean().optional()
})


/**
* Unauthenticated endpoint to create a playlist - management only
* @throws {400} - Invalid body
* @throws {400} - Playlist with this ID already exists
* @throws {500} - Internal Server Error
* @returns {Object} - Created playlist
*/
export default defineEventHandler(async (event) => {
const result = await readValidatedBody(event, body => schema.safeParse(body))

if (!result.success) {
setResponseStatus(event, 400);
return {error: result.error.issues};
}

const token = await getSpotifyToken();
const coverUrl = await getPlaylistCover(token, result.data.spotifyId);

const playlistInsert = {
id: result.data.id,
name: result.data.name,
spotifyId: result.data.spotifyId,
cover: coverUrl
}

const client = serverSupabaseServiceRole(event)
const {data, error} = await client.from('playlists').insert(playlistInsert as never).select().single(); //todo: fix type error!

if (error) {
setResponseStatus(event, 400);
if (error.code === UNIQUE_VIOLATION)
return {error: 'Playlist with this ID already exists'};
setResponseStatus(event, 500);
return {error: error.message};
}


setResponseStatus(event, 201);
return data;
})
36 changes: 36 additions & 0 deletions server/api/v1/playlist/playlist.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
@baseUrl = http://localhost:3000/api/v1
@authCookie =

### Get all Playlists
GET {{baseUrl}}/playlist
Cookie: {{authCookie}}


### Get specific Playlist
@playlistId = 37i9dQZF1EIYE32WUF6sxN

GET {{baseUrl}}/playlist/{{playlistId}}
Cookie: {{authCookie}}


### Add a Spotify Playlist to our system
POST {{baseUrl}}/playlist

{
"id": "37i9dQZF1EIYE32WUF6sxN",
"name": "Hardstyle Popular",
"spotifyId": "37i9dQZF1EIYE32WUF6sxN",
"categories": ["hardstyle", "remix"]
}

### Add a Disabled Spotify Playlist to our system
POST {{baseUrl}}/playlist

{
"id": "4DC0uhUb7itYVuVVF3goSN",
"name": "Memes",
"spotifyId": "4DC0uhUb7itYVuVVF3goSN",
"categories": ["meme"],
"enabled": false
}

4 changes: 4 additions & 0 deletions server/utils/postgres-errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const UNIQUE_VIOLATION = '23505';
export const FOREIGN_KEY_VIOLATION = '23503';
export const NOT_NULL_VIOLATION = '23502';

46 changes: 46 additions & 0 deletions server/utils/spotify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
let spotifyToken: string | null = null
let tokenExpiry: number = 0

export async function getSpotifyToken() {
// Return cached token if valid
if (spotifyToken && tokenExpiry > Date.now()) {
return spotifyToken
}

const auth = Buffer
.from(`${process.env.SPOTIFY_CLIENT_ID}:${process.env.SPOTIFY_CLIENT_SECRET}`)
.toString('base64')

try {
const res = await fetch('https://accounts.spotify.com/api/token', {
method: 'POST',
headers: {
'Authorization': `Basic ${auth}`,
'Content-Type': 'application/x-www-form-urlencoded'
},
body: 'grant_type=client_credentials'
})

const data = await res.json()
spotifyToken = data.access_token
tokenExpiry = Date.now() + (data.expires_in - 60) * 1000

return spotifyToken
} catch (error) {
console.error('Spotify token error:', error)
throw createError({
statusCode: 500,
message: 'Failed to get Spotify access token'
})
}
}

export async function getPlaylistCover(token: string | null, playlistId: string): Promise<string | undefined> {
if (!token) return undefined;

const res = await fetch(`https://api.spotify.com/v1/playlists/${playlistId}?fields=images`, {
headers: {'Authorization': `Bearer ${token}`}
})
const data = await res.json()
return data.images[0].url;
}

0 comments on commit 4f14f2f

Please sign in to comment.