Since my last post about my now Playing Display code for the Pimoroni Presto, I've made some improvements to the code.
Whats changed
- Pressing the buttons is now more responsive
- Skip Button now skips
- Pressing the screen when you have paused the media with resume playing
- Album/Now Playing art now handled better
- Text Centers better
- Server app on the PC now running in non-debug mode.
The new code is shown below along with instructions for getting this setup.
Setting up
Getting the code to the Presto: Save the presto code to your Presto using a tool like Thonny. Save the file as main.py if you would like to to run on startup of the device.
Running the PC server code:
Clone the following repository and follow the instructions:
git clone https://github.com/vwillcox/PrestoMediaPlaying.git
cd PrestoMediaPlaying
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
Now if you just want to test this you can run in debug mode:
python3 media-server.py
Now install the Presto Code to your Presto using Thonny and run Play some media on your PC (Youtube, Spotify etc) and the item should show on your Presto. If it does not you need to install one more component
ARCH Based Linux
sudo pacman -S playerctl
Debian Based Linux
sudo apt install -y playerctl
and now ensure it is running:
playerctld daemon
If all is working and you want to run the server code in a production environment, there is one more step
playerctld daemon
export FLASK_ENV=production
export FLASK_APP=media-server.py
gunicorn media-server:app --bind 0.0.0.0:5000 --daemon
Presto Code
from presto import Presto
from picovector import ANTIALIAS_BEST, PicoVector, Polygon, Transform
from touch import Button
import urequests
import jpegdec
import gc
import time
machine.freq(200000000)
# --- Configuration ---
BACKLIGHT_RANGE = 20
BASE_URL = "http://192.168.50.173:5000"
ENDPOINTS = {
"media": f"{BASE_URL}/now_playing",
"art": f"{BASE_URL}/album_art",
"toggle": f"{BASE_URL}/toggle_play",
"skip": f"{BASE_URL}/skip",
}
# --- Hardware Setup ---
presto = Presto(ambient_light=False, full_res=True)
display = presto.display
WIDTH, HEIGHT = display.get_bounds()
CX, CY = WIDTH // 2, HEIGHT // 2
touch = presto.touch
# --- Graphics & UI Setup ---
WHITE, RED, GREEN, BLACK = (display.create_pen(r, g, b) for r, g, b in [(255, 255, 255), (230, 60, 45), (9, 185, 120), (0, 0, 0)])
vector = PicoVector(display)
vector.set_antialiasing(ANTIALIAS_BEST)
vector.set_font("Roboto-Medium.af", 32)
# Define UI elements
pause_button = Button(1, HEIGHT - 50, CX - 2, 49)
skip_button = Button(WIDTH - CX, HEIGHT - 50, CX - 2, 49)
ui_elements = {
"pause_rect": Polygon().rectangle(*pause_button.bounds, (5,)*4),
"skip_rect": Polygon().rectangle(*skip_button.bounds, (5,)*4),
"outline": Polygon().rectangle(5, 20, WIDTH - 10, HEIGHT - 80, (5,)*4, 2)
}
# --- Helper Functions ---
def set_backlight(level):
"""Sets the screen backlight level (0-20)."""
presto.set_backlight(level / BACKLIGHT_RANGE)
def make_request(endpoint_key, method='get', log_errors=True):
"""Makes a request to a predefined endpoint."""
try:
if method == 'post':
response = urequests.post(ENDPOINTS[endpoint_key])
else:
response = urequests.get(ENDPOINTS[endpoint_key])
content = response.json() if 'json' in response.headers.get('Content-Type', '') else response.content
response.close()
return content
except Exception as e:
if log_errors:
print(f"Request failed for '{endpoint_key}': {e}")
return None
def draw_ui(data):
"""Draws the main user interface when media is playing."""
display.set_pen(BLACK)
display.clear()
# Draw buttons and outline
display.set_pen(GREEN)
vector.draw(ui_elements["pause_rect"])
display.set_pen(RED)
vector.draw(ui_elements["skip_rect"])
display.set_pen(WHITE)
vector.draw(ui_elements["outline"])
# Draw text
def draw_centered_text(text, y):
text_to_draw = text[:35] if len(text) > 35 else text
_, _, w, _ = vector.measure_text(text_to_draw)
vector.text(text_to_draw, int(CX - w / 2), y)
draw_centered_text(f"{data.get('title', '')}", 45)
draw_centered_text(data.get('album', ''), 80)
# Draw button labels
vector.text("Pause", pause_button.bounds[0] + 83, pause_button.bounds[1] + 33)
vector.text("Skip", skip_button.bounds[0] + 83, skip_button.bounds[1] + 33)
# Draw album art
art_content = make_request('art', log_errors=False)
if art_content:
try:
j = jpegdec.JPEG(display)
j.open_RAM(art_content)
# Set scale and dimensions based on the art_source flag
if data.get("art_source") == "url":
scale = jpegdec.JPEG_SCALE_HALF
# If original is 640x640, scaled is 320x320
img_width, img_height = 320, 320
else:
scale = jpegdec.JPEG_SCALE_FULL
# FIX: Assume a default size for non-URL sources since we can't get dimensions
img_width, img_height = 250, 250
# Calculate position to center the art in the main outline area
art_area_width = WIDTH - 10
art_area_height = HEIGHT - 100 # This is the area between y=20 and y=HEIGHT-80
img_x = 5 + (art_area_width - img_width) // 2
if data.get("art_source") == "url":
img_y = 60 + (art_area_height - img_height) // 2
else:
img_y = 20 + (art_area_height - img_height) // 2
j.decode(img_x, img_y, scale, dither=True)
del j
except Exception as e:
print(f"Image decode error: {e}")
presto.update()
def handle_touch(status):
"""Handles all touch input based on player status."""
touch.poll()
if not touch.state:
return
if status == "Paused":
#print("Screen touched while paused. Toggling play.")
make_request('toggle', 'post')
time.sleep(0.5) # Debounce
return
if pause_button.is_pressed():
#print("Toggled play/pause via button")
make_request('toggle', 'post')
time.sleep(0.5) # Debounce
elif skip_button.is_pressed():
#print("Pressed Skip")
make_request('skip', 'post')
time.sleep(0.5) # Debounce
# --- Main Loop ---
set_backlight(0)
if not presto.connect():
print("Connection failed. Halting.")
else:
#print("Connection successful.")
while True:
try:
media_data = make_request('media')
status = media_data.get('status') if media_data else "Error"
#print(f"Status: {status}")
if status == "Playing":
set_backlight(10)
draw_ui(media_data)
handle_touch(status)
else: # Paused, stopped, or error
set_backlight(0)
if status == "Paused":
handle_touch(status)
time.sleep(0.1 if status == "Paused" else 1)
gc.collect()
except Exception as e:
print(f"Main loop error: {e}")
set_backlight(0)
time.sleep(2)
gc.collect()
Updated Server Code
from flask import Flask, jsonify, send_file
import subprocess
import requests
import os
from urllib.parse import urlparse, unquote
from PIL import Image
import io
app = Flask(__name__)
def get_media_info():
try:
title = subprocess.check_output(['playerctl', 'metadata', 'xesam:title']).decode('utf-8').strip()
artist = subprocess.check_output(['playerctl', 'metadata', 'xesam:artist']).decode('utf-8').strip()
album = subprocess.check_output(['playerctl', 'metadata', 'xesam:album']).decode('utf-8').strip()
art_url = subprocess.check_output(['playerctl', 'metadata', 'mpris:artUrl']).decode('utf-8').strip()
status = subprocess.check_output(['playerctl','status']).decode('utf-8').strip()
#print(art_url)
art_path = None
art_source = None
if art_url.startswith("http://") or art_url.startswith("https://"):
response = requests.get(art_url)
if response.status_code == 200:
with open("/tmp/album_art.jpg", "wb") as f:
f.write(response.content)
art_path = "/tmp/album_art.jpg"
art_source = "url"
elif art_url.startswith("file:///"):
parsed = urlparse(art_url)
art_path = unquote(parsed.path)
art_source = "file"
return {
"title": title,
"artist": artist,
"album": album,
"art_path": art_path,
"status": status,
"art_source": art_source
}
except subprocess.CalledProcessError:
return None
@app.route('/now_playing')
def now_playing():
media_info = get_media_info()
if media_info:
return jsonify({
"title": media_info["title"],
"artist": media_info["artist"],
"album": media_info["album"],
"is_playing": True,
"status": media_info["status"],
"art_source": media_info["art_source"]
})
else:
return jsonify({"error": "No media playing"}), 404
@app.route('/album_art')
def album_art():
info = get_media_info()
art_path = info.get("art_path") if info else None
if art_path and os.path.exists(art_path):
try:
if art_path.lower().endswith((".jpg", ".jpeg")):
return send_file(art_path, mimetype='image/jpeg')
else:
with Image.open(art_path) as img:
img = img.convert("RGB")
buffer = io.BytesIO()
img.save(buffer, format="JPEG")
buffer.seek(0)
return send_file(buffer, mimetype='image/jpeg')
except Exception as e:
return f"Image processing error: {e}", 500
return "No album art", 404
@app.route('/toggle_play', methods=['POST'])
def toggle_play():
os.system("playerctl play-pause")
return jsonify({"status": "toggled"})
@app.route('/skip', methods=['POST'])
def skip():
os.system("playerctl next")
return jsonify({"Status": "skipped"})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)