@ -1,4 +1,5 @@ | |||
.vercel | |||
venv | |||
__pycache__ | |||
.cache* | |||
env | |||
.env | |||
node_modules | |||
.DS_STORE |
@ -1,5 +1 @@ | |||
**Now Playing** | |||
<a href="https://novatorem.vercel.app/now-playing?open"> | |||
<img src="https://novatorem.vercel.app/now-playing" width="256" height="64" alt="Now Playing"> | |||
</a> | |||
![Spotify](https://novatorem.vercel.app/api/spotify-playing) |
@ -1,47 +0,0 @@ | |||
import { renderToString } from 'react-dom/server'; | |||
import { NowRequest, NowResponse } from "@vercel/node"; | |||
import { decode } from 'querystring'; | |||
import { nowPlaying } from "../utils/spotify"; | |||
import { Player } from '../components/NowPlaying'; | |||
export default async function (req: NowRequest, res: NowResponse) { | |||
const { item = {}, is_playing: isPlaying = false, progress_ms: progress = 0 } = await nowPlaying(); | |||
const params = decode(req.url.split('?')[1]) as any; | |||
if (params && typeof params.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"); | |||
res.setHeader("Cache-Control", "no-cache"); | |||
const { | |||
duration_ms: duration, | |||
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( | |||
Player({ cover: coverImg, artist, track, isPlaying, progress, duration }) | |||
); | |||
return res.status(200).send(text); | |||
} |
@ -0,0 +1,3 @@ | |||
flask==1.1.2 | |||
requests==2.24.0 | |||
python-dotenv==0.14.0 |
@ -0,0 +1,153 @@ | |||
from flask import Flask, Response, jsonify, render_template | |||
from base64 import b64encode | |||
from dotenv import load_dotenv, find_dotenv | |||
load_dotenv(find_dotenv()) | |||
import requests | |||
import json | |||
import os | |||
import random | |||
print("Starting Server") | |||
SPOTIFY_CLIENT_ID = os.getenv("SPOTIFY_CLIENT_ID") | |||
SPOTIFY_SECRET_ID = os.getenv("SPOTIFY_SECRET_ID") | |||
SPOTIFY_REFRESH_TOKEN = os.getenv("SPOTIFY_REFRESH_TOKEN") | |||
# scope user-read-currently-playing,user-read-recently-played | |||
SPOTIFY_URL_REFRESH_TOKEN = "https://accounts.spotify.com/api/token" | |||
SPOTIFY_URL_NOW_PLAYING = "https://api.spotify.com/v1/me/player/currently-playing" | |||
SPOTIFY_URL_RECENTLY_PLAY = "https://api.spotify.com/v1/me/player/recently-played?limit=10" | |||
app = Flask(__name__) | |||
def get_authorization(): | |||
return b64encode(f"{SPOTIFY_CLIENT_ID}:{SPOTIFY_SECRET_ID}".encode()).decode("ascii") | |||
def refresh_token(): | |||
data = { | |||
"grant_type": "refresh_token", | |||
"refresh_token": SPOTIFY_REFRESH_TOKEN, | |||
} | |||
headers = {"Authorization": "Basic {}".format(get_authorization())} | |||
response = requests.post(SPOTIFY_URL_REFRESH_TOKEN, data=data, headers=headers) | |||
repsonse_json = response.json() | |||
return repsonse_json["access_token"] | |||
def get_recently_play(): | |||
token = refresh_token() | |||
headers = {"Authorization": f"Bearer {token}"} | |||
response = requests.get(SPOTIFY_URL_RECENTLY_PLAY, headers=headers) | |||
if response.status_code == 204: | |||
return {} | |||
repsonse_json = response.json() | |||
return repsonse_json | |||
def get_now_playing(): | |||
token = refresh_token() | |||
headers = {"Authorization": f"Bearer {token}"} | |||
response = requests.get(SPOTIFY_URL_NOW_PLAYING, headers=headers) | |||
if response.status_code == 204: | |||
return {} | |||
repsonse_json = response.json() | |||
return repsonse_json | |||
def generate_css_bar(num_bar=75): | |||
css_bar = "" | |||
left = 1 | |||
for i in range(1, num_bar + 1): | |||
anim = random.randint(350, 500) | |||
css_bar += ".bar:nth-child({}) {{ left: {}px; animation-duration: {}ms; }}".format( | |||
i, left, anim | |||
) | |||
left += 4 | |||
return css_bar | |||
def load_image_b64(url): | |||
resposne = requests.get(url) | |||
return b64encode(resposne.content).decode("ascii") | |||
def make_svg(data): | |||
height = 445 | |||
num_bar = 75 | |||
title_text = "Now playing" | |||
content_bar = "".join(["<div class='bar'></div>" for i in range(num_bar)]) | |||
css_bar = generate_css_bar(num_bar) | |||
if data == {}: | |||
# Get recently play | |||
title_text = "Currently playing" | |||
content_bar = "" | |||
recent_plays = get_recently_play() | |||
size_recent_play = len(recent_plays["items"]) | |||
idx = random.randint(0, size_recent_play - 1) | |||
item = recent_plays["items"][idx]["track"] | |||
else: | |||
item = data["item"] | |||
img = load_image_b64(item["album"]["images"][1]["url"]) | |||
artist_name = item["artists"][0]["name"] | |||
song_name = item["name"] | |||
url = item["external_urls"]["spotify"] | |||
rendered_data = { | |||
"height": height, | |||
"num_bar": num_bar, | |||
"content_bar": content_bar, | |||
"css_bar": css_bar, | |||
"title_text": title_text, | |||
"artist_name": artist_name, | |||
"song_name": song_name, | |||
"content_bar": content_bar, | |||
"img": img, | |||
} | |||
return render_template("spotify.html.j2", **rendered_data) | |||
@app.route("/", defaults={"path": ""}) | |||
@app.route("/<path:path>") | |||
def catch_all(path): | |||
data = get_now_playing() | |||
svg = make_svg(data) | |||
resp = Response(svg, mimetype="image/svg+xml") | |||
resp.headers["Cache-Control"] = "s-maxage=1" | |||
return resp | |||
if __name__ == "__main__": | |||
app.run(debug=True) |
@ -0,0 +1,110 @@ | |||
<svg width="320" height="{{height}}" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | |||
<foreignObject width="320" height="{{height}}"> | |||
<div xmlns="http://www.w3.org/1999/xhtml" class="container"> | |||
<style> | |||
div { | |||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji; | |||
} | |||
.container { | |||
background-color: #121212; | |||
border-radius: 10px; | |||
padding: 10px 10px | |||
} | |||
.playing { | |||
font-weight: bold; | |||
color: #53b14f; | |||
text-align: center; | |||
display: flex; | |||
justify-content: center; | |||
align-items: center; | |||
} | |||
.not-play { | |||
color: #ff1616; | |||
} | |||
.artist { | |||
font-weight: bold; | |||
font-size: 20px; | |||
color: #fff; | |||
text-align: center; | |||
margin-top: 5px; | |||
} | |||
.song { | |||
font-size: 16px; | |||
color: #b3b3b3; | |||
text-align: center; | |||
margin-top: 5px; | |||
margin-bottom: 15px; | |||
} | |||
.logo { | |||
margin-left: 5px; | |||
margin-top: 5px; | |||
} | |||
.cover { | |||
border-radius: 5px; | |||
margin-top: 9px; | |||
} | |||
#bars { | |||
height: 30px; | |||
margin: -20px 0 0 0px; | |||
position: absolute; | |||
width: 40px; | |||
} | |||
.bar { | |||
background: #53b14f; | |||
bottom: 1px; | |||
height: 3px; | |||
position: absolute; | |||
width: 3px; | |||
animation: sound 0ms -800ms linear infinite alternate; | |||
} | |||
@keyframes sound { | |||
0% { | |||
opacity: .35; | |||
height: 3px; | |||
} | |||
100% { | |||
opacity: 1; | |||
height: 28px; | |||
} | |||
} | |||
{{css_bar|safe}} | |||
</style> | |||
{% if song_name %} | |||
<div class="playing"> | |||
{{title_text}} on | |||
<img class="logo" | |||
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACMAAAAjCAYAAAAe2bNZAAAE5ElEQVRYR81WS08bVxQ+dzweY48fQCCBllJDmqpRSx6gKizSiAUbILZjA21KVEhF20hNlLb/IKu2m3QRKaqQogIRkAXhYQxFSJGCQqVKrVSBQtpCFqEQkrQhxnb8Yjxzp7oTmdjjsWd4LHIlLzz3PL57zvedexG8Qgu9Qlhg22Bqumr0+/L2VdAMOpBvtdgCoedBnhPvsw/YpcFLg9x2DrklMA19DVajkf4B0dQnAMDkSJgQedwfWot9fevcraBWYJrA1HXX5RUWWG8jhGq1Bk7aiVj8PfGfWOc754uq+aqCOXXT8bFOTw2oBVLb5zmhw9s6fj2XXU4wniFnF6LRF2qJtO6LPPQMN3s/zWafFcxuA0kCwILYO+IZO6sESBHMbrUmWwVwQjw70jLWK9/PAEPIuqfQFtNa+u3acU8wKyd1BhjPqPNXJdVgHkP8WRwiqxGI/RsDLsSBiEXQGXRgKDCAqcQk/RgbA4hS1QXx/WPYPVaTepg0LzJHTBZmcy5wQQ5mL89CeCUMIGqvAQFjLjeD/aQdiqqLsoKLh/nCiTMT68nIaWA8Q45riKY6k5szF2YgEU5oR5HFkn2dhUNfHQLjXmOaBRZw34jHRwaotNLANHtdG6mTdaF3AVZvrwJCCNgyFoqOFIHtgA2MxUbQGXXSdyxg4MM8RJ9EYf2vdfDf80PsaUyxkqSNtd+nzU1+yOXVZ4Ahd429pCzjTiG80MIBeVH4GA/Lk8uw/PMyEL4lV9XFKiiuLt78H/2Hy5u8OEmK8LIyjb2Nbxvz9Qu5eiIKokTcuD8OfISXTk+zNOQV5r0grk6ZuIGFAMxdnpMIf+LHE0Dpqc00XCT+nq9t6l4aGOeNpia9iR6XgyHKuXv1rjYSI5BaWN5QDqUflAJFv0ya7ZBCHLeMfuQbSgPTPnG6LcLH+uVOM+dnIBFRIHGyCDlURgh7+JvDYCo1ZS04qzd3Xm/s/0lTZQJ/B2BxYBHK6sug+Ggx6M16Ge1BahdRHSHvytQKhB6E0pITMLXfKV/4QgK3jLbIKqOFM1vROFHW/NX5zdFw7NtjwL7GZoSIB/mqifaJ+bTKtF5qZfBRTmK14hIBuOccRB5GIPIoAhv+DeDjPDBWBsxvmMFaaQVDviGjaut/rkNgMQAVpyoUwyqqiVg2e11E2pu6l7xFgDtf3gEiVS2LgNv/4X4oPV6qxVx5zhBPz5CjG9FU2vUuiiJMd05LcrRWWKHg3QKwvGkBJv/FHUQkHl4Ow9rsGhAJp86USncl2F32rKCwgG+MeHxtSYO0wVDfVW+zlbABLUfKLlUBFvsW4fEvjyWTg50HJZkrrfDT+J6pz6b8imCk6ow4f0MUen8ngKTuCiIsjS+B3WFXnOAiFueG3WNHUvNkjExHl8PElFCRnYJR83/2MGiZPj8dzgmGbLoGT7bTjC7jJaaWQOs+zwmfe1vHr8ntc7yBXd2IBsW3qtakSnYixgPDbt8Zpb2cTzL3sLOH0qGOnSRP9c0FhNipvg/dN5s6KD3ds1NA2Vqjyhl5YkJq/V40gyhUvVVQRDX+R6HjcrJuuU1yh6b+pgLGSF2hdNRpAKBzAOOxgAejfu5C6hxRO4hqm7IFaLjSYKCL8Fs6HfOO1WK1haKhYCKKF3AA30++3NSSa1bTVgPthv22K7MbyeUx/gfIiuIzZiZJFQAAAABJRU5ErkJggg==" /> | |||
</div> | |||
<div class="artist">{{artist_name}}</div> | |||
<div class="song">{{song_name}}</div> | |||
<div id='bars'> | |||
{{content_bar|safe}} | |||
</div> | |||
<a href="{}" target="_BLANK"> | |||
<center> | |||
<img src="data:image/png;base64, {{img}}" width="300" height="300" class="cover" /> | |||
</center> | |||
</a> | |||
{% else %} | |||
<div class="playing not-play">Nothing playing on Spotify</div> | |||
{% endif %} | |||
</div> | |||
</foreignObject> | |||
</svg> |
@ -1,162 +0,0 @@ | |||
import React from "react"; | |||
import ReadmeImg from "./ReadmeImg"; | |||
import Text from "./Text"; | |||
export interface Props { | |||
cover?: string; | |||
track: string; | |||
artist: string; | |||
progress: number; | |||
duration: number; | |||
isPlaying: boolean; | |||
} | |||
export const Player: React.FC<Props> = ({ | |||
cover, | |||
track, | |||
artist, | |||
progress, | |||
duration, | |||
isPlaying, | |||
}) => { | |||
return ( | |||
<ReadmeImg width="256" height="64"> | |||
<style> | |||
{` | |||
.paused { | |||
animation-play-state: paused !important; | |||
background: #e1e4e8 !important; | |||
} | |||
img:not([src]) { | |||
content: url("data:image/gif;base64,R0lGODlhAQABAPAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="); | |||
border-radius: 6px; | |||
background: #FFF; | |||
border: 1px solid #e1e4e8; | |||
} | |||
p { | |||
display: block; | |||
opacity: 0; | |||
} | |||
.progress-bar { | |||
position: relative; | |||
width: 100%; | |||
height: 4px; | |||
margin: -1px; | |||
border: 1px solid #e1e4e8; | |||
border-radius: 4px; | |||
overflow: hidden; | |||
padding: 2px; | |||
z-index: 0; | |||
} | |||
#progress { | |||
position: absolute; | |||
top: -1px; | |||
left: 0; | |||
width: 100%; | |||
height: 6px; | |||
transform-origin: left center; | |||
background-color: #24292e; | |||
animation: progress ${duration}ms linear; | |||
animation-delay: -${progress}ms; | |||
} | |||
.progress-bar, | |||
#track, | |||
#artist, | |||
#cover { | |||
opacity: 0; | |||
animation: appear 300ms ease-out forwards; | |||
} | |||
#track { | |||
animation-delay: 400ms; | |||
} | |||
#artist { | |||
animation-delay: 500ms; | |||
} | |||
.progress-bar { | |||
animation-delay: 550ms; | |||
margin-top: 4px; | |||
} | |||
#cover { | |||
animation-name: cover-appear; | |||
animation-delay: 300ms; | |||
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; | |||
} | |||
@keyframes cover-appear { | |||
from { | |||
opacity: 0; | |||
transform: scale(0.8); | |||
} | |||
to { | |||
opacity: 1; | |||
transform: scale(1); | |||
} | |||
} | |||
@keyframes appear { | |||
from { | |||
opacity: 0; | |||
transform: translateX(-8px); | |||
} | |||
to { | |||
opacity: 1; | |||
transform: translateX(0); | |||
} | |||
} | |||
@keyframes progress { | |||
from { | |||
transform: scaleX(0) | |||
} | |||
to { | |||
transform: scaleX(1) | |||
} | |||
} | |||
`} | |||
</style> | |||
<div | |||
className={isPlaying ? "disabled" : ""} | |||
style={{ | |||
display: "flex", | |||
alignItems: "center", | |||
paddingTop: 8, | |||
paddingLeft: 4, | |||
}} | |||
> | |||
<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> | |||
{track && ( | |||
<div className="progress-bar"> | |||
<div id="progress" className={!isPlaying ? "paused" : ""} /> | |||
</div> | |||
)} | |||
</div> | |||
</div> | |||
</ReadmeImg> | |||
); | |||
}; |
@ -1,27 +0,0 @@ | |||
import React from "react"; | |||
const ReadmeImg = ({ width, height, children }) => { | |||
return ( | |||
<svg | |||
fill="none" | |||
width={width} | |||
height={height} | |||
viewBox={`0 0 ${width} ${height}`} | |||
xmlns="http://www.w3.org/2000/svg" | |||
> | |||
<foreignObject width={width} height={height}> | |||
<div {...{ xmlns: "http://www.w3.org/1999/xhtml" }}> | |||
<style>{` | |||
* { | |||
margin: 0; | |||
box-sizing: border-box; | |||
} | |||
`}</style> | |||
{children} | |||
</div> | |||
</foreignObject> | |||
</svg> | |||
); | |||
}; | |||
export default ReadmeImg; |
@ -1,51 +0,0 @@ | |||
import React from "react"; | |||
const sizes = { | |||
default: 14, | |||
small: 12, | |||
}; | |||
const colors = { | |||
default: "#24292e", | |||
"gray-light": "#e1e4e8", | |||
gray: "#586069", | |||
"gray-dark": "#24292e", | |||
}; | |||
const families = { | |||
default: | |||
"-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji", | |||
mono: "SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace", | |||
}; | |||
const weights = { | |||
default: 400, | |||
bold: 600, | |||
}; | |||
const Text: React.FC<any> = ({ | |||
children = "", | |||
weight = "default", | |||
family = "default", | |||
color = "default", | |||
size = "default", | |||
...props | |||
}) => { | |||
return ( | |||
<p | |||
style={{ | |||
whiteSpace: "pre", | |||
fontSize: `${sizes[size]}px`, | |||
lineHeight: 1.5, | |||
fontFamily: families[family], | |||
color: colors[color], | |||
fontWeight: weights[weight], | |||
}} | |||
{...props} | |||
> | |||
{children} | |||
</p> | |||
); | |||
}; | |||
export default Text; |
@ -1,216 +0,0 @@ | |||
{ | |||
"name": "novatorem", | |||
"version": "1.0.0", | |||
"lockfileVersion": 1, | |||
"requires": true, | |||
"dependencies": { | |||
"@types/node": { | |||
"version": "14.0.22", | |||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.22.tgz", | |||
"integrity": "sha512-emeGcJvdiZ4Z3ohbmw93E/64jRzUHAItSHt8nF7M4TGgQTiWqFVGB8KNpLGFmUHmHLvjvBgFwVlqNcq+VuGv9g==", | |||
"dev": true | |||
}, | |||
"@types/prop-types": { | |||
"version": "15.7.3", | |||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", | |||
"integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==", | |||
"dev": true | |||
}, | |||
"@types/react": { | |||
"version": "16.9.42", | |||
"resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.42.tgz", | |||
"integrity": "sha512-iGy6HwfVfotqJ+PfRZ4eqPHPP5NdPZgQlr0lTs8EfkODRBV9cYy8QMKcC9qPCe1JrESC1Im6SrCFR6tQgg74ag==", | |||
"dev": true, | |||
"requires": { | |||
"@types/prop-types": "*", | |||
"csstype": "^2.2.0" | |||
} | |||
}, | |||
"@types/react-dom": { | |||
"version": "16.9.8", | |||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.8.tgz", | |||
"integrity": "sha512-ykkPQ+5nFknnlU6lDd947WbQ6TE3NNzbQAkInC2EKY1qeYdTKp7onFusmYZb+ityzx2YviqT6BXSu+LyWWJwcA==", | |||
"dev": true, | |||
"requires": { | |||
"@types/react": "*" | |||
} | |||
}, | |||
"@vercel/node": { | |||
"version": "1.7.2", | |||
"resolved": "https://registry.npmjs.org/@vercel/node/-/node-1.7.2.tgz", | |||
"integrity": "sha512-XV5lrLC+K/cxsaFj8H2OoGu1zliOqnxcrOnPInI8HmQjR/Tztt+0nzgpt+7sx8wXcrib0Nu7lK303jP7VjSETw==", | |||
"dev": true, | |||
"requires": { | |||
"@types/node": "*", | |||
"ts-node": "8.9.1", | |||
"typescript": "3.9.3" | |||
}, | |||
"dependencies": { | |||
"typescript": { | |||
"version": "3.9.3", | |||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.3.tgz", | |||
"integrity": "sha512-D/wqnB2xzNFIcoBG9FG8cXRDjiqSTbG2wd8DMZeQyJlP1vfTkIxH4GKveWaEBYySKIg+USu+E+EDIR47SqnaMQ==", | |||
"dev": true | |||
} | |||
} | |||
}, | |||
"arg": { | |||
"version": "4.1.3", | |||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", | |||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", | |||
"dev": true | |||
}, | |||
"buffer-from": { | |||
"version": "1.1.1", | |||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", | |||
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", | |||
"dev": true | |||
}, | |||
"csstype": { | |||
"version": "2.6.11", | |||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.11.tgz", | |||
"integrity": "sha512-l8YyEC9NBkSm783PFTvh0FmJy7s5pFKrDp49ZL7zBGX3fWkO+N4EEyan1qqp8cwPLDcD0OSdyY6hAMoxp34JFw==", | |||
"dev": true | |||
}, | |||
"diff": { | |||
"version": "4.0.2", | |||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", | |||
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", | |||
"dev": true | |||
}, | |||
"isomorphic-unfetch": { | |||
"version": "3.0.0", | |||
"resolved": "https://registry.npmjs.org/isomorphic-unfetch/-/isomorphic-unfetch-3.0.0.tgz", | |||
"integrity": "sha512-V0tmJSYfkKokZ5mgl0cmfQMTb7MLHsBMngTkbLY0eXvKqiVRRoZP04Ly+KhKrJfKtzC9E6Pp15Jo+bwh7Vi2XQ==", | |||
"requires": { | |||
"node-fetch": "^2.2.0", | |||
"unfetch": "^4.0.0" | |||
} | |||
}, | |||
"js-tokens": { | |||
"version": "4.0.0", | |||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", | |||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" | |||
}, | |||
"loose-envify": { | |||
"version": "1.4.0", | |||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", | |||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", | |||
"requires": { | |||
"js-tokens": "^3.0.0 || ^4.0.0" | |||
} | |||
}, | |||
"make-error": { | |||
"version": "1.3.6", | |||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", | |||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", | |||
"dev": true | |||
}, | |||
"node-fetch": { | |||
"version": "2.6.0", | |||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", | |||
"integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" | |||
}, | |||
"object-assign": { | |||
"version": "4.1.1", | |||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", | |||
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" | |||
}, | |||
"prop-types": { | |||
"version": "15.7.2", | |||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", | |||
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", | |||
"requires": { | |||
"loose-envify": "^1.4.0", | |||
"object-assign": "^4.1.1", | |||
"react-is": "^16.8.1" | |||
} | |||
}, | |||
"querystring": { | |||
"version": "0.2.0", | |||
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", | |||
"integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" | |||
}, | |||
"react": { | |||
"version": "16.13.1", | |||
"resolved": "https://registry.npmjs.org/react/-/react-16.13.1.tgz", | |||
"integrity": "sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w==", | |||
"requires": { | |||
"loose-envify": "^1.1.0", | |||
"object-assign": "^4.1.1", | |||
"prop-types": "^15.6.2" | |||
} | |||
}, | |||
"react-dom": { | |||
"version": "16.13.1", | |||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.1.tgz", | |||
"integrity": "sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag==", | |||
"requires": { | |||
"loose-envify": "^1.1.0", | |||
"object-assign": "^4.1.1", | |||
"prop-types": "^15.6.2", | |||
"scheduler": "^0.19.1" | |||
} | |||
}, | |||
"react-is": { | |||
"version": "16.13.1", | |||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", | |||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" | |||
}, | |||
"scheduler": { | |||
"version": "0.19.1", | |||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", | |||
"integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", | |||
"requires": { | |||
"loose-envify": "^1.1.0", | |||
"object-assign": "^4.1.1" | |||
} | |||
}, | |||
"source-map": { | |||
"version": "0.6.1", | |||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", | |||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", | |||
"dev": true | |||
}, | |||
"source-map-support": { | |||
"version": "0.5.19", | |||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", | |||
"integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", | |||
"dev": true, | |||
"requires": { | |||
"buffer-from": "^1.0.0", | |||
"source-map": "^0.6.0" | |||
} | |||
}, | |||
"ts-node": { | |||
"version": "8.9.1", | |||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.9.1.tgz", | |||
"integrity": "sha512-yrq6ODsxEFTLz0R3BX2myf0WBCSQh9A+py8PBo1dCzWIOcvisbyH6akNKqDHMgXePF2kir5mm5JXJTH3OUJYOQ==", | |||
"dev": true, | |||
"requires": { | |||
"arg": "^4.1.0", | |||
"diff": "^4.0.1", | |||
"make-error": "^1.1.1", | |||
"source-map-support": "^0.5.17", | |||
"yn": "3.1.1" | |||
} | |||
}, | |||
"typescript": { | |||
"version": "3.9.6", | |||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.6.tgz", | |||
"integrity": "sha512-Pspx3oKAPJtjNwE92YS05HQoY7z2SFyOpHo9MqJor3BXAGNaPUs83CuVp9VISFkSjyRfiTpmKuAYGJB7S7hOxw==", | |||
"dev": true | |||
}, | |||
"unfetch": { | |||
"version": "4.1.0", | |||
"resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.1.0.tgz", | |||
"integrity": "sha512-crP/n3eAPUJxZXM9T80/yv0YhkTEx2K1D3h7D1AJM6fzsWZrxdyRuLN0JH/dkZh1LNH8LxCnBzoPFCPbb2iGpg==" | |||
}, | |||
"yn": { | |||
"version": "3.1.1", | |||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", | |||
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", | |||
"dev": true | |||
} | |||
} | |||
} |
@ -1,31 +0,0 @@ | |||
{ | |||
"name": "novatorem", | |||
"private": true, | |||
"version": "1.0.0", | |||
"description": "Github profile", | |||
"main": "index.js", | |||
"dependencies": { | |||
"isomorphic-unfetch": "^3.0.0", | |||
"querystring": "^0.2.0", | |||
"react": "^16.13.1", | |||
"react-dom": "^16.13.1", | |||
"typescript": "^3.9.6" | |||
}, | |||
"devDependencies": { | |||
"@types/node": "^14.0.22", | |||
"@types/react": "^16.9.42", | |||
"@types/react-dom": "^16.9.8", | |||
"@vercel/node": "^1.7.2" | |||
}, | |||
"repository": { | |||
"type": "git", | |||
"url": "git+https://github.com/novatorem/novatorem.git" | |||
}, | |||
"keywords": [], | |||
"author": "Andrew Novac", | |||
"license": "MIT", | |||
"bugs": { | |||
"url": "https://github.com/novatorem/novatorem/issues" | |||
}, | |||
"homepage": "https://github.com/novatorem/novatorem#readme" | |||
} |
@ -1,10 +0,0 @@ | |||
{ | |||
"compilerOptions": { | |||
"module": "ES2015", | |||
"target": "ES2017", | |||
"jsx": "react", | |||
"esModuleInterop": true, | |||
"skipLibCheck": true, | |||
"forceConsistentCasingInFileNames": true | |||
} | |||
} |
@ -1,49 +0,0 @@ | |||
import fetch from "isomorphic-unfetch"; | |||
import { stringify } from "querystring"; | |||
const { | |||
SPOTIFY_CLIENT_ID: client_id, | |||
SPOTIFY_CLIENT_SECRET: client_secret, | |||
SPOTIFY_REFRESH_TOKEN: refresh_token, | |||
} = process.env; | |||
const basic = Buffer.from(`${client_id}:${client_secret}`).toString( | |||
"base64" | |||
); | |||
const Authorization = `Basic ${basic}`; | |||
async function getAuthorizationToken() { | |||
const url = new URL("https://accounts.spotify.com/api/token"); | |||
const body = stringify({ | |||
grant_type: "refresh_token", | |||
refresh_token, | |||
}); | |||
const response = await fetch(`${url}`, { | |||
method: "POST", | |||
headers: { | |||
Authorization, | |||
"Content-Type": "application/x-www-form-urlencoded", | |||
}, | |||
body, | |||
}).then((r) => r.json()); | |||
return `Bearer ${response.access_token}`; | |||
} | |||
const NOW_PLAYING_ENDPOINT = `https://api.spotify.com/v1/me/player/currently-playing`; | |||
export async function nowPlaying () { | |||
const Authorization = await getAuthorizationToken(); | |||
const response = await fetch(NOW_PLAYING_ENDPOINT, { | |||
headers: { | |||
Authorization, | |||
}, | |||
}); | |||
const { status } = response; | |||
if (status === 204) { | |||
return {}; | |||
} else if (status === 200) { | |||
const data = await response.json(); | |||
return data; | |||
} | |||
} |
@ -1,6 +0,0 @@ | |||
{ | |||
"version": 2, | |||
"rewrites": [ | |||
{ "source": "/now-playing", "destination": "/api/now-playing" } | |||
] | |||
} |