|
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'%5D%5D, |
|
'KEY_KP8' : ['Jazz',False,['mplayer','–quiet','-playlist','http://stream2138.init7.net/listen.pls'%5D%5D, |
|
'KEY_KP9' : ['WMHT',False,['mplayer','–quiet','http://wmht.streamguys1.com/wmht1'%5D%5D, |
|
'KEY_KP4' : ['Classic 1000',True,['mplayer','–quiet','-playlist','http://listen.radionomy.com/1000classicalhits.m3u'%5D%5D, |
|
'KEY_KP5' : ['DCNY 911',False,['mplayer','–quiet','-playlist','http://www.broadcastify.com/scripts/playlists/1/12305/-5857889408.m3u'%5D%5D, |
|
'KEY_KP6' : ['WAMC',False,['mplayer','–quiet','http://pubint.ic.llnwd.net/stream/pubint_wamc'%5D%5D, |
|
'KEY_KP1' : ['60s',True,['mplayer','–quiet','-playlist','http://listen.radionomy.com/all60sallthetime-keepfreemusiccom.m3u'%5D%5D, |
|
'KEY_KP2' : ['50-70s',True,['mplayer','–quiet','-playlist','http://listen.radionomy.com/golden-50-70s-hits.m3u'%5D%5D, |
|
'KEY_KP3' : ['Soft Rock',True,['mplayer','–quiet','-playlist','http://listen.radionomy.com/softrockradio.m3u'%5D%5D, |
|
'KEY_KP0' : ['Zen',True,['mplayer','–quiet','-playlist','http://listen.radionomy.com/zen-for-you.m3u'%5D%5D, |
|
'KEY_KPDOT' : ['Ambient',False,['mplayer','–quiet','http://185.32.125.42:7331/maschinengeist.org.mp3'%5D%5D |
|
} |
|
|
|
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() |