Skip to content

Commit

Permalink
add /api/music Spotify endpoint (top tracks and now playing)
Browse files Browse the repository at this point in the history
  • Loading branch information
jakejarvis committed Jun 6, 2021
1 parent 9314c7e commit 96a644d
Show file tree
Hide file tree
Showing 8 changed files with 199 additions and 34 deletions.
23 changes: 9 additions & 14 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
ALGOLIA_APP_ID=
ALGOLIA_API_KEY=
ALGOLIA_INDEX_NAME=
ALGOLIA_INDEX_FILE=
ALGOLIA_BASE_URL=

LHCI_SERVER_BASE_URL=
LHCI_TOKEN=

PERCY_TOKEN=

WEBMENTIONS_TOKEN=

FAUNADB_ADMIN_SECRET=
FAUNADB_SERVER_SECRET=

GH_PUBLIC_TOKEN=
SPOTIFY_REFRESH_TOKEN=
SPOTIFY_CLIENT_SECRET=
SPOTIFY_CLIENT_ID=
WEBMENTIONS_TOKEN=
PERCY_TOKEN=
LHCI_SERVER_BASE_URL=
LHCI_TOKEN=
LHCI_ADMIN_TOKEN=
LHCI_GITHUB_APP_TOKEN=
10 changes: 3 additions & 7 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,13 @@ updates:
interval: daily
versioning-strategy: increase
ignore:
- dependency-name: "faunadb"
- dependency-name: "hugo-extended"
- dependency-name: "@types/*"
- dependency-name: "@fontsource/*"
commit-message:
prefix: "📦 npm:"

- package-ecosystem: docker
directory: "/"
schedule:
interval: daily
commit-message:
prefix: "📦 docker:"

- package-ecosystem: github-actions
directory: "/"
schedule:
Expand Down
2 changes: 1 addition & 1 deletion api/hits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
163 changes: 163 additions & 0 deletions api/music.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
};
6 changes: 3 additions & 3 deletions api/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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);
Expand All @@ -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);

Expand Down
17 changes: 8 additions & 9 deletions api/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand Down Expand Up @@ -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);

Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"node-fetch": "^2.6.1",
"numeral": "^2.0.6",
"pluralize": "^8.0.0",
"querystring": "^0.2.1",
"rss-parser": "^3.12.0",
"twemoji": "13.1.0",
"twemoji-emojis": "13.1.0"
Expand All @@ -59,6 +60,7 @@
"@types/node-fetch": "^2.5.10",
"@types/numeral": "^2.0.1",
"@types/pluralize": "^0.0.29",
"@types/twemoji": "^12.1.1",
"@types/xml2js": "^0.4.8",
"@typescript-eslint/eslint-plugin": "^4.26.0",
"@typescript-eslint/parser": "^4.26.0",
Expand Down
10 changes: 10 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1108,6 +1108,11 @@
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24"
integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==

"@types/twemoji@^12.1.1":
version "12.1.1"
resolved "https://registry.yarnpkg.com/@types/twemoji/-/twemoji-12.1.1.tgz#34c5dcecff438b5be173889a6ee8ad51ba90445f"
integrity sha512-dW1B1WHTfrWmEzXb/tp8xsZqQHAyMB9JwLwbBqkIQVzmNUI02R7lJqxUpKFM114ygNZHKA1r74oPugCAiYHt1A==

"@types/unist@*", "@types/unist@^2.0.0", "@types/unist@^2.0.2":
version "2.0.3"
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e"
Expand Down Expand Up @@ -6190,6 +6195,11 @@ query-string@^5.0.1:
object-assign "^4.1.0"
strict-uri-encode "^1.0.0"

querystring@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.1.tgz#40d77615bb09d16902a85c3e38aa8b5ed761c2dd"
integrity sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==

queue-microtask@^1.2.2:
version "1.2.3"
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
Expand Down

1 comment on commit 96a644d

@vercel
Copy link

@vercel vercel bot commented on 96a644d Jun 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.