Hero Image

A Practical Pimoroni Presto Project

How I'm using the Presto screen for my media consumption

I am a Customer support manager who works from home. This means lots of reading and writing reports and emails whilst sat at home. So - to stem the silence I do what most people do and play music during the day. I tend to listen to random playlists or just let play what comes on. Since I play the music through my PC, but work on a laptop supplied by work - I can't see what's one the screen as I share it with my work machine.

So I purchased one of the new Presto displays from Pimoroni and set to work coming up with an idea. Originally I used the Spotify API to get the details to display on the screen. However, this does not always work and restricted me to Spotify. So I wanted to find a way to tap into the "MediaInfo" that is displayed on most widgets on Linux. I found a tool called "playerctrl" and set to work finding out to use this.

Using what I already knew and some help from Co-pilot I was able to get some basic code working within a few hours. So the project currently looks like

  • PlayerCTRL installed on my Linux desktop
  • Flask Python3 server running on my Linux Desktop
  • Microphython running on the Pimoroni Presto showing the now playing details.

Now, whilst the basic code worked, it needed lots of work if i wanted to play media not from the desktop version of Spotify due the way Playerctrl gets the album art. From Spotify I got a resonable sized image in JPEG. But if I played media from YouTube in a browser window, this gave me a smaller PNG file. So I needed to add some Pillow magic to convert the PNG to a JPEG. The problem then was the different sizes meant that the Presto needed it's code changing to scale the two different sized imaged. So I added a new flag to be sent from Flask to the Presto and a new line of code to change the size used.

So - now I have a working prototype that I am happy to demonstrate.

I plan on putting a video together soon showing how to use this code, for now - here is an image to wet your apatite.

nowplaying

Microphython running on the Presto

  from presto import Presto
from picovector import ANTIALIAS_BEST, PicoVector, Polygon, Transform
from machine import PWM, Pin
import datetime, time, re, math, ntptime, plasma
from touch import Button
import urequests, jpegdec, gc

def screen_backlight(i):
    bl = i / BACKLIGHT_RANGE
    print(f"Backlight: {bl}")
    presto.set_backlight(bl)

# Setup for the Presto display
presto = Presto(ambient_light=False, full_res=True)
display = presto.display
WIDTH, HEIGHT = display.get_bounds()
CX, CY = WIDTH // 2, HEIGHT // 2
BACKLIGHT_RANGE = 20

screen_backlight(0)
presto.connect()

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)
    ]
)

touch = presto.touch
vector = PicoVector(display)
vector.set_antialiasing(ANTIALIAS_BEST)
vector.set_font("Roboto-Medium.af", 54)
vector.set_font_letter_spacing(100)
vector.set_font_word_spacing(100)
vector.set_font_size(32)
vector.set_transform(Transform())

# Buttons
start_button = Button(1, HEIGHT - 50, CX - 2, 49)
skip_button = Button(WIDTH - CX, HEIGHT - 50, CX - 2, 49)
start = Polygon().rectangle(*start_button.bounds, (5, 5, 5, 5))
skip = Polygon().rectangle(*skip_button.bounds, (5, 5, 5, 5))

outline = Polygon()
outline.rectangle(5, 20, WIDTH - 10, HEIGHT - 80, (5, 5, 5, 5), 2)

# Flask Endpoints
# Base URL
BASE_URL = "http://192.168.50.173:5000"

# Flask Endpoints
MEDIA_ENDPOINT = f"{BASE_URL}/now_playing"
IMAGE_ENDPOINT = f"{BASE_URL}/album_art"
TOGGLE_ENDPOINT = f"{BASE_URL}/toggle_play"

def truncate_string(s, max_length=35):
    return s[:max_length] if len(s) > max_length else s

# Track previous song to avoid redundant image loads
last_title = None

# --- Main Loop ---
while True:
    try:
        response = urequests.get(MEDIA_ENDPOINT)
        if response.status_code == 200:
            data = response.json()
            title = data.get("title", "")
            artist = data.get("artist", "")
            album = data.get("album", "")
            status = data.get("status", "")
            is_playing = True
            art_source = data.get("art_source", "")
        else:
            title = ""
            artist = ""
            album = ""
            is_playing = False

        if status == "Paused":
            screen_backlight(0)
            time.sleep(1)
            continue
        # Backlight control
        if not is_playing:
            screen_backlight(0)
            time.sleep(1)
            continue
        else:
            screen_backlight(10)

        display.set_pen(BLACK)
        display.clear()

        display.set_pen(GREEN)
        vector.draw(start)

        display.set_pen(RED)
        vector.draw(skip)

        display.set_pen(WHITE)
        vector.draw(outline)

        # Track + Album text
        string1 = truncate_string(artist + " / " + title)
        x, y, w, h = vector.measure_text(string1, x=0, y=0, angle=None)
        text_x = int(CX - (w // 2))
        text_y = int(CY + (h // 2))
        text_x_offset = text_x + 2
        text_y_offset = text_y + 2
        #vector.text(artist_name, text_x, 50)
        vector.text(string1, text_x, 45)

        string2 = truncate_string(album)
        x, y, w, h = vector.measure_text(string2, x=0, y=0, angle=None)
        text_x = int(CX - (w // 2))
        text_y = int(CY + (h // 2))
        text_x_offset = text_x + 2
        text_y_offset = text_y + 2
        #vector.text(string2, 40, 80)
        vector.text(string2, text_x, 80)

        # Only decode album art if the track has changed
        if is_playing:
            try:
                img_response = urequests.get(IMAGE_ENDPOINT)
                if img_response.status_code == 200:
                    j = jpegdec.JPEG(display)  # Recreate to free memory
                    j.open_file(img_response.content)
                    print(j)
                    img_x = 5 + (470 - 300) // 2
                    img_y = 20 + (380 - 240) // 2
                    if art_source == "url":
                        j.decode(img_x, img_y, jpegdec.JPEG_SCALE_HALF, dither=True)
                    else:
                        j.decode(img_x, img_y, jpegdec.JPEG_SCALE_FULL, dither=True)
                    del j
                    gc.collect()
                    img_response.close()
            except Exception as e:
                print("Image error:", e)

        # Button labels
        vector.text("Pause", start_button.bounds[0] + 83, start_button.bounds[1] + 33)
        vector.text("Skip", skip_button.bounds[0] + 83, skip_button.bounds[1] + 33)

        # Toggle playback
        touch.poll()
        if start_button.is_pressed():
            print("toggled play/pause")
            try:
                urequests.post(TOGGLE_ENDPOINT)
            except Exception as e:
                print("Toggle error:", e)

        presto.update()
        gc.collect()
        time.sleep(0.1)

    except Exception as e:
        print("Main loop error:", e)
        gc.collect()

  

Python running on my PC

  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()

        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"})

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)