Some experience suggested different default stations & volume settings for the streamers in various rooms, so the Python code now parses its command line to determine how to configure itself:
import argparse as args cmdline = args.ArgumentParser(description='Streaming Radio Player',epilog='KE4ZNU - http://softsolder.com') cmdline.add_argument('Loc',help='Location: BR1 BR2 ...',default='any',nargs='?') args = cmdline.parse_args()
I should definitely pick a different variable name to avoid the obvious clash.
With that in hand, the customization takes very effort:
CurrentKC = 'KEY_KP7' MuteDelay = 8.5 # delay before non-music track; varies with buffering UnMuteDelay = 7.5 # delay after non-music track MixerVol = '15' # mixer gain Location = vars(args)['Loc'].upper() print 'Player location: ',Location logging.info('Player setup for: ' + Location) if Location == 'BR1': CurrentKC = 'KEY_KPDOT' MixerVol = '10' elif Location == 'BR2': MuteDelay = 6.0 UnMuteDelay = 8.0 MixerVol = '5'
The Location = vars()
idiom returns a dictionary of all the variables and their values, of which there’s only one at the moment. The rest of the line extracts the value and normalizes it to uppercase.
Now we can poke the button and get appropriate music without having to think very hard.
Life is good!
The Python source code, which remains in dire need of refactoring, as a GitHub Gist:
from evdev import InputDevice,ecodes,KeyEvent | |
import subprocess32 as subp | |
import select | |
import re | |
import sys | |
import time | |
import logging | |
import os.path | |
import argparse as args | |
cmdline = args.ArgumentParser(description='Streaming Radio Player',epilog='KE4ZNU - http://softsolder.com') | |
cmdline.add_argument('Loc',help='Location: BR1 BR2 ...',default='any',nargs='?') | |
args = cmdline.parse_args() | |
Media = {'KEY_KP7' : ['Classical',False,['mplayer','--quiet','-playlist','http://stream2137.init7.net/listen.pls']], | |
'KEY_KP8' : ['Jazz',False,['mplayer','--quiet','-playlist','http://stream2138.init7.net/listen.pls']], | |
'KEY_KP9' : ['WMHT',False,['mplayer','--quiet','http://wmht.streamguys1.com/wmht1']], | |
'KEY_KP4' : ['Classic 1000',True,['mplayer','--quiet','-playlist','http://listen.radionomy.com/1000classicalhits.m3u']], | |
'KEY_KP5' : ['DCNY 911',False,['mplayer','--quiet','-playlist','http://www.broadcastify.com/scripts/playlists/1/12305/-5857889408.m3u']], | |
'KEY_KP6' : ['WAMC',False,['mplayer','--quiet','http://pubint.ic.llnwd.net/stream/pubint_wamc']], | |
'KEY_KP1' : ['60s',True,['mplayer','--quiet','-playlist','http://listen.radionomy.com/all60sallthetime-keepfreemusiccom.m3u']], | |
'KEY_KP2' : ['50-70s',True,['mplayer','--quiet','-playlist','http://listen.radionomy.com/golden-50-70s-hits.m3u']], | |
'KEY_KP3' : ['Soft Rock',True,['mplayer','--quiet','-playlist','http://listen.radionomy.com/softrockradio.m3u']], | |
'KEY_KP0' : ['Zen',True,['mplayer','--quiet','-playlist','http://listen.radionomy.com/zen-for-you.m3u']], | |
'KEY_KPDOT' : ['Ambient',False,['mplayer','--quiet','http://185.32.125.42:7331/maschinengeist.org.mp3']] | |
} | |
Controls = {'KEY_KPSLASH' : '//////', | |
'KEY_KPASTERISK' : '******', | |
'KEY_KPENTER' : ' ', | |
'KEY_KPMINUS' : '<', | |
'KEY_KPPLUS' : '>', | |
'KEY_VOLUMEUP' : '*', | |
'KEY_VOLUMEDOWN' : '/' | |
} | |
MuteStrings = ["TargetSpot","[Unknown]","Advert:","+++","---","SRR","Srr","ZEN FOR","Intro of","Jingle - ","*bumper*"] | |
Muted = False # keep track of muted state | |
MixerChannel = 'PCM' # which amixer thing to use | |
logging.basicConfig(format='%(asctime)s %(levelname)s: %(message)s',filename='/tmp/Streamer.log',level=logging.INFO) | |
logger = logging.getLogger() | |
# Set up defaults based on where we are | |
CurrentKC = 'KEY_KP7' | |
MuteDelay = 8.5 # delay before non-music track; varies with buffering | |
UnMuteDelay = 7.5 # delay after non-music track | |
MixerVol = '15' # mixer gain | |
Location = vars(args)['Loc'].upper() | |
print 'Player location: ',Location | |
logging.info('Player setup for: ' + Location) | |
if Location == 'BR1': | |
CurrentKC = 'KEY_KPDOT' | |
MixerVol = '10' | |
elif Location == 'BR2': | |
MuteDelay = 6.0 | |
UnMuteDelay = 8.0 | |
MixerVol = '5' | |
# set up event inputs and polling objects | |
# This requires some udev magic to create the symlinks | |
k = InputDevice('/dev/input/keypad') | |
k.grab() | |
kp = select.poll() | |
kp.register(k.fileno(),select.POLLIN + select.POLLPRI + select.POLLERR) | |
# if volume control knob exists, then set up its events | |
VolumeDevice = '/dev/input/volume' | |
vp = select.poll() | |
if os.path.exists(VolumeDevice): | |
v = InputDevice(VolumeDevice) | |
v.grab() | |
vp.register(v.fileno(),select.POLLIN + select.POLLPRI + select.POLLERR) | |
# set up file for output tracing | |
lw = open('/tmp/mp.log','w') # mplayer piped output | |
# set the mixer output low enough that the initial stream won't wake the dead | |
subp.call(['amixer','sset',MixerChannel,MixerVol]) | |
# Start the player with the default stream, set up for polling | |
print 'Starting mplayer on',Media[CurrentKC][0],' -> ',Media[CurrentKC][-1][-1] | |
logging.info('Starting mplayer on %s -> %s',Media[CurrentKC][0],Media[CurrentKC][-1][-1]) | |
p = subp.Popen(Media[CurrentKC][-1], | |
stdin=subp.PIPE,stdout=subp.PIPE,stderr=subp.STDOUT) | |
pp = select.poll() # this may be valid for other invocations, but is not pretty | |
pp.register(p.stdout.fileno(),select.POLLIN + select.POLLPRI + select.POLLERR) | |
print ' ... running' | |
#-------------------- | |
#--- Play the streams | |
while True: | |
# pluck next line from mplayer and decode it | |
if [] != pp.poll(10): | |
text = p.stdout.readline() | |
if 'ICY Info: ' in text: # these lines may contain track names | |
lw.write(text) | |
lw.flush() | |
trkinfo = text.split(';') # also splits at semicolon embedded in track name | |
# logging.info('Raw split line: %s', trkinfo) | |
for ln in trkinfo: | |
if 'StreamTitle' in ln: # this part probably contains the track name | |
NeedMute = False # assume a listenable track | |
trkhit = re.search(r"StreamTitle='(.*)'",ln) # extract title if possible | |
if trkhit: # regex returned valid result? | |
TrackName = trkhit.group(1) # get string between two quotes | |
else: | |
print ' ... regex failed for line: ', ln | |
logging.info('Regex failed for line: [' + ln + ']') | |
TrackName = "Invalid StreamTitle format" | |
print 'Track name: [', TrackName, ']' | |
logging.info('Track name: [%s]', TrackName) | |
if Media[CurrentKC][1] and ( (len(TrackName) == 0) or any(m in TrackName for m in MuteStrings) ) : | |
NeedMute = True | |
if NeedMute: | |
print ' ... muting:', | |
if Media[CurrentKC][1] and not Muted: | |
time.sleep(MuteDelay) # brute-force assumption about buffer leadtime | |
subp.call(['amixer','-q','sset',MixerChannel,'mute']) | |
Muted = True | |
print 'done' | |
logging.info('Track muted') | |
else: | |
print ' ... unmuting:', | |
if Muted: | |
if Media[CurrentKC][1]: | |
time.sleep(UnMuteDelay) # another brute-force timing assumption | |
Muted = False | |
subp.call(['amixer','-q','sset',MixerChannel,'unmute']) | |
print 'done' | |
logging.info('Track unmuted') | |
elif 'Exiting.' in text: # mplayer just imploded | |
lw.write(text) | |
lw.flush() | |
print 'Got EOF / stream cutoff: []',text,']' | |
logging.info('EOF or stream cutoff') | |
print ' ... killing dead mplayer' | |
pp.unregister(p.stdout.fileno()) | |
p.terminate() # p.kill() | |
p.wait() | |
# print ' ... flushing pipes' | |
# lw.truncate(0) | |
print ' ... discarding keys' | |
while [] != kp.poll(0): | |
kev = k.read | |
time.sleep(10) | |
print ' ... restarting mplayer: ',Media[CurrentKC][0] | |
logging.info('Restarting mplayer') | |
p = subp.Popen(Media[CurrentKC][-1], | |
stdin=subp.PIPE,stdout=subp.PIPE,stderr=subp.STDOUT) | |
pp.register(p.stdout.fileno(),select.POLLIN + select.POLLPRI + select.POLLERR) | |
print ' ... running' | |
logging.info(' ... running') | |
# accept pending events from volume control knob | |
if [] != vp.poll(10): | |
vev = v.read() | |
lw.write('Volume') | |
lw.flush() | |
for e in vev: | |
if e.type == ecodes.EV_KEY: | |
kc = KeyEvent(e).keycode | |
# print 'Volume kc: ',kc | |
if kc in Controls: | |
print 'Vol Control: ',kc | |
try: | |
p.stdin.write(Controls[kc]) | |
except Exception as e: | |
print "Can't send control: ",e | |
print ' ... restarting player: ',Media[CurrentKC][0] | |
logging.info('Error sending volume, restarting player: ' + str(e)) | |
try: | |
pp.unregister(p.stdout.fileno()) | |
except Exception as e: | |
print 'Trouble unregistering stdout: ',e | |
logging.info('Cannot unregister stdout: ' + str(e)) | |
time.sleep(2) | |
p = subp.Popen(Media[CurrentKC][-1], | |
stdin=subp.PIPE,stdout=subp.PIPE,stderr=subp.STDOUT) | |
pp.register(p.stdout.fileno(),select.POLLIN + select.POLLPRI + select.POLLERR) | |
print ' ... running' | |
logging.info(' ... running') | |
# accept pending events from keypad | |
if [] != kp.poll(10): | |
kev = k.read() | |
lw.write("Keypad") | |
lw.flush() | |
for e in kev: | |
if e.type == ecodes.EV_KEY: | |
kc = KeyEvent(e).keycode | |
if kc == 'KEY_NUMLOCK': # discard these, as we don't care | |
continue | |
# print 'Got: ',kc | |
if (kc == 'KEY_BACKSPACE') and (KeyEvent(e).keystate == KeyEvent.key_hold): | |
if True: | |
print 'Backspace = shutdown!' | |
p.kill() | |
logging.shutdown() | |
q = subp.call(['sudo','shutdown','-P','now']) | |
q.wait() | |
time.sleep(5) | |
print "Oddly, we did not die..." | |
else: | |
print 'BS = bail from main!' | |
logging.shutdown() | |
sys.exit(0) | |
break | |
if KeyEvent(e).keystate != KeyEvent.key_down: # discard key up & other rubbish | |
continue | |
if kc in Controls: | |
print 'Control:', kc | |
logging.info('Control: ' + kc) | |
try: | |
p.stdin.write(Controls[kc]) | |
except Exception as e: | |
print "Can't send control: ",e | |
print ' ... restarting player: ',Media[CurrentKC][0] | |
logging.info('Error sending controls, restarting player: ' + str(e)) | |
try: | |
pp.unregister(p.stdout.fileno()) | |
except Exception as e: | |
print 'Trouble unregistering stdout: ',e | |
logging.info('Cannot unregister stdout: ' + str(e)) | |
p.terminate() # p.kill() | |
p.wait() | |
time.sleep(2) | |
p = subp.Popen(Media[CurrentKC][-1], | |
stdin=subp.PIPE,stdout=subp.PIPE,stderr=subp.STDOUT) | |
pp.register(p.stdout.fileno(),select.POLLIN + select.POLLPRI + select.POLLERR) | |
print ' ... running' | |
logging.info(' ... running') | |
if kc in Media: | |
print 'Switching stream to ',Media[kc][0],' -> ',Media[kc][-1][-1] | |
logging.info('Switching stream: ' + Media[kc][0] + ' -> ' + Media[kc][-1][-1]) | |
CurrentKC = kc | |
print ' ... halting player' | |
try: | |
pp.unregister(p.stdout.fileno()) | |
except Exception as e: | |
print 'Trouble unregistering stdout: ',e | |
logging.info('Cannot unregister stdout: ' + str(e)) | |
try: | |
p.communicate(input='q') | |
except Exception as e: | |
print 'Perhaps mplayer already died? ',e | |
logging.info('Already died? ' + str(e)) | |
try: | |
p.terminate() # p.kill() | |
p.wait() | |
except Exception as e: | |
print 'Trouble with terminate or wait: ',e | |
logging.info('Trouble with terminate or wait: ' + str(e)) | |
time.sleep(2) | |
print ' ... restarting player: ',Media[CurrentKC][0] | |
p = subp.Popen(Media[CurrentKC][-1], | |
stdin=subp.PIPE,stdout=subp.PIPE,stderr=subp.STDOUT) | |
pp.register(p.stdout.fileno(),select.POLLIN + select.POLLPRI + select.POLLERR) | |
print ' ... running' | |
logging.info(' ... running') | |
print 'Out of loop!' | |
logging.shutdown() |
One thought on “Raspberry Pi Streaming Radio Player: Command Line Parsing”
Comments are closed.