diff --git a/ChangeLog b/ChangeLog index 35669ee8..b5645db0 100644 --- a/ChangeLog +++ b/ChangeLog @@ -18162,3 +18162,13 @@ 2018-12-10 Fred Gleason * Fixed a bug in the 'pypad_udp.py' script that threw an exception when processing multi-byte UTF-8 characters. +2018-12-11 Fred Gleason + * Renamed 'apis/PyPAD/examples/pypad_test.py' to + 'apisPyPAD/tests/pad_test.py'. + * Added a 'hostName' field to the JSON PAD 'padUpdate' object. + * Added a 'PyPAD.Update::hostName()' method. + * Added a 'shortHostName' field to the JSON PAD 'padUpdate' object. + * Added a 'PyPAD.Update::shortHostName()' method. + * Added a 'PyPAD.Update::resolveFilepath()' method. + * Added 'apis/PyPAD/tests/filepath_test.py. + * Added 'api/PyPAD/examples/pypad_filewrite.py'. diff --git a/apis/PyPAD/Makefile.am b/apis/PyPAD/Makefile.am index a2c95b53..4e5f15c1 100644 --- a/apis/PyPAD/Makefile.am +++ b/apis/PyPAD/Makefile.am @@ -21,7 +21,8 @@ ## Use automake to process this into a Makefile.in SUBDIRS = api\ - examples + examples\ + tests CLEANFILES = *~\ *.idb\ diff --git a/apis/PyPAD/api/PyPAD.py b/apis/PyPAD/api/PyPAD.py index df3c134f..00e8c175 100644 --- a/apis/PyPAD/api/PyPAD.py +++ b/apis/PyPAD/api/PyPAD.py @@ -260,6 +260,20 @@ class Update(object): return self.__escapeJson(string) raise ValueError('invalid esc value') + def hostName(self): + """ + Returns the host name of the machine whence this PAD update + originated (string). + """ + return self.__fields['padUpdate']['hostName'] + + def shortHostName(self): + """ + Returns the short host name of the machine whence this PAD update + originated (string). + """ + return self.__fields['padUpdate']['shortHostName'] + def machine(self): """ Returns the log machine number to which this update pertains @@ -454,8 +468,197 @@ class Update(object): """ return self.__fields['padUpdate'][pad_type][pad_field] + def resolveFilepath(self,string,dt): + """ + Returns a string with any Rivendell Filepath wildcards resolved + (See Appdendix C of the Rivendell Operations Guide for a list). + Takes two arguments: + + string - The string to resolve. + dt - A Python 'datetime' object to use for the resolution. + """ + ret='' + upper_case=False + initial_case=False + offset=0 + i=0 + while i12: + hour=hour-12 + if hour==0: + hour=12 + field='%2d' % hour + found=True + + if string[i]=='J': # Hour, unpadded, 12 hour + hour=dt.hour + if hour>12: + hour=hour-12 + if hour==0: + hour=12 + field=str(hour) + found=True + + if string[i]=='j': # Day of year + field=dt.strftime('%j') + found=True + + if string[i]=='k': # Hour, space padded, 24 hour + field=dt.strftime('%k') + found=True + + if string[i]=='M': # Minute, zero padded + field=dt.strftime('%M') + found=True + + if string[i]=='m': # Month (01 - 12) + field=dt.strftime('%m') + found=True + + if string[i]=='p': # AM/PM string + field=dt.strftime('%p') + found=True + + if string[i]=='r': # Rivendell host name + field=self.hostName() + found=True + + if string[i]=='R': # Rivendell short host name + field=self.shortHostName() + found=True + + if string[i]=='S': # Second (SS) + field=dt.strftime('%S') + found=True + + if string[i]=='s': # Rivendell service name + if self.hasService(): + field=self.serviceName() + else: + field='' + found=True + + if string[i]=='u': # Day of week (numeric, 1..7, 1=Monday) + field=dt.strftime('%u') + found=True + + if (string[i]=='V') or (string[i]=='W'): # Week # (as per ISO 8601) + field=dt.strftime('%V') + found=True + + if string[i]=='w': # Day of week (numeric, 0..6, 0=Sunday) + field=dt.strftime('%w') + found=True + + if string[i]=='y': # Year (yy) + field=dt.strftime('%y') + found=True + + if string[i]=='Y': # Year (yyyy) + field=dt.strftime('%Y') + found=True + + if string[i]=='%': + field='%' + found=True + + if not found: # No recognized wildcard, rollback! + i=-offset + field=string[i] + + if upper_case: + field=field.upper(); + if initial_case: + field=field[0].upper()+field[1::] + ret+=field + upper_case=False + initial_case=False + i=i+1 + + return ret + class Receiver(object): def __init__(self): diff --git a/apis/PyPAD/examples/Makefile.am b/apis/PyPAD/examples/Makefile.am index 2d61a9c9..b4275c20 100644 --- a/apis/PyPAD/examples/Makefile.am +++ b/apis/PyPAD/examples/Makefile.am @@ -21,7 +21,7 @@ ## Use automake to process this into a Makefile.in EXTRA_DIST = now_and_next.py\ - pypad_test.py\ + pypad_filewrite.py\ pypad_udp.py CLEANFILES = *~\ diff --git a/apis/PyPAD/examples/now_and_next.py b/apis/PyPAD/examples/now_and_next.py index f0a174c1..8b1df372 100755 --- a/apis/PyPAD/examples/now_and_next.py +++ b/apis/PyPAD/examples/now_and_next.py @@ -36,6 +36,8 @@ import PyPAD # def ProcessPad(update): print + print 'Filepath: '+update.resolveFilepath('string %$a',update.dateTime()) + if update.hasPadType(PyPAD.TYPE_NOW): print "Log %03d NOW: " % update.machine()+update.resolvePadFields("%a - %t",PyPAD.ESCAPE_NONE) else: diff --git a/apis/PyPAD/examples/pypad_filewrite.py b/apis/PyPAD/examples/pypad_filewrite.py new file mode 100755 index 00000000..6673b9c2 --- /dev/null +++ b/apis/PyPAD/examples/pypad_filewrite.py @@ -0,0 +1,90 @@ +#!/usr/bin/python + +# pypad_filewrite.py +# +# Write PAD updates to files +# +# (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. +# + +#from __future__ import print_function + +import sys +import ConfigParser +import PyPAD + +#def eprint(*args,**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): + n=1 + try: + while(True): + section='File'+str(n) + if processUpdate(update,section): + fmtstr=config.get(section,'FormatString') + mode='w' + if config.get(section,'Append')=='1': + mode='a' + f=open(update.resolveFilepath(config.get(section,'Filename'),update.dateTime()),mode) + f.write(update.resolvePadFields(fmtstr,int(config.get(section,'Encoding'))).encode('utf-8')) + f.close() + n=n+1 + + except ConfigParser.NoSectionError: + return + +# +# Read Configuration +# +if len(sys.argv)>=2: + fp=open(sys.argv[1]) + config=ConfigParser.ConfigParser() + config.readfp(fp) + fp.close() +else: + eprint('pypad_filewrite.py: you must specify a configuration file') + sys.exit(1) + +rcvr=PyPAD.Receiver() +rcvr.setCallback(ProcessPad) +rcvr.start("localhost",PyPAD.PAD_TCP_PORT) diff --git a/apis/PyPAD/tests/Makefile.am b/apis/PyPAD/tests/Makefile.am new file mode 100644 index 00000000..cf90181d --- /dev/null +++ b/apis/PyPAD/tests/Makefile.am @@ -0,0 +1,39 @@ +## automake.am +## +## Automake.am for Rivendell PyPAD/tests +## +## (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 as +## published by the Free Software Foundation; either version 2 of +## the License, or (at your option) any later version. +## +## 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. +## +## Use automake to process this into a Makefile.in + +EXTRA_DIST = filepath_test.py + pad_test.py + +CLEANFILES = *~\ + *.idb\ + *ilk\ + *.obj\ + *.pdb\ + *.qm\ + moc_* + +MAINTAINERCLEANFILES = *~\ + *.tar.gz\ + aclocal.m4\ + configure\ + Makefile.in\ + moc_* diff --git a/apis/PyPAD/tests/filepath_test.py b/apis/PyPAD/tests/filepath_test.py new file mode 100755 index 00000000..d7c7c955 --- /dev/null +++ b/apis/PyPAD/tests/filepath_test.py @@ -0,0 +1,78 @@ +#!/usr/bin/python + +# filepath_test.py +# +# PyPAD regression test script for Rivendell +# +# Exercise every filepath wildcard in 'PyPAD.Update::resolveFilepath()' +# +# (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 PyPAD + +def ProcessPad(update): + print + print 'DateTime: '+update.dateTime().isoformat(' ') + print + print 'Abbreviated weekday name [%a | %$a | %^a]: '+update.resolveFilepath('%a | %$a | %^a',update.dateTime()) + print 'Full weekday name [%A | %$A | %^A]: '+update.resolveFilepath('%A | %$A | %^A',update.dateTime()) + print 'Abbreviated month name [%b | %$b | %^b]: '+update.resolveFilepath('%b | %$b | %^b',update.dateTime()) + print 'Full month name [%B | %$B | %^B]: '+update.resolveFilepath('%B | %$B | %^B',update.dateTime()) + print 'Century [%C | %$C | %^C]: '+update.resolveFilepath('%C | %$C | %^C',update.dateTime()) + print 'Day of the month, zero padded [01 - 31] [%d | %$d | %^d]: '+update.resolveFilepath('%d | %$d | %^d',update.dateTime()) + print 'Date (mm-dd-yy) [%D | %$D | %^D]: '+update.resolveFilepath('%D | %$D | %^D',update.dateTime()) + print 'Day of the month, space padded [ 1 - 31] [%e | %$e | %^e]: '+update.resolveFilepath('%e | %$e | %^e',update.dateTime()) + print 'Day of the month, unpadded [ 1 - 31] [%E | %$E | %^E]: '+update.resolveFilepath('%E | %$E | %^E',update.dateTime()) + print 'Date (yyyy-mm-dd) [%F | %$F | %^F]: '+update.resolveFilepath('%F | %$F | %^F',update.dateTime()) + print 'Two digit year [%g | %$g | %^g]: '+update.resolveFilepath('%g | %$g | %^g',update.dateTime()) + print 'Four digit year [%G | %$G | %^G]: '+update.resolveFilepath('%G | %$G | %^G',update.dateTime()) + print 'Abbreviated month name [%h | %$h | %^h]: '+update.resolveFilepath('%h | %$h | %^h',update.dateTime()) + print 'Hour, 24 hour, zero padded [00 - 23] [%H | %$H | %^H]: '+update.resolveFilepath('%H | %$H | %^H',update.dateTime()) + print 'Hour, 12 hour, space padded [00 - 23] [%i | %$i | %^i]: '+update.resolveFilepath('%i | %$i | %^i',update.dateTime()) + print 'Hour, 12 hour, zero padded [00 - 23] [%I | %$I | %^I]: '+update.resolveFilepath('%I | %$I | %^I',update.dateTime()) + print 'Day of year, zero padded [%j | %$j | %^j]: '+update.resolveFilepath('%j | %$j | %^j',update.dateTime()) + print 'Hour, 12 hour, unpadded [00 - 23] [%J | %$J | %^J]: '+update.resolveFilepath('%J | %$J | %^J',update.dateTime()) + print 'Hour, 24 hour, space padded [%k | %$k | %^k]: '+update.resolveFilepath('%k | %$k | %^k',update.dateTime()) + print 'Month, zero padded (01 - 12) [%m | %$m | %^m]: '+update.resolveFilepath('%m | %$m | %^m',update.dateTime()) + print 'Minute, zero padded (00 - 59) [%M | %$M | %^M]: '+update.resolveFilepath('%M | %$M | %^M',update.dateTime()) + print 'AM/PM string [%p | %$p | %^p]: '+update.resolveFilepath('%p | %$p | %^p',update.dateTime()) + print 'Rivendell host name [%r | %$r | %^r]: '+update.resolveFilepath('%r | %$r | %^r',update.dateTime()) + print 'Rivendell short host name [%R | %$R | %^R]: '+update.resolveFilepath('%R | %$R | %^R',update.dateTime()) + print 'Rivendell service name [%s | %$s | %^s]: '+update.resolveFilepath('%s | %$s | %^s',update.dateTime()) + print 'Seconds, zero padded (SS) [%S | %$S | %^S]: '+update.resolveFilepath('%S | %$S | %^S',update.dateTime()) + print 'Day of the week, numeric, 1=Monday, 7=Sunday [%u | %$u | %^u]: '+update.resolveFilepath('%u | %$u | %^u',update.dateTime()) + print 'Week number, as per ISO 8601 [00 - 23] [%V | %$V | %^V]: '+update.resolveFilepath('%V | %$V | %^V',update.dateTime()) + print 'Two digit year [%y | %$y | %^y]: '+update.resolveFilepath('%y | %$y | %^y',update.dateTime()) + print 'Four digit year [00 - 23] [%Y | %$Y | %^Y]: '+update.resolveFilepath('%Y | %$Y | %^Y',update.dateTime()) + print "Literal '%' [%%]: "+update.resolveFilepath('%%',update.dateTime()) + +# +# Create an instance of 'PyPADReceiver' +# +rcvr=PyPAD.Receiver() + +# +# Tell it to use the callback +# +rcvr.setCallback(ProcessPad) + +# +# Start the receiver, giving it the hostname or IP address and TCP port of +# the target Rivendell system. Once started, all further processing can only +# be done in the callback method! +# +rcvr.start("localhost",PyPAD.PAD_TCP_PORT) diff --git a/apis/PyPAD/examples/pypad_test.py b/apis/PyPAD/tests/pad_test.py similarity index 97% rename from apis/PyPAD/examples/pypad_test.py rename to apis/PyPAD/tests/pad_test.py index a9444663..91c25ccd 100755 --- a/apis/PyPAD/examples/pypad_test.py +++ b/apis/PyPAD/tests/pad_test.py @@ -1,10 +1,10 @@ #!/usr/bin/python -# pypad_test.py +# pad_test.py # # PyPAD regression test script for Rivendell # -# Exercise every method of 'PyPAD.Update' for each update. +# Exercise every PAD accessor method of 'PyPAD.Update' for each update. # # (C) Copyright 2018 Fred Gleason # @@ -29,6 +29,8 @@ def ProcessPad(update): print '*** Log %03d Update ***********************************************' % update.machine() print '** HEADER INFO **' print ' dateTime(): '+update.dateTime().isoformat(' ') + print ' hostName(): '+update.hostName() + print ' shortHostName(): '+update.shortHostName() print ' machine(): %d' % update.machine() print ' mode(): '+update.mode() print ' onairFlag(): '+str(update.onairFlag()) diff --git a/configure.ac b/configure.ac index 4e7273fe..a8ae77ca 100644 --- a/configure.ac +++ b/configure.ac @@ -470,6 +470,7 @@ AC_CONFIG_FILES([rivendell.spec \ apis/PyPAD/Makefile \ apis/PyPAD/api/Makefile \ apis/PyPAD/examples/Makefile \ + apis/PyPAD/tests/Makefile \ apis/rivwebcapi/Makefile \ apis/rivwebcapi/rivwebcapi.pc \ apis/rivwebcapi/rivwebcapi/Makefile \ diff --git a/lib/rdlogplay.cpp b/lib/rdlogplay.cpp index a903a817..9d1c0804 100644 --- a/lib/rdlogplay.cpp +++ b/lib/rdlogplay.cpp @@ -2963,7 +2963,12 @@ void RDLogPlay::SendNowNext() // play_pad_socket->write(QString("{\r\n").toUtf8()); play_pad_socket->write(QString(" \"padUpdate\": {\r\n").toUtf8()); - play_pad_socket->write(RDJsonField("dateTime",QDateTime::currentDateTime(),8).toUtf8()); + play_pad_socket->write(RDJsonField("dateTime",QDateTime::currentDateTime(),8). + toUtf8()); + play_pad_socket->write(RDJsonField("hostName", + rda->station()->name(),8).toUtf8()); + play_pad_socket->write(RDJsonField("shortHostName", + rda->station()->shortName(),8).toUtf8()); play_pad_socket->write(RDJsonField("machine",play_id+1,8)); play_pad_socket->write(RDJsonField("onairFlag",play_onair_flag,8)); play_pad_socket->write(RDJsonField("mode",RDAirPlayConf::logModeText(play_op_mode),8));