diff --git a/ChangeLog b/ChangeLog index a6585410..d769d691 100644 --- a/ChangeLog +++ b/ChangeLog @@ -18292,3 +18292,11 @@ * Added a 'pypad_spottrap.py' PyPAD script. * Removed the 'rlm_spottrap' RLM. * Removed the 'rlm_test' RLM. +2019-01-07 Patrick Linstruth + * Added a 'pypad_tunein.py' PyPAD script. + * Added a 'pypad_icecast2.py' PyPAD script. +2019-01-07 Fred Gleason + * Fixed a bug in 'pypad_icecast2.py' that caused double-escaping of + metadata fields. + * Fixed a bug in 'pypad_tunein.py' that caused double-escaping of + metadata fields. diff --git a/apis/PyPAD/scripts/Makefile.am b/apis/PyPAD/scripts/Makefile.am index f92ddc63..91a8e64e 100644 --- a/apis/PyPAD/scripts/Makefile.am +++ b/apis/PyPAD/scripts/Makefile.am @@ -26,6 +26,8 @@ install-exec-am: cp pypad_ando.exemplar $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_ando.exemplar ../../../helpers/install_python.sh pypad_filewrite.py $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_filewrite.py cp pypad_filewrite.exemplar $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_filewrite.exemplar + ../../../helpers/install_python.sh pypad_icecast2.py $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_icecast2.py + cp pypad_icecast2.exemplar $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_icecast2.exemplar ../../../helpers/install_python.sh pypad_inno713.py $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_inno713.py cp pypad_inno713.exemplar $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_inno713.exemplar ../../../helpers/install_python.sh pypad_live365.py $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_live365.py @@ -38,6 +40,8 @@ install-exec-am: cp pypad_spinitron.exemplar $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_spinitron.exemplar ../../../helpers/install_python.sh pypad_spottrap.py $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_spottrap.py cp pypad_spottrap.exemplar $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_spottrap.exemplar + ../../../helpers/install_python.sh pypad_tunein.py $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_tunein.py + cp pypad_tunein.exemplar $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_tunein.exemplar ../../../helpers/install_python.sh pypad_udp.py $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_udp.py cp pypad_udp.exemplar $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_udp.exemplar ../../../helpers/install_python.sh pypad_urlwrite.py $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_urlwrite.py @@ -54,6 +58,8 @@ uninstall-local: rm -f $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_ando.py rm -f $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_filewrite.exemplar rm -f $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_filewrite.py + rm -f $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_icecast2.exemplar + rm -f $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_icecast2.py rm -f $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_inno713.exemplar rm -f $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_inno713.py rm -f $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_live365.exemplar @@ -66,6 +72,8 @@ uninstall-local: rm -f $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_spinitron.py rm -f $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_spottrap.exemplar rm -f $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_spottrap.py + rm -f $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_tunein.exemplar + rm -f $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_tunein.py rm -f $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_udp.exemplar rm -f $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_udp.py rm -f $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_urlwrite.exemplar @@ -81,6 +89,8 @@ EXTRA_DIST = pypad_ando.exemplar\ pypad_ando.py\ pypad_filewrite.exemplar\ pypad_filewrite.py\ + pypad_icecast2.exemplar\ + pypad_icecast2.py\ pypad_inno713.exemplar\ pypad_inno713.py\ pypad_liqcomp.exemplar\ @@ -91,6 +101,8 @@ EXTRA_DIST = pypad_ando.exemplar\ pypad_spinitron.py\ pypad_spottrap.exemplar\ pypad_spottrap.py\ + pypad_tunein.exemplar\ + pypad_tunein.py\ pypad_udp.exemplar\ pypad_udp.py\ pypad_urlwrite.exemplar\ diff --git a/apis/PyPAD/scripts/pypad_icecast2.exemplar b/apis/PyPAD/scripts/pypad_icecast2.exemplar new file mode 100644 index 00000000..d9fa656f --- /dev/null +++ b/apis/PyPAD/scripts/pypad_icecast2.exemplar @@ -0,0 +1,106 @@ +; This is the sample configuration for the 'pypad_icecast2.py' PyPAD script +; for Rivendell, which can be used to update the metadata on an Icecast2 +; mountpoint using Now & Next data. +; + +; Section Header +; +; One section per Icecast2 mountpoint is configured, starting with +; 'Icecast1' and working up consecutively +[Icecast1] + +; User Name +; +; The username of the Icecast2 account to which to send updates. +Username=source + +; Password +; +; The password of the Icecast2 account to which to send updates. +Password=hackme + +; Host Name +; +; The fully-qualified domain name or IP address of the Icecast2 server +Hostname=icecast.example.com + +; Host Port +; +; The TCP port number of the Icecast2 server +Tcpport=8000 + +; Mountpoint +; +; The Icecast2 mountpoint +Mountpoint=/audio.mp3 + +; Format String. The metadata to be sent each time RDAirPlay changes +; play state, including any wildcards as placeholders for metadata values. +; +; The list of available wildcards can be found in the 'metadata_wildcards.txt' +; file in the Rivendell documentation directory. +; +FormatString=%a - %t + +; Log Selection +; +; Set the status for each log to 'Yes', 'No' or 'Onair' to indicate whether +; state changes on that log should be output to this account. If set +; to 'Onair', then output will be generated only if RDAirPlays OnAir flag +; is active. +MasterLog=Yes +Aux1Log=Yes +Aux2Log=Yes +VLog101=No +VLog102=No +VLog103=No +VLog104=No +VLog105=No +VLog106=No +VLog107=No +VLog108=No +VLog109=No +VLog110=No +VLog111=No +VLog112=No +VLog113=No +VLog114=No +VLog115=No +VLog116=No +VLog117=No +VLog118=No +VLog119=No +VLog120=No + + +; Additional Icecast2 mountpoints can be configured by adding new sections... +;[Icecast2] +;Username=source +;Password=letmein +;Hostname=anotherone.example.com +;Tcpport=80 +;Mountpoint=moreaudio.mp3 +;FormatString=%t by %a +;MasterLog=Yes +;Aux1Log=No +;Aux2Log=Onair +;VLog101=No +;VLog102=No +;VLog103=No +;VLog104=No +;VLog105=No +;VLog106=No +;VLog107=No +;VLog108=No +;VLog109=No +;VLog110=No +;VLog111=No +;VLog112=No +;VLog113=No +;VLog114=No +;VLog115=No +;VLog116=No +;VLog117=No +;VLog118=No +;VLog119=No +;VLog120=No diff --git a/apis/PyPAD/scripts/pypad_icecast2.py b/apis/PyPAD/scripts/pypad_icecast2.py new file mode 100755 index 00000000..16b65cda --- /dev/null +++ b/apis/PyPAD/scripts/pypad_icecast2.py @@ -0,0 +1,98 @@ +#!%PYTHON_BANGPATH% + +# pypad_icecast2.py +# +# Send PAD updates to Icecast2 mountpoint +# +# (C) Copyright 2018 Fred Gleason +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public +# License along with this program; if not, write to the Free Software +# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +# + +import os +import sys +import socket +import requests +from requests.auth import HTTPBasicAuth +import xml.etree.ElementTree as ET +import syslog +import PyPAD +import configparser + +def eprint(*args,**kwargs): + print(pypad_name+': ',file=sys.stderr,end='') + print(*args,file=sys.stderr) + syslog.syslog(syslog.LOG_ERR,*args) + +def iprint(*args,**kwargs): + print(pypad_name+': ',file=sys.stdout,end='') + print(*args,file=sys.stdout) + syslog.syslog(syslog.LOG_INFO,*args) + +def isTrue(string): + l=['Yes','On','True','1'] + return string.lower() in map(str.lower,l) + +def ProcessPad(update): + if update.hasPadType(PyPAD.TYPE_NOW): + n=1 + while(True): + section='Icecast'+str(n) + try: + values={} + values['mount']=update.config().get(section,'Mountpoint') + values['song']=update.resolvePadFields(update.config().get(section,'FormatString'),PyPAD.ESCAPE_NONE) + values['mode']='updinfo' + iprint('Updating '+update.config().get(section,'Hostname')+': song='+values['song']) + url="http://%s:%s/admin/metadata" % (update.config().get(section,'Hostname'),update.config().get(section,'Tcpport')) + try: + response=requests.get(url,auth=HTTPBasicAuth(update.config().get(section,'Username'),update.config().get(section,'Password')),params=values) + response.raise_for_status() + except requests.exceptions.RequestException as e: + eprint(str(e)) + n=n+1 + except configparser.NoSectionError: + if(n==1): + eprint('No icecast config found') + return + +# +# Program Name +# +pypad_name=os.path.basename(__file__) + +# +# Open Syslog +# +syslog.openlog(pypad_name,logoption=syslog.LOG_PID,facility=syslog.LOG_DAEMON) + + +# +# Create Send Socket +# +send_sock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) + +# +# Start Receiver +# +rcvr=PyPAD.Receiver() +try: + rcvr.setConfigFile(sys.argv[3]) +except IndexError: + eprint('You must specify a configuration file') + sys.exit(1) +rcvr.setCallback(ProcessPad) +iprint('Started') +rcvr.start(sys.argv[1],int(sys.argv[2])) +iprint('Stopped') diff --git a/apis/PyPAD/scripts/pypad_tunein.exemplar b/apis/PyPAD/scripts/pypad_tunein.exemplar new file mode 100644 index 00000000..7586d03f --- /dev/null +++ b/apis/PyPAD/scripts/pypad_tunein.exemplar @@ -0,0 +1,105 @@ +; This is the sample configuration for the 'pypad_tunein.py' PyPAD script for +; Rivendell, which can be used to update the metadata at TuneIn server +; using Now & Next data. +; + +; Section Header +; +; One section per TuneIn station is configured, starting with +; 'Station1' and working up consecutively +[Station1] + + +; StationID +; +; The Station ID of the TuneIn station to which to send updates. +StationID=s123456 + + +; PartnerID +; +; The Partner ID of the TuneIn station to which to send updates. +PartnerID=changeme + + +; PartnerKey +; +; The Partner Key of the TuneIn station to which to send updates. +PartnerKey=changeme + + +; Metadata String. The metadata fields to be sent each time RDAirPlay changes +; play state, including any wildcards as placeholders for metadata values. +; +; The list of available wildcards can be found in the 'metadata_wildcards.txt' +; file in the Rivendell documentation directory. +; +TitleString=%t +ArtistString=%a +AlbumString=%l + + +; Log Selection +; +; Set the status for each log to 'Yes', 'No' or 'Onair' to indicate whether +; state changes on that log should be output to this station. If set +; to 'Onair', then output will be generated only if RDAirPlays OnAir flag +; is active. +MasterLog=Yes +Aux1Log=Yes +Aux2Log=Yes +VLog101=No +VLog102=No +VLog103=No +VLog104=No +VLog105=No +VLog106=No +VLog107=No +VLog108=No +VLog109=No +VLog110=No +VLog111=No +VLog112=No +VLog113=No +VLog114=No +VLog115=No +VLog116=No +VLog117=No +VLog118=No +VLog119=No +VLog120=No + + +; Additional TuneIn stations can be configured by adding new +; sections... +; +;[Station2] +;StationID=s654321 +;PartnerID=changeme +;PartnerKey=changeme +;TitleString=%t +;ArtistString=%a +;AlbumString=%l +;MasterLog=No +;Aux1Log=Yes +;Aux2Log=No +;VLog101=No +;VLog102=No +;VLog103=No +;VLog104=No +;VLog105=No +;VLog106=No +;VLog107=No +;VLog108=No +;VLog109=No +;VLog110=No +;VLog111=No +;VLog112=No +;VLog113=No +;VLog114=No +;VLog115=No +;VLog116=No +;VLog117=No +;VLog118=No +;VLog119=No +;VLog120=No diff --git a/apis/PyPAD/scripts/pypad_tunein.py b/apis/PyPAD/scripts/pypad_tunein.py new file mode 100755 index 00000000..7f9734bf --- /dev/null +++ b/apis/PyPAD/scripts/pypad_tunein.py @@ -0,0 +1,104 @@ +#!%PYTHON_BANGPATH% + +# pypad_tunein.py +# +# Send PAD updates to TuneIn +# +# (C) Copyright 2018 Fred Gleason +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public +# License along with this program; if not, write to the Free Software +# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +# + +import os +import sys +import socket +import requests +import xml.etree.ElementTree as ET +import syslog +import PyPAD +import configparser + +def eprint(*args,**kwargs): + print(pypad_name+': ',file=sys.stderr,end='') + print(*args,file=sys.stderr) + syslog.syslog(syslog.LOG_ERR,*args) + +def iprint(*args,**kwargs): + print(pypad_name+': ',file=sys.stdout,end='') + print(*args,file=sys.stdout) + syslog.syslog(syslog.LOG_INFO,*args) + +def isTrue(string): + l=['Yes','On','True','1'] + return string.lower() in map(str.lower,l) + +def ProcessPad(update): + if update.hasPadType(PyPAD.TYPE_NOW): + n=1 + while(True): + section='Station'+str(n) + try: + values={} + values['id']=update.config().get(section,'StationID') + values['partnerId']=update.config().get(section,'PartnerID') + values['partnerKey']=update.config().get(section,'PartnerKey') + values['title']=update.resolvePadFields(update.config().get(section,'TitleString'),PyPAD.ESCAPE_NONE) + values['artist']=update.resolvePadFields(update.config().get(section,'ArtistString'),PyPAD.ESCAPE_NONE) + values['album']=update.resolvePadFields(update.config().get(section,'AlbumString'),PyPAD.ESCAPE_NONE) + iprint('Updating TuneIn: artist='+values['artist']+' title='+values['title']+' album='+values['album']) + try: + response=requests.get('http://air.radiotime.com/Playing.ashx',params=values) + response.raise_for_status() + except requests.exceptions.RequestException as e: + eprint(str(e)) + else: + xml=ET.fromstring(response.text) + status=xml.find('./head/status') + if(status.text!='200'): + eprint('Update Failed: '+xml.find('./head/fault').text) + n=n+1 + except configparser.NoSectionError: + if(n==1): + eprint('No station config found') + return + +# +# Program Name +# +pypad_name=os.path.basename(__file__) + +# +# Open Syslog +# +syslog.openlog(pypad_name,logoption=syslog.LOG_PID,facility=syslog.LOG_DAEMON) + + +# +# Create Send Socket +# +send_sock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) + +# +# Start Receiver +# +rcvr=PyPAD.Receiver() +try: + rcvr.setConfigFile(sys.argv[3]) +except IndexError: + eprint('You must specify a configuration file') + sys.exit(1) +rcvr.setCallback(ProcessPad) +iprint('Started') +rcvr.start(sys.argv[1],int(sys.argv[2])) +iprint('Stopped')