** MAJOR speed improvements **

Refactored code for TRUE multithreaded performance
startup time decreased from 2 secs to 0.2 secs
updates song time decreased from 3 secs to 0.1 secs
introduced proper and full error handler, can fully recover from errors
(including performing a full restart)
implemented web server to push updates and restart application (flask)
getting lyrics is truely mulithreaded now, all in memory.
This commit is contained in:
Brandon4466
2023-05-25 17:25:20 -07:00
parent 90b1448d93
commit 1e40570d57
8 changed files with 251 additions and 113 deletions

2
.cache
View File

@@ -1 +1 @@
{"access_token": "BQAlP-N225jrOKxkXWbI3uWIbprdAPRTdpgCbqYXPy4F1ZW5j_50Y5uheK72ndz8aHDMwW93zqIZj0w8lODitTO9pUVCN6OQaKlaPe5DZsLB2MSqi5bn6nXVAfELO76yEqqATSVSXNPqObjbbNzE1BFZJvkEM5pMie2prbadewOtAIPIqogNxsiQXLAvql_gJU6xGm077lIaTyh7pcgM", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "AQAXp9B05jv8qP5hp9cqwkz1nXQ9yTC1KUSJV_6V3LUr0KWA8SxhFHyZXN7tqjlRYQiQTP-43t22DQFDDTwLQRGvRWBcunA7ME_Rj5SnCQJOCpvmcyRRJm6JRwDAFnMkpnU", "scope": "user-library-modify user-library-read user-modify-playback-state user-read-playback-state", "expires_at": 1680996947}
{"access_token": "BQAPv3RjiQEQ5b-JCwQelxSZraGjsil9xLAlyVJfTXqYAiRYpM6tXEbY2KgICsgoG0N8Cal51hGasiPpw4GGILK0f4YcXYzblcmDrWILRzOCfZ5ju0VjByNCEOjVgJWoAQoalmBuXRwNhC30TI7v9oKkB2kmmmTRdWFPvHgSsRdGqZ1UOaqntt-X3arhUFjm6BiJqFim9n8SYfBTJmc", "token_type": "Bearer", "expires_in": 3600, "scope": "user-library-modify user-library-read user-modify-playback-state user-read-playback-state", "expires_at": 1685062757, "refresh_token": "AQAby3zEGc-H8o8zciCRMZm-O6Gj3FAup6Vb0sRrbtiO48VyMTJMzU4DoJ_wrhk8LmOiN8Hvt0Fb_Ag-09XVEDgQe3VUBDD3HdoMk6aZA1n02VLygxQNMNUQgAGw6oUUdI0"}

View File

@@ -1,6 +0,0 @@
def handler():
try:
exec(open('spotifycontroller.py').read())
except:
print("An error has ocurred, reestablishing the application now.")
exec(open('spotifycontroller.py').read())

View File

@@ -5,3 +5,5 @@ spotipy
sv_ttk
syncedlyrics
numpy
flask
psutil

View File

@@ -9,10 +9,11 @@ import syncedlyrics
from PIL import Image, ImageTk, ImageDraw, ImageFilter
from io import BytesIO
import math
from time import sleep
from time import sleep, time
import threading
import queue
import numpy
from sys import exit
# SpotifyGUI - Made by Brandon Brunson
@@ -20,7 +21,7 @@ import numpy
if os.name == 'posix':
client_id = "df61ecadf09941eb87e693d37f3ad178"
client_secret = "ba97992d614e48d6b0d023998b2957cb"
os.system("xset -display :0 s 21600")
os.system("xset -display :0 s 1800")
if os.path.isfile("updated.cfg"):
print("NEW VER")
os.remove("updated.cfg")
@@ -42,6 +43,10 @@ redirect_uri = "http://127.0.0.1:8888/callback"
# client_id = "df61ecadf09941eb87e693d37f3ad178"
# client_secret = "ba97992d614e48d6b0d023998b2957cb"
# misc client id and secret (SpotifyGUI 3)
# client_id = "21ca0dc87c9441ceaf875a05d0199756"
# client_secret = "dd430e634ae94471aa70dfc22936be10"
# Set the user's Spotify username
username = "thebrandon45"
password = "Mariposa2502$"
@@ -80,14 +85,15 @@ count = 0
# wait_time = 6500
# hotword = "magical"
# sleep = False
# Create the tkinter window
root = ttk.Tk()
root.title("Media Controller")
root.geometry("1280x400")
root.attributes("-topmost", True)
root.overrideredirect(1)
if os.name == 'posix':
root.overrideredirect(1)
sv_ttk.use_dark_theme()
# Function to call the Spotify API to play the current track
@@ -102,7 +108,20 @@ def controlNext():
spotify.next_track()
def controlPrevious():
spotify.previous_track()
if track_progress < 5000:
spotify.previous_track()
else:
spotify.seek_track(0)
# def sleep():
# while not sleep:
# sleep(900000)
# kill(kill=sleep)
# development purposes
def kill(kill):
if kill is sleep:
exit(1)
def likeSong():
if spotify.current_user_saved_tracks_contains(tracks=[(spotify.current_playback()["item"]["id"])])[0] is False:
@@ -171,13 +190,13 @@ def start_playback_on_device():
def get_devices():
global count
# global count
# global wait_time
# global wait_time0
# count +=1
# if count < 69:
# wait_time = 6500
# elif count >= 69:
# wait_time = 3600000
count += 1
# count += 1
list_of_devices = spotify.devices()
# if list_of_devices == "{'devices': []}":
@@ -185,27 +204,32 @@ def get_devices():
# loadSearching_Devices()
# root.after(1000, get_devices)
# else:
if count > 210:
unloadDevices_list()
loadSleep()
root.after(3600, get_devices)
# if count > 210:
# unloadDevices_list()
# loadSleep()
# root.after(3600, get_devices)
if spotify.current_playback() != None:
count = 0
# unloadSearching_Devices()
unloadDevices_list()
loadNow_playing()
root.after(200, update_song_label)
root.after(800, update_song_label)
else:
count += 1
if count > 210:
kill(kill=sleep)
# unloadSearching_Devices()
# loadDevices_list()
devices_list.delete(0, ttk.END)
# list_of_devices = spotify.devices()
for num_of_device, garbage in enumerate(list_of_devices["devices"]):
devices_list.insert(num_of_device, list_of_devices["devices"][num_of_device]["name"])
else:
devices_list.delete(0, ttk.END)
# list_of_devices = spotify.devices()
for num_of_device, garbage in enumerate(list_of_devices["devices"]):
devices_list.insert(num_of_device, list_of_devices["devices"][num_of_device]["name"])
root.after(8500, get_devices)
def wakeup():
global count
count = 0
# def wakeup():
# global count
# count = 0
# def wakeup():
# global count
@@ -322,8 +346,12 @@ def get_colors(image_file, resize=150):
def getLyrics(artist_name, track_name):
global lrc
lrc = syncedlyrics.search("[" + track_name + "] [" + artist_name + "]")
q.put(lrc)
# def upNext():
# print(spotify.queue())
# up_next_label.config(text="Placeholder")
# def rgb_to_hex(r, g, b):
# return ('{:X}{:X}{:X}').format(r, g, b)
@@ -375,107 +403,123 @@ searching_for_devices_label = tk.Label(root, text="Searching for Devices...", fo
device_name_label = tk.Label(frame_artist_song, text="", font=("Helvetica", 12))
lyrics_label = tk.Label(lyrics_label_frame, text="", font=("Helvetica", 32), wraplength=(1280/3), justify=ttk.CENTER, background=bg_color)
album_art_label = tk.Label(album_art_frame, image=album_art_img)
# up_next_label = tk.Label(frame_artist_song, text="", font=("Helvetica", 12), wraplength=(1280/3), justify=ttk.CENTER, background=bg_color)
play_button.bind("<Button-1>", lambda e:controlPlay())
pause_button.bind("<Button-1>", lambda e:controlPause())
next_button.bind("<Button-1>", lambda e:controlNext())
previous_button.bind("<Button-1>", lambda e:controlPrevious())
# previous_button.bind("<Double-Button-1>", lambda e:controlPrevious(double=True))
album_art_label.bind("<Button-1>", lambda e:likeSong())
sleep_frame.bind("<Button-1>", lambda e:wakeup())
# development restart script (will push to update.py)
song_label.bind("<Double-Button-1>", lambda e:kill())
devices_list.bind("<Double-Button-1>", lambda e:kill())
# sleep_frame.bind("<Button-1>", lambda e:wakeup())
# devices_list.bind("<Button-1>", lambda e:wakeup())
# Function to update the song label with the current track's name
def update_song_label():
global lrc
global album_art_img
global isBright
# Get the current playback information
current_playback = spotify.current_playback()
# If there is no current playback, set the text of the song label to "No playback"
if current_playback is None:
unloadNow_playing()
loadDevices_list()
try:
global lrc
global album_art_img
global isBright
global track_progress
# Get the current playback information
current_playback = spotify.current_playback()
# If there is no current playback, set the text of the song label to "No playback"
if current_playback is None:
unloadNow_playing()
loadDevices_list()
root.after(200, get_devices)
# Update the song label every 1 second
elif current_playback.get("item") is not None:
track_name = current_playback["item"]["name"]
track_progress = current_playback["progress_ms"]
playing_status = current_playback["is_playing"]
track_progress_min = track_progress//(1000*60)%60
track_progress_sec = (track_progress//1000)%60
if track_name == song_label.cget("text"):
track_progress_formatted = ("{}:{:02d}".format(track_progress_min, track_progress_sec))
progress_bar.config(value=track_progress)
for line in str(lrc).splitlines():
if track_progress_formatted in line:
lyric = line.split("]")[1]
lyrics_label.config(text=lyric)
root.after(800, update_song_label)
root.after(200, get_devices)
# Update the song label every 1 second
elif current_playback.get("item") is not None:
track_name = current_playback["item"]["name"]
track_progress = current_playback["progress_ms"]
playing_status = current_playback["is_playing"]
track_progress_min = track_progress//(1000*60)%60
track_progress_sec = (track_progress//1000)%60
if track_name == song_label.cget("text"):
track_progress_formatted = ("{}:{:02d}".format(track_progress_min, track_progress_sec))
progress_bar.config(value=track_progress)
for line in str(lrc).splitlines():
if track_progress_formatted in line:
lyric = line.split("]")[1]
lyrics_label.config(text=lyric)
# if track_progress < 5000:
# threading.Thread(target=upNext).start()
root.after(800, update_song_label)
else:
start_time = time()
artist_name = current_playback["item"]["artists"][0]["name"]
threading.Thread(target=getLyrics, args=(artist_name, track_name)).start()
track_id = current_playback["item"]["id"]
track_duration = current_playback["item"]["duration_ms"]
device_name = current_playback["device"]["name"]
album_art_url = current_playback["item"]["album"]["images"][0]["url"]
device_name_label.config(text=device_name)
song_label.config(text=track_name)
artist_label.config(text=artist_name)
progress_bar.config(maximum=track_duration)
lyrics_label.config(text="")
album_art_img_data = requests.get(album_art_url).content
album_art_img_open = Image.open(BytesIO(album_art_img_data))
# bg_color_img = album_art_img_open.resize((1,1), resample=0)
# bg_color_img_pixel = bg_color_img.getpixel((0,0))
# bg_color_rgb = get_colors(album_art_img_open)
dominant_color = get_colors(album_art_img_open)
bg_color = "#" + '%02x%02x%02x' % (dominant_color)
# print(bg_color)
album_art_img_with_corners = addCorners(album_art_img_open, 15)
# addDropShadow(album_art_img_with_corners)
album_art_img = ImageTk.PhotoImage(album_art_img_with_corners.resize((300,300)))
album_art_label.config(image=album_art_img, background=bg_color)
root.config(background=bg_color)
frame_artist_song.config(background=bg_color)
device_name_label.config(background=bg_color)
song_label.config(background=bg_color)
artist_label.config(background=bg_color)
play_button.config(background=bg_color)
pause_button.config(background=bg_color)
next_button.config(background=bg_color)
previous_button.config(background=bg_color)
lyrics_label_frame.config(background=bg_color)
lyrics_label.config(background=bg_color)
isBright = colorUI(track_id, dominant_color)
# print(oauth.get_cached_token()["access_token"])
# print(current_playback["item"]["id"])
# canvas_url = canvas.get_canvas_for_track(access_token=oauth.get_cached_token()["access_token"], track_id=current_playback["item"]["id"])
# print(canvas_url)
# lrc = q.get()
print(time() - start_time)
root.after(500, update_song_label)
if playing_status == True:
play_button.grid_forget()
pause_button.grid(row=3, column=1, pady=(0,30))
elif playing_status == False:
pause_button.grid_forget()
play_button.grid(row=3, column=1, pady=(0,30))
else:
pass
else:
artist_name = current_playback["item"]["artists"][0]["name"]
threading.Thread(target=getLyrics, args=(artist_name, track_name)).start()
track_id = current_playback["item"]["id"]
track_duration = current_playback["item"]["duration_ms"]
device_name = current_playback["device"]["name"]
album_art_url = current_playback["item"]["album"]["images"][0]["url"]
device_name_label.config(text=device_name)
song_label.config(text=track_name)
artist_label.config(text=artist_name)
progress_bar.config(maximum=track_duration)
lyrics_label.config(text="")
album_art_img_data = requests.get(album_art_url).content
album_art_img_open = Image.open(BytesIO(album_art_img_data))
# bg_color_img = album_art_img_open.resize((1,1), resample=0)
# bg_color_img_pixel = bg_color_img.getpixel((0,0))
# bg_color_rgb = get_colors(album_art_img_open)
dominant_color = get_colors(album_art_img_open)
bg_color = "#" + '%02x%02x%02x' % (dominant_color)
# print(bg_color)
album_art_img_with_corners = addCorners(album_art_img_open, 15)
# addDropShadow(album_art_img_with_corners)
album_art_img = ImageTk.PhotoImage(album_art_img_with_corners.resize((300,300)))
album_art_label.config(image=album_art_img, background=bg_color)
root.config(background=bg_color)
frame_artist_song.config(background=bg_color)
device_name_label.config(background=bg_color)
song_label.config(background=bg_color)
artist_label.config(background=bg_color)
play_button.config(background=bg_color)
pause_button.config(background=bg_color)
next_button.config(background=bg_color)
previous_button.config(background=bg_color)
lyrics_label_frame.config(background=bg_color)
lyrics_label.config(background=bg_color)
isBright = colorUI(track_id, dominant_color)
# print(oauth.get_cached_token()["access_token"])
# print(current_playback["item"]["id"])
# canvas_url = canvas.get_canvas_for_track(access_token=oauth.get_cached_token()["access_token"], track_id=current_playback["item"]["id"])
# print(canvas_url)
lrc = q.get()
root.after(500, update_song_label)
if playing_status == True:
play_button.grid_forget()
pause_button.grid(row=3, column=1, pady=(0,20))
elif playing_status == False:
pause_button.grid_forget()
play_button.grid(row=3, column=1, pady=(0,20))
else:
pass
else:
root.after(1000, get_devices)
root.after(1000, get_devices)
except Exception as e:
print(e)
root.after(5000, update_song_label)
def loadNow_playing():
frame_artist_song.grid(row=0, column=1, rowspan=3, pady=(30,0), sticky="n")
device_name_label.grid(row=0, column=1)
artist_label.grid(row=2, column=1)
song_label.grid(row=1, column=1)
previous_button.grid(row=3, column=1, padx=(0,200), pady=(0,20))
play_button.grid(row=3, column=1, pady=(0,20))
next_button.grid(row=3, column=1, padx=(200,0), pady=(0,20))
# up_next_label.grid(row=3, column=1)
previous_button.grid(row=3, column=1, padx=(0,200), pady=(0,30))
play_button.grid(row=3, column=1, pady=(0,30))
next_button.grid(row=3, column=1, padx=(200,0), pady=(0,30))
progress_bar.grid(row=3, column=0, columnspan=3, sticky="wse")
album_art_frame.grid(row=0, column=0, rowspan=4)
album_art_label.grid(sticky="w")

10
testing.py Normal file
View File

@@ -0,0 +1,10 @@
from pynput import mouse
from functools import partial
def click(x, y, button, pressed, foo):
return False
bar = partial(click, foo="bar")
with mouse.Listener(on_click=bar) as listener:
listener.join()

View File

@@ -82,3 +82,20 @@ make it so update.py that kills the main script after 6 hours and turn the displ
04/08/2023:
background of artist image
hold previous button to start song over
04/10/2023:
if title is held for over 5 seconds then also shutdown the update.py by throwing an exception that the update.py will catch and then kill the update script
also, if you hold it for over 10 seconds then shutdown the pi entirely. all by sending certain sigterms with exit()
04/13/2023:
update.py modified to handle click (untested), spotify.py functions created. TODO: tie get_devices into sleep & kill functions as those are setup to kill script and handoff to update.py
04/28/2023:
periodically check the spotify api, if it is playing something then click a button to wake the display
04/29/2023:
experiment with converting project to flask/js and browser based.
05/07/2023:
detect previous song change by checking if song is at less than 5 seconds into song, if yes then go to previous song, if not then restart song.

View File

@@ -4,6 +4,9 @@ from zipfile import ZipFile
from io import BytesIO
from time import sleep
import subprocess
from pynput import mouse
from functools import partial
from os import name, path, remove
# while True:
@@ -14,6 +17,9 @@ import subprocess
# except:
# pass
if name == 'nt':
print("Windows detected. Shutting down...")
exit()
# try:
# print("Checking for updates...")
@@ -24,21 +30,38 @@ import subprocess
# print("No update available.")
# pass
def click(x, y, button, pressed, foo):
return False
bar = partial(click, foo="bar", button=True, pressed=True)
while True:
try:
urlopen('http://bbrunson.com', timeout=1)
print("Connection to server established.")
try:
if path.exists('web/update/update.zip'):
print("Update found. Installing...")
with ZipFile('web/update/update.zip') as zipObj:
zipObj.extractall()
remove('web/update/update.zip')
print("Uploaded update successfully installed.")
print("Checking for updates...")
with ZipFile(BytesIO((urlopen('http://files.bbrunson.com/spotify-gui/update.zip')).read())) as zipObj:
zipObj.extractall()
print("Update successfully installed.")
print("Update successfully downloaded and installed.")
except urllib.error.HTTPError:
print("No update available.")
pass
subprocess.Popen(['python3', 'web/web.py'])
subprocess.check_call(['python3', 'spotifycontroller.py'])
except:
print("An error has ocurred, checking server connection, checking for update, and then reestablishing the application in 10 seconds.")
sleep(10)
except Exception as e:
if e.args[0] == 1:
print("Program has requested sleep mode.")
with mouse.Listener(on_move=bar, on_click=bar, on_scroll=bar) as listener:
listener.join()
else:
print(e)
print("Restart procedure initiated: Checking server connection, checking for update, and then reestablishing the application in 10 seconds.")
sleep(10)
continue

48
web/web.py Normal file
View File

@@ -0,0 +1,48 @@
from flask import Flask, Blueprint, render_template, redirect, url_for, request, flash
from werkzeug.utils import secure_filename
import os
import signal
import psutil
app = Flask(__name__)
app.config['SECRET_KEY'] = 'spotify-gui'
app.config['UPLOAD_FOLDER'] = 'update'
@app.route('/', methods=['GET', 'POST'])
def update():
if request.method == 'POST':
if 'file' not in request.files:
flash('No file part')
return redirect(request.url)
file = request.files['file']
if file.filename == '':
flash('No selected file')
return redirect(request.url)
if file and file.filename.endswith('.zip'):
file.save('web/update/update.zip')
return redirect(request.url)
return '''
<!doctype html>
<title>Upload new File</title>
<h1>Upload new File</h1>
<form method=post enctype=multipart/form-data>
<input type=file name=file>
<input type=submit value=Upload>
</form><br>
<form method="POST" action="/restart">
<button>Restart</button>
</form>
'''
@app.route('/restart', methods=['POST'])
def restart():
for line in os.popen("ps ax | grep " + "spotifycontroller.py" + " | grep -v grep"):
fields = line.split()
pid = fields[0]
os.kill(int(pid), signal.SIGKILL)
exit()
return redirect(url_for('update'))
if __name__ == "__main__":
app.run(host='0.0.0.0', port=80)