Skip to content

Instantly share code, notes, and snippets.

@bonelifer
Created January 16, 2025 19:37
Show Gist options
  • Save bonelifer/dec7e625e3411fc548b291909a800813 to your computer and use it in GitHub Desktop.
Save bonelifer/dec7e625e3411fc548b291909a800813 to your computer and use it in GitHub Desktop.
MPD Tray application(similar to RadioTray-NG) to manage and play categorized radio station URLs.
#!/usr/bin/python3
"""
MPD Tray application(similar to RadioTray-NG) to manage and play categorized radio station URLs.
Provides Stop, Stations (organized by categories), Refresh, and Exit options in the tray menu.
Uses `mpc load URL` equivalent functionality, ensuring reconnection to MPD.
Pressing Stop clears the queue.
"""
# Editable variables
menu_icon = False # Set to True to use system icon (XFce add-folder-to-archive icon), False for custom icon
STATIONS_FILE = "stations.txt" # Path to the stations file
MPD_HOST = "localhost" # MPD server host
MPD_PORT = 6600 # MPD server port
SYSTEM_ICON_PATH = "/usr/share/icons/hicolor/16x16/actions/add-folder-to-archive.png" # Path to system icon
import os
from collections import defaultdict
from mpd import MPDClient
from pystray import Icon, Menu, MenuItem
from PIL import Image, ImageDraw, ImageOps
import psutil
def connect_mpd():
"""Connect to the MPD server and return a client instance."""
client = MPDClient()
try:
client.connect(MPD_HOST, MPD_PORT)
except Exception as e:
print(f"Error connecting to MPD: {e}")
return None
return client
def load_stations():
"""Load stations from the stations.txt file, organized by categories."""
stations_by_category = defaultdict(list)
if not os.path.exists(STATIONS_FILE):
print(f"{STATIONS_FILE} not found.")
return stations_by_category
with open(STATIONS_FILE, "r") as f:
for line in f:
line = line.strip()
# Skip comments or empty lines
if not line or line.startswith("#"):
continue
if "|" in line:
category, name, url = line.split("|", 2)
stations_by_category[category].append((name, url))
return stations_by_category
def load_station(url):
"""Simulate `mpc load URL` functionality."""
client = connect_mpd()
if not client:
return
try:
client.clear() # Clear the current playlist
client.load(url) # Load the URL directly into MPD
client.play() # Start playback
print(f"Loaded and playing: {url}")
except Exception as e:
print(f"Error loading URL: {e}")
finally:
client.close()
client.disconnect()
def play_station_wrapper(url):
"""Create a wrapper function to load the station URL."""
def wrapper(icon, item):
load_station(url)
return wrapper
def stop_playback(icon, item):
"""Stop MPD playback and clear the queue."""
client = connect_mpd()
if not client:
return
try:
client.stop() # Stop playback
client.clear() # Clear the playlist/queue
print("Playback stopped and queue cleared.")
except Exception as e:
print(f"Error stopping playback: {e}")
finally:
client.close()
client.disconnect()
def refresh(icon, item):
"""Refresh the station list in the tray menu."""
icon.menu = create_menu(icon)
def create_icon_image():
"""Create a tray icon image with a minimalistic design and a zigzag line."""
# Create a 64x64 pixel image with transparent background
image = Image.new("RGBA", (64, 64), (0, 0, 0, 0)) # Transparent background
draw = ImageDraw.Draw(image)
# Define the icon's fill and outline colors based on dark mode compatibility
fill_color = (255, 255, 255) # White for a light icon
outline_color = (0, 0, 0) # Black outline
# Draw a circle icon with a simple line across the middle
draw.ellipse((10, 10, 54, 54), fill=fill_color, outline=outline_color)
# Define the points for the zigzag line inside the circle
zigzag_points = [
(18, 32), # Starting point
(22, 28), # Zigzag down
(26, 32), # Zigzag up
(30, 28), # Zigzag down
(34, 32), # Zigzag up
(38, 28), # Zigzag down
(42, 32), # Zigzag up
(46, 28) # End point
]
# Draw the zigzag line (connecting the points)
draw.line(zigzag_points, fill=outline_color, width=2)
return image
def get_system_icon(icon_path):
"""Load a system icon from the given path."""
try:
icon = Image.open(icon_path)
icon = icon.convert("RGBA") # Ensure the icon is in a format pystray can use
return icon
except Exception as e:
print(f"Error loading system icon: {e}")
return None
def create_menu(icon):
"""Create a dynamic tray menu with categories as individual items and a separator."""
stations_by_category = load_stations()
# Create the main menu items
menu_items = []
# Add Stop item
menu_items.append(MenuItem("Stop", stop_playback))
# Add categories as menu items with subitems for each station
for category, stations in stations_by_category.items():
category_item = MenuItem(
category,
Menu(
*[
MenuItem(name, play_station_wrapper(url))
for name, url in stations
]
)
)
menu_items.append(category_item)
# Add Refresh and Exit items
menu_items.append(MenuItem("Refresh", refresh))
menu_items.append(MenuItem("Exit", lambda icon, item: icon.stop()))
# Create the main menu with the items
return Menu(*menu_items)
def main():
"""Main function to run the tray application."""
# Determine which icon to use
if menu_icon:
icon_image = get_system_icon(SYSTEM_ICON_PATH)
if icon_image is None:
print("System icon loading failed. Falling back to custom icon.")
icon_image = create_icon_image()
else:
icon_image = create_icon_image() # Use custom icon
icon = Icon("MPD", icon_image, menu=create_menu(None))
icon.run()
if __name__ == "__main__":
main()
#Category|Station Name|URL
Country|181.FM Classic Hits|http://www.181.fm/winamp.pls?station=181-greatoldies&file=181-greatoldies.pls
Covers|Covers|http://somafm.com/covers.pls
Country|Highway 181|http://www.181.fm/winamp.pls?station=181-highway&file=181-highway.pls
Radio NOIR|AM600 Conyers OTR|http://www.conyersradio.net/listen.m3u
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment