diff --git a/.gitignore b/.gitignore index 0ffd267..3510eea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ -.vercel +venv +__pycache__ +.cache* +env .env -node_modules -.DS_STORE diff --git a/README.md b/README.md index 8d9d90b..a1b7742 100644 --- a/README.md +++ b/README.md @@ -1,5 +1 @@ -**Now Playing** - - - - +![Spotify](https://novatorem.vercel.app/api/spotify-playing) diff --git a/api/now-playing.ts b/api/now-playing.ts deleted file mode 100644 index 5598ffe..0000000 --- a/api/now-playing.ts +++ /dev/null @@ -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); -} diff --git a/api/requirements.txt b/api/requirements.txt new file mode 100644 index 0000000..4b21b4d --- /dev/null +++ b/api/requirements.txt @@ -0,0 +1,3 @@ +flask==1.1.2 +requests==2.24.0 +python-dotenv==0.14.0 \ No newline at end of file diff --git a/api/spotify-playing.py b/api/spotify-playing.py new file mode 100644 index 0000000..52dd1d1 --- /dev/null +++ b/api/spotify-playing.py @@ -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(["
" 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("/- {children} -
- ); -}; - -export default Text; diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 1c641f3..0000000 --- a/package-lock.json +++ /dev/null @@ -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 - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 814c06a..0000000 --- a/package.json +++ /dev/null @@ -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" -} diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index 67bb076..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "compilerOptions": { - "module": "ES2015", - "target": "ES2017", - "jsx": "react", - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true - } -} diff --git a/utils/spotify.ts b/utils/spotify.ts deleted file mode 100644 index 8b9d33c..0000000 --- a/utils/spotify.ts +++ /dev/null @@ -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; - } -} diff --git a/vercel.json b/vercel.json deleted file mode 100644 index 2832ae7..0000000 --- a/vercel.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "version": 2, - "rewrites": [ - { "source": "/now-playing", "destination": "/api/now-playing" } - ] -}