mirror of
https://github.com/jakejarvis/jarv.is.git
synced 2025-04-26 04:45:22 -04:00
add /api/music Spotify endpoint (top tracks and now playing)
This commit is contained in:
parent
9314c7eb15
commit
96a644da85
23
.env.example
23
.env.example
@ -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
.github/dependabot.yml
vendored
10
.github/dependabot.yml
vendored
@ -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:
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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"
|
||||
@ -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",
|
||||
|
10
yarn.lock
10
yarn.lock
@ -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"
|
||||
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user