-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(api): Implement playlist endpoint
- Loading branch information
Showing
7 changed files
with
189 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |