auto syncing lyrics, canvas replaces album art (if available), fixed layout (margins, padding, etc.), refactored some code

This commit is contained in:
2024-04-25 16:11:17 -07:00
parent eb837080fa
commit ad989ad50b
11 changed files with 236 additions and 42 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1 @@
{"token": "240425a54f5df91735a14b241dcfb1e5df28f942d95b9cad8ea6a4", "expiration_time": 1714086845}

Binary file not shown.

57
_canvas.py Executable file
View File

@@ -0,0 +1,57 @@
import requests
import random
from protos.canvas_pb2 import EntityCanvazRequest, EntityCanvazResponse
API_HOST = "https://spclient.wg.spotify.com"
CANVAS_ROUTE = "/canvaz-cache/v0/canvases"
TOKEN_ENDPOINT = "https://open.spotify.com/get_access_token?reason=transport"
TOKEN_RENEW_TIME = 900
TRACK_URI_PREFIX = "spotify:track:"
OAUTH_SCOPES = "playlist-read"
def get_access_token():
try:
response = requests.get(TOKEN_ENDPOINT)
data = response.json()
return data["accessToken"]
except Exception as e:
raise Exception(e)
def get_canvas_for_track(access_token, track_id):
canvas_request = EntityCanvazRequest()
canvas_request_entities = canvas_request.entities.add()
canvas_request_entities.entity_uri = TRACK_URI_PREFIX + track_id
try:
resp = requests.post(
API_HOST + CANVAS_ROUTE,
headers={
"Accept": "application/protobuf",
"Content-Type": "application/x-www-form-urlencoded",
"Accept-Language": "en",
"User-Agent": "Spotify/8.5.49 iOS/Version 13.3.1 (Build 17D50)",
"Accept-Encoding": "gzip, deflate, br",
"Authorization": "Bearer %s" % access_token
},
data=canvas_request.SerializeToString(),
)
except:
raise ConnectionError
canvas_response = EntityCanvazResponse()
canvas_response.ParseFromString(resp.content)
if(len(canvas_response.canvases) == 0):
raise AttributeError
canvas = random.choice(canvas_response.canvases)
if canvas.url is not None:
return canvas.url
else:
return None
# access_token = get_access_token()
# print(get_canvas_for_track(access_token, "1kADZJDyRUbmlLxYiqi077"))

2
code
View File

@@ -1 +1 @@
AQBtjZ5AgmaU073h9RsK82t8w2XMeQ_HTSbqbAyd3PNVQe2YNnkM883WOcpbDfKt4ll2xu0LoUWEMX5sEUmtPSWCgFs1k_3GFUxtUJV8VslZUvgIYBUSSbqNWrxZqRtGJ-cL_Rdq6gbBKttHkkMdnZVeO2jUtJM2zCHSYEW49o-aoRpcMkzPm66_H88O9S8hVEegS7nG4uP0AUYwe0EiW6YMvFkCVG6XJ7AJpcO97U0tP0PhRbBrx1Y5lRAequ5YaY-BC4FFHwBcA8XePKvAnuDku0sbRd1vbw6dahcRdco
AQDU_0NUgm9-EMYMDBYUTnBApWyYO6m4EDUExbHzjM_CcVkoGptsE69uePQ7ejw5NooRTCpQqIU7ZfOTfE5Nx8zj6amQBFRkgv7vMa5ViqCbH_hqos--D6mj-eG3jqW-sf-Oyq0vmPUaam6R0fB--ghWzM8EG8SRVQBGTnEN-OLf1PBX5I1sOS22GXrMud5BcFziAiYQxHn_GcnPXEyXigkHAsk60PFiWDUwW6fLdUKW2FtyJnnYH01al28eex1QEeqr2gg8e5ht9XGKfQej0ciFM62ESwIbpDZDi6bWdSk

Binary file not shown.

Binary file not shown.

37
protos/canvas_pb2.py Executable file
View File

@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: canvas.proto
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0c\x63\x61nvas.proto\x12\x17\x63om.spotify.canvazcache\"3\n\x06\x41rtist\x12\x0b\n\x03uri\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0e\n\x06\x61vatar\x18\x03 \x01(\t\"\xe6\x02\n\x14\x45ntityCanvazResponse\x12\x46\n\x08\x63\x61nvases\x18\x01 \x03(\x0b\x32\x34.com.spotify.canvazcache.EntityCanvazResponse.Canvaz\x12\x16\n\x0ettl_in_seconds\x18\x02 \x01(\x03\x1a\xed\x01\n\x06\x43\x61nvaz\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0b\n\x03url\x18\x02 \x01(\t\x12\x0f\n\x07\x66ile_id\x18\x03 \x01(\t\x12+\n\x04type\x18\x04 \x01(\x0e\x32\x1d.com.spotify.canvazcache.Type\x12\x12\n\nentity_uri\x18\x05 \x01(\t\x12/\n\x06\x61rtist\x18\x06 \x01(\x0b\x32\x1f.com.spotify.canvazcache.Artist\x12\x10\n\x08\x65xplicit\x18\x07 \x01(\x08\x12\x13\n\x0buploaded_by\x18\x08 \x01(\t\x12\x0c\n\x04\x65tag\x18\t \x01(\t\x12\x12\n\ncanvas_uri\x18\x0b \x01(\t\"\x88\x01\n\x13\x45ntityCanvazRequest\x12\x45\n\x08\x65ntities\x18\x01 \x03(\x0b\x32\x33.com.spotify.canvazcache.EntityCanvazRequest.Entity\x1a*\n\x06\x45ntity\x12\x12\n\nentity_uri\x18\x01 \x01(\t\x12\x0c\n\x04\x65tag\x18\x02 \x01(\t*R\n\x04Type\x12\t\n\x05IMAGE\x10\x00\x12\t\n\x05VIDEO\x10\x01\x12\x11\n\rVIDEO_LOOPING\x10\x02\x12\x18\n\x14VIDEO_LOOPING_RANDOM\x10\x03\x12\x07\n\x03GIF\x10\x04\x42\x16\n\x12\x63om.spotify.canvazH\x02\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'canvas_pb2', _globals)
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\022com.spotify.canvazH\002'
_globals['_TYPE']._serialized_start=594
_globals['_TYPE']._serialized_end=676
_globals['_ARTIST']._serialized_start=41
_globals['_ARTIST']._serialized_end=92
_globals['_ENTITYCANVAZRESPONSE']._serialized_start=95
_globals['_ENTITYCANVAZRESPONSE']._serialized_end=453
_globals['_ENTITYCANVAZRESPONSE_CANVAZ']._serialized_start=216
_globals['_ENTITYCANVAZRESPONSE_CANVAZ']._serialized_end=453
_globals['_ENTITYCANVAZREQUEST']._serialized_start=456
_globals['_ENTITYCANVAZREQUEST']._serialized_end=592
_globals['_ENTITYCANVAZREQUEST_ENTITY']._serialized_start=550
_globals['_ENTITYCANVAZREQUEST_ENTITY']._serialized_end=592
# @@protoc_insertion_point(module_scope)

View File

@@ -1,11 +1,11 @@
from flask import Flask, render_template, request, url_for, redirect, send_from_directory
import time
import requests
from urllib.parse import urlencode
import webbrowser
import base64
import json
import os
import _canvas as SpotifyCanvas
import time
import syncedlyrics
app = Flask(__name__)
@@ -88,6 +88,7 @@ def callback():
@app.route('/appdata')
def appdata():
global access_token
stime = time.time()
user_headers = {
"Authorization": "Bearer " + access_token,
"Content-Type": "application/json"
@@ -96,9 +97,12 @@ def appdata():
currently_playing = requests.get("https://api.spotify.com/v1/me/player/currently-playing", headers=user_headers)
if currently_playing.content:
if currently_playing.json()["is_playing"] is True and currently_playing.json()["item"]["id"] == request.args.get('id'):
return { 'progress_ms': currently_playing.json()["progress_ms"]
print("QUICK" + str(time.time() - stime))
return { 'progress_ms': currently_playing.json()["progress_ms"],
}
elif currently_playing.json()["is_playing"] is True and currently_playing.json()["item"]["id"] != request.args.get('id'):
print("FULL" + str(time.time() - stime))
return { 'id': currently_playing.json()["item"]["id"],
'name': currently_playing.json()["item"]["name"],
'artist': currently_playing.json()["item"]["artists"][0]["name"],
@@ -107,20 +111,19 @@ def appdata():
'is_playing': currently_playing.json()["is_playing"],
'progress_ms': currently_playing.json()["progress_ms"],
'duration_ms': currently_playing.json()["item"]["duration_ms"],
'is_liked': requests.get("https://api.spotify.com/v1/me/tracks/contains?ids=" + currently_playing.json()["item"]["id"], headers=user_headers).json()[0]
'is_liked': requests.get("https://api.spotify.com/v1/me/tracks/contains?ids=" + currently_playing.json()["item"]["id"], headers=user_headers).json()[0],
'canvas': False,
'fetchlyrics': 'fetch'
}
elif currently_playing.json()["is_playing"] is False:
return { 'name': "Not Playing",
'is_playing': False
return { 'is_playing': False
}
else:
return { 'name': "Error",
'artist': "Error"
}
else:
return { 'name': "Not Playing",
'image': "https://cdn.psychologytoday.com/sites/default/files/styles/article-inline-half-caption/public/field_blog_entry_images/2022-02/pause.png",
'is_playing': False
return { 'is_playing': False
}
@app.route('/control', methods=['POST'])
@@ -157,7 +160,26 @@ def control():
def icons(filename):
return send_from_directory('icons', filename)
@app.route('/canvas')
def canvas():
print("CANVAS")
id = request.args.get('id')
try:
return { 'canvas_url': SpotifyCanvas.get_canvas_for_track(SpotifyCanvas.get_access_token(), id) }
except AttributeError:
return { 'canvas_url': None }
@app.route('/lyrics')
def lyrics():
name = request.args.get('name')
artist = request.args.get('artist')
if name and artist is not None:
full_lyrics = syncedlyrics.search("[" + name + "] [" + artist + "]")
else:
return { 'lyrics': '' }
if full_lyrics is None:
return { 'lyrics': "no lyrics" }
return { 'lyrics': full_lyrics }
if __name__ == '__main__':
app.run(port=8888, debug=True)

View File

@@ -21,31 +21,48 @@
}
.song-text {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
font-size: 20px;
font-size: 25px;
font-weight: bold;
text-align: center;
padding-left: 20px;
padding-right: 20px;
margin-bottom: 5px;
}
.artist-text {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
font-size: 15px;
font-size: 20px;
text-align: center;
padding-left: 20px;
padding-right: 20px;
margin-top: 5px;
}
.middle {
flex: 1;
text-align: center;
display: inline-block;
vertical-align: top;
padding-left: 20px;
padding-right: 20px;
}
.right {
height: 100%;
display: inline-block;
width: 33%;
}
.lyrics {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
font-size: 32px;
text-align: center;
}
.buttons {
display: initial;
padding-top: 10%;
}
.total {
display: flex;
align-items: center;
}
</style>
</head>
<body>
<!-- <link href="http://127.0.0.1:8888/font" rel="stylesheet"> -->
<div class="total">
<img id="image" src="{{ data['image'] }}" alt="Song Image" type="image/jpeg" style="max-width: 300px; max-height: 300px;">
<video wdith="300" height="300" autoplay muted playsinline loop id="canvas_url" src="{{ data['canvas_url'] }}" type="video/mp4" style="display: none;"></video>
<div class="middle">
<p id="name" class="song-text">{{ data['name'] }}</p>
<p id="artist" class="artist-text">{{ data['artist'] }}</p>
@@ -53,17 +70,23 @@
<p id="progress_ms">{{ data['progress_ms'] }}</p>
<p id="duration_ms">{{ data['duration_ms'] }}</p> -->
<p>
<a><img id="previous" src="http://127.0.0.1:8888/icons/previous-black.png" style="width:96px;height:96px;"></a>
<a><img id="playpause" src="http://127.0.0.1:8888/icons/play-black.png" style="width:96px;height:96px;"></a>
<a><img id="next" src="http://127.0.0.1:8888/icons/next-black.png" style="width:96px;height:96px;"></a>
<a><img id="like" src="http://127.0.0.1:8888/icons/heart-black.png" style="width:96px;height:96px;"></a>
<a><img id="previous" src="http://127.0.0.1:8888/icons/previous-black.png" class="buttons"></a>
<a><img id="playpause" src="http://127.0.0.1:8888/icons/play-black.png" class="buttons"></a>
<a><img id="next" src="http://127.0.0.1:8888/icons/next-black.png" class="buttons"></a>
<a><img id="like" src="http://127.0.0.1:8888/icons/heart-black.png" class="buttons" style="display:none;"></a>
</p>
</div>
<div class="right">
<p id="lyric" class="lyrics">{{ data['lyric'] }}</p>
</div>
</div>
<div style="padding-top: 5px;">
<div class="progress-container">
<div id="progress_bar" class="progress-bar"></div>
</div>
</div>
<script>
let id, is_playing, is_liked, duration_ms;
let id, is_playing, is_liked, duration_ms, canvas_url, lyrics;
// Function to update the values every second
function updateValues() {
$.ajax({
@@ -77,11 +100,10 @@
$('#is_playing').text(data['is_playing']);
$('#progress_ms').text(data['progress_ms']);
$('#duration_ms').text(data['duration_ms']);
// $('#progress_min').text(Math.floor(data['progress_ms'] / (1000 * 60)) % 60);
// $('#progress_sec').text(Math.floor((data['progress_ms'] / 1000) % 60));
// $('#duration_min').text(Math.floor(data['duration_ms'] / (1000 * 60) % 60));
// $('#duration_sec').text(Math.floor(data['duration_ms'] / 1000) % 60);
$('#is_liked').text(data['is_liked']);
$('#canvas').attr('src', data['canvas']);
if (data['id'] !== undefined) {
id = data['id'];
@@ -113,12 +135,62 @@
// Change button to like text
document.getElementById('like').src = 'http://127.0.0.1:8888/icons/heart-black.png';
}
if (data['canvas'] !== undefined) {
getCanvas(data['id']);
} else if (canvas_url == undefined) {
document.getElementById('canvas_url').style.display = "none";
document.getElementById('image').style.display = "";
}
getLyrics(data['name'], data['artist'], data['progress_ms'], data['fetchlyrics'])
}
});
}
// Update values every second
setInterval(updateValues, 3000);
setInterval(updateValues, 1000);
// Function to run alongside the looped function if a variable is true
function getCanvas(id) {
$.ajax({
url: '/canvas',
data: { id: id },
success: function(canvas_data) {
if (canvas_data['canvas_url'] !== undefined) {
$('#canvas_url').attr('src', canvas_data['canvas_url']);
document.getElementById('canvas_url').style.display = "";
document.getElementById('image').style.display = "none";
canvas_url = canvas_data['canvas_url'];
} else {
document.getElementById('canvas_url').style.display = "none";
document.getElementById('image').style.display = "";
}
}
});
}
function getLyrics(name, artist, progress_ms, fetchlyrics) {
if (lyrics == undefined || fetchlyrics !== undefined && name !== undefined) {
$('#lyric').text('');
$.ajax({
url: '/lyrics',
data: {name: name, artist: artist},
success: function(lyrics_data) {
lyrics = lyrics_data['lyrics'];
}
});
} else if (lyrics == 'no lyrics') {
} else {
progressMin = Math.floor(progress_ms / (1000 * 60)) % 60
progressSec = Math.floor((progress_ms / 1000) % 60)
const progressFormatted = `${progressMin}:${String(progressSec).padStart(2, '0')}`;
const lines = lyrics.split('\n');
for (const line of lines) {
if (line.includes(progressFormatted)) {
lyric = (line.split(']')[1])
$('#lyric').text(lyric);
}
}
}
}
// BUTTONS
$('#playpause').click(function() {
$.ajax({

View File

@@ -10,3 +10,8 @@ provide updated song info, otherwise have it return just song progress ONLY.
include color calculation, probably in python then give color value
to js app, but maybe have js app do it from album art, explore which is better.
CANT GET LYRICS VAR TO BE ACCESSED OUTSIDE OF FUNCTION. AHHHH
MAKE album/canvas 33.3%, middle 33.3%, lyrics 33.3% to make sure nothing
moves around if the album switches to a canvas