diff --git a/tft_moode_coverart.py b/tft_moode_coverart.py index dddcc02..d0dbb4f 100644 --- a/tft_moode_coverart.py +++ b/tft_moode_coverart.py @@ -1,383 +1,264 @@ -from PIL import Image, ImageDraw, ImageColor, ImageFont, ImageStat -import subprocess +#!/usr/bin/env python + import time -import musicpd +from colorsys import hsv_to_rgb +from PIL import ImageFont, Image, ImageDraw, ImageStat import os import os.path from os import path -import RPi.GPIO as GPIO -from mediafile import MediaFile +import ST7789 as ST7789 +from socketIO_client import SocketIO, LoggingNamespace +import requests from io import BytesIO -from numpy import mean -import ST7789 -from PIL import ImageFilter -import yaml +from numpy import mean +import sys +import logging -# set default config for pirate audio - -__version__ = "0.0.5" +# logging.getLogger('socketIO-client').setLevel(logging.DEBUG) +# logging.basicConfig() +import signal +import RPi.GPIO as GPIO # get the path of the script -script_path = os.path.dirname(os.path.abspath( __file__ )) -# set script path as current directory - +# script_path = os.path.dirname(os.path.abspath( __file__ )) +script_path = os.path.dirname(os.path.abspath(__file__)) +# set script path as current directory os.chdir(script_path) -MODE=0 -OVERLAY=2 -TIMEBAR=1 -BLANK=0 -confile = 'config.yml' - -# Read config.yml for user config -if path.exists(confile): - - with open(confile) as config_file: - data = yaml.load(config_file, Loader=yaml.FullLoader) - displayConf = data['display'] - OVERLAY = displayConf['overlay'] - MODE = displayConf['mode'] - TIMEBAR = displayConf['timebar'] - BLANK = displayConf['blank'] +def on_connect(): + print("connect") +def on_disconnect(): + print("disconnect") + img = Image.new("RGBA", (240, 240), color=(0, 0, 0, 25)) + img = Image.open("/volumio/app/plugins/miscellanea/albumart/default.jpg") + img = img.resize((WIDTH, HEIGHT)) + draw = ImageDraw.Draw(img, "RGBA") + draw.text((10, 200), "shuting down ...", font=font_m, fill=(255, 255, 255)) + disp.display(img) + + +def on_push_state(*args): + img = Image.new("RGBA", (240, 240), color=(0, 0, 0, 25)) + global status + global service + status = args[0]["status"] + service = args[0]["service"] + # Load albumcover or radio cover + albumart = args[0]["albumart"] + + if len(albumart) == 0: # to catch a empty field on start + albumart = "http://localhost:3000/albumart" + if "http:" not in albumart: + albumart = "http://localhost:3000" + args[0]["albumart"] + + print(albumart) + response = requests.get(albumart) + img = Image.open(BytesIO(response.content)) + img = img.resize((WIDTH, HEIGHT)) + + # Light / Dark Symbols an bars, depending on background + im_stat = ImageStat.Stat(img) + im_mean = im_stat.mean + mn = mean(im_mean) + + txt_col = (255, 255, 255) + # bar_col = (255, 255, 255, 255) + bar_bgcol = (200, 200, 200) + bar_col = (255, 255, 255) + dark = False + if mn > 175: + txt_col = (55, 55, 55) + dark = True + # bar_col = (100, 100, 100, 225) + bar_bgcol = (255, 255, 255) + bar_col = (100, 100, 100) + if mn < 80: + txt_col = (200, 200, 200) + + # paste button symbol overlay in light/dark mode + if status == "play": + if dark is False: + img.paste(pause_icons, (0, 0), pause_icons) + else: + img.paste(pause_icons_dark, (0, 0), pause_icons_dark) + else: + if dark is False: + img.paste(play_icons, (0, 0), play_icons) + else: + img.paste(play_icons_dark, (0, 0), play_icons_dark) + + draw = ImageDraw.Draw(img, "RGBA") + + top = 7 + if "artist" in args[0]: + x1 = 20 + w1, y1 = draw.textsize(args[0]["artist"], font_m) + x1 = x1 - 20 + if x1 < (WIDTH - w1 - 20): + x1 = 0 + if w1 <= WIDTH: + x1 = (WIDTH - w1) // 2 + draw.text((x1, top), args[0]["artist"], font=font_m, fill=txt_col) + top = 35 + + if "album" in args[0]: + if args[0]["album"] is not None: + x2 = 20 + w2, y2 = draw.textsize(args[0]["album"], font_s) + x2 = x2 - 20 + if x2 < (WIDTH - w2 - 20): + x2 = 0 + if w2 <= WIDTH: + x2 = (WIDTH - w2) // 2 + draw.text((x2, top), args[0]["album"], font=font_s, fill=txt_col) + + if "title" in args[0]: + x3 = 20 + w3, y3 = draw.textsize(args[0]["title"], font_l) + x3 = x3 - 20 + if x3 < (WIDTH - w3 - 20): + x3 = 0 + if w3 <= WIDTH: + x3 = (WIDTH - w3) // 2 + # thin border + # shadowcolor = "black" + # draw.text((x3-1, 105), args[0]['title'], font=font_l, + # fill=shadowcolor) + # draw.text((x3+1, 105), args[0]['title'], font=font_l, + # fill=shadowcolor) + # draw.text((x3, 105-1), args[0]['title'], font=font_l, + # fill=shadowcolor) + # draw.text((x3, 105+1), args[0]['title'], font=font_l, + # fill=shadowcolor) + draw.text( + (x3, 105), args[0]["title"], font=font_l, fill=txt_col + ) # fill by mean + + # volume bar + vol_x = int((float(args[0]["volume"]) / 100) * (WIDTH - 33)) + # draw.rectangle((5, 184, WIDTH-34, 184+8), (255, 255, 255, 145)) + draw.rectangle((5, 184, WIDTH - 34, 184 + 8), bar_bgcol) # background + draw.rectangle((5, 184, vol_x, 184 + 8), bar_col) + + # time bar + if "duration" in args[0]: + duration = args[0]["duration"] # seconds + if duration != 0: + if "seek" in args[0]: + seek = args[0]["seek"] # time elapsed seconds + print("seek: ", seek) + if seek != 0 and seek is not None: + el_time = int(float(seek) / 1000) + du_time = int(float(args[0]["duration"])) + dur_x = int((float(el_time) / float(du_time)) * (WIDTH - 10)) + # draw.rectangle((5, 222, WIDTH-5, 222 + 12), (255, 255, 255, 145)) + draw.rectangle( + (5, 222, WIDTH - 5, 222 + 12), bar_bgcol + ) # background + draw.rectangle((5, 222, dur_x, 222 + 12), bar_col) + + # Draw the image on the display hardware + disp.display(img) + + +socketIO = SocketIO("localhost", 3000) +socketIO.on("connect", on_connect) - -# Standard SPI connections for ST7789 # Create ST7789 LCD display class. -if MODE == 3: - disp = ST7789.ST7789( - port=0, - cs=ST7789.BG_SPI_CS_FRONT, # GPIO 8, Physical pin 24 - dc=9, - rst=22, - backlight=13, - mode=3, - rotation=0, - spi_speed_hz=80 * 1000 * 1000 - ) -else: - disp = ST7789.ST7789( - port=0, - cs=ST7789.BG_SPI_CS_FRONT, # GPIO 8, Physical pin 24 - dc=9, - backlight=13, - spi_speed_hz=80 * 1000 * 1000 - ) - - +disp = ST7789.ST7789( + rotation=90, # Needed to display the right way up on Pirate Audio + port=0, # SPI port + cs=1, # SPI port Chip-select channel + dc=9, # BCM pin used for data/command + backlight=13, + spi_speed_hz=80 * 1000 * 1000, +) # Initialize display. disp.begin() - WIDTH = 240 HEIGHT = 240 -font_s = ImageFont.truetype(script_path + '/fonts/Roboto-Medium.ttf',20) -font_m = ImageFont.truetype(script_path + '/fonts/Roboto-Medium.ttf',24) -font_l = ImageFont.truetype(script_path + '/fonts/Roboto-Medium.ttf',30) +font_s = ImageFont.truetype(script_path + "/fonts/Roboto-Medium.ttf", 20) +font_m = ImageFont.truetype(script_path + "/fonts/Roboto-Medium.ttf", 24) +font_l = ImageFont.truetype(script_path + "/fonts/Roboto-Medium.ttf", 30) + +play_icons = Image.open("images/controls-play.png").resize((240, 240)) +play_icons_dark = Image.open("images/controls-play-dark.png").resize((240, 240)) +pause_icons = Image.open("images/controls-pause.png").resize((240, 240)) +pause_icons_dark = Image.open("images/controls-pause-dark.png").resize((240, 240)) + +# vol_icons = Image.open(script_path + '/images/controls-vol.png').resize((240,240), resample=Image.LANCZOS).convert("RGBA") +# vol_icons_dark = Image.open(script_path + '/images/controls-vol-dark.png').resize((240,240), resample=Image.LANCZOS).convert("RGBA") + +# bt_back = Image.open(script_path + '/images/bta.png').resize((240,240), resample=Image.LANCZOS).convert("RGBA") +# ap_back = Image.open(script_path + '/images/airplay.png').resize((240,240), resample=Image.LANCZOS).convert("RGBA") +# jp_back = Image.open(script_path + '/images/jack.png').resize((240,240), resample=Image.LANCZOS).convert("RGBA") +# sp_back = Image.open(script_path + '/images/spotify.png').resize((240,240), resample=Image.LANCZOS).convert("RGBA") +# sq_back = Image.open(script_path + '/images/squeeze.png').resize((240,240), resample=Image.LANCZOS).convert("RGBA") + +BUTTONS = [5, 6, 16, 20] +# BUTTONS = [16] +LABELS = ["A", "B", "X", "Y"] +# Set up RPi.GPIO with the "BCM" numbering scheme +GPIO.setmode(GPIO.BCM) +# Buttons connect to ground when pressed, so we should set them up +# with a "PULL UP", which weakly pulls the input signal to 3.3V. +GPIO.setup(BUTTONS, GPIO.IN, pull_up_down=GPIO.PUD_UP) -img = Image.new('RGB', (240, 240), color=(0, 0, 0, 25)) - -play_icons = Image.open(script_path + '/images/controls-play.png').resize((240,240), resample=Image.LANCZOS).convert("RGBA") -play_icons_dark = Image.open(script_path + '/images/controls-play-dark.png').resize((240,240), resample=Image.LANCZOS).convert("RGBA") - -pause_icons = Image.open(script_path + '/images/controls-pause.png').resize((240,240), resample=Image.LANCZOS).convert("RGBA") -pause_icons_dark = Image.open(script_path + '/images/controls-pause-dark.png').resize((240,240), resample=Image.LANCZOS).convert("RGBA") - -vol_icons = Image.open(script_path + '/images/controls-vol.png').resize((240,240), resample=Image.LANCZOS).convert("RGBA") -vol_icons_dark = Image.open(script_path + '/images/controls-vol-dark.png').resize((240,240), resample=Image.LANCZOS).convert("RGBA") - -bt_back = Image.open(script_path + '/images/bta.png').resize((240,240), resample=Image.LANCZOS).convert("RGBA") -ap_back = Image.open(script_path + '/images/airplay.png').resize((240,240), resample=Image.LANCZOS).convert("RGBA") -jp_back = Image.open(script_path + '/images/jack.png').resize((240,240), resample=Image.LANCZOS).convert("RGBA") -sp_back = Image.open(script_path + '/images/spotify.png').resize((240,240), resample=Image.LANCZOS).convert("RGBA") -sq_back = Image.open(script_path + '/images/squeeze.png').resize((240,240), resample=Image.LANCZOS).convert("RGBA") - -draw = ImageDraw.Draw(img, 'RGBA') - - -def isServiceActive(service): - - waiting = True - count = 0 - active = False - - while (waiting == True): - - process = subprocess.run(['systemctl','is-active',service], check=False, stdout=subprocess.PIPE, universal_newlines=True) - output = process.stdout - stat = output[:6] - - if stat == 'active': - waiting = False - active = True - - if count > 29: - waiting = False - - count += 1 - time.sleep(1) - - return active - - -def getMoodeMetadata(filename): - # Initalise dictionary - metaDict = {} - - if path.exists(filename): - # add each line fo a list removing newline - nowplayingmeta = [line.rstrip('\n') for line in open(filename)] - i = 0 - while i < len(nowplayingmeta): - # traverse list converting to a dictionary - (key, value) = nowplayingmeta[i].split('=') - metaDict[key] = value - i += 1 - - metaDict['source'] = 'library' - if 'file' in metaDict: - if (metaDict['file'].find('http://', 0) > -1) or (metaDict['file'].find('https://', 0) > -1): - # set radio stream to true - metaDict['source'] = 'radio' - # if radio station has arist and title in one line separated by a hyphen, split into correct keys - if metaDict['title'].find(' - ', 0) > -1: - (art,tit) = metaDict['title'].split(' - ', 1) - metaDict['artist'] = art - metaDict['title'] = tit - elif metaDict['file'].find('Bluetooth Active', 0) > -1: - metaDict['source'] = 'bluetooth' - elif metaDict['file'].find('Airplay Active', 0) > -1: - metaDict['source'] = 'airplay' - elif metaDict['file'].find('Spotify Active', 0) > -1: - metaDict['source'] = 'spotify' - elif metaDict['file'].find('Squeezelite Active', 0) > -1: - metaDict['source'] = 'squeeze' - elif metaDict['file'].find('Input Active', 0) > -1: - metaDict['source'] = 'input' - - - # return metadata - return metaDict - -def get_cover(metaDict): - - cover = None - cover = Image.open(script_path + '/images/default-cover-v6.jpg') - covers = ['Cover.jpg', 'cover.jpg', 'Cover.jpeg', 'cover.jpeg', 'Cover.png', 'cover.png', 'Cover.tif', 'cover.tif', 'Cover.tiff', 'cover.tiff', - 'Folder.jpg', 'folder.jpg', 'Folder.jpeg', 'folder.jpeg', 'Folder.png', 'folder.png', 'Folder.tif', 'folder.tif', 'Folder.tiff', 'folder.tiff'] - if metaDict['source'] == 'radio': - if 'coverurl' in metaDict: - rc = '/var/local/www/' + metaDict['coverurl'] - if path.exists(rc): - if rc != '/var/local/www/images/default-cover-v6.svg': - cover = Image.open(rc) - - elif metaDict['source'] == 'airplay': - cover = ap_back - elif metaDict['source'] == 'bluetooth': - cover = bt_back - elif metaDict['source'] == 'input': - cover = jp_back - elif metaDict['source'] == 'spotify': - cover = sp_back - elif metaDict['source'] == 'squeeze': - cover = sq_back - else: - if 'file' in metaDict: - if len(metaDict['file']) > 0: - - fp = '/var/lib/mpd/music/' + metaDict['file'] - mf = MediaFile(fp) - if mf.art: - cover = Image.open(BytesIO(mf.art)) - return cover - else: - for it in covers: - cp = os.path.dirname(fp) + '/' + it - - if path.exists(cp): - cover = Image.open(cp) - return cover - return cover +def handle_button(pin): + label = LABELS[BUTTONS.index(pin)] + print("Button press detected on pin: {} label: {}".format(pin, label)) + if pin == 5: + # print('Play/pause') + print(status, service) + if (status == "play") and (service == "webradio"): + print("stop - webradio") + socketIO.emit("stop") + elif status == "play": + print("pause") + socketIO.emit("pause") + else: + print("play") + socketIO.emit("play") + if pin == 6: + print("volDown") + socketIO.emit("volume", "-") + if pin == 16: + print("Next") + socketIO.emit("next") + if pin == 20: + print("volUp") + socketIO.emit("volume", "+") def main(): - - disp.set_backlight(True) - - filename = '/var/local/www/currentsong.txt' - - c = 0 - p = 0 - k=0 - ol=0 - ss = 0 - x1 = 20 - x2 = 20 - x3 = 20 - title_top = 105 - volume_top = 184 - time_top = 222 - act_mpd = isServiceActive('mpd') - - if act_mpd == True: - while True: - client = musicpd.MPDClient() # create client object - try: - client.connect() # use MPD_HOST/MPD_PORT - except: - pass - else: - moode_meta = getMoodeMetadata(filename) - - mpd_current = client.currentsong() - mpd_status = client.status() - cover = get_cover(moode_meta) + while True: + print("In main") + for pin in BUTTONS: + GPIO.add_event_detect(pin, GPIO.FALLING, handle_button, bouncetime=100) + disp.set_backlight(True) + # mit socket verbinden + socketIO.on("pushState", on_push_state) + # get initial state + socketIO.emit("getState", "", on_push_state) + socketIO.wait() + # socketIO.wait_for_callbacks(seconds=1) + # time.sleep(1) + time.sleep(0.01) - - mn = 50 - if OVERLAY == 3: - img.paste(cover.resize((WIDTH,HEIGHT), Image.LANCZOS).convert('RGB')) - else: - img.paste(cover.resize((WIDTH,HEIGHT), Image.LANCZOS).filter(ImageFilter.GaussianBlur).convert('RGB')) - - if 'state' in mpd_status: - if (mpd_status['state'] == 'stop') and (BLANK != 0): - if ss < BLANK: - ss = ss + 1 - else: - disp.set_backlight(False) - else: - ss = 0 - disp.set_backlight(True) - - - im_stat = ImageStat.Stat(cover) - im_mean = im_stat.mean - mn = mean(im_mean) - - #txt_col = (255-int(im_mean[0]), 255-int(im_mean[1]), 255-int(im_mean[2])) - txt_col = (255,255,255) - bar_col = (255, 255, 255, 255) - dark = False - if mn > 175: - txt_col = (55,55,55) - dark=True - bar_col = (100,100,100,225) - if mn < 80: - txt_col = (200,200,200) - - if (moode_meta['source'] == 'library') or (moode_meta['source'] == 'radio'): - - if (OVERLAY > 0) and (OVERLAY < 3): - if 'state' in mpd_status: - if OVERLAY == 2: - if mpd_status['state'] != 'play': - if dark is False: - img.paste(pause_icons, (0,0), pause_icons) - else: - img.paste(pause_icons_dark, (0,0), pause_icons_dark) - else: - if dark is False: - img.paste(play_icons, (0,0), play_icons) - else: - img.paste(play_icons_dark, (0,0), play_icons_dark) - elif OVERLAY == 1: - if dark is False: - img.paste(vol_icons, (0,0), vol_icons) - else: - img.paste(vol_icons_dark, (0,0), vol_icons_dark) - - else: - img.paste(play_icons, (0,0), play_icons) - - if 'volume' in mpd_status: - vol = int(mpd_status['volume']) - vol_x = int((vol/100)*(WIDTH - 33)) - draw.rectangle((5, volume_top, WIDTH-34, volume_top+8), (255,255,255,145)) - draw.rectangle((5, volume_top, vol_x, volume_top+8), bar_col) - - if OVERLAY < 3: - if TIMEBAR == 1: - if 'elapsed' in mpd_status: - el_time = int(float(mpd_status['elapsed'])) - if 'duration' in mpd_status: - du_time = int(float(mpd_status['duration'])) - dur_x = int((el_time/du_time)*(WIDTH-10)) - draw.rectangle((5, time_top, WIDTH-5, time_top + 12), (255,255,255,145)) - draw.rectangle((5, time_top, dur_x, time_top + 12), bar_col) - - - top = 7 - if 'artist' in moode_meta: - w1, y1 = draw.textsize(moode_meta['artist'], font_m) - x1 = x1-20 - if x1 < (WIDTH - w1 - 20): - x1 = 0 - if w1 <= WIDTH: - x1 = (WIDTH - w1)//2 - draw.text((x1, top), moode_meta['artist'], font=font_m, fill=txt_col) - - top = 35 - - if 'album' in moode_meta: - w2, y2 = draw.textsize(moode_meta['album'], font_s) - x2 = x2-20 - if x2 < (WIDTH - w2 - 20): - x2 = 0 - if w2 <= WIDTH: - x2 = (WIDTH - w2)//2 - draw.text((x2, top), moode_meta['album'], font=font_s, fill=txt_col) - - - if 'title' in moode_meta: - w3, y3 = draw.textsize(moode_meta['title'], font_l) - x3 = x3-20 - if x3 < (WIDTH - w3 - 20): - x3 = 0 - if w3 <= WIDTH: - x3 = (WIDTH - w3)//2 - draw.text((x3, title_top), moode_meta['title'], font=font_l, fill=txt_col) - - - else: - if 'file' in moode_meta: - txt = moode_meta['file'].replace(' ', '\n') - w3, h3 = draw.multiline_textsize(txt, font_l, spacing=6) - x3 = (WIDTH - w3)//2 - y3 = (HEIGHT - h3)//2 - draw.text((x3, y3), txt, font=font_l, fill=txt_col, spacing=6, align="center") - - - disp.display(img) - - if c == 0: - im7 = img.save(script_path+'/dump.jpg') - c += 1 - - - time.sleep(1) - ol += 1 - - client.disconnect() - else: - draw.rectangle((0,0,240,240), fill=(0,0,0)) - txt = 'MPD not Active!\nEnsure MPD is running\nThen restart script' - mlw, mlh = draw.multiline_textsize(txt, font=font_m, spacing=4) - draw.multiline_text(((WIDTH-mlw)//2, 20), txt, fill=(255,255,255), font=font_m, spacing=4, align="center") - disp.display(img) - - - - -if __name__ == '__main__': +if __name__ == "__main__": try: main() except KeyboardInterrupt: - disp.reset() + img = Image.new("RGB", (240, 240), color=(0, 0, 0)) + draw = ImageDraw.Draw(img) + draw.rectangle((0, 0, 240, 240), (0, 0, 0)) + disp.display(img) disp.set_backlight(False) pass