** 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 sv_ttk
syncedlyrics syncedlyrics
numpy numpy
flask
psutil

View File

@@ -9,10 +9,11 @@ import syncedlyrics
from PIL import Image, ImageTk, ImageDraw, ImageFilter from PIL import Image, ImageTk, ImageDraw, ImageFilter
from io import BytesIO from io import BytesIO
import math import math
from time import sleep from time import sleep, time
import threading import threading
import queue import queue
import numpy import numpy
from sys import exit
# SpotifyGUI - Made by Brandon Brunson # SpotifyGUI - Made by Brandon Brunson
@@ -20,7 +21,7 @@ import numpy
if os.name == 'posix': if os.name == 'posix':
client_id = "df61ecadf09941eb87e693d37f3ad178" client_id = "df61ecadf09941eb87e693d37f3ad178"
client_secret = "ba97992d614e48d6b0d023998b2957cb" client_secret = "ba97992d614e48d6b0d023998b2957cb"
os.system("xset -display :0 s 21600") os.system("xset -display :0 s 1800")
if os.path.isfile("updated.cfg"): if os.path.isfile("updated.cfg"):
print("NEW VER") print("NEW VER")
os.remove("updated.cfg") os.remove("updated.cfg")
@@ -42,6 +43,10 @@ redirect_uri = "http://127.0.0.1:8888/callback"
# client_id = "df61ecadf09941eb87e693d37f3ad178" # client_id = "df61ecadf09941eb87e693d37f3ad178"
# client_secret = "ba97992d614e48d6b0d023998b2957cb" # client_secret = "ba97992d614e48d6b0d023998b2957cb"
# misc client id and secret (SpotifyGUI 3)
# client_id = "21ca0dc87c9441ceaf875a05d0199756"
# client_secret = "dd430e634ae94471aa70dfc22936be10"
# Set the user's Spotify username # Set the user's Spotify username
username = "thebrandon45" username = "thebrandon45"
password = "Mariposa2502$" password = "Mariposa2502$"
@@ -80,14 +85,15 @@ count = 0
# wait_time = 6500 # wait_time = 6500
# hotword = "magical" # hotword = "magical"
# sleep = False
# Create the tkinter window # Create the tkinter window
root = ttk.Tk() root = ttk.Tk()
root.title("Media Controller") root.title("Media Controller")
root.geometry("1280x400") root.geometry("1280x400")
root.attributes("-topmost", True) root.attributes("-topmost", True)
root.overrideredirect(1) if os.name == 'posix':
root.overrideredirect(1)
sv_ttk.use_dark_theme() sv_ttk.use_dark_theme()
# Function to call the Spotify API to play the current track # Function to call the Spotify API to play the current track
@@ -102,7 +108,20 @@ def controlNext():
spotify.next_track() spotify.next_track()
def controlPrevious(): def controlPrevious():
if track_progress < 5000:
spotify.previous_track() 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(): def likeSong():
if spotify.current_user_saved_tracks_contains(tracks=[(spotify.current_playback()["item"]["id"])])[0] is False: 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(): def get_devices():
global count global count
# global count # global count
# global wait_time # global wait_time0
# count +=1 # count +=1
# if count < 69: # if count < 69:
# wait_time = 6500 # wait_time = 6500
# elif count >= 69: # elif count >= 69:
# wait_time = 3600000 # wait_time = 3600000
count += 1 # count += 1
list_of_devices = spotify.devices() list_of_devices = spotify.devices()
# if list_of_devices == "{'devices': []}": # if list_of_devices == "{'devices': []}":
@@ -185,27 +204,32 @@ def get_devices():
# loadSearching_Devices() # loadSearching_Devices()
# root.after(1000, get_devices) # root.after(1000, get_devices)
# else: # else:
if count > 210: # if count > 210:
unloadDevices_list() # unloadDevices_list()
loadSleep() # loadSleep()
root.after(3600, get_devices) # root.after(3600, get_devices)
if spotify.current_playback() != None: if spotify.current_playback() != None:
count = 0
# unloadSearching_Devices() # unloadSearching_Devices()
unloadDevices_list() unloadDevices_list()
loadNow_playing() loadNow_playing()
root.after(200, update_song_label) root.after(800, update_song_label)
else: else:
count += 1
if count > 210:
kill(kill=sleep)
# unloadSearching_Devices() # unloadSearching_Devices()
# loadDevices_list() # loadDevices_list()
else:
devices_list.delete(0, ttk.END) devices_list.delete(0, ttk.END)
# list_of_devices = spotify.devices() # list_of_devices = spotify.devices()
for num_of_device, garbage in enumerate(list_of_devices["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"]) devices_list.insert(num_of_device, list_of_devices["devices"][num_of_device]["name"])
root.after(8500, get_devices) root.after(8500, get_devices)
def wakeup(): # def wakeup():
global count # global count
count = 0 # count = 0
# def wakeup(): # def wakeup():
# global count # global count
@@ -322,8 +346,12 @@ def get_colors(image_file, resize=150):
def getLyrics(artist_name, track_name): def getLyrics(artist_name, track_name):
global lrc
lrc = syncedlyrics.search("[" + track_name + "] [" + artist_name + "]") 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): # def rgb_to_hex(r, g, b):
# return ('{:X}{:X}{:X}').format(r, g, b) # return ('{:X}{:X}{:X}').format(r, g, b)
@@ -375,23 +403,31 @@ searching_for_devices_label = tk.Label(root, text="Searching for Devices...", fo
device_name_label = tk.Label(frame_artist_song, text="", font=("Helvetica", 12)) 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) 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) 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()) play_button.bind("<Button-1>", lambda e:controlPlay())
pause_button.bind("<Button-1>", lambda e:controlPause()) pause_button.bind("<Button-1>", lambda e:controlPause())
next_button.bind("<Button-1>", lambda e:controlNext()) next_button.bind("<Button-1>", lambda e:controlNext())
previous_button.bind("<Button-1>", lambda e:controlPrevious()) 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()) 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()) # devices_list.bind("<Button-1>", lambda e:wakeup())
# Function to update the song label with the current track's name # Function to update the song label with the current track's name
def update_song_label(): def update_song_label():
try:
global lrc global lrc
global album_art_img global album_art_img
global isBright global isBright
global track_progress
# Get the current playback information # Get the current playback information
current_playback = spotify.current_playback() current_playback = spotify.current_playback()
# If there is no current playback, set the text of the song label to "No playback" # If there is no current playback, set the text of the song label to "No playback"
@@ -414,8 +450,11 @@ def update_song_label():
if track_progress_formatted in line: if track_progress_formatted in line:
lyric = line.split("]")[1] lyric = line.split("]")[1]
lyrics_label.config(text=lyric) lyrics_label.config(text=lyric)
# if track_progress < 5000:
# threading.Thread(target=upNext).start()
root.after(800, update_song_label) root.after(800, update_song_label)
else: else:
start_time = time()
artist_name = current_playback["item"]["artists"][0]["name"] artist_name = current_playback["item"]["artists"][0]["name"]
threading.Thread(target=getLyrics, args=(artist_name, track_name)).start() threading.Thread(target=getLyrics, args=(artist_name, track_name)).start()
track_id = current_playback["item"]["id"] track_id = current_playback["item"]["id"]
@@ -455,27 +494,32 @@ def update_song_label():
# print(current_playback["item"]["id"]) # 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"]) # canvas_url = canvas.get_canvas_for_track(access_token=oauth.get_cached_token()["access_token"], track_id=current_playback["item"]["id"])
# print(canvas_url) # print(canvas_url)
lrc = q.get() # lrc = q.get()
print(time() - start_time)
root.after(500, update_song_label) root.after(500, update_song_label)
if playing_status == True: if playing_status == True:
play_button.grid_forget() play_button.grid_forget()
pause_button.grid(row=3, column=1, pady=(0,20)) pause_button.grid(row=3, column=1, pady=(0,30))
elif playing_status == False: elif playing_status == False:
pause_button.grid_forget() pause_button.grid_forget()
play_button.grid(row=3, column=1, pady=(0,20)) play_button.grid(row=3, column=1, pady=(0,30))
else: else:
pass pass
else: 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(): def loadNow_playing():
frame_artist_song.grid(row=0, column=1, rowspan=3, pady=(30,0), sticky="n") frame_artist_song.grid(row=0, column=1, rowspan=3, pady=(30,0), sticky="n")
device_name_label.grid(row=0, column=1) device_name_label.grid(row=0, column=1)
artist_label.grid(row=2, column=1) artist_label.grid(row=2, column=1)
song_label.grid(row=1, column=1) song_label.grid(row=1, column=1)
previous_button.grid(row=3, column=1, padx=(0,200), pady=(0,20)) # up_next_label.grid(row=3, column=1)
play_button.grid(row=3, column=1, pady=(0,20)) previous_button.grid(row=3, column=1, padx=(0,200), pady=(0,30))
next_button.grid(row=3, column=1, padx=(200,0), pady=(0,20)) 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") progress_bar.grid(row=3, column=0, columnspan=3, sticky="wse")
album_art_frame.grid(row=0, column=0, rowspan=4) album_art_frame.grid(row=0, column=0, rowspan=4)
album_art_label.grid(sticky="w") 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: 04/08/2023:
background of artist image 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 io import BytesIO
from time import sleep from time import sleep
import subprocess import subprocess
from pynput import mouse
from functools import partial
from os import name, path, remove
# while True: # while True:
@@ -14,6 +17,9 @@ import subprocess
# except: # except:
# pass # pass
if name == 'nt':
print("Windows detected. Shutting down...")
exit()
# try: # try:
# print("Checking for updates...") # print("Checking for updates...")
@@ -24,21 +30,38 @@ import subprocess
# print("No update available.") # print("No update available.")
# pass # pass
def click(x, y, button, pressed, foo):
return False
bar = partial(click, foo="bar", button=True, pressed=True)
while True: while True:
try: try:
urlopen('http://bbrunson.com', timeout=1) urlopen('http://bbrunson.com', timeout=1)
print("Connection to server established.") print("Connection to server established.")
try: 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...") print("Checking for updates...")
with ZipFile(BytesIO((urlopen('http://files.bbrunson.com/spotify-gui/update.zip')).read())) as zipObj: with ZipFile(BytesIO((urlopen('http://files.bbrunson.com/spotify-gui/update.zip')).read())) as zipObj:
zipObj.extractall() zipObj.extractall()
print("Update successfully installed.") print("Update successfully downloaded and installed.")
except urllib.error.HTTPError: except urllib.error.HTTPError:
print("No update available.") print("No update available.")
pass pass
subprocess.Popen(['python3', 'web/web.py'])
subprocess.check_call(['python3', 'spotifycontroller.py']) subprocess.check_call(['python3', 'spotifycontroller.py'])
except: except Exception as e:
print("An error has ocurred, checking server connection, checking for update, and then reestablishing the application in 10 seconds.") 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) sleep(10)
continue 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)