mirror of
https://github.com/jakejarvis/jarv.is.git
synced 2026-07-22 00:15:59 -04:00
add /api/music Spotify endpoint (top tracks and now playing)
This commit is contained in:
+9
-14
@@ -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_ADMIN_SECRET=
|
||||||
FAUNADB_SERVER_SECRET=
|
FAUNADB_SERVER_SECRET=
|
||||||
|
|
||||||
GH_PUBLIC_TOKEN=
|
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=
|
||||||
|
|||||||
@@ -7,17 +7,13 @@ updates:
|
|||||||
interval: daily
|
interval: daily
|
||||||
versioning-strategy: increase
|
versioning-strategy: increase
|
||||||
ignore:
|
ignore:
|
||||||
|
- dependency-name: "faunadb"
|
||||||
|
- dependency-name: "hugo-extended"
|
||||||
|
- dependency-name: "@types/*"
|
||||||
- dependency-name: "@fontsource/*"
|
- dependency-name: "@fontsource/*"
|
||||||
commit-message:
|
commit-message:
|
||||||
prefix: "📦 npm:"
|
prefix: "📦 npm:"
|
||||||
|
|
||||||
- package-ecosystem: docker
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: daily
|
|
||||||
commit-message:
|
|
||||||
prefix: "📦 docker:"
|
|
||||||
|
|
||||||
- package-ecosystem: github-actions
|
- package-ecosystem: github-actions
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
|
|||||||
+1
-1
@@ -50,7 +50,7 @@ export default async (req: VercelRequest, res: VercelResponse) => {
|
|||||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||||
|
|
||||||
// send client the *new* hit count
|
// send client the *new* hit count
|
||||||
res.json(hits);
|
res.status(200).json(hits);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
||||||
|
|||||||
+163
@@ -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 });
|
||||||
|
}
|
||||||
|
};
|
||||||
+3
-3
@@ -70,7 +70,7 @@ async function fetchRepos(sort: string, limit: number) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const response = await client.request(query, { sort, limit });
|
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 }) => ({
|
({ node: repo }: { [key: string]: Repository }) => ({
|
||||||
...repo,
|
...repo,
|
||||||
description: escape(repo.description),
|
description: escape(repo.description),
|
||||||
@@ -96,7 +96,7 @@ export default async (req: VercelRequest, res: VercelResponse) => {
|
|||||||
|
|
||||||
// default to latest repos
|
// default to latest repos
|
||||||
let sortBy = "PUSHED_AT";
|
let sortBy = "PUSHED_AT";
|
||||||
// get most popular repos (/projects?top)
|
// get most popular repos (/projects/?top)
|
||||||
if (typeof req.query.top !== "undefined") sortBy = "STARGAZERS";
|
if (typeof req.query.top !== "undefined") sortBy = "STARGAZERS";
|
||||||
|
|
||||||
const repos = await fetchRepos(sortBy, 16);
|
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-Methods", "GET");
|
||||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||||
|
|
||||||
res.json(repos);
|
res.status(200).json(repos);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
||||||
|
|||||||
+8
-9
@@ -38,11 +38,6 @@ export default async (req: VercelRequest, res: VercelResponse) => {
|
|||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
type SiteStats = {
|
|
||||||
hits: number;
|
|
||||||
pretty_hits?: string;
|
|
||||||
pretty_unit?: string;
|
|
||||||
};
|
|
||||||
type PageStats = {
|
type PageStats = {
|
||||||
title: string;
|
title: string;
|
||||||
url: string;
|
url: string;
|
||||||
@@ -53,11 +48,15 @@ export default async (req: VercelRequest, res: VercelResponse) => {
|
|||||||
pretty_unit: string;
|
pretty_unit: string;
|
||||||
};
|
};
|
||||||
type OverallStats = {
|
type OverallStats = {
|
||||||
total: SiteStats;
|
total: {
|
||||||
pages: Array<PageStats>;
|
hits: number;
|
||||||
|
pretty_hits?: string;
|
||||||
|
pretty_unit?: string;
|
||||||
|
};
|
||||||
|
pages: PageStats[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const pages: Array<PageStats> = result.data;
|
const pages: PageStats[] = result.data;
|
||||||
const stats: OverallStats = {
|
const stats: OverallStats = {
|
||||||
total: { hits: 0 },
|
total: { hits: 0 },
|
||||||
pages,
|
pages,
|
||||||
@@ -97,7 +96,7 @@ export default async (req: VercelRequest, res: VercelResponse) => {
|
|||||||
res.setHeader("Access-Control-Allow-Methods", "GET");
|
res.setHeader("Access-Control-Allow-Methods", "GET");
|
||||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||||
|
|
||||||
res.json(stats);
|
res.status(200).json(stats);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,7 @@
|
|||||||
"node-fetch": "^2.6.1",
|
"node-fetch": "^2.6.1",
|
||||||
"numeral": "^2.0.6",
|
"numeral": "^2.0.6",
|
||||||
"pluralize": "^8.0.0",
|
"pluralize": "^8.0.0",
|
||||||
|
"querystring": "^0.2.1",
|
||||||
"rss-parser": "^3.12.0",
|
"rss-parser": "^3.12.0",
|
||||||
"twemoji": "13.1.0",
|
"twemoji": "13.1.0",
|
||||||
"twemoji-emojis": "13.1.0"
|
"twemoji-emojis": "13.1.0"
|
||||||
@@ -59,6 +60,7 @@
|
|||||||
"@types/node-fetch": "^2.5.10",
|
"@types/node-fetch": "^2.5.10",
|
||||||
"@types/numeral": "^2.0.1",
|
"@types/numeral": "^2.0.1",
|
||||||
"@types/pluralize": "^0.0.29",
|
"@types/pluralize": "^0.0.29",
|
||||||
|
"@types/twemoji": "^12.1.1",
|
||||||
"@types/xml2js": "^0.4.8",
|
"@types/xml2js": "^0.4.8",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.26.0",
|
"@typescript-eslint/eslint-plugin": "^4.26.0",
|
||||||
"@typescript-eslint/parser": "^4.26.0",
|
"@typescript-eslint/parser": "^4.26.0",
|
||||||
|
|||||||
@@ -1108,6 +1108,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24"
|
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24"
|
||||||
integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==
|
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":
|
"@types/unist@*", "@types/unist@^2.0.0", "@types/unist@^2.0.2":
|
||||||
version "2.0.3"
|
version "2.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e"
|
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e"
|
||||||
@@ -6190,6 +6195,11 @@ query-string@^5.0.1:
|
|||||||
object-assign "^4.1.0"
|
object-assign "^4.1.0"
|
||||||
strict-uri-encode "^1.0.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:
|
queue-microtask@^1.2.2:
|
||||||
version "1.2.3"
|
version "1.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
|
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
|
||||||
|
|||||||
Reference in New Issue
Block a user