Skip to content

Commit

Permalink
feat: add top tracks endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
natemoo-re committed Nov 18, 2020
1 parent d8d3dd1 commit d266693
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 11 deletions.
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@

# Hey, I'm Nate!

**Now Playing** on Spotify
| Now Playing |
| ------------------------------------------------------------------------------------------------------------------------------ |
| <a href="https://status.nmoo.dev/now-playing?open"><img src="https://status.nmoo.dev/now-playing" width="540" height="64"></a> |

<a href="https://status.nmoo.dev/now-playing?open">
<img src="https://status.nmoo.dev/now-playing" width="256" height="64" alt="Now Playing">
</a>
| Top Tracks |
| ------------------------------------------------------------------------------------------------------------------------------------ |
| <a href="https://status.nmoo.dev/top-tracks?i=1&open"><img src="https://status.nmoo.dev/top-tracks?i=1" width="540" height="64"></a> |
| <a href="https://status.nmoo.dev/top-tracks?i=2&open"><img src="https://status.nmoo.dev/top-tracks?i=2" width="540" height="64"></a> |
| <a href="https://status.nmoo.dev/top-tracks?i=3&open"><img src="https://status.nmoo.dev/top-tracks?i=3" width="540" height="64"></a> |
2 changes: 1 addition & 1 deletion api/now-playing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { nowPlaying } from "../utils/spotify";

export default async function (req: NowRequest, res: NowResponse) {
const {
item = {},
item = ({} as any),
is_playing: isPlaying = false,
progress_ms: progress = 0,
} = await nowPlaying();
Expand Down
43 changes: 43 additions & 0 deletions api/top-tracks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { NowRequest, NowResponse } from "@vercel/node";
import { renderToString } from "react-dom/server";
import { Track } from "../components/Track";
import { topTrack } from "../utils/spotify";

export default async function (req: NowRequest, res: NowResponse) {
let { i, open } = req.query;
i = Array.isArray(i) ? i[0] : i;
const item = await topTrack({ index: Number.parseInt(i) });

if (!item) {
return res.status(404).end();
}

if (typeof open !== "undefined") {
if (item && item.external_urls) {
res.writeHead(302, {
Location: item.external_urls.spotify,
});
return res.end();
}
return res.status(200).end();
}

res.setHeader("Content-Type", "image/svg+xml");
res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate");

const { name: track } = item;
const { images = [] } = item.album || {};

const cover = images[images.length - 1]?.url;
let coverImg = null;
if (cover) {
const buff = await (await fetch(cover)).arrayBuffer();
coverImg = `data:image/jpeg;base64,${Buffer.from(buff).toString("base64")}`;
}

const artist = (item.artists || []).map(({ name }) => name).join(", ");
const text = renderToString(
Track({ index: Number.parseInt(i), cover: coverImg, artist, track })
);
return res.status(200).send(text);
}
8 changes: 6 additions & 2 deletions components/NowPlaying.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const Player: React.FC<Props> = ({
isPlaying,
}) => {
return (
<ReadmeImg width="256" height="64">
<ReadmeImg width="540" height="64">
<style>
{`
.paused {
Expand All @@ -30,11 +30,14 @@ export const Player: React.FC<Props> = ({
img:not([src]) {
content: url("data:image/gif;base64,R0lGODlhAQABAPAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==");
border-radius: 6px;
background: #FFF;
border: 1px solid #e1e4e8;
}
img {
border-radius: 3px;
}
p {
display: block;
opacity: 0;
Expand Down Expand Up @@ -134,6 +137,7 @@ export const Player: React.FC<Props> = ({
paddingLeft: 4,
}}
>
<div style={{ width: '16px', marginRight: '16px' }}></div>
<img id="cover" src={cover ?? null} width="48" height="48" />
<div
style={{
Expand Down
3 changes: 3 additions & 0 deletions components/Text.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from "react";

const sizes = {
large: 16,
default: 14,
small: 12,
};
Expand Down Expand Up @@ -29,11 +30,13 @@ const Text: React.FC<any> = ({
family = "default",
color = "default",
size = "default",
style = {},
...props
}) => {
return (
<p
style={{
...style,
whiteSpace: "pre",
fontSize: `${sizes[size]}px`,
lineHeight: 1.5,
Expand Down
78 changes: 78 additions & 0 deletions components/Track.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React from "react";
import ReadmeImg from "./ReadmeImg";
import Text from "./Text";

export interface Props {
index?: number,
cover?: string;
track: string;
artist: string;
}

export const Track: React.FC<Props> = ({
index,
cover,
track,
artist,
}) => {
return (
<ReadmeImg width="540" height="64">
<style>
{`
@media (prefers-color-scheme: dark) {
color: #FFF;
}
img:not([src]) {
content: url("data:image/gif;base64,R0lGODlhAQABAPAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==");
background: #FFF;
border: 1px solid #e1e4e8;
}
img {
border-radius: 3px;
}
p {
display: block;
}
#cover {
box-shadow: 0 1px 3px rgba(0,0,0,0.1), 0 3px 10px rgba(0,0,0,0.05);
}
#cover:not([src]) {
box-shadow: none;
}
`}
</style>
<div
style={{
display: "flex",
alignItems: "center",
paddingTop: 8,
paddingLeft: 4,
}}
>
<Text style={{ width: '16px', marginRight: '16px' }} size="large" weight="bold">{index}</Text>
<img id="cover" src={cover ?? null} width="48" height="48" />
<div
style={{
display: "flex",
flex: 1,
flexDirection: "column",
marginTop: -4,
marginLeft: 8,
}}
>
<Text id="track" weight="bold">
{`${track ?? ""} `.trim()}
</Text>
<Text id="artist" color={!track ? "gray" : undefined}>
{artist || "Nothing playing..."}
</Text>
</div>
</div>
</ReadmeImg>
);
};
29 changes: 26 additions & 3 deletions utils/spotify.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import fetch from "isomorphic-unfetch";
import { stringify } from "querystring";
import { URLSearchParams } from 'url';

const {
SPOTIFY_CLIENT_ID: client_id,
Expand All @@ -9,6 +10,7 @@ const {

const basic = Buffer.from(`${client_id}:${client_secret}`).toString("base64");
const Authorization = `Basic ${basic}`;
const BASE_URL = `https://api.spotify.com/v1`;

async function getAuthorizationToken() {
const url = new URL("https://accounts.spotify.com/api/token");
Expand All @@ -28,10 +30,10 @@ async function getAuthorizationToken() {
return `Bearer ${response.access_token}`;
}

const NOW_PLAYING_ENDPOINT = `https://api.spotify.com/v1/me/player/currently-playing`;
export async function nowPlaying() {
const NOW_PLAYING_ENDPOINT = `/me/player/currently-playing`;
export async function nowPlaying(): Promise<Partial<SpotifyApi.CurrentlyPlayingResponse>> {
const Authorization = await getAuthorizationToken();
const response = await fetch(NOW_PLAYING_ENDPOINT, {
const response = await fetch(`${BASE_URL}${NOW_PLAYING_ENDPOINT}`, {
headers: {
Authorization,
},
Expand All @@ -44,3 +46,24 @@ export async function nowPlaying() {
return data;
}
}

const TOP_TRACKS_ENDPOINT = `/me/top/tracks`;
export async function topTrack({ index, timeRange = 'short_term' }: { index: number, timeRange?: 'long_term'|'medium_term'|'short_term' }): Promise<SpotifyApi.TrackObjectFull> {
const Authorization = await getAuthorizationToken();
const params = new URLSearchParams();
params.set('limit', '1');
params.set('offset', `${index}`);
params.set('time_range', `${timeRange}`);
const response = await fetch(`${BASE_URL}${TOP_TRACKS_ENDPOINT}?${params}`, {
headers: {
Authorization
},
});
const { status } = response;
if (status === 204) {
return null;
} else if (status === 200) {
const data = await response.json() as SpotifyApi.UsersTopTracksResponse;
return data.items[0];
}
}

1 comment on commit d266693

@vercel
Copy link

@vercel vercel bot commented on d266693 Nov 18, 2020

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.