├── .gitattributes ├── .gitignore ├── README.md ├── clear_display.py ├── config.yml ├── fonts └── Roboto-Medium.ttf ├── images ├── airplay.png ├── bta.png ├── controls-pause-dark.png ├── controls-pause.png ├── controls-play-dark.png ├── controls-play.png ├── controls-vol-dark.png ├── controls-vol.png ├── default-cover-v6.jpg ├── jack.png ├── spotify.png └── squeeze.png ├── install_service.sh ├── pics └── display.jpg ├── remove_service.sh ├── tft-moodecoverart.service ├── tft_moode_coverart.py └── tft_moode_coverart.sh /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | backlight.py 3 | backlight.sh 4 | 5 | stst.py 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PLEASE NOTE 2 | 3 | I no longer use or update these scripts, so I am unable to provide any support or updates 4 | 5 | # TFT-MoodeCoverArt 6 | *version = "0.0.9" : changes* 7 | * added config option for blanking display on pause 8 | 9 | *version = "0.0.8" : changes* 10 | * added config option for switching play and pause icons 11 | 12 | *version = "0.0.7" : changes* 13 | 14 | * update for moode 7.6.0 - BUG FIX: URL encoding for radio station logos 15 | 16 | *version = "0.0.6" : changes* 17 | 18 | * added option for text shadow 19 | 20 | *version = "0.0.5" : changes* 21 | 22 | * radio icons location changed 23 | 24 | *version = "0.0.4" : changes* 25 | 26 | * added option to display cover art only without overlays (see config file for instructions) 27 | 28 | *version = "0.0.3" : changes* 29 | 30 | * added option to turn off backlight when mpd state = stop (see config file for instructions) 31 | 32 | ------------------------------------------------------- 33 | 34 | 35 | Based on the look of the pirate audio plugin for mopidy. 36 | 37 | Works with Pimoroni pirate audio boards with 240*240 TFT (ST7789), as well as standalone ST7789 boards. 38 | 39 | ![Sample Image](/pics/display.jpg) 40 | 41 | ### Features. 42 | 43 | The script will display cover art (where available) for the moode library or radio stations. 44 | 45 | * For the moode library, embedded art will be displayed first, then folder or cover images if there is no embedded art. 46 | * For radio stations, the moode images are used. 47 | * If no artwork is found a default image is displayed. 48 | 49 | Metadata displayed: 50 | * Artist 51 | * Album/Radio Station 52 | * Title 53 | 54 | Overlays with a Time bar, Volume bar and Play/Pause, Next and Volume icons to match the Pirate Audio buttons are optional. 55 | 56 | There is also an option in config.yml to not display metadata 57 | 58 | The script has a built in test to see if the mpd service is running. This should allow enough delay when 59 | used as a service. If a running mpd service is not found after around 30 seconds the script displays the following and stops. 60 | 61 | ``` 62 | MPD not Active! 63 | Ensure MPD is running 64 | Then restart script 65 | ``` 66 | 67 | **Limitations** 68 | 69 | Metadata will only be displayed for Radio Stations and the Library. 70 | 71 | For the `Airplay`, `Spotify`, `Bluetooth`, `Squeezelite` and `Dac Input` renderers, different backgrounds will display. 72 | 73 | The overlay colours adjust for light and dark artwork, but can be hard to read with some artwork. 74 | 75 | The script does not search online for artwork 76 | 77 | ### Assumptions. 78 | 79 | **You can SSH into your RPI, enter commands at the shell prompt, and use the nano editor.** 80 | 81 | **Your moode installation works and produces audio** 82 | 83 | If your pirate audio board doesn't output anything 84 | 85 | Choose "Pimoroni pHAT DAC" or "HiFiBerry DAC" in moode audio config 86 | 87 | See the Installation section [**here**](https://github.com/pimoroni/pirate-audio) about gpio pin 25. 88 | 89 | 90 | ### Preparation. 91 | 92 | **Enable SPI pn your RPI** 93 | 94 | see [**Configuring SPI**](https://learn.adafruit.com/adafruits-raspberry-pi-lesson-4-gpio-setup/configuring-spi) 95 | 96 | Install these pre-requisites: 97 | ``` 98 | sudo apt-get update 99 | sudo apt-get install python3-rpi.gpio python3-spidev python3-pip python3-pil python3-numpy 100 | sudo pip3 install mediafile 101 | sudo pip3 install pyyaml 102 | ``` 103 | Install the TFT driver. 104 | 105 | I have forked the Pimoroni driver and modified it to work with other ST7789 boards. Install it with the following command: 106 | 107 | ``` 108 | sudo pip3 install RPI-ST7789 109 | ``` 110 | 111 | ***Ensure 'Metadata file' is turned on in Moode System Configuration*** 112 | 113 | ### Install the TFT-MoodeCoverArt script 114 | 115 | ``` 116 | cd /home/pi 117 | git clone https://github.com/rusconi/TFT-MoodeCoverArt.git 118 | ``` 119 | 120 | ### Config File 121 | 122 | The default config should work with Pirate Audio boards 123 | 124 | The config.yml file can be edited to: 125 | 126 | * suit different ST7789 boards 127 | * set overlay display options 128 | * display the text with a shadow 129 | 130 | The comments in 'config.yml' should be self explanatory 131 | 132 | 133 | **Make the shell scripts executable:** 134 | 135 | ``` 136 | chmod 777 *.sh 137 | ``` 138 | 139 | Test the script: 140 | 141 | ``` 142 | python3 /home/pi/TFT-MoodeCoverArt/tft_moode_coverart.py 143 | 144 | 145 | Ctrl-c to quit 146 | ``` 147 | 148 | **If the script works, you may want to start the display at boot:** 149 | 150 | ### Install as a service. 151 | 152 | ``` 153 | cd /home/pi/TFT-MoodeCoverArt 154 | ./install_service.sh 155 | ``` 156 | 157 | Follow the prompts. 158 | 159 | If you wish to remove the script as a service: 160 | 161 | ``` 162 | cd /home/pi/TFT-MoodeCoverArt 163 | ./remove_service.sh 164 | ``` 165 | 166 | -------------------------------------------------------------------------------- /clear_display.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | import ST7789 4 | from PIL import Image, ImageDraw 5 | from os import path 6 | import yaml 7 | 8 | # set default config for pirate audio 9 | 10 | MODE=0 11 | 12 | OVERLAY=2 13 | 14 | confile = 'config.yml' 15 | 16 | # Read conf.json for user config 17 | if path.exists(confile): 18 | 19 | with open(confile) as config_file: 20 | data = yaml.load(config_file, Loader=yaml.FullLoader) 21 | displayConf = data['display'] 22 | OVERLAY = displayConf['overlay'] 23 | MODE = displayConf['mode'] 24 | 25 | 26 | # Standard SPI connections for ST7789 27 | # Create ST7789 LCD display class. 28 | if MODE == 3: 29 | disp = ST7789.ST7789( 30 | port=0, 31 | cs=ST7789.BG_SPI_CS_FRONT, # GPIO 8, Physical pin 24 32 | dc=9, 33 | rst=22, 34 | backlight=13, 35 | mode=3, 36 | rotation=0, 37 | spi_speed_hz=80 * 1000 * 1000 38 | ) 39 | else: 40 | disp = ST7789.ST7789( 41 | port=0, 42 | cs=ST7789.BG_SPI_CS_FRONT, # GPIO 8, Physical pin 24 43 | dc=9, 44 | backlight=13, 45 | spi_speed_hz=80 * 1000 * 1000 46 | ) 47 | 48 | 49 | disp.begin() 50 | img = Image.new('RGB', (240, 240), color=(0, 0, 0)) 51 | draw = ImageDraw.Draw(img) 52 | draw.rectangle((0, 0, 240, 240), (0, 0, 0)) 53 | disp.display(img) 54 | 55 | disp.set_backlight(False) 56 | -------------------------------------------------------------------------------- /config.yml: -------------------------------------------------------------------------------- 1 | # version = "0.0.9" 2 | display: 3 | # Display control overlays: 4 | # 3=Artwork only 5 | # 2=Full overlay displayed 6 | # 1=Volume icon only. 7 | # 0=no overlay displayed 8 | overlay: 2 9 | 10 | # Display Time Bar: 11 | # 1=time bar displayed 12 | # 0=no time bar displayed 13 | timebar: 1 14 | 15 | # mode=0 for pirate audio and display boards with cs pin 16 | # mode=3 for display boards without cs pin 17 | mode: 0 18 | 19 | # Turn backlight off when mpd state=stop 20 | # back on when play restarted 21 | # blank=0 screen will not blank when mpd state=stop 22 | # blank=n screen will blank after roughly n seconds when mpd state=stop 23 | blank: 0 24 | 25 | # Option for screen to blank on pause 26 | # pauseblank = 0 for display to stay on 27 | # pauseblank = 1 for display to blank 28 | pauseblank: 0 29 | 30 | # Add shadow to text 31 | # Shadow is displayed below and to the right 32 | # shadow=0 for no shadow text 33 | # shadow=n : n is the shadow pixel offset 34 | shadow: 3 35 | 36 | # switch play/ pause button display 37 | # [switch of play and pause requested for more intuitive operation; 38 | # original coding kept as this is a personal preference] 39 | # ppbutton = 0 for original play pause putton display 40 | # ppbutton = 1 for play and pause buttons switched for display 41 | ppbutton: 0 42 | 43 | 44 | -------------------------------------------------------------------------------- /fonts/Roboto-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusconi/TFT-MoodeCoverArt/b0b7cde6708c5d72e4541aeab5049f1d72ff33f1/fonts/Roboto-Medium.ttf -------------------------------------------------------------------------------- /images/airplay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusconi/TFT-MoodeCoverArt/b0b7cde6708c5d72e4541aeab5049f1d72ff33f1/images/airplay.png -------------------------------------------------------------------------------- /images/bta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusconi/TFT-MoodeCoverArt/b0b7cde6708c5d72e4541aeab5049f1d72ff33f1/images/bta.png -------------------------------------------------------------------------------- /images/controls-pause-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusconi/TFT-MoodeCoverArt/b0b7cde6708c5d72e4541aeab5049f1d72ff33f1/images/controls-pause-dark.png -------------------------------------------------------------------------------- /images/controls-pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusconi/TFT-MoodeCoverArt/b0b7cde6708c5d72e4541aeab5049f1d72ff33f1/images/controls-pause.png -------------------------------------------------------------------------------- /images/controls-play-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusconi/TFT-MoodeCoverArt/b0b7cde6708c5d72e4541aeab5049f1d72ff33f1/images/controls-play-dark.png -------------------------------------------------------------------------------- /images/controls-play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusconi/TFT-MoodeCoverArt/b0b7cde6708c5d72e4541aeab5049f1d72ff33f1/images/controls-play.png -------------------------------------------------------------------------------- /images/controls-vol-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusconi/TFT-MoodeCoverArt/b0b7cde6708c5d72e4541aeab5049f1d72ff33f1/images/controls-vol-dark.png -------------------------------------------------------------------------------- /images/controls-vol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusconi/TFT-MoodeCoverArt/b0b7cde6708c5d72e4541aeab5049f1d72ff33f1/images/controls-vol.png -------------------------------------------------------------------------------- /images/default-cover-v6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusconi/TFT-MoodeCoverArt/b0b7cde6708c5d72e4541aeab5049f1d72ff33f1/images/default-cover-v6.jpg -------------------------------------------------------------------------------- /images/jack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusconi/TFT-MoodeCoverArt/b0b7cde6708c5d72e4541aeab5049f1d72ff33f1/images/jack.png -------------------------------------------------------------------------------- /images/spotify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusconi/TFT-MoodeCoverArt/b0b7cde6708c5d72e4541aeab5049f1d72ff33f1/images/spotify.png -------------------------------------------------------------------------------- /images/squeeze.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusconi/TFT-MoodeCoverArt/b0b7cde6708c5d72e4541aeab5049f1d72ff33f1/images/squeeze.png -------------------------------------------------------------------------------- /install_service.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo -e "Install TFT-MoodeCoverArt Service. \n" 4 | cd /home/pi/TFT-MoodeCoverArt 5 | 6 | while true 7 | do 8 | read -p "Do you wish to install TFT-MoodeCoverArt as a service?" yn 9 | case $yn in 10 | [Yy]* ) echo -e "Installing Service \n" 11 | sudo cp tft-moodecoverart.service /lib/systemd/system 12 | sudo chmod 644 /lib/systemd/system/tft-moodecoverart.service 13 | sudo systemctl daemon-reload 14 | sudo systemctl enable tft-moodecoverart.service 15 | echo -e "\nTFT-MoodeCoverArt installed as a service.\n" 16 | echo -e "Please reboot the Raspberry Pi.\n" 17 | break;; 18 | [Nn]* ) echo -e "Service not installed \n"; break;; 19 | * ) echo "Please answer yes or no.";; 20 | esac 21 | done 22 | 23 | while true 24 | do 25 | read -p "Do you wish to reboot now?" yn 26 | case $yn in 27 | [Yy]* ) echo -e "Rebooting \n" 28 | sudo reboot 29 | break;; 30 | [Nn]* ) echo -e "Not rebooting \n" 31 | break;; 32 | * ) echo "Please answer yes or no.";; 33 | esac 34 | done 35 | 36 | echo "TFT-MoodeCoverArt install complete" -------------------------------------------------------------------------------- /pics/display.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusconi/TFT-MoodeCoverArt/b0b7cde6708c5d72e4541aeab5049f1d72ff33f1/pics/display.jpg -------------------------------------------------------------------------------- /remove_service.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo -e "Remove TFT-MoodeCoverArt Service\n" 4 | 5 | 6 | while true 7 | do 8 | read -p "Do you wish to Remove TFT-MoodeCoverArt as a service?" yn 9 | case $yn in 10 | [Yy]* ) echo -e "Removing Service \n" 11 | sudo systemctl stop tft-moodecoverart.service 12 | sudo systemctl disable tft-moodecoverart.service 13 | sudo rm /etc/systemd/system/tft-moodecoverart.service 14 | sudo systemctl daemon-reload 15 | sudo systemctl reset-failed 16 | echo -e "\nTFT-MoodeCoverArt removed as a service.\n" 17 | echo -e "Please reboot the Raspberry Pi.\n" 18 | break;; 19 | [Nn]* ) echo -e "Service not removed \n"; break;; 20 | * ) echo "Please answer yes or no.";; 21 | esac 22 | done 23 | 24 | while true 25 | do 26 | read -p "Do you wish to reboot now?" yn 27 | case $yn in 28 | [Yy]* ) echo -e "Rebooting \n" 29 | sudo reboot 30 | break;; 31 | [Nn]* ) echo -e "Not rebooting \n" 32 | break;; 33 | * ) echo "Please answer yes or no.";; 34 | esac 35 | done 36 | 37 | echo "TFT-MoodeCoverArt service removal complete" -------------------------------------------------------------------------------- /tft-moodecoverart.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=TFT-MoodeCoverArt Display 3 | Requires=mpd.socket mpd.service 4 | After=mpd.socket mpd.service 5 | 6 | [Service] 7 | Type=simple 8 | ExecStart=/usr/bin/python3 /home/pi/TFT-MoodeCoverArt/tft_moode_coverart.py & 9 | #ExecStartPre=/bin/sleep 15 10 | ExecStop=/home/pi/TFT-MoodeCoverArt/tft_moode_coverart.sh -q 11 | Restart=on-abort 12 | 13 | [Install] 14 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /tft_moode_coverart.py: -------------------------------------------------------------------------------- 1 | from PIL import Image, ImageDraw, ImageColor, ImageFont, ImageStat 2 | import subprocess 3 | import time 4 | import musicpd 5 | import os 6 | import os.path 7 | from os import path 8 | import RPi.GPIO as GPIO 9 | from mediafile import MediaFile 10 | from io import BytesIO 11 | from numpy import mean 12 | import ST7789 13 | from PIL import ImageFilter 14 | import yaml 15 | import urllib.parse 16 | 17 | # set default config for pirate audio 18 | 19 | __version__ = "0.0.9" 20 | 21 | # get the path of the script 22 | script_path = os.path.dirname(os.path.abspath( __file__ )) 23 | # set script path as current directory - 24 | os.chdir(script_path) 25 | 26 | MODE=0 27 | OVERLAY=2 28 | TIMEBAR=1 29 | BLANK=0 30 | SHADE=0 31 | PPBUTTON=0 32 | PAUSEBLANK=0 33 | 34 | confile = 'config.yml' 35 | 36 | # Read config.yml for user config 37 | if path.exists(confile): 38 | 39 | with open(confile) as config_file: 40 | data = yaml.load(config_file, Loader=yaml.FullLoader) 41 | displayConf = data['display'] 42 | OVERLAY = displayConf['overlay'] 43 | MODE = displayConf['mode'] 44 | TIMEBAR = displayConf['timebar'] 45 | BLANK = displayConf['blank'] 46 | SHADE = displayConf['shadow'] 47 | PPBUTTON = displayConf['ppbutton'] 48 | PAUSEBLANK = displayConf["pauseblank"] 49 | 50 | 51 | 52 | 53 | # Standard SPI connections for ST7789 54 | # Create ST7789 LCD display class. 55 | if MODE == 3: 56 | disp = ST7789.ST7789( 57 | port=0, 58 | cs=ST7789.BG_SPI_CS_FRONT, # GPIO 8, Physical pin 24 59 | dc=9, 60 | rst=22, 61 | backlight=13, 62 | mode=3, 63 | rotation=0, 64 | spi_speed_hz=80 * 1000 * 1000 65 | ) 66 | else: 67 | disp = ST7789.ST7789( 68 | port=0, 69 | cs=ST7789.BG_SPI_CS_FRONT, # GPIO 8, Physical pin 24 70 | dc=9, 71 | backlight=13, 72 | spi_speed_hz=80 * 1000 * 1000 73 | ) 74 | 75 | 76 | 77 | # Initialize display. 78 | disp.begin() 79 | 80 | 81 | WIDTH = 240 82 | HEIGHT = 240 83 | font_s = ImageFont.truetype(script_path + '/fonts/Roboto-Medium.ttf',20) 84 | font_m = ImageFont.truetype(script_path + '/fonts/Roboto-Medium.ttf',24) 85 | font_l = ImageFont.truetype(script_path + '/fonts/Roboto-Medium.ttf',30) 86 | 87 | 88 | img = Image.new('RGB', (240, 240), color=(0, 0, 0, 25)) 89 | 90 | if PPBUTTON == 1: 91 | # reversed play and pause icons 92 | pause_icons = Image.open(script_path + '/images/controls-play.png').resize((240,240), resample=Image.LANCZOS).convert("RGBA") 93 | pause_icons_dark = Image.open(script_path + '/images/controls-play-dark.png').resize((240,240), resample=Image.LANCZOS).convert("RGBA") 94 | play_icons = Image.open(script_path + '/images/controls-pause.png').resize((240,240), resample=Image.LANCZOS).convert("RGBA") 95 | play_icons_dark = Image.open(script_path + '/images/controls-pause-dark.png').resize((240,240), resample=Image.LANCZOS).convert("RGBA") 96 | else: 97 | # original play and pause icons 98 | play_icons = Image.open(script_path + '/images/controls-play.png').resize((240,240), resample=Image.LANCZOS).convert("RGBA") 99 | play_icons_dark = Image.open(script_path + '/images/controls-play-dark.png').resize((240,240), resample=Image.LANCZOS).convert("RGBA") 100 | pause_icons = Image.open(script_path + '/images/controls-pause.png').resize((240,240), resample=Image.LANCZOS).convert("RGBA") 101 | pause_icons_dark = Image.open(script_path + '/images/controls-pause-dark.png').resize((240,240), resample=Image.LANCZOS).convert("RGBA") 102 | 103 | vol_icons = Image.open(script_path + '/images/controls-vol.png').resize((240,240), resample=Image.LANCZOS).convert("RGBA") 104 | vol_icons_dark = Image.open(script_path + '/images/controls-vol-dark.png').resize((240,240), resample=Image.LANCZOS).convert("RGBA") 105 | 106 | bt_back = Image.open(script_path + '/images/bta.png').resize((240,240), resample=Image.LANCZOS).convert("RGBA") 107 | ap_back = Image.open(script_path + '/images/airplay.png').resize((240,240), resample=Image.LANCZOS).convert("RGBA") 108 | jp_back = Image.open(script_path + '/images/jack.png').resize((240,240), resample=Image.LANCZOS).convert("RGBA") 109 | sp_back = Image.open(script_path + '/images/spotify.png').resize((240,240), resample=Image.LANCZOS).convert("RGBA") 110 | sq_back = Image.open(script_path + '/images/squeeze.png').resize((240,240), resample=Image.LANCZOS).convert("RGBA") 111 | 112 | draw = ImageDraw.Draw(img, 'RGBA') 113 | 114 | 115 | def isServiceActive(service): 116 | 117 | waiting = True 118 | count = 0 119 | active = False 120 | 121 | while (waiting == True): 122 | 123 | process = subprocess.run(['systemctl','is-active',service], check=False, stdout=subprocess.PIPE, universal_newlines=True) 124 | output = process.stdout 125 | stat = output[:6] 126 | 127 | if stat == 'active': 128 | waiting = False 129 | active = True 130 | 131 | if count > 29: 132 | waiting = False 133 | 134 | count += 1 135 | time.sleep(1) 136 | 137 | return active 138 | 139 | 140 | def getMoodeMetadata(filename): 141 | # Initalise dictionary 142 | metaDict = {} 143 | 144 | if path.exists(filename): 145 | # add each line fo a list removing newline 146 | nowplayingmeta = [line.rstrip('\n') for line in open(filename)] 147 | i = 0 148 | while i < len(nowplayingmeta): 149 | # traverse list converting to a dictionary 150 | (key, value) = nowplayingmeta[i].split('=', 1) 151 | metaDict[key] = value 152 | i += 1 153 | 154 | metaDict['coverurl'] = urllib.parse.unquote(metaDict['coverurl']) 155 | 156 | metaDict['source'] = 'library' 157 | if 'file' in metaDict: 158 | if (metaDict['file'].find('http://', 0) > -1) or (metaDict['file'].find('https://', 0) > -1): 159 | # set radio stream to true 160 | metaDict['source'] = 'radio' 161 | # if radio station has arist and title in one line separated by a hyphen, split into correct keys 162 | if metaDict['title'].find(' - ', 0) > -1: 163 | (art,tit) = metaDict['title'].split(' - ', 1) 164 | metaDict['artist'] = art 165 | metaDict['title'] = tit 166 | elif metaDict['file'].find('Bluetooth Active', 0) > -1: 167 | metaDict['source'] = 'bluetooth' 168 | elif metaDict['file'].find('Airplay Active', 0) > -1: 169 | metaDict['source'] = 'airplay' 170 | elif metaDict['file'].find('Spotify Active', 0) > -1: 171 | metaDict['source'] = 'spotify' 172 | elif metaDict['file'].find('Squeezelite Active', 0) > -1: 173 | metaDict['source'] = 'squeeze' 174 | elif metaDict['file'].find('Input Active', 0) > -1: 175 | metaDict['source'] = 'input' 176 | 177 | 178 | # return metadata 179 | return metaDict 180 | 181 | def get_cover(metaDict): 182 | 183 | cover = None 184 | cover = Image.open(script_path + '/images/default-cover-v6.jpg') 185 | covers = ['Cover.jpg', 'cover.jpg', 'Cover.jpeg', 'cover.jpeg', 'Cover.png', 'cover.png', 'Cover.tif', 'cover.tif', 'Cover.tiff', 'cover.tiff', 186 | 'Folder.jpg', 'folder.jpg', 'Folder.jpeg', 'folder.jpeg', 'Folder.png', 'folder.png', 'Folder.tif', 'folder.tif', 'Folder.tiff', 'folder.tiff'] 187 | if metaDict['source'] == 'radio': 188 | if 'coverurl' in metaDict: 189 | rc = '/var/local/www/' + metaDict['coverurl'] 190 | if path.exists(rc): 191 | if rc != '/var/local/www/images/default-cover-v6.svg': 192 | cover = Image.open(rc) 193 | 194 | elif metaDict['source'] == 'airplay': 195 | cover = ap_back 196 | elif metaDict['source'] == 'bluetooth': 197 | cover = bt_back 198 | elif metaDict['source'] == 'input': 199 | cover = jp_back 200 | elif metaDict['source'] == 'spotify': 201 | cover = sp_back 202 | elif metaDict['source'] == 'squeeze': 203 | cover = sq_back 204 | else: 205 | if 'file' in metaDict: 206 | if len(metaDict['file']) > 0: 207 | 208 | fp = '/var/lib/mpd/music/' + metaDict['file'] 209 | mf = MediaFile(fp) 210 | if mf.art: 211 | cover = Image.open(BytesIO(mf.art)) 212 | return cover 213 | else: 214 | for it in covers: 215 | cp = os.path.dirname(fp) + '/' + it 216 | 217 | if path.exists(cp): 218 | cover = Image.open(cp) 219 | return cover 220 | return cover 221 | 222 | 223 | def main(): 224 | 225 | disp.set_backlight(True) 226 | 227 | filename = '/var/local/www/currentsong.txt' 228 | 229 | c = 0 230 | p = 0 231 | k=0 232 | ol=0 233 | ss = 0 234 | x1 = 20 235 | x2 = 20 236 | x3 = 20 237 | title_top = 105 238 | volume_top = 184 239 | time_top = 222 240 | act_mpd = isServiceActive('mpd') 241 | SHADE = displayConf['shadow'] 242 | 243 | if act_mpd == True: 244 | while True: 245 | client = musicpd.MPDClient() # create client object 246 | try: 247 | client.connect() # use MPD_HOST/MPD_PORT 248 | except: 249 | pass 250 | else: 251 | moode_meta = getMoodeMetadata(filename) 252 | 253 | mpd_current = client.currentsong() 254 | mpd_status = client.status() 255 | cover = get_cover(moode_meta) 256 | 257 | 258 | 259 | mn = 50 260 | if OVERLAY == 3: 261 | img.paste(cover.resize((WIDTH,HEIGHT), Image.LANCZOS).convert('RGB')) 262 | else: 263 | img.paste(cover.resize((WIDTH,HEIGHT), Image.LANCZOS).filter(ImageFilter.GaussianBlur).convert('RGB')) 264 | 265 | if 'state' in mpd_status: 266 | if ((mpd_status['state'] == 'stop') and (BLANK != 0)) or ((mpd_status['state'] == 'pause') and (BLANK != 0) and (PAUSEBLANK != 0)): 267 | if ss < BLANK: 268 | ss = ss + 1 269 | else: 270 | disp.set_backlight(False) 271 | else: 272 | ss = 0 273 | disp.set_backlight(True) 274 | 275 | 276 | im_stat = ImageStat.Stat(cover) 277 | im_mean = im_stat.mean 278 | mn = mean(im_mean) 279 | 280 | #txt_col = (255-int(im_mean[0]), 255-int(im_mean[1]), 255-int(im_mean[2])) 281 | txt_col = (255,255,255) 282 | str_col = (15,15,15) 283 | bar_col = (255, 255, 255, 255) 284 | dark = False 285 | if mn > 175: 286 | txt_col = (55,55,55) 287 | str_col = (200,200,200) 288 | dark=True 289 | bar_col = (100,100,100,225) 290 | if mn < 80: 291 | txt_col = (200,200,200) 292 | str_col = (55,55,55) 293 | 294 | if (moode_meta['source'] == 'library') or (moode_meta['source'] == 'radio'): 295 | 296 | if (OVERLAY > 0) and (OVERLAY < 3): 297 | if 'state' in mpd_status: 298 | if OVERLAY == 2: 299 | if mpd_status['state'] != 'play': 300 | if dark is False: 301 | img.paste(pause_icons, (0,0), pause_icons) 302 | else: 303 | img.paste(pause_icons_dark, (0,0), pause_icons_dark) 304 | else: 305 | if dark is False: 306 | img.paste(play_icons, (0,0), play_icons) 307 | else: 308 | img.paste(play_icons_dark, (0,0), play_icons_dark) 309 | elif OVERLAY == 1: 310 | if dark is False: 311 | img.paste(vol_icons, (0,0), vol_icons) 312 | else: 313 | img.paste(vol_icons_dark, (0,0), vol_icons_dark) 314 | 315 | else: 316 | img.paste(play_icons, (0,0), play_icons) 317 | 318 | if 'volume' in mpd_status: 319 | vol = int(mpd_status['volume']) 320 | vol_x = int((vol/100)*(WIDTH - 33)) 321 | draw.rectangle((5, volume_top, WIDTH-34, volume_top+8), (255,255,255,145)) 322 | draw.rectangle((5, volume_top, vol_x, volume_top+8), bar_col) 323 | 324 | if OVERLAY < 3: 325 | if TIMEBAR == 1: 326 | if 'elapsed' in mpd_status: 327 | el_time = int(float(mpd_status['elapsed'])) 328 | if 'duration' in mpd_status: 329 | du_time = int(float(mpd_status['duration'])) 330 | dur_x = int((el_time/du_time)*(WIDTH-10)) 331 | draw.rectangle((5, time_top, WIDTH-5, time_top + 12), (255,255,255,145)) 332 | draw.rectangle((5, time_top, dur_x, time_top + 12), bar_col) 333 | 334 | 335 | top = 7 336 | if 'artist' in moode_meta: 337 | w1, y1 = draw.textsize(moode_meta['artist'], font_m) 338 | x1 = x1-20 339 | if x1 < (WIDTH - w1 - 20): 340 | x1 = 0 341 | if w1 <= WIDTH: 342 | x1 = (WIDTH - w1)//2 343 | 344 | if SHADE != 0: 345 | draw.text((x1+SHADE, top+SHADE), moode_meta['artist'], font=font_m, fill=str_col) 346 | 347 | draw.text((x1, top), moode_meta['artist'], font=font_m, fill=txt_col) 348 | 349 | top = 35 350 | 351 | if 'album' in moode_meta: 352 | w2, y2 = draw.textsize(moode_meta['album'], font_s) 353 | x2 = x2-20 354 | if x2 < (WIDTH - w2 - 20): 355 | x2 = 0 356 | if w2 <= WIDTH: 357 | x2 = (WIDTH - w2)//2 358 | if SHADE != 0: 359 | draw.text((x2+SHADE, top+SHADE), moode_meta['album'], font=font_s, fill=str_col) 360 | draw.text((x2, top), moode_meta['album'], font=font_s, fill=txt_col) 361 | 362 | 363 | if 'title' in moode_meta: 364 | w3, y3 = draw.textsize(moode_meta['title'], font_l) 365 | x3 = x3-20 366 | if x3 < (WIDTH - w3 - 20): 367 | x3 = 0 368 | if w3 <= WIDTH: 369 | x3 = (WIDTH - w3)//2 370 | if SHADE != 0: 371 | draw.text((x3+SHADE, title_top+SHADE), moode_meta['title'], font=font_l, fill=str_col) 372 | draw.text((x3, title_top), moode_meta['title'], font=font_l, fill=txt_col) 373 | 374 | 375 | else: 376 | if 'file' in moode_meta: 377 | txt = moode_meta['file'].replace(' ', '\n') 378 | w3, h3 = draw.multiline_textsize(txt, font_l, spacing=6) 379 | x3 = (WIDTH - w3)//2 380 | y3 = (HEIGHT - h3)//2 381 | if SHADE != 0: 382 | draw.text((x3+SHADE, y3+SHADE), txt, font=font_l, fill=str_col) 383 | draw.text((x3, y3), txt, font=font_l, fill=txt_col, spacing=6, align="center") 384 | 385 | 386 | disp.display(img) 387 | 388 | if c == 0: 389 | im7 = img.save(script_path+'/dump.jpg') 390 | c += 1 391 | 392 | 393 | time.sleep(1) 394 | ol += 1 395 | 396 | client.disconnect() 397 | else: 398 | draw.rectangle((0,0,240,240), fill=(0,0,0)) 399 | txt = 'MPD not Active!\nEnsure MPD is running\nThen restart script' 400 | mlw, mlh = draw.multiline_textsize(txt, font=font_m, spacing=4) 401 | draw.multiline_text(((WIDTH-mlw)//2, 20), txt, fill=(255,255,255), font=font_m, spacing=4, align="center") 402 | disp.display(img) 403 | 404 | 405 | 406 | 407 | if __name__ == '__main__': 408 | try: 409 | main() 410 | except KeyboardInterrupt: 411 | disp.reset() 412 | disp.set_backlight(False) 413 | pass 414 | -------------------------------------------------------------------------------- /tft_moode_coverart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | while getopts ":sq" opt; do 4 | case ${opt} in 5 | s ) sudo pkill -f tft_moode_coverart.py; sudo /usr/bin/python3 /home/pi/TFT-MoodeCoverArt/clear_display.py; sudo /usr/bin/python3 /home/pi/TFT-MoodeCoverArt/tft_moode_coverart.py & 6 | ;; 7 | q ) sudo pkill -f tft_moode_coverart.py; sudo python3 /home/pi/TFT-MoodeCoverArt/clear_display.py 8 | ;; 9 | \? ) echo "Usage: cmd [-s] [-q]" 10 | ;; 11 | esac 12 | done --------------------------------------------------------------------------------