mirror of
https://github.com/jakejarvis/jarv.is.git
synced 2025-07-03 17:46:39 -04:00
add /api/music Spotify endpoint (top tracks and now playing)
This commit is contained in:
@ -50,7 +50,7 @@ export default async (req: VercelRequest, res: VercelResponse) => {
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
|
||||
// send client the *new* hit count
|
||||
res.json(hits);
|
||||
res.status(200).json(hits);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
|
163
api/music.ts
Normal file
163
api/music.ts
Normal file
@ -0,0 +1,163 @@
|
||||
"use strict";
|
||||
|
||||
// Heavily inspired by @leerob: https://leerob.io/snippets/spotify
|
||||
|
||||
import { VercelRequest, VercelResponse } from "@vercel/node";
|
||||
import fetch from "node-fetch";
|
||||
import querystring from "querystring";
|
||||
|
||||
const { SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET, SPOTIFY_REFRESH_TOKEN } = process.env;
|
||||
|
||||
const basic = Buffer.from(`${SPOTIFY_CLIENT_ID}:${SPOTIFY_CLIENT_SECRET}`).toString("base64");
|
||||
const TOKEN_ENDPOINT = `https://accounts.spotify.com/api/token`;
|
||||
|
||||
// https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-the-users-currently-playing-track
|
||||
const NOW_PLAYING_ENDPOINT = `https://api.spotify.com/v1/me/player/currently-playing`;
|
||||
// https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-top-artists-and-tracks
|
||||
const TOP_TRACKS_ENDPOINT = `https://api.spotify.com/v1/me/top/tracks?time_range=long_term&limit=10`;
|
||||
|
||||
type TrackSchema = {
|
||||
name: string;
|
||||
artists: Array<{
|
||||
name: string;
|
||||
}>;
|
||||
album: {
|
||||
name: string;
|
||||
images: Array<{
|
||||
url: string;
|
||||
}>;
|
||||
};
|
||||
imageUrl?: string;
|
||||
external_urls: {
|
||||
spotify: string;
|
||||
};
|
||||
};
|
||||
|
||||
type Track = {
|
||||
isPlaying: boolean;
|
||||
artist?: string;
|
||||
title?: string;
|
||||
album?: string;
|
||||
imageUrl?: string;
|
||||
songUrl?: string;
|
||||
};
|
||||
|
||||
const getAccessToken = async () => {
|
||||
const response = await fetch(TOKEN_ENDPOINT, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Basic ${basic}`,
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: querystring.stringify({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: SPOTIFY_REFRESH_TOKEN,
|
||||
}),
|
||||
});
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
const getNowPlaying = async (): Promise<Track> => {
|
||||
const { access_token } = await getAccessToken();
|
||||
|
||||
const response = await fetch(NOW_PLAYING_ENDPOINT, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${access_token}`,
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
type Activity = {
|
||||
is_playing: boolean;
|
||||
item?: TrackSchema;
|
||||
};
|
||||
|
||||
if (response.status === 204 || response.status > 400) {
|
||||
return { isPlaying: false };
|
||||
}
|
||||
|
||||
const active: Activity = await response.json();
|
||||
|
||||
if (active.is_playing === true && active.item) {
|
||||
const isPlaying = active.is_playing;
|
||||
const artist = active.item.artists.map((_artist) => _artist.name).join(", ");
|
||||
const title = active.item.name;
|
||||
const album = active.item.album.name;
|
||||
const imageUrl = active.item.album.images[0].url;
|
||||
const songUrl = active.item.external_urls.spotify;
|
||||
|
||||
return {
|
||||
isPlaying,
|
||||
artist,
|
||||
title,
|
||||
album,
|
||||
imageUrl,
|
||||
songUrl,
|
||||
};
|
||||
} else {
|
||||
return { isPlaying: false };
|
||||
}
|
||||
};
|
||||
|
||||
const getTopTracks = async (): Promise<Track[]> => {
|
||||
const { access_token } = await getAccessToken();
|
||||
|
||||
const response = await fetch(TOP_TRACKS_ENDPOINT, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${access_token}`,
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const { items } = await response.json();
|
||||
|
||||
const tracks: Track[] = items.map((track: TrackSchema) => ({
|
||||
artist: track.artists.map((_artist) => _artist.name).join(", "),
|
||||
title: track.name,
|
||||
album: track.album.name,
|
||||
imageUrl: track.album.images[0].url,
|
||||
songUrl: track.external_urls.spotify,
|
||||
}));
|
||||
|
||||
return tracks;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export default async (req: VercelRequest, res: VercelResponse) => {
|
||||
try {
|
||||
// some rudimentary error handling
|
||||
if (req.method !== "GET") {
|
||||
throw new Error(`Method ${req.method} not allowed.`);
|
||||
}
|
||||
if (!process.env.SPOTIFY_CLIENT_ID || !process.env.SPOTIFY_CLIENT_SECRET || !process.env.SPOTIFY_REFRESH_TOKEN) {
|
||||
throw new Error("Spotify API credentials aren't set.");
|
||||
}
|
||||
|
||||
// default to top tracks
|
||||
let response;
|
||||
// get currently playing track (/music/?now), otherwise top 10 tracks
|
||||
if (typeof req.query.now !== "undefined") {
|
||||
response = await getNowPlaying();
|
||||
|
||||
// let Vercel edge cache results for 5 mins
|
||||
res.setHeader("Cache-Control", "public, s-maxage=300, stale-while-revalidate");
|
||||
} else {
|
||||
response = await getTopTracks();
|
||||
|
||||
// let Vercel edge cache results for 3 hours
|
||||
res.setHeader("Cache-Control", "public, s-maxage=10800, stale-while-revalidate");
|
||||
}
|
||||
|
||||
res.setHeader("Access-Control-Allow-Methods", "GET");
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
|
||||
res.status(200).json(response);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
res.status(400).json({ message: error.message });
|
||||
}
|
||||
};
|
@ -70,7 +70,7 @@ async function fetchRepos(sort: string, limit: number) {
|
||||
};
|
||||
|
||||
const response = await client.request(query, { sort, limit });
|
||||
const currentRepos: Array<Repository> = response.user.repositories.edges.map(
|
||||
const currentRepos: Repository[] = response.user.repositories.edges.map(
|
||||
({ node: repo }: { [key: string]: Repository }) => ({
|
||||
...repo,
|
||||
description: escape(repo.description),
|
||||
@ -96,7 +96,7 @@ export default async (req: VercelRequest, res: VercelResponse) => {
|
||||
|
||||
// default to latest repos
|
||||
let sortBy = "PUSHED_AT";
|
||||
// get most popular repos (/projects?top)
|
||||
// get most popular repos (/projects/?top)
|
||||
if (typeof req.query.top !== "undefined") sortBy = "STARGAZERS";
|
||||
|
||||
const repos = await fetchRepos(sortBy, 16);
|
||||
@ -106,7 +106,7 @@ export default async (req: VercelRequest, res: VercelResponse) => {
|
||||
res.setHeader("Access-Control-Allow-Methods", "GET");
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
|
||||
res.json(repos);
|
||||
res.status(200).json(repos);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
|
17
api/stats.ts
17
api/stats.ts
@ -38,11 +38,6 @@ export default async (req: VercelRequest, res: VercelResponse) => {
|
||||
),
|
||||
]);
|
||||
|
||||
type SiteStats = {
|
||||
hits: number;
|
||||
pretty_hits?: string;
|
||||
pretty_unit?: string;
|
||||
};
|
||||
type PageStats = {
|
||||
title: string;
|
||||
url: string;
|
||||
@ -53,11 +48,15 @@ export default async (req: VercelRequest, res: VercelResponse) => {
|
||||
pretty_unit: string;
|
||||
};
|
||||
type OverallStats = {
|
||||
total: SiteStats;
|
||||
pages: Array<PageStats>;
|
||||
total: {
|
||||
hits: number;
|
||||
pretty_hits?: string;
|
||||
pretty_unit?: string;
|
||||
};
|
||||
pages: PageStats[];
|
||||
};
|
||||
|
||||
const pages: Array<PageStats> = result.data;
|
||||
const pages: PageStats[] = result.data;
|
||||
const stats: OverallStats = {
|
||||
total: { hits: 0 },
|
||||
pages,
|
||||
@ -97,7 +96,7 @@ export default async (req: VercelRequest, res: VercelResponse) => {
|
||||
res.setHeader("Access-Control-Allow-Methods", "GET");
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
|
||||
res.json(stats);
|
||||
res.status(200).json(stats);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
|
Reference in New Issue
Block a user