Skip to content

Instantly share code, notes, and snippets.

@Inobtenio
Last active October 20, 2025 07:27
Show Gist options
  • Select an option

  • Save Inobtenio/9e1840cbe3eb82a02c7e7c0ff9d57867 to your computer and use it in GitHub Desktop.

Select an option

Save Inobtenio/9e1840cbe3eb82a02c7e7c0ff9d57867 to your computer and use it in GitHub Desktop.
updater gets what's playing on Spotify, looks at the album cover and extracts the most dominant/eye-catching color as well as the track data, server serves said data alongside a preformatted album cover for its use in the Vobot Mini Dock. More info at https://inobtenio.com/posts/vobot-mini-dock-spotify/.
import json
from flask import Flask, send_from_directory
from flask_restful import Resource, Api
app = Flask(__name__)
api = Api(app)
class CurrentSong(Resource):
def get(self):
with open('data.json') as f:
d = json.load(f)
return d
class CurrentSongCover(Resource):
def get(self):
return send_from_directory('', 'cover.jpg')
class CurrentSongFullCover(Resource):
def get(self):
return send_from_directory('', 'full_cover.jpg')
api.add_resource(CurrentSong, '/current')
api.add_resource(CurrentSongCover, '/cover')
api.add_resource(CurrentSongFullCover, '/full-cover')
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=4444)
import PIL
import json
import asyncio
import requests
import binascii
from PIL import Image, ImageDraw
from io import BytesIO
import math
import colorsys
import numpy
import numpy as np
from sklearn.cluster import KMeans
qC = 4.9226
qD = 1.4060
qR = 0.7932
def patch_asscalar(a):
return a.item()
setattr(numpy, "asscalar", patch_asscalar)
client_id = "..."
client_secret = "..."
refresh_token = "..."
access_token = None
current_track_data = {}
image = None
top_tracks = None
def is_color_light(rgb):
r, g, b = rgb
brightness = 0.299 * r + 0.587 * g + 0.114 * b
return brightness > 135
def calculate_dark_colorfulness(rgb):
red, green, blue = [x / 255.0 for x in rgb]
red_greenness = red - green # or the a component
yellow_blueness = (red + green)/2 - blue # or the b component. red + green output yellow in additive color (light)
chroma = math.sqrt(red_greenness ** 2 + yellow_blueness ** 2)
luminance = 0.2126 * red + 0.7152 * green + 0.0722 * blue # Relative luminance
darkness = 1 - luminance
return (chroma, darkness)
def increase_saturation(rgb, amount=0.2):
r, g, b = [x / 255.0 for x in rgb]
h, l, s = colorsys.rgb_to_hls(r, g, b)
s = min(1.0, s + amount) # Cap at 1.0
r_new, g_new, b_new = colorsys.hls_to_rgb(h, l, s)
return tuple(int(x * 255) for x in (r_new, g_new, b_new))
def improve_white_contrast(rgb, brightness_factor=0.85, saturation_boost=1.1):
"""
Improve contrast of a color against white by darkening and boosting saturation.
Args:
rgb (tuple): (R, G, B) tuple in 0–255.
brightness_factor (float): < 1 to darken.
saturation_boost (float): > 1 to increase saturation.
Returns:
tuple: Adjusted (R, G, B) in 0–255.
"""
r, g, b = [x / 255 for x in rgb]
h, s, v = colorsys.rgb_to_hsv(r, g, b)
s = min(s * saturation_boost, 1.0)
v = max(v * brightness_factor, 0.0) # darken
r, g, b = colorsys.hsv_to_rgb(h, s, v)
return tuple(int(x * 255) for x in (r, g, b))
def extract_color_clusters(num_clusters):
image = Image.open("./full_cover.jpg").convert("RGB")
w, h = image.size
try:
pixels = np.array(image).reshape(-1, 3) # Turn a RGB matrix into an RGB 2D array
# Cluster colors using K-Means
kmeans = KMeans(n_clusters=num_clusters, random_state=0, n_init="auto")
kmeans.fit(pixels)
labels = kmeans.labels_
colors = []
# Group colors by cluster and calculate average for each
clusters = [[] for _ in range(num_clusters)]
for i, label in enumerate(labels):
clusters[label].append(pixels[i])
for group in clusters:
color = np.mean(group, axis=0)
chroma, darkness = calculate_dark_colorfulness(color)
dominance = len(group)/(w*h)
score = chroma * qC + darkness * qD + dominance * qR
colors.append({
'color': tuple(int(c) for c in color),
'chroma': chroma,
'darkness': darkness,
'dominance': dominance,
'score': score#,
})
return colors
except Exception as e:
return [{
'color': (128,128,128),
'chroma': 0,
'darkness': 0.75,
'dominance': 1.0,
'score': 1.0
}]
def get_best_color():
return max(extract_color_clusters(20), key=lambda c: c['score'])
def get_basic_auth_header(client_id, client_secret):
creds = f"{client_id}:{client_secret}"
return binascii.b2a_base64(creds.encode()).strip().decode()
def get_access_token():
auth_header = get_basic_auth_header(client_id, client_secret)
headers = {
"Authorization": f"Basic {auth_header}",
"Content-Type": "application/x-www-form-urlencoded"
}
payload = "grant_type=refresh_token&refresh_token=" + refresh_token
response = requests.post(
"https://accounts.spotify.com/api/token",
data=payload,
headers=headers
)
data = response.json()
return data["access_token"]
def get_currently_playing(access_token):
headers = {
"Authorization": f"Bearer {access_token}"
}
response = requests.get(
"https://api.spotify.com/v1/me/player/currently-playing?additional_types=episode",
headers=headers
)
if response.status_code == 200 and response.content:
data = response.json()
track = data["item"]
current_track_data["id"] = track["id"]
current_track_data["song_name"] = track["name"]
if data["currently_playing_type"] == "episode":
current_track_data["artist_names"] = track["show"]["publisher"]
get_currently_playing_cover(track["images"][0]["url"])
else:
current_track_data["artist_names"] = ', '.join(artist["name"] for artist in track["artists"])
get_currently_playing_cover(track["album"]["images"][0]["url"])
highlighted_color = improve_white_contrast(get_best_color()["color"], 0.80, 1.7)
print(highlighted_color)
if isinstance(highlighted_color, int):
highlighted_color = (highlighted_color, highlighted_color, highlighted_color)
current_track_data["album_cover_main_color"] = '#%02x%02x%02x' % highlighted_color
current_track_data["album_cover_main_color_is_light"] = is_color_light(highlighted_color)
current_track_data["progress"] = data["progress_ms"] / 1000
current_track_data["duration"] = track["duration_ms"] / 1000
current_track_data["is_playing"] = data["is_playing"]
print(f'{current_track_data["song_name"]} - {current_track_data["artist_names"]}')
with open("data.json", 'w') as file:
#json.dump(track_response.json(), file, indent=2)
json.dump(current_track_data, file, indent=2)
elif response.status_code == 401:
access_token = get_access_token()
else:
print(response.content)
print("Nothing is currently playing.")
def get_currently_playing_cover(url):
response = requests.get(url)
if response.status_code == 200 and response.content:
image = Image.open(BytesIO(response.content))
new_image = image.resize((160, 160), PIL.Image.BILINEAR)
#new_image = new_image.convert("RGBA")
image.save("full_cover.jpg", format="JPEG")
new_image.save("cover.jpg", format="JPEG", quality=40, optimize=True)
return image
else:
print("Nothing is currently playing.")
async def run_every_n_seconds():
while True:
await asyncio.to_thread(get_currently_playing, access_token)
await asyncio.sleep(2)
if __name__ == '__main__':
access_token = get_access_token()
asyncio.run(run_every_n_seconds())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment