Dynamic realtime profile ReadMe linked with spotify
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

216 lines
7.7 KiB

5 years ago
  1. from flask import Flask, Response, jsonify
  2. from base64 import b64encode
  3. from dotenv import load_dotenv, find_dotenv
  4. load_dotenv(find_dotenv())
  5. import requests
  6. import json
  7. import os
  8. import random
  9. """
  10. Inspired from https://github.com/natemoo-re
  11. """
  12. print("Starting Server")
  13. SPOTIFY_CLIENT_ID = os.getenv("SPOTIFY_CLIENT_ID")
  14. SPOTIFY_SECRET_ID = os.getenv("SPOTIFY_SECRET_ID")
  15. SPOTIFY_REFRESH_TOKEN = os.getenv("SPOTIFY_REFRESH_TOKEN")
  16. SPOTIFY_URL_REFRESH_TOKEN = "https://accounts.spotify.com/api/token"
  17. SPOTIFY_URL_NOW_PLAYING = "https://api.spotify.com/v1/me/player/currently-playing"
  18. LATEST_PLAY = None
  19. app = Flask(__name__)
  20. def get_authorization():
  21. return b64encode(f"{SPOTIFY_CLIENT_ID}:{SPOTIFY_SECRET_ID}".encode()).decode("ascii")
  22. def refresh_token():
  23. data = {
  24. "grant_type": "refresh_token",
  25. "refresh_token": SPOTIFY_REFRESH_TOKEN,
  26. }
  27. headers = {"Authorization": "Basic {}".format(get_authorization())}
  28. response = requests.post(SPOTIFY_URL_REFRESH_TOKEN, data=data, headers=headers)
  29. repsonse_json = response.json()
  30. return repsonse_json["access_token"]
  31. def get_now_playing():
  32. token = refresh_token()
  33. headers = {"Authorization": f"Bearer {token}"}
  34. response = requests.get(SPOTIFY_URL_NOW_PLAYING, headers=headers)
  35. if response.status_code == 204:
  36. return {}
  37. repsonse_json = response.json()
  38. return repsonse_json
  39. def get_svg_template():
  40. css_bar = ""
  41. left = 1
  42. for i in range(1, 76):
  43. anim = random.randint(350, 500)
  44. css_bar += ".bar:nth-child({}) {{{{ left: {}px; animation-duration: {}ms; }}}}".format(
  45. i, left, anim
  46. )
  47. left += 4
  48. svg = (
  49. """
  50. <svg width="320" height="445" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  51. <foreignObject width="320" height="445">
  52. <div xmlns="http://www.w3.org/1999/xhtml" class="container">
  53. <style>
  54. div {{font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;}}
  55. .container {{background-color: #121212; border-radius: 10px; padding: 10px 10px}}
  56. .playing {{ font-weight: bold; color: #53b14f; text-align: center; display: flex; justify-content: center; align-items: center;}}
  57. .not-play {{color: #ff1616;}}
  58. .artist {{ font-weight: bold; font-size: 20px; color: #fff; text-align: center; margin-top: 5px; }}
  59. .song {{ font-size: 16px; color: #b3b3b3; text-align: center; margin-top: 5px; margin-bottom: 15px; }}
  60. .logo {{ margin-left: 5px; margin-top: 5px; }}
  61. .cover {{ border-radius: 5px; margin-top: 9px; }}
  62. #bars {{
  63. height: 30px;
  64. margin: -20px 0 0 0px;
  65. position: absolute;
  66. width: 40px;
  67. }}
  68. .bar {{
  69. background: #53b14f;
  70. bottom: 1px;
  71. height: 3px;
  72. position: absolute;
  73. width: 3px;
  74. animation: sound 0ms -800ms linear infinite alternate;
  75. }}
  76. @keyframes sound {{
  77. 0% {{
  78. opacity: .35;
  79. height: 3px;
  80. }}
  81. 100% {{
  82. opacity: 1;
  83. height: 28px;
  84. }}
  85. }}
  86. """
  87. + css_bar
  88. + """
  89. </style>
  90. {}
  91. </div>
  92. </foreignObject>
  93. </svg>
  94. """
  95. )
  96. return svg
  97. def load_image_b64(url):
  98. resposne = requests.get(url)
  99. return b64encode(resposne.content).decode("ascii")
  100. def make_svg(data):
  101. global LATEST_PLAY
  102. template = get_svg_template()
  103. text = "Now playing"
  104. content_bar = "".join(["<div class='bar'></div>" for i in range(75)])
  105. if data == {} and LATEST_PLAY is not None:
  106. data = LATEST_PLAY
  107. text = "Latest play"
  108. content_bar = ""
  109. elif data == {}:
  110. content = """
  111. <div class="playing not-play">Nothing playing on Spotify</div>
  112. """
  113. return template.format(content)
  114. content = """
  115. <div class="playing">{} 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>
  116. <div class="artist">{}</div>
  117. <div class="song">{}</div>
  118. <div id='bars'>
  119. {}
  120. </div>
  121. <a href="{}" target="_BLANK">
  122. <center>
  123. <img src="data:image/png;base64, {}" width="300" height="300" class="cover"/>
  124. </center>
  125. </a>
  126. """
  127. item = data["item"]
  128. """
  129. print(json.dumps(item))
  130. print(item["artists"][0]["name"])
  131. print(item["external_urls"]["spotify"])
  132. print(item["album"]["images"][0]["url"])
  133. """
  134. img = load_image_b64(item["album"]["images"][1]["url"])
  135. artist_name = item["artists"][0]["name"].replace("&", "&amp;")
  136. song_name = item["name"].replace("&", "&amp;")
  137. content_rendered = content.format(
  138. text,
  139. artist_name,
  140. song_name,
  141. content_bar,
  142. item["external_urls"]["spotify"],
  143. img,
  144. )
  145. return template.format(content_rendered)
  146. @app.route("/", defaults={"path": ""})
  147. @app.route("/<path:path>")
  148. def catch_all(path):
  149. global LATEST_PLAY
  150. # TODO: caching
  151. data = get_now_playing()
  152. svg = make_svg(data)
  153. # cache lastest data
  154. if data != {}:
  155. LATEST_PLAY = data
  156. resp = Response(svg, mimetype="image/svg+xml")
  157. resp.headers["Cache-Control"] = "s-maxage=1"
  158. return resp
  159. if __name__ == "__main__":
  160. app.run(debug=True)