Merge remote-tracking branch 'upstream/rlm2' into rlm2

Conflicts:
	ChangeLog
This commit is contained in:
Patrick Linstruth 2018-12-13 22:14:30 -08:00
commit 998c58b055
8 changed files with 178 additions and 152 deletions

View File

@ -18178,5 +18178,17 @@
* Documented the Python 3.4 dependency in 'INSTALL'. * Documented the Python 3.4 dependency in 'INSTALL'.
2018-12-12 Fred Gleason <fredg@paravelsystems.com> 2018-12-12 Fred Gleason <fredg@paravelsystems.com>
* Renamed rdrlmd(8) to rdpadd(8). * Renamed rdrlmd(8) to rdpadd(8).
2018-12-12 Patrick Linstruth <patrick@deltecent.com> 2018-12-13 Fred Gleason <fredg@paravelsystems.com>
* Added a 'make install' rule in 'apis/PyPAD/examples/' to install
PyPAD scripts in '${libdir}/rivendell/PyPAD/'.
* Fixed a typo in the pseudo-bangpath line in the example PyPAD
scripts.
2018-12-13 Fred Gleason <fredg@paravelsystems.com>
* Added a 'PyPAD.Receiver::setConfigFile()' method.
* Added a 'PyPAD.Update::shouldBeProcessed()' method.
* Updated the 'pypad_filewrite.py' script to use the
'PyPAD.Update::shouldBeProcessed()' method.
* Updated the 'pypad_udp.py' script to use the
'PyPAD.Update::shouldBeProcessed()' method.
2018-12-13 Patrick Linstruth <patrick@deltecent.com>
* Added a 'pypad_tunein.py' PyPAD script. * Added a 'pypad_tunein.py' PyPAD script.

View File

@ -18,6 +18,7 @@
# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
# #
import configparser
import datetime import datetime
import socket import socket
import json import json
@ -73,8 +74,9 @@ FIELD_EXTERNAL_ANNC_TYPE='externalAnncType'
PAD_TCP_PORT=34289 PAD_TCP_PORT=34289
class Update(object): class Update(object):
def __init__(self,pad_data): def __init__(self,pad_data,config):
self.__fields=pad_data; self.__fields=pad_data
self.__config=config
def __fromIso8601(self,string): def __fromIso8601(self,string):
try: try:
@ -222,6 +224,15 @@ class Update(object):
string=self.__replaceDatetimePattern(string,pattern[1]) string=self.__replaceDatetimePattern(string,pattern[1])
return string return string
def config(self):
"""
If a valid configuration file was set in
'PyPAD.Receiver::setConfigFile()', this will return a
parserconfig object created from it. If no configuration file
was specified, returns None.
"""
return self.__config
def dateTimeString(self): def dateTimeString(self):
""" """
Returns the date-time of the update in ISO 8601 format (string). Returns the date-time of the update in ISO 8601 format (string).
@ -237,16 +248,20 @@ class Update(object):
def escape(self,string,esc): def escape(self,string,esc):
""" """
Returns an 'escaped' version of the specified string. Returns an 'escaped' version of the specified string.
Take two arguments:
Takes two arguments:
string - The string to be processed. string - The string to be processed.
esc - The type of escaping to be applied. The following values esc - The type of escaping to be applied. The following values
are valid: are valid:
PyPAD.ESCAPE_JSON - Escaping for JSON string values PyPAD.ESCAPE_JSON - Escape for use in JSON string values
PyPAD.ESCAPE_NONE - No escaping applied (as per ECMA-404)
PyPAD.ESCAPE_URL - Escaping for using in URLs PyPAD.ESCAPE_NONE - String is passed through unchanged
PyPAD.ESCAPE_XML - Escaping for use in XML PyPAD.ESCAPE_URL - Escape for use in URLs
(as per RFC 2396)
PyPAD.ESCAPE_XML - Escape for use in XML
(as per XML-v1.0)
""" """
if(esc==0): if(esc==0):
return string return string
@ -348,16 +363,8 @@ class Update(object):
Guide for a list of recognized wildcards. Guide for a list of recognized wildcards.
esc - Character escaping to be applied to the PAD fields. esc - Character escaping to be applied to the PAD fields.
Must be one of the following: See the documentation for the 'escape()' method for valid
field values.
PyPAD.ESCAPE_NONE - No escaping
PyPAD.ESCAPE_XML - "XML" escaping: Escape reserved
characters as per XML-v1.0
PyPAD.ESCAPE_URL - "URL" escaping: Escape reserved
characters as per RFC 2396
Section 2.4
PyPAD.ESCAPE_JSON - "JSON" escaping: Escape reserved
characters as per ECMA-404.
""" """
string=self.__replaceWildcardPair('a','artist',string,esc) string=self.__replaceWildcardPair('a','artist',string,esc)
string=self.__replaceWildcardPair('b','label',string,esc) string=self.__replaceWildcardPair('b','label',string,esc)
@ -396,10 +403,12 @@ class Update(object):
def hasPadType(self,pad_type): def hasPadType(self,pad_type):
""" """
Indicates if this update includes the specified PAD type Indicates if this update includes the specified PAD type
Takes one argument: Takes one argument:
pad_type - The type of PAD value. Valid values are:
PyPAD.NOW - Now playing data pad_type - The type of PAD value. Valid values are:
PyPAD.NEXT - Next to play data PyPAD.NOW - Now playing data
PyPAD.NEXT - Next to play data
""" """
try: try:
return self.__fields['padUpdate'][pad_type]!=None return self.__fields['padUpdate'][pad_type]!=None
@ -409,10 +418,12 @@ class Update(object):
def startDateTime(self,pad_type): def startDateTime(self,pad_type):
""" """
Returns the start datetime of the specified PAD type Returns the start datetime of the specified PAD type
Takes one argument: Takes one argument:
pad_type - The type of PAD value. Valid values are:
PyPAD.NOW - Now playing data pad_type - The type of PAD value. Valid values are:
PyPAD.NEXT - Next to play data PyPAD.NOW - Now playing data
PyPAD.NEXT - Next to play data
""" """
try: try:
return self.__fromIso8601(self.__fields['padUpdate'][pad_type]['startDateTime']) return self.__fromIso8601(self.__fields['padUpdate'][pad_type]['startDateTime'])
@ -422,47 +433,46 @@ class Update(object):
def padField(self,pad_type,pad_field): def padField(self,pad_type,pad_field):
""" """
Returns the raw value of the specified PAD field. Returns the raw value of the specified PAD field.
Takes two arguments:
pad_type - The type of PAD value. Valid values are:
PyPAD.NOW - Now playing data
PyPAD.NEXT - Next to play data
pad_field - The specific field. Valid values are: Takes two arguments:
PyPAD.FIELD_AGENCY - The 'Agency' field (string)
PyPAD.FIELD_ALBUM - The 'Album' field (string) pad_type - The type of PAD value. Valid values are:
PyPAD.FIELD_ARTIST - The 'Artist' field (string) PyPAD.NOW - Now playing data
PyPAD.FIELD_CART_NUMBER - The 'Cart Number' field PyPAD.NEXT - Next to play data
(integer)
PyPAD.FIELD_CART_TYPE - 'The 'Cart Type' field pad_field - The specific field. Valid values are:
(string) PyPAD.FIELD_AGENCY - The 'Agency' field (string)
PyPAD.FIELD_CLIENT - The 'Client' field (string) PyPAD.FIELD_ALBUM - The 'Album' field (string)
PyPAD.FIELD_COMPOSER - The 'Composer' field (string) PyPAD.FIELD_ARTIST - The 'Artist' field (string)
PyPAD.FIELD_CONDUCTOR - The 'Conductor' field (string) PyPAD.FIELD_CART_NUMBER - The 'Cart Number' field
PyPAD.FIELD_CUT_NUMER - The 'Cut Number' field (integer)
(integer) PyPAD.FIELD_CART_TYPE - 'The 'Cart Type' field (string)
PyPAD.FIELD_DESCRIPTION - The 'Description' field PyPAD.FIELD_CLIENT - The 'Client' field (string)
(string) PyPAD.FIELD_COMPOSER - The 'Composer' field (string)
PyPAD.FIELD_EXTERNAL_ANNC_TYPE - The 'EXT_ANNC_TYPE' PyPAD.FIELD_CONDUCTOR - The 'Conductor' field (string)
field (string) PyPAD.FIELD_CUT_NUMER - The 'Cut Number' field (integer)
PyPAD.FIELD_EXTERNAL_DATA - The 'EXT_DATA' field PyPAD.FIELD_DESCRIPTION - The 'Description' field
(string) (string)
PyPAD.FIELD_EXTERNAL_EVENT_ID - The 'EXT_EVENT_ID' PyPAD.FIELD_EXTERNAL_ANNC_TYPE - The 'EXT_ANNC_TYPE'
field (string) field (string)
PyPAD.FIELD_GROUP_NAME - The 'GROUP_NAME' field PyPAD.FIELD_EXTERNAL_DATA - The 'EXT_DATA' field
(string) (string)
PyPAD.FIELD_ISRC - The 'ISRC' field (string) PyPAD.FIELD_EXTERNAL_EVENT_ID - The 'EXT_EVENT_ID'
PyPAD.FIELD_ISCI - The 'ISCI' field (string) field (string)
PyPAD.FIELD_LABEL - The 'Label' field (string) PyPAD.FIELD_GROUP_NAME - The 'GROUP_NAME' field (string)
PyPAD.FIELD_LENGTH - The 'Length' field (integer) PyPAD.FIELD_ISRC - The 'ISRC' field (string)
PyPAD.FIELD_OUTCUE - The 'Outcue' field (string) PyPAD.FIELD_ISCI - The 'ISCI' field (string)
PyPAD.FIELD_PUBLISHER - The 'Publisher' field (string) PyPAD.FIELD_LABEL - The 'Label' field (string)
PyPAD.FIELD_SONG_ID - The 'Song ID' field (string) PyPAD.FIELD_LENGTH - The 'Length' field (integer)
PyPAD.FIELD_START_DATETIME - The 'Start DateTime field PyPAD.FIELD_OUTCUE - The 'Outcue' field (string)
(string) PyPAD.FIELD_PUBLISHER - The 'Publisher' field (string)
PyPAD.FIELD_TITLE - The 'Title' field (string) PyPAD.FIELD_SONG_ID - The 'Song ID' field (string)
PyPAD.FIELD_USER_DEFINED - 'The 'User Defined' field PyPAD.FIELD_START_DATETIME - The 'Start DateTime field
(string) (string)
PyPAD.FIELD_YEAR - The 'Year' field (integer) PyPAD.FIELD_TITLE - The 'Title' field (string)
PyPAD.FIELD_USER_DEFINED - 'The 'User Defined' field
(string)
PyPAD.FIELD_YEAR - The 'Year' field (integer)
""" """
return self.__fields['padUpdate'][pad_type][pad_field] return self.__fields['padUpdate'][pad_type][pad_field]
@ -470,6 +480,7 @@ class Update(object):
""" """
Returns a string with any Rivendell Filepath wildcards resolved Returns a string with any Rivendell Filepath wildcards resolved
(See Appdendix C of the Rivendell Operations Guide for a list). (See Appdendix C of the Rivendell Operations Guide for a list).
Takes two arguments: Takes two arguments:
string - The string to resolve. string - The string to resolve.
@ -656,11 +667,50 @@ class Update(object):
i=i+1 i=i+1
return ret return ret
def shouldBeProcessed(self,section):
"""
Reads the Log Selection and SendNullUpdate parameters of the
config and returns a boolean to indicate whether or not this
update should be processed (boolean).
Takes one argument:
section - The '[<section>]' of the INI configuration from which
to take the parameters.
"""
try:
if self.__config.get(section,'ProcessNullUpdates')=='0':
return True
if self.__config.get(section,'ProcessNullUpdates')=='1':
return self.hasPadType(PyPAD.TYPE_NOW)
if self.__config.get(section,'ProcessNullUpdates')=='2':
return self.hasPadType(PyPAD.TYPE_NEXT)
if self.__config.get(section,'ProcessNullUpdates')=='3':
return self.hasPadType(PyPAD.TYPE_NOW) and self.hasPadType(PyPAD.TYPE_NEXT)
except configparser.NoOptionError:
return True
log_dict={1: 'MasterLog',2: 'Aux1Log',3: 'Aux2Log',
101: 'VLog101',102: 'VLog102',103: 'VLog103',104: 'VLog104',
105: 'VLog105',106: 'VLog106',107: 'VLog107',108: 'VLog108',
109: 'VLog109',110: 'VLog110',111: 'VLog111',112: 'VLog112',
113: 'VLog113',114: 'VLog114',115: 'VLog115',116: 'VLog116',
117: 'VLog117',118: 'VLog118',119: 'VLog119',120: 'VLog120'}
if self.__config.get(section,log_dict[self.machine()]).lower()=='yes':
return True
if self.__config.get(section,log_dict[self.machine()]).lower()=='no':
return False
if self.__config.get(section,log_dict[self.machine()]).lower()=='onair':
return self.onairFlag()
class Receiver(object): class Receiver(object):
def __init__(self): def __init__(self):
self.__callback=None self.__callback=None
self.__config_parser=None
def __PyPAD_Process(self,pad): def __PyPAD_Process(self,pad):
self.__callback(pad) self.__callback(pad)
@ -671,11 +721,24 @@ class Receiver(object):
""" """
self.__callback=cb self.__callback=cb
def setConfigFile(self,filename):
"""
Set a file whence to get configuration information. If set,
the 'PyPAD.Update::config()' method will return a parserconfig
object created from the specified file. The file must be in INI
format.
"""
fp=open(filename)
self.__config_parser=configparser.ConfigParser(interpolation=None)
self.__config_parser.readfp(fp)
fp.close()
def start(self,hostname,port): def start(self,hostname,port):
""" """
Connect to a Rivendell system and begin processing PAD events. Connect to a Rivendell system and begin processing PAD events.
Once started, a PyPAD object can be interacted with Once started, a PyPAD object can be interacted with
only within one of its callback methods. only within its callback method.
Takes the following arguments: Takes the following arguments:
hostname - The hostname or IP address of the Rivendell system. hostname - The hostname or IP address of the Rivendell system.
@ -695,7 +758,7 @@ class Receiver(object):
if c[0]==10: if c[0]==10:
msg+=line.decode('utf-8') msg+=line.decode('utf-8')
if line.decode('utf-8')=="\r\n": if line.decode('utf-8')=="\r\n":
self.__PyPAD_Process(Update(json.loads(msg))) self.__PyPAD_Process(Update(json.loads(msg),self.__config_parser))
msg="" msg=""
line=bytes() line=bytes()

View File

@ -20,6 +20,17 @@
## ##
## Use automake to process this into a Makefile.in ## Use automake to process this into a Makefile.in
install-exec-am:
mkdir -p $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD
../../../helpers/install_python.sh now_and_next.py $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/now_and_next.py
../../../helpers/install_python.sh pypad_filewrite.py $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_filewrite.py
../../../helpers/install_python.sh pypad_udp.py $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_udp.py
uninstall-local:
rm -f $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/now_and_next.py
rm -f $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_filewrite.py
rm -f $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_udp.py
EXTRA_DIST = now_and_next.py\ EXTRA_DIST = now_and_next.py\
pypad_filewrite.py\ pypad_filewrite.py\
pypad_udp.py pypad_udp.py

View File

@ -1,4 +1,4 @@
#%PYTHON_BANGPATH% #!%PYTHON_BANGPATH%
# now_and_next.py # now_and_next.py
# #
@ -36,8 +36,6 @@ import PyPAD
# #
def ProcessPad(update): def ProcessPad(update):
print print
print('Filepath: '+update.resolveFilepath('string %$a',update.dateTime()))
if update.hasPadType(PyPAD.TYPE_NOW): if update.hasPadType(PyPAD.TYPE_NOW):
print("Log %03d NOW: " % update.machine()+update.resolvePadFields("%a - %t",PyPAD.ESCAPE_NONE)) print("Log %03d NOW: " % update.machine()+update.resolvePadFields("%a - %t",PyPAD.ESCAPE_NONE))
else: else:
@ -62,4 +60,4 @@ rcvr.setCallback(ProcessPad)
# the target Rivendell system. Once started, all further processing can only # the target Rivendell system. Once started, all further processing can only
# be done in the callback method! # be done in the callback method!
# #
rcvr.start("localhost",PyPAD.PAD_TCP_PORT) rcvr.start('localhost',PyPAD.PAD_TCP_PORT)

View File

@ -1,4 +1,4 @@
#%PYTHON_BANGPATH% #!%PYTHON_BANGPATH%
# pypad_filewrite.py # pypad_filewrite.py
# #
@ -27,44 +27,18 @@ import PyPAD
def eprint(*args,**kwargs): def eprint(*args,**kwargs):
print(*args,file=sys.stderr,**kwargs) print(*args,file=sys.stderr,**kwargs)
def processUpdate(update,section):
try:
if config.get(section,'ProcessNullUpdates')=='0':
return True
if config.get(section,'ProcessNullUpdates')=='1':
return update.hasPadType(PyPAD.TYPE_NOW)
if config.get(section,'ProcessNullUpdates')=='2':
return update.hasPadType(PyPAD.TYPE_NEXT)
if config.get(section,'ProcessNullUpdates')=='3':
return update.hasPadType(PyPAD.TYPE_NOW) and update.hasPadType(PyPAD.TYPE_NEXT)
except configparser.NoOptionError:
return True
log_dict={1: 'MasterLog',2: 'Aux1Log',3: 'Aux2Log',
101: 'VLog101',102: 'VLog102',103: 'VLog103',104: 'VLog104',
105: 'VLog105',106: 'VLog106',107: 'VLog107',108: 'VLog108',
109: 'VLog109',110: 'VLog110',111: 'VLog111',112: 'VLog112',
113: 'VLog113',114: 'VLog114',115: 'VLog115',116: 'VLog116',
117: 'VLog117',118: 'VLog118',119: 'VLog119',120: 'VLog120'}
if config.get(section,log_dict[update.machine()]).lower()=='yes':
return True
if config.get(section,log_dict[update.machine()]).lower()=='no':
return False
if config.get(section,log_dict[update.machine()]).lower()=='onair':
return update.onairFlag()
def ProcessPad(update): def ProcessPad(update):
n=1 n=1
try: try:
while(True): while(True):
section='File'+str(n) section='File'+str(n)
if processUpdate(update,section): if update.shouldBeProcessed(section):
fmtstr=config.get(section,'FormatString') fmtstr=update.config().get(section,'FormatString')
mode='w' mode='w'
if config.get(section,'Append')=='1': if update.config().get(section,'Append')=='1':
mode='a' mode='a'
f=open(update.resolveFilepath(config.get(section,'Filename'),update.dateTime()),mode) f=open(update.resolveFilepath(update.config().get(section,'Filename'),update.dateTime()),mode)
f.write(update.resolvePadFields(fmtstr,int(config.get(section,'Encoding')))) f.write(update.resolvePadFields(fmtstr,int(update.config().get(section,'Encoding'))))
f.close() f.close()
n=n+1 n=n+1
@ -72,17 +46,13 @@ def ProcessPad(update):
return return
# #
# Read Configuration # 'Main' function
# #
if len(sys.argv)>=2: rcvr=PyPAD.Receiver()
fp=open(sys.argv[1]) try:
config=configparser.ConfigParser(interpolation=None) rcvr.setConfigFile(sys.argv[1])
config.readfp(fp) except IndexError:
fp.close()
else:
eprint('pypad_filewrite.py: you must specify a configuration file') eprint('pypad_filewrite.py: you must specify a configuration file')
sys.exit(1) sys.exit(1)
rcvr=PyPAD.Receiver()
rcvr.setCallback(ProcessPad) rcvr.setCallback(ProcessPad)
rcvr.start('localhost',PyPAD.PAD_TCP_PORT) rcvr.start('localhost',PyPAD.PAD_TCP_PORT)

View File

@ -1,4 +1,4 @@
#%PYTHON_BANGPATH% #!%PYTHON_BANGPATH%
# pypad_udp.py # pypad_udp.py
# #
@ -28,59 +28,31 @@ import PyPAD
def eprint(*args,**kwargs): def eprint(*args,**kwargs):
print(*args,file=sys.stderr,**kwargs) print(*args,file=sys.stderr,**kwargs)
def processUpdate(update,section):
if config.get(section,'ProcessNullUpdates')=='0':
return True
if config.get(section,'ProcessNullUpdates')=='1':
return update.hasPadType(PyPAD.TYPE_NOW)
if config.get(section,'ProcessNullUpdates')=='2':
return update.hasPadType(PyPAD.TYPE_NEXT)
if config.get(section,'ProcessNullUpdates')=='3':
return update.hasPadType(PyPAD.TYPE_NOW) and update.hasPadType(PyPAD.TYPE_NEXT)
log_dict={1: 'MasterLog',2: 'Aux1Log',3: 'Aux2Log',
101: 'VLog101',102: 'VLog102',103: 'VLog103',104: 'VLog104',
105: 'VLog105',106: 'VLog106',107: 'VLog107',108: 'VLog108',
109: 'VLog109',110: 'VLog110',111: 'VLog111',112: 'VLog112',
113: 'VLog113',114: 'VLog114',115: 'VLog115',116: 'VLog116',
117: 'VLog117',118: 'VLog118',119: 'VLog119',120: 'VLog120'}
if config.get(section,log_dict[update.machine()]).lower()=='yes':
return True
if config.get(section,log_dict[update.machine()]).lower()=='no':
return False
if config.get(section,log_dict[update.machine()]).lower()=='onair':
return update.onairFlag()
def ProcessPad(update): def ProcessPad(update):
n=1 n=1
while(True): while(True):
section='Udp'+str(n) section='Udp'+str(n)
try: try:
if processUpdate(update,section): if update.shouldBeProcessed(section):
fmtstr=config.get(section,'FormatString') fmtstr=update.config().get(section,'FormatString')
send_sock.sendto(update.resolvePadFields(fmtstr,int(config.get(section,'Encoding'))).encode('utf-8'), send_sock.sendto(update.resolvePadFields(fmtstr,int(update.config().get(section,'Encoding'))).encode('utf-8'),
(config.get(section,'IpAddress'),int(config.get(section,'UdpPort')))) (update.config().get(section,'IpAddress'),int(update.config().get(section,'UdpPort'))))
n=n+1 n=n+1
except configparser.NoSectionError: except configparser.NoSectionError:
return return
# #
# Read Configuration # 'Main' function
#
if len(sys.argv)>=2:
fp=open(sys.argv[1])
config=configparser.ConfigParser(interpolation=None)
config.readfp(fp)
fp.close()
else:
eprint('pypad_udp.py: you must specify a configuration file')
sys.exit(1)
# #
# Create Send Socket # Create Send Socket
# #
send_sock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) send_sock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
rcvr=PyPAD.Receiver() rcvr=PyPAD.Receiver()
try:
rcvr.setConfigFile(sys.argv[1])
except IndexError:
eprint('pypad_udp.py: you must specify a configuration file')
sys.exit(1)
rcvr.setCallback(ProcessPad) rcvr.setCallback(ProcessPad)
rcvr.start("localhost",PyPAD.PAD_TCP_PORT) rcvr.start("localhost",PyPAD.PAD_TCP_PORT)

View File

@ -1,4 +1,4 @@
#%PYTHON_BANGPATH% #!%PYTHON_BANGPATH%
# filepath_test.py # filepath_test.py
# #
@ -75,4 +75,4 @@ rcvr.setCallback(ProcessPad)
# the target Rivendell system. Once started, all further processing can only # the target Rivendell system. Once started, all further processing can only
# be done in the callback method! # be done in the callback method!
# #
rcvr.start(localhost',PyPAD.PAD_TCP_PORT) rcvr.start('localhost',PyPAD.PAD_TCP_PORT)

View File

@ -1,4 +1,4 @@
#%PYTHON_BANGPATH% #!%PYTHON_BANGPATH%
# pad_test.py # pad_test.py
# #