diff --git a/.gitignore b/.gitignore index df9345b4..2403beda 100644 --- a/.gitignore +++ b/.gitignore @@ -69,6 +69,7 @@ missing Makefile.in Makefile moc_* +py-compile rdadmin/rdadmin rdairplay/rdairplay rdcastmanager/rdcastmanager @@ -83,6 +84,7 @@ rdmonitor/rdmonitor rdpanel/rdpanel rdrepld/rdrepld rdrepld-suse +rdrlmd/rdrlmd rdselect/rdselect rdservice/rdservice rdvairplayd/rdvairplayd diff --git a/ChangeLog b/ChangeLog index 119daba1..94615112 100644 --- a/ChangeLog +++ b/ChangeLog @@ -18094,6 +18094,50 @@ 2018-11-30 Patrick Linstruth * Fixed regression with rdimport(1) that threw SQL errors when importing into an existing cart. -2018-12-10 Patrick Linstruth - * Fixed a bug that was causing rdairplay(1) to segfault when no - log was loaded. +2018-12-04 Fred Gleason + * Added an rdrlmd(8) service. + * Implemented JSON-formatted PAD output on TCP port 34289. +2018-12-05 Fred Gleason + * Added a set of enclosing '{}' braces around the JSON-formatted PAD + output to make it well-formed. +2018-12-05 Fred Gleason + * Added a set of Python classes for processing PAD updates. +2018-12-06 Fred Gleason + * Added comments to the 'now_and_next.py' PyPAD script. +2018-12-06 Fred Gleason + * Removed support for the 'priv' argument in callbacks in PyPAD + scripts. +2018-12-06 Fred Gleason + * Added support for '\b', '\f', '\n' '\r' and '\t' control escapes + in the 'PyPADUpdate::padFields()' method. +2018-12-06 Fred Gleason + * Added a 'pypad_udp.py' PyPAD script. +2018-12-07 Fred Gleason + * Changed the Python namespace of the PyPAD classes from + 'rivendell.PyPAD' to 'PyPAD'. + * Renamed the 'PyPAD.PyPADReceiver' class to 'PyPAD.Receiver'. + * Renamed the 'PyPAD.PyPADUpdate' class to 'PyPAD.Update'. + * Added a 'port' argument to the 'PyPAD.Receiver::start()' method. + * Added an 'escaping' argument to the 'PyPAD.Update::padFields()' + method. + * Added support for the 'Encoding=' directive to the 'pypad_udp.py' + script. +2018-12-08 Fred Gleason + * Renamed the 'PyPAD.Update::dateTime()' method to + 'PyPAD.Update::dateTimeString()'. + * Changed the format of the 'dateTime' field in the PAD JSON structure + from RFC822 to ISO 8601. + * Added a 'startDateTime' field to the 'now' and 'next' objects in + the PAD JSON structure. + * Added a PAD Type enumeration for use in 'PyPAD.Update'. + * Added 'PyPAD.Update::hasPadType()' and 'PyPAD.Update.startDateTime()' + methods. + * Removed 'PyPAD.Update::hasNowPad()' and 'PyPAD.Update:hasNextPad()' + methods. + * Added support for '%d(
)' and '%D(
)' wildcards in + 'PyPAD.Update::padFields()'. +2018-12-09 Fred Gleason + * Renamed the 'PyPAD.Update::padFields()' method to + 'PyPAD.Update::resolvePadFields()'. + * Added a 'PyPAD.Update::padField()' method. + * Added a 'PyPAD.Update::escape()' method. diff --git a/Makefile.am b/Makefile.am index 62e2772a..7b6b06d4 100644 --- a/Makefile.am +++ b/Makefile.am @@ -50,6 +50,7 @@ SUBDIRS = icons\ rdmonitor\ rdpanel\ rdrepld\ + rdrlmd\ rdselect\ rdservice\ rdvairplayd\ diff --git a/apis/Makefile.am b/apis/Makefile.am index 231f9775..320f61dd 100644 --- a/apis/Makefile.am +++ b/apis/Makefile.am @@ -20,7 +20,8 @@ ## ## Use automake to process this into a Makefile.in -SUBDIRS = rivwebcapi\ +SUBDIRS = PyPAD\ + rivwebcapi\ rlm CLEANFILES = *~\ diff --git a/apis/PyPAD/Makefile.am b/apis/PyPAD/Makefile.am new file mode 100644 index 00000000..a2c95b53 --- /dev/null +++ b/apis/PyPAD/Makefile.am @@ -0,0 +1,39 @@ +## automake.am +## +## Automake.am for Rivendell PyPAD +## +## (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 + +SUBDIRS = api\ + examples + +CLEANFILES = *~\ + *.idb\ + *ilk\ + *.obj\ + *.pdb\ + *.qm\ + moc_* + +MAINTAINERCLEANFILES = *~\ + *.tar.gz\ + aclocal.m4\ + configure\ + Makefile.in\ + moc_* diff --git a/apis/PyPAD/api/Makefile.am b/apis/PyPAD/api/Makefile.am new file mode 100644 index 00000000..94181f77 --- /dev/null +++ b/apis/PyPAD/api/Makefile.am @@ -0,0 +1,39 @@ +## automake.am +## +## Automake.am for Rivendell PyPAD/api +## +## (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 + +rivendelldir = $(pyexecdir) +rivendell_PYTHON = PyPAD.py + +CLEANFILES = *~\ + *.idb\ + *ilk\ + *.obj\ + *.pdb\ + *.qm\ + moc_* + +MAINTAINERCLEANFILES = *~\ + *.tar.gz\ + aclocal.m4\ + configure\ + Makefile.in\ + moc_* diff --git a/apis/PyPAD/api/PyPAD.py b/apis/PyPAD/api/PyPAD.py new file mode 100644 index 00000000..2ca84962 --- /dev/null +++ b/apis/PyPAD/api/PyPAD.py @@ -0,0 +1,484 @@ +#!/usr/bin/python + +# PyPAD.py +# +# PAD processor for Rivendell +# +# (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 datetime +import socket +import json + +# +# Enumerated Constants (sort of) +# +# Escape types +# +ESCAPE_NONE=0 +ESCAPE_XML=1 +ESCAPE_URL=2 +ESCAPE_JSON=3 + +# +# PAD Types +# +TYPE_NOW='now' +TYPE_NEXT='next' + +# +# Field Names +# +FIELD_START_DATETIME='startDateTime' +FIELD_CART_NUMBER='cartNumber' +FIELD_CART_TYPE='cartType' +FIELD_LENGTH='length' +FIELD_YEAR='year' +FIELD_GROUP_NAME='groupName' +FIELD_TITLE='title' +FIELD_ARTIST='artist' +FIELD_PUBLISHER='publisher' +FIELD_COMPOSER='composer' +FIELD_ALBUM='album' +FIELD_LABEL='label' +FIELD_CLIENT='client' +FIELD_AGENCY='agency' +FIELD_CONDUCTOR='conductor' +FIELD_USER_DEFINED='userDefined' +FIELD_SONG_ID='songId' +FIELD_OUTCUE='outcue' +FIELD_DESCRIPTION='description' +FIELD_ISRC='isrc' +FIELD_ISCI='isci' +FIELD_EXTERNAL_EVENT_ID='externalEventId' +FIELD_EXTERNAL_DATA='externalData' +FIELD_EXTERNAL_ANNC_TYPE='externalAnncType' + +# +# Default TCP port for connecting to Rivendell's PAD service +# +PAD_TCP_PORT=34289 + +class Update(object): + def __init__(self,pad_data): + self.__fields=pad_data; + + def __fromIso8601(self,string): + try: + return datetime.datetime.strptime(string.strip()[:19],'%Y-%m-%dT%H:%M:%S') + except AttributeError: + return '' + + def __escapeXml(self,string): + string=string.replace("&","&") + string=string.replace("<","<") + string=string.replace(">",">") + string=string.replace("'","'") + string=string.replace("\"",""") + return string + + def __escapeWeb(self,string): + string=string.replace("%","%25") + string=string.replace(" ","%20") + string=string.replace("<","%3C") + string=string.replace(">","%3E") + string=string.replace("#","%23") + string=string.replace("\"","%22") + string=string.replace("{","%7B") + string=string.replace("}","%7D") + string=string.replace("|","%7C") + string=string.replace("\\","%5C") + string=string.replace("^","%5E") + string=string.replace("[","%5B") + string=string.replace("]","%5D") + string=string.replace("`","%60") + string=string.replace("\a","%07") + string=string.replace("\b","%08") + string=string.replace("\f","%0C") + string=string.replace("\n","%0A") + string=string.replace("\r","%0D") + string=string.replace("\t","%09") + string=string.replace("\v","%0B") + return string + + def __escapeJson(self,string): + string=string.replace("\\","\\\\") + string=string.replace("\"","\\\"") + string=string.replace("/","\\/") + string=string.replace("\b","\\b") + string=string.replace("\f","\\f") + string=string.replace("\n","\\n") + string=string.replace("\r","\\r") + string=string.replace("\t","\\t") + return string + + def __replaceWildcard(self,wildcard,sfield,stype,string,esc): + try: + if isinstance(self.__fields['padUpdate'][stype][sfield],unicode): + string=string.replace('%'+wildcard,self.escape(self.__fields['padUpdate'][stype][sfield],esc)) + else: + string=string.replace('%'+wildcard,str(self.__fields['padUpdate'][stype][sfield])) + except TypeError: + string=string.replace('%'+wildcard,'') + except KeyError: + string=string.replace('%'+wildcard,'') + return string + + def __replaceWildcardPair(self,wildcard,sfield,string,esc): + string=self.__replaceWildcard(wildcard,sfield,'now',string,esc); + string=self.__replaceWildcard(wildcard.upper(),sfield,'next',string,esc); + return string; + + def __findDatetimePattern(self,pos,wildcard,string): + start=string.find('%'+wildcard+'(',pos) + if start>=0: + end=string.find(")",start+3) + if end>0: + return (end+2,string[start:end+1]) + return None + + def __replaceDatetimePattern(self,string,pattern): + stype='now' + if pattern[1]=='D': + stype='next' + try: + dt=self.__fromIso8601(self.__fields['padUpdate'][stype]['startDateTime']) + except TypeError: + string=string.replace(pattern,'') + return string + + dt_pattern=pattern[3:-1] + + try: + dt_pattern=dt_pattern.replace('dddd',dt.strftime('%A')) + dt_pattern=dt_pattern.replace('ddd',dt.strftime('%a')) + dt_pattern=dt_pattern.replace('dd',dt.strftime('%d')) + dt_pattern=dt_pattern.replace('d',str(dt.day)) + + dt_pattern=dt_pattern.replace('MMMM',dt.strftime('%B')) + dt_pattern=dt_pattern.replace('MMM',dt.strftime('%b')) + dt_pattern=dt_pattern.replace('MM',dt.strftime('%m')) + dt_pattern=dt_pattern.replace('M',str(dt.month)) + + dt_pattern=dt_pattern.replace('yyyy',dt.strftime('%Y')) + dt_pattern=dt_pattern.replace('yy',dt.strftime('%y')) + + miltime=(dt_pattern.find('ap')<0)and(dt_pattern.find('AP')<0) + if not miltime: + if dt.hour<13: + dt_pattern=dt_pattern.replace('ap','am') + dt_pattern=dt_pattern.replace('AP','AM') + else: + dt_pattern=dt_pattern.replace('ap','pm') + dt_pattern=dt_pattern.replace('AP','PM') + if miltime: + dt_pattern=dt_pattern.replace('hh',dt.strftime('%H')) + dt_pattern=dt_pattern.replace('h',str(dt.hour)) + else: + dt_pattern=dt_pattern.replace('hh',dt.strftime('%I')) + hour=dt.hour + if hour==0: + hour=12 + dt_pattern=dt_pattern.replace('h',str(hour)) + + dt_pattern=dt_pattern.replace('mm',dt.strftime('%M')) + dt_pattern=dt_pattern.replace('m',str(dt.minute)) + + dt_pattern=dt_pattern.replace('ss',dt.strftime('%S')) + dt_pattern=dt_pattern.replace('s',str(dt.second)) + except AttributeError: + string=string.replace(pattern,'') + return string + + string=string.replace(pattern,dt_pattern) + return string + + def __replaceDatetimePair(self,string,wildcard): + pos=0 + pattern=(0,'') + while(pattern!=None): + pattern=self.__findDatetimePattern(pattern[0],wildcard,string) + if pattern!=None: + string=self.__replaceDatetimePattern(string,pattern[1]) + return string + + def dateTimeString(self): + """ + Returns the date-time of the update in ISO 8601 format (string). + """ + return self.__fields['padUpdate']['dateTime'] + + def dateTime(self): + """ + Returns the date-time of the PAD update (datetime) + """ + return self.__fromIso8601(pad_data['padUpdate']['dateTime']) + + def escape(self,string,esc): + """ + Returns an 'escaped' version of the specified string. + Take two arguments: + + string - The string to be processed. + + esc - The type of escaping to be applied. The following values + are valid: + PyPAD.ESCAPE_JSON - Escaping for JSON string values + PyPAD.ESCAPE_NONE - No escaping applied + PyPAD.ESCAPE_URL - Escaping for using in URLs + PyPAD.ESCAPE_XML - Escaping for use in XML + """ + if(esc==0): + return string + if(esc==1): + return self.__escapeXml(string) + if(esc==2): + return self.__escapeWeb(string) + if(esc==3): + return self.__escapeJson(string) + raise ValueError('invalid esc value') + + def logMachine(self): + """ + Returns the log machine number to which this update pertains + (integer). + """ + return self.__fields['padUpdate']['logMachine'] + + def onairFlag(self): + """ + Returns the state of the on-air flag (boolean). + """ + return self.__fields['padUpdate']['onairFlag'] + + def hasService(self): + """ + Indicates if service information is included with this update + (boolean). + """ + try: + return self.__fields['padUpdate']['service']!=None + except TypeError: + return False; + + def serviceName(self): + """ + Returns the name of the service associated with this update (string). + """ + return self.__fields['padUpdate']['service']['name'] + + def serviceDescription(self): + """ + Returns the description of the service associated with this update + (string). + """ + return self.__fields['padUpdate']['service']['description'] + + def serviceProgramCode(self): + """ + Returns the Program Code of the service associated with this update + (string). + """ + return self.__fields['padUpdate']['service']['programCode'] + + def hasLog(self): + """ + Indicates if log information is included with this update + (boolean). + """ + try: + return self.__fields['padUpdate']['log']!=None + except TypeError: + return False; + + def logName(self): + """ + Returns the name of the log associated with this update (string). + """ + return self.__fields['padUpdate']['log']['name'] + + def resolvePadFields(self,string,esc): + """ + Takes two arguments: + + string - A string containing one or more PAD wildcards, which it + will resolve into the appropriate values. See the + 'Metadata Wildcards' section of the Rivendell Operations + Guide for a list of recognized wildcards. + + esc - Character escaping to be applied to the PAD fields. + Must be one of the following: + + 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('b','label',string,esc) + string=self.__replaceWildcardPair('c','client',string,esc) + string=self.__replaceDatetimePair(string,'d') # %d(
) Handler + string=self.__replaceDatetimePair(string,'D') # %D(
) Handler + string=self.__replaceWildcardPair('e','agency',string,esc) + #string=self.__replaceWildcardPair('f',sfield,string,esc) # Unassigned + string=self.__replaceWildcardPair('g','groupName',string,esc) + string=self.__replaceWildcardPair('h','length',string,esc) + string=self.__replaceWildcardPair('i','description',string,esc) + string=self.__replaceWildcardPair('j','cutNumber',string,esc) + #string=self.__replaceWildcardPair('k',sfield,string,esc) # Start time for rdimport + string=self.__replaceWildcardPair('l','album',string,esc) + string=self.__replaceWildcardPair('m','composer',string,esc) + string=self.__replaceWildcardPair('n','cartNumber',string,esc) + string=self.__replaceWildcardPair('o','outcue',string,esc) + string=self.__replaceWildcardPair('p','publisher',string,esc) + #string=self.__replaceWildcardPair('q',sfield,string,esc) # Start date for rdimport + string=self.__replaceWildcardPair('r','conductor',string,esc) + string=self.__replaceWildcardPair('s','songId',string,esc) + string=self.__replaceWildcardPair('t','title',string,esc) + string=self.__replaceWildcardPair('u','userDefined',string,esc) + #string=self.__replaceWildcardPair('v',sfield,string,esc) # Length, rounded down + #string=self.__replaceWildcardPair('w',sfield,string,esc) # Unassigned + #string=self.__replaceWildcardPair('x',sfield,string,esc) # Unassigned + string=self.__replaceWildcardPair('y','year',string,esc) + #string=self.__replaceWildcardPair('z',sfield,string,esc) # Unassigned + string=string.replace('\\b','\b') + string=string.replace('\\f','\f') + string=string.replace('\\n','\n') + string=string.replace('\\r','\r') + string=string.replace('\\t','\t') + return string + + def hasPadType(self,pad_type): + """ + Indicates if this update includes the specified PAD type + Takes one argument: + pad_type - The type of PAD value. Valid values are: + PyPAD.NOW - Now playing data + PyPAD.NEXT - Next to play data + """ + try: + return self.__fields['padUpdate'][pad_type]!=None + except TypeError: + return False; + + def startDateTime(self,pad_type): + """ + Returns the start datetime of the specified PAD type + Takes one argument: + pad_type - The type of PAD value. Valid values are: + PyPAD.NOW - Now playing data + PyPAD.NEXT - Next to play data + """ + try: + return self.__fromIso8601(self.__fields['padUpdate'][pad_type]['startDateTime']) + except AttributeError: + return None + + def padField(self,pad_type,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: + PyPAD.FIELD_AGENCY - The 'Agency' field (string) + PyPAD.FIELD_ALBUM - The 'Album' field (string) + PyPAD.FIELD_ARTIST - The 'Artist' field (string) + PyPAD.FIELD_CART_NUMBER - The 'Cart Number' field + (integer) + PyPAD.FIELD_CART_TYPE - 'The 'Cart Type' field + (string) + PyPAD.FIELD_CLIENT - The 'Client' field (string) + PyPAD.FIELD_COMPOSER - The 'Composer' field (string) + PyPAD.FIELD_CONDUCTOR - The 'Conductor' field (string) + PyPAD.FIELD_DESCRIPTION - The 'Description' field + (string) + PyPAD.FIELD_EXTERNAL_ANNC_TYPE - The 'EXT_ANNC_TYPE' + field (string) + PyPAD.FIELD_EXTERNAL_DATA - The 'EXT_DATA' field + (string) + PyPAD.FIELD_EXTERNAL_EVENT_ID - The 'EXT_EVENT_ID' + field (string) + PyPAD.FIELD_GROUP_NAME - The 'GROUP_NAME' field + (string) + PyPAD.FIELD_ISRC - The 'ISRC' field (string) + PyPAD.FIELD_ISCI - The 'ISCI' field (string) + PyPAD.FIELD_LABEL - The 'Label' field (string) + PyPAD.FIELD_LENGTH - The 'Length' field (integer) + PyPAD.FIELD_OUTCUE - The 'Outcue' field (string) + PyPAD.FIELD_PUBLISHER - The 'Publisher' field (string) + PyPAD.FIELD_SONG_ID - The 'Song ID' field (string) + PyPAD.FIELD_START_DATETIME - The 'Start DateTime field + (string) + 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] + + + + +class Receiver(object): + def __init__(self): + self.__callback=None + + def __PyPAD_Process(self,pad): + self.__callback(pad) + + def setCallback(self,cb): + """ + Set the processing callback. + """ + self.__callback=cb + + def start(self,hostname,port): + """ + Connect to a Rivendell system and begin processing PAD events. + Once started, a PyPAD object can be interacted with + only within one of its callback methods. + Takes the following arguments: + + hostname - The hostname or IP address of the Rivendell system. + + port - The TCP port to connect to. For most cases, just use + 'PyPAD.PAD_TCP_PORT'. + """ + sock=socket.socket(socket.AF_INET) + conn=sock.connect((hostname,port)) + c="" + line="" + msg="" + + while 1<2: + c=sock.recv(1) + line+=c + if c[0]=="\n": + msg+=line + if line=="\r\n": + self.__PyPAD_Process(Update(json.loads(msg))) + msg="" + line="" + + diff --git a/apis/PyPAD/examples/Makefile.am b/apis/PyPAD/examples/Makefile.am new file mode 100644 index 00000000..f6efe3a3 --- /dev/null +++ b/apis/PyPAD/examples/Makefile.am @@ -0,0 +1,39 @@ +## automake.am +## +## Automake.am for Rivendell PyPAD/examples +## +## (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 = now_and_next.py\ + pypad_udp.py + +CLEANFILES = *~\ + *.idb\ + *ilk\ + *.obj\ + *.pdb\ + *.qm\ + moc_* + +MAINTAINERCLEANFILES = *~\ + *.tar.gz\ + aclocal.m4\ + configure\ + Makefile.in\ + moc_* diff --git a/apis/PyPAD/examples/now_and_next.py b/apis/PyPAD/examples/now_and_next.py new file mode 100755 index 00000000..3859b000 --- /dev/null +++ b/apis/PyPAD/examples/now_and_next.py @@ -0,0 +1,63 @@ +#!/usr/bin/python + +# now_and_next.py +# +# Example PyPAD script for Rivendell +# +# (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. +# + +# +# To see the full documentation of these classes, enter the following at +# a python interactive prompt: +# +# import PyPAD +# help(PyPAD) +# +import PyPAD + +# +# First, we create a callback method, that will be called every time a +# log machine updates its PAD. An instance of 'PyPAD.Update' that contains +# the PAD information is supplied as the single argument. +# +def ProcessPad(update): + print + if update.hasPadType(PyPAD.TYPE_NOW): + print "Log %03d NOW: " % update.logMachine()+update.resolvePadFields("%a - %t",PyPAD.ESCAPE_NONE) + else: + print "Log %03d NOW: [none]" % update.logMachine() + if update.hasPadType(PyPAD.TYPE_NEXT): + print "Log %03d NEXT: " % update.logMachine()+update.resolvePadFields("%A - %T",PyPAD.ESCAPE_NONE) + else: + print "Log %03d NEXT: [none]" % update.logMachine() + +# +# 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_udp.py b/apis/PyPAD/examples/pypad_udp.py new file mode 100755 index 00000000..e8ae425a --- /dev/null +++ b/apis/PyPAD/examples/pypad_udp.py @@ -0,0 +1,64 @@ +#!/usr/bin/python + +# pypad_udp.py +# +# Send PAD updates via UDP +# +# (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 socket +import ConfigParser +import PyPAD + +def eprint(*args,**kwargs): + print(*args,file=sys.stderr,**kwargs) + +def ProcessPad(update): + n=1 + while(True): + section='Udp'+str(n) + try: + fmtstr=config.get(section,'FormatString') + send_sock.sendto(update.resolvePadFields(fmtstr,int(config.get(section,'Encoding'))), + (config.get(section,'IpAddress'),int(config.get(section,'UdpPort')))) + 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_udp.py: you must specify a configuration file') + sys.exit(1) + +# +# Create Send Socket +# +send_sock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) + +rcvr=PyPAD.Receiver() +rcvr.setCallback(ProcessPad) +rcvr.start("localhost",PyPAD.PAD_TCP_PORT) diff --git a/configure.ac b/configure.ac index 7a30ce22..4e7273fe 100644 --- a/configure.ac +++ b/configure.ac @@ -247,6 +247,11 @@ AC_CHECK_HEADER(security/pam_appl.h,[],[AC_MSG_ERROR([*** PAM not found ***])]) # AC_CHECK_HEADER(soundtouch/SoundTouch.h,[],[AC_MSG_ERROR([*** SoundTouch not found ***])]) +# +# Check for Python +# +AM_PATH_PYTHON([2.7]) + # # Check for FLAC # @@ -462,6 +467,9 @@ AC_CONFIG_FILES([rivendell.spec \ icons/Makefile \ helpers/Makefile \ apis/Makefile \ + apis/PyPAD/Makefile \ + apis/PyPAD/api/Makefile \ + apis/PyPAD/examples/Makefile \ apis/rivwebcapi/Makefile \ apis/rivwebcapi/rivwebcapi.pc \ apis/rivwebcapi/rivwebcapi/Makefile \ @@ -507,6 +515,7 @@ AC_CONFIG_FILES([rivendell.spec \ rdmonitor/Makefile \ rdpanel/Makefile \ rdrepld/Makefile \ + rdrlmd/Makefile \ rdselect/Makefile \ rdservice/Makefile \ rdvairplayd/Makefile \ diff --git a/lib/Makefile.am b/lib/Makefile.am index 1039062c..6b8e1f9f 100644 --- a/lib/Makefile.am +++ b/lib/Makefile.am @@ -233,6 +233,8 @@ dist_librd_la_SOURCES = dbversion.h\ rdurl.cpp rdurl.h\ rduser.cpp rduser.h\ rdupload.cpp rdupload.h\ + rdunixserver.cpp rdunixserver.h\ + rdunixsocket.cpp rdunixsocket.h\ rdversion.cpp rdversion.h\ rdwavedata.cpp rdwavedata.h\ rdwavedata_dialog.cpp rdwavedata_dialog.h\ @@ -333,6 +335,8 @@ nodist_librd_la_SOURCES = moc_rdadd_cart.cpp\ moc_rdtransportbutton.cpp\ moc_rdtrimaudio.cpp\ moc_rdttydevice.cpp\ + moc_rdunixserver.cpp\ + moc_rdunixsocket.cpp\ moc_rdupload.cpp\ moc_rdwavedata_dialog.cpp\ moc_schedcartlist.cpp diff --git a/lib/rd.h b/lib/rd.h index 395dafbc..536ddfa7 100644 --- a/lib/rd.h +++ b/lib/rd.h @@ -593,5 +593,11 @@ */ #define RD_STATUS_BACKGROUND_COLOR "#AAFFFF" +/* + * RLM2 Connection Points + */ +#define RD_RLM2_CLIENT_TCP_PORT 34289 +#define RD_RLM2_SOURCE_UNIX_ADDRESS "m4w8n8fsfddf-473fdueusurt-8954" + #endif // RD_H diff --git a/lib/rdconf.cpp b/lib/rdconf.cpp index 0cd28dbd..08e16bb5 100644 --- a/lib/rdconf.cpp +++ b/lib/rdconf.cpp @@ -991,7 +991,15 @@ int RDTimeZoneOffset() tm=gmtime(&t); time_t gmt_time=3600*tm->tm_hour+60*tm->tm_min+tm->tm_sec; - return gmt_time-local_time; + int offset=gmt_time-local_time; + if(offset>43200) { + offset=offset-86400; + } + if(offset<-43200) { + offset=offset+86400; + } + + return offset; } diff --git a/lib/rdlogplay.cpp b/lib/rdlogplay.cpp index 5242e4f9..21475ee8 100644 --- a/lib/rdlogplay.cpp +++ b/lib/rdlogplay.cpp @@ -33,6 +33,7 @@ #include "rdmixer.h" #include "rdnownext.h" #include "rdsvc.h" +#include "rdweb.h" // // Debug Settings @@ -41,9 +42,8 @@ //#define SHOW_METER_SLOTS RDLogPlay::RDLogPlay(int id,RDEventPlayer *player,Q3SocketDevice *nn_sock, - QString logname,std::vector *rlm_hosts, - QObject *parent) - : QObject(parent),RDLogEvent(logname) + std::vector *rlm_hosts,QObject *parent) + : QObject(parent),RDLogEvent("") { // // Initialize Data Structures @@ -51,6 +51,7 @@ RDLogPlay::RDLogPlay(int id,RDEventPlayer *player,Q3SocketDevice *nn_sock, play_log=NULL; play_id=id; play_event_player=player; + // play_pad_socket=pad_sock; play_rlm_hosts=rlm_hosts; play_onair_flag=false; play_segue_length=rda->airplayConf()->segueLength()+1; @@ -76,6 +77,14 @@ RDLogPlay::RDLogPlay(int id,RDEventPlayer *player,Q3SocketDevice *nn_sock, play_slot_id[i]=i; } + // + // RLM2 Connection + // + play_pad_socket=new RDUnixSocket(this); + if(!play_pad_socket->connectToAbstract(RD_RLM2_SOURCE_UNIX_ADDRESS)) { + fprintf(stderr,"RLMHost: unable to connect to rdrlmd\n"); + } + // // CAE Connection // @@ -2869,10 +2878,12 @@ void RDLogPlay::SendNowNext() // // Get NOW PLAYING Event // + /* if(play_nownext_address.isNull()&&play_nownext_rml.isEmpty()&& (play_rlm_hosts->size()==0)) { return; } + */ QString cmd=play_nownext_string; int lines[TRANSPORT_QUANTITY]; int running=runningEvents(lines,false); @@ -2946,10 +2957,75 @@ void RDLogPlay::SendNowNext() if(svcname.isEmpty()) { svcname=play_defaultsvc_name; } + + // + // RLM2 + // + 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("logMachine",play_id+1,8)); + play_pad_socket->write(RDJsonField("onairFlag",play_onair_flag,8)); + play_pad_socket->write(RDJsonField("logMode",RDAirPlayConf::logModeText(play_op_mode),8)); + + // + // Service + // + RDSvc *svc=new RDSvc(svcname,rda->station(),rda->config(),this); + play_pad_socket->write(QString(" \"service\": {\r\n").toUtf8()); + play_pad_socket->write(RDJsonField("name",svcname,12).toUtf8()); + play_pad_socket-> + write(RDJsonField("description",svc->description(),12).toUtf8()); + play_pad_socket-> + write(RDJsonField("programCode",svc->programCode(),12,true).toUtf8()); + play_pad_socket->write(QString(" },\r\n").toUtf8()); + delete svc; + + // + // Log + // + play_pad_socket->write(QString(" \"log\": {\r\n").toUtf8()); + play_pad_socket->write(RDJsonField("name",logName(),12,true).toUtf8()); + play_pad_socket->write(QString(" },\r\n").toUtf8()); + + // + // Now + // + QDateTime start_datetime; + if(logline[0]!=NULL) { + start_datetime= + QDateTime(QDate::currentDate(),logline[0]->startTime(RDLogLine::Actual)); + } + play_pad_socket-> + write(GetPadJson("now",logline[0],start_datetime,8,false).toUtf8()); + + // + // Next + // + QDateTime next_datetime; + if((mode()==RDAirPlayConf::Auto)&&(logline[0]!=NULL)) { + next_datetime=start_datetime.addSecs(logline[0]->forcedLength()/1000); + } + play_pad_socket->write(GetPadJson("next",logline[1], + next_datetime,8,true).toUtf8()); + + // + // Commit the update + // + play_pad_socket->write(QString(" }\r\n").toUtf8()); + play_pad_socket->write(QString("}\r\n\r\n").toUtf8()); + + // + // Old-style RLM Hosts + // for(unsigned i=0;isize();i++) { play_rlm_hosts->at(i)-> sendEvent(svcname,logName(),play_id,logline,play_onair_flag,play_op_mode); } + + // + // Premordial integrated interface + // RDResolveNowNext(&cmd,logline,0); play_nownext_socket-> writeBlock(cmd,cmd.length(),play_nownext_address,play_nownext_port); @@ -2970,6 +3046,63 @@ void RDLogPlay::SendNowNext() } +QString RDLogPlay::GetPadJson(const QString &name,RDLogLine *ll, + const QDateTime &start_datetime,int padding, + bool final) const +{ + QString ret; + + if(ll==NULL) { + ret=RDJsonNullField(name,padding,final); + } + else { + ret+=RDJsonPadding(padding)+"\""+name+"\": {\r\n"; + if(start_datetime.isValid()) { + ret+=RDJsonField("startDateTime",start_datetime,4+padding); + } + else { + ret+=RDJsonNullField("startDateTime",4+padding); + } + ret+=RDJsonField("cartNumber",ll->cartNumber(),4+padding); + ret+=RDJsonField("cartType",RDCart::typeText(ll->cartType()),4+padding); + ret+=RDJsonField("length",ll->forcedLength(),4+padding); + if(ll->year().isValid()) { + ret+=RDJsonField("year",ll->year().year(),4+padding); + } + else { + ret+=RDJsonNullField("year",4+padding); + } + ret+=RDJsonField("groupName",ll->groupName(),4+padding); + ret+=RDJsonField("title",ll->title(),4+padding); + ret+=RDJsonField("artist",ll->artist(),4+padding); + ret+=RDJsonField("publisher",ll->publisher(),4+padding); + ret+=RDJsonField("composer",ll->composer(),4+padding); + ret+=RDJsonField("album",ll->album(),4+padding); + ret+=RDJsonField("label",ll->label(),4+padding); + ret+=RDJsonField("client",ll->client(),4+padding); + ret+=RDJsonField("agency",ll->agency(),4+padding); + ret+=RDJsonField("conductor",ll->conductor(),4+padding); + ret+=RDJsonField("userDefined",ll->userDefined(),4+padding); + ret+=RDJsonField("songId",ll->songId(),4+padding); + ret+=RDJsonField("outcue",ll->outcue(),4+padding); + ret+=RDJsonField("description",ll->description(),4+padding); + ret+=RDJsonField("isrc",ll->isrc(),4+padding); + ret+=RDJsonField("isci",ll->isci(),4+padding); + ret+=RDJsonField("externalEventId",ll->extEventId(),4+padding); + ret+=RDJsonField("externalData",ll->extData(),4+padding); + ret+=RDJsonField("externalAnncType",ll->extAnncType(),4+padding,true); + if(final) { + ret+=RDJsonPadding(padding)+"}\r\n"; + } + else { + ret+=RDJsonPadding(padding)+"},\r\n"; + } + } + + return ret; +} + + void RDLogPlay::LogTraffic(RDLogLine *logline,RDLogLine::PlaySource src, RDAirPlayConf::TrafficAction action,bool onair_flag) const diff --git a/lib/rdlogplay.h b/lib/rdlogplay.h index a113f4fe..4cb69acd 100644 --- a/lib/rdlogplay.h +++ b/lib/rdlogplay.h @@ -39,6 +39,7 @@ #include #include #include +#include // // Widget Settings @@ -53,8 +54,8 @@ class RDLogPlay : public QObject,public RDLogEvent { Q_OBJECT public: - RDLogPlay(int id,RDEventPlayer *player,Q3SocketDevice *nn_sock,QString logname, - std::vector *rlm_hosts,QObject *parent=0); + RDLogPlay(int id,RDEventPlayer *player,Q3SocketDevice *nn_sock, + std::vector *rlm_hosts,QObject *parent=0); QString serviceName() const; void setServiceName(const QString &svcname); QString defaultServiceName() const; @@ -188,6 +189,9 @@ class RDLogPlay : public QObject,public RDLogEvent RDLogLine::TransType GetTransType(const QString &logname,int line); bool ClearBlock(int start_line); void SendNowNext(); + QString GetPadJson(const QString &name,RDLogLine *ll, + const QDateTime &start_datetime,int padding, + bool final=false) const; void LogTraffic(RDLogLine *logline,RDLogLine::PlaySource src, RDAirPlayConf::TrafficAction action,bool onair_flag) const; RDCae *play_cae; @@ -246,6 +250,7 @@ class RDLogPlay : public QObject,public RDLogEvent bool play_audition_head_played; int play_audition_preroll; RDEventPlayer *play_event_player; + RDUnixSocket *play_pad_socket; }; diff --git a/lib/rdunixserver.cpp b/lib/rdunixserver.cpp new file mode 100644 index 00000000..72b96bf3 --- /dev/null +++ b/lib/rdunixserver.cpp @@ -0,0 +1,191 @@ +// rdunixserver.cpp +// +// UNIX Socket Server +// +// (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. +// + +#include +#include +#include +#include + +#include "rdunixserver.h" + +RDUnixServer::RDUnixServer(QObject *parent) + : QObject(parent) +{ + unix_socket=-1; + unix_notifier=NULL; + unix_is_listening=false; + unix_max_pending_connections=3; + unix_error_string="ok"; +} + + +RDUnixServer::~RDUnixServer() +{ + close(); + if(unix_notifier!=NULL) { + delete unix_notifier; + } +} + + +void RDUnixServer::close() +{ + if(unix_socket>=0) { + shutdown(unix_socket,SHUT_RDWR); + unix_socket=-1; + unix_is_listening=false; + } +} + + +QString RDUnixServer::errorString() const +{ + return unix_error_string; +} + + +bool RDUnixServer::hasPendingConnections() const +{ + return false; +} + + +bool RDUnixServer::isListening() const +{ + return unix_is_listening; +} + + +bool RDUnixServer::listenToPathname(const QString &pathname) +{ + struct sockaddr_un sa; + + if((unix_socket=socket(AF_UNIX,SOCK_STREAM,0))<0) { + unix_error_string=QString("unable to create socket")+" ["+ + QString(strerror(errno))+"]"; + return false; + } + memset(&sa,0,sizeof(sa)); + sa.sun_family=AF_UNIX; + strncpy(sa.sun_path,pathname.toUtf8(),UNIX_PATH_MAX); + if(bind(unix_socket,(struct sockaddr *)(&sa),sizeof(sa))<0) { + unix_error_string=QString("unable to bind address")+" ["+ + QString(strerror(errno))+"]"; + return false; + } + if(listen(unix_socket,unix_max_pending_connections)<0) { + unix_error_string=QString("unable to listen")+" ["+ + QString(strerror(errno))+"]"; + return false; + } + unix_is_listening=true; + unix_notifier=new QSocketNotifier(unix_socket,QSocketNotifier::Read,this); + connect(unix_notifier,SIGNAL(activated(int)), + this,SLOT(newConnectionData(int))); + + return true; +} + + +bool RDUnixServer::listenToAbstract(const QString &addr) +{ + struct sockaddr_un sa; + + if((unix_socket=socket(AF_UNIX,SOCK_STREAM,0))<0) { + unix_error_string=QString("unable to create socket")+" ["+ + QString(strerror(errno))+"]"; + return false; + } + memset(&sa,0,sizeof(sa)); + sa.sun_family=AF_UNIX; + strncpy(sa.sun_path+1,addr.toUtf8(),UNIX_PATH_MAX-1); + if(bind(unix_socket,(struct sockaddr *)(&sa),sizeof(sa))<0) { + unix_error_string=QString("unable to bind address")+" ["+ + QString(strerror(errno))+"]"; + return false; + } + if(listen(unix_socket,unix_max_pending_connections)<0) { + unix_error_string=QString("unable to listen")+" ["+ + QString(strerror(errno))+"]"; + return false; + } + unix_is_listening=true; + unix_notifier=new QSocketNotifier(unix_socket,QSocketNotifier::Read,this); + connect(unix_notifier,SIGNAL(activated(int)), + this,SLOT(newConnectionData(int))); + + return true; +} + + +QTcpSocket *RDUnixServer::nextPendingConnection() +{ + int sock; + QTcpSocket *tcpsock=NULL; + struct sockaddr_un sa; + socklen_t sa_len=sizeof(sa); + + memset(&sa,0,sizeof(sa)); + + if((sock=accept(unix_socket,(struct sockaddr *)(&sa),&sa_len))<0) { + unix_error_string=QString("accept failed [")+QString(strerror(errno)); + return NULL; + } + tcpsock=new QTcpSocket(this); + tcpsock->setSocketDescriptor(sock,QAbstractSocket::ConnectedState); + + return tcpsock; +} + + +int RDUnixServer::maxPendingConnections() const +{ + return unix_max_pending_connections; +} + + +void RDUnixServer::setMaxPendingConnections(int num) +{ + unix_max_pending_connections=num; +} + + +int RDUnixServer::socketDescriptor() const +{ + return unix_socket; +} + + +void RDUnixServer::setSocketDescriptor(int sock) +{ + unix_socket=sock; + if(unix_notifier!=NULL) { + delete unix_notifier; + } + unix_notifier=new QSocketNotifier(unix_socket,QSocketNotifier::Read,this); + connect(unix_notifier,SIGNAL(activated(int)), + this,SLOT(newConnectionData(int))); +} + + +void RDUnixServer::newConnectionData(int fd) +{ + emit newConnection(); +} diff --git a/lib/rdunixserver.h b/lib/rdunixserver.h new file mode 100644 index 00000000..a06a3af1 --- /dev/null +++ b/lib/rdunixserver.h @@ -0,0 +1,68 @@ +// rdunixserver.h +// +// UNIX Socket Server +// +// (C) Copyright 2018 Fred Gleason +// +// This class works much the same way as QTcpServer, only it supports +// SOCK_STREAM connections via UNIX sockets rather than TCP/IP. +// +// See the unix(7) man page for a description of the difference between +// 'pathname' and 'abstract' socket addresses. +// +// +// 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. +// + +#ifndef RDUNIXSERVER_H +#define RDUNIXSERVER_H + +#include +#include +#include + +class RDUnixServer : public QObject +{ + Q_OBJECT + public: + RDUnixServer(QObject *parent=0); + ~RDUnixServer(); + void close(); + QString errorString() const; + bool hasPendingConnections() const; + bool isListening() const; + bool listenToPathname(const QString &pathname); + bool listenToAbstract(const QString &addr); + QTcpSocket *nextPendingConnection(); + int maxPendingConnections() const; + void setMaxPendingConnections(int num); + int socketDescriptor() const; + void setSocketDescriptor(int sock); + + signals: + void newConnection(); + + private slots: + void newConnectionData(int fd); + + private: + int unix_socket; + bool unix_is_listening; + int unix_max_pending_connections; + QSocketNotifier *unix_notifier; + QString unix_error_string; +}; + + +#endif // RDUNIXSERVER_H diff --git a/lib/rdunixsocket.cpp b/lib/rdunixsocket.cpp new file mode 100644 index 00000000..5972c339 --- /dev/null +++ b/lib/rdunixsocket.cpp @@ -0,0 +1,58 @@ +// rdunixsocket.cpp +// +// UNIX Socket +// +// (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. +// + +#include +#include +#include + +#include "rdunixsocket.h" + +RDUnixSocket::RDUnixSocket(QObject *parent) + : QTcpSocket(parent) +{ +} + + +bool RDUnixSocket::connectToPathname(const QString &pathname, + QAbstractSocket::OpenMode mode) +{ + return false; +} + + +bool RDUnixSocket::connectToAbstract(const QString &addr, + QAbstractSocket::OpenMode mode) +{ + int sock; + struct sockaddr_un sa; + + if((sock=::socket(AF_UNIX,SOCK_STREAM,0))<0) { + return false; + } + memset(&sa,0,sizeof(sa)); + sa.sun_family=AF_UNIX; + strncpy(sa.sun_path+1,addr.toUtf8(),UNIX_PATH_MAX-1); + if(::connect(sock,(struct sockaddr *)(&sa),sizeof(sa))<0) { + return false; + } + setSocketDescriptor(sock,QAbstractSocket::ConnectedState,mode); + + return true; +} diff --git a/lib/rdunixsocket.h b/lib/rdunixsocket.h new file mode 100644 index 00000000..c9bcd171 --- /dev/null +++ b/lib/rdunixsocket.h @@ -0,0 +1,43 @@ +// rdunixsocket.h +// +// UNIX Socket +// +// (C) Copyright 2018 Fred Gleason +// +// This class works much the same way as QTcpSocket, only it supports +// SOCK_STREAM connections via UNIX sockets rather than TCP/IP. +// +// See the unix(7) man page for a description of the difference between +// 'pathname' and 'abstract' socket addresses. +// +// +// 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. +// + +#ifndef RDUNIXSOCKET_H +#define RDUNIXSOCKET_H + +#include + +class RDUnixSocket : public QTcpSocket +{ + Q_OBJECT + public: + RDUnixSocket(QObject *parent=0); + bool connectToPathname(const QString &pathname,OpenMode mode=ReadWrite); + bool connectToAbstract(const QString &addr,OpenMode mode=ReadWrite); +}; + + +#endif // RDUNIXSOCKET_H diff --git a/lib/rdweb.cpp b/lib/rdweb.cpp index d12aab01..bbe881e4 100644 --- a/lib/rdweb.cpp +++ b/lib/rdweb.cpp @@ -1043,6 +1043,110 @@ QString RDXmlUnescape(const QString &str) } +QString RDJsonPadding(int padding) +{ + QString ret=""; + + for(int i=0;iconnectToAbstract(RD_RLM2_SOURCE_UNIX_ADDRESS)) { + fprintf(stderr,"RLMHost: unable to connect to rdrlmd\n"); + } + */ + // // Log Machines // @@ -324,8 +334,8 @@ MainWidget::MainWidget(QWidget *parent) connect(rename_mapper,SIGNAL(mapped(int)),this,SLOT(logRenamedData(int))); QString default_svcname=rda->airplayConf()->defaultSvc(); for(int i=0;isetDefaultServiceName(default_svcname); air_log[i]->setNowCart(rda->airplayConf()->logNowCart(i)); air_log[i]->setNextCart(rda->airplayConf()->logNextCart(i)); @@ -813,6 +823,7 @@ MainWidget::MainWidget(QWidget *parent) } delete q; + // // Create the HotKeyList object // rdkeylist=new RDHotKeyList(); diff --git a/rdairplay/rdairplay.h b/rdairplay/rdairplay.h index 32d0384e..b3116a17 100644 --- a/rdairplay/rdairplay.h +++ b/rdairplay/rdairplay.h @@ -52,6 +52,7 @@ #include #include #include +#include #include #include "button_log.h" diff --git a/rdrlmd/Makefile.am b/rdrlmd/Makefile.am new file mode 100644 index 00000000..a2e9a9c6 --- /dev/null +++ b/rdrlmd/Makefile.am @@ -0,0 +1,48 @@ +## Makefile.am +## +## Rivendell RLM Consolidation Server +## +## (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. +## +## +## Use automake to process this into a Makefile.in + +AM_CPPFLAGS = -Wall -DPREFIX=\"$(prefix)\" -I$(top_srcdir)/lib @QT4_CFLAGS@ -I/usr/include/Qt3Support +LIBS = -L$(top_srcdir)/lib -L$(top_srcdir)/rdhpi +MOC = @QT_MOC@ + +# The dependency for qt's Meta Object Compiler (moc) +moc_%.cpp: %.h + $(MOC) $< -o $@ + + +sbin_PROGRAMS = rdrlmd + +dist_rdrlmd_SOURCES = rdrlmd.cpp rdrlmd.h + +nodist_rdrlmd_SOURCES = moc_rdrlmd.cpp + +rdrlmd_LDADD = @LIB_RDLIBS@ @LIBVORBIS@ @QT4_LIBS@ -lQt3Support + +CLEANFILES = *~\ + *.idb\ + *ilk\ + *.obj\ + *.pdb\ + moc_* + +MAINTAINERCLEANFILES = *~\ + Makefile.in diff --git a/rdrlmd/rdrlmd.cpp b/rdrlmd/rdrlmd.cpp new file mode 100644 index 00000000..a65ea06f --- /dev/null +++ b/rdrlmd/rdrlmd.cpp @@ -0,0 +1,207 @@ +// rdrlmd.cpp +// +// Rivendell RLM Consolidation Server +// +// (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. +// + +#include +#include + +#include +#include + +#include +#include + +#include "rdrlmd.h" + +MetadataSource::MetadataSource(QTcpSocket *sock) +{ + meta_socket=sock; + meta_committed=true; +} + + +QByteArray MetadataSource::buffer() const +{ + return meta_buffer; +} + + +bool MetadataSource::appendBuffer(const QByteArray &data) +{ + printf("data: %s\n",(const char *)data); + + if(meta_committed) { + meta_buffer.clear(); + } + meta_buffer+=data; + meta_committed=meta_buffer.endsWith("\r\n\r\n"); + + return meta_committed; +} + + +bool MetadataSource::isCommitted() const +{ + return meta_committed; +} + + +QTcpSocket *MetadataSource::socket() const +{ + return meta_socket; +} + + + + +MainObject::MainObject(QObject *parent) + : QObject(parent) +{ + new RDCmdSwitch(qApp->argc(),qApp->argv(),"rdrlmd",RDRLMD_USAGE); + + // + // Client Server + // + rlm_client_disconnect_mapper=new QSignalMapper(this); + connect(rlm_client_disconnect_mapper,SIGNAL(mapped(int)), + this,SLOT(clientDisconnected(int))); + + rlm_client_server=new QTcpServer(this); + connect(rlm_client_server,SIGNAL(newConnection()), + this,SLOT(newClientConnectionData())); + if(!rlm_client_server->listen(QHostAddress::Any,RD_RLM2_CLIENT_TCP_PORT)) { + fprintf(stderr,"rdrlmd: unable to bind client port %d\n", + RD_RLM2_CLIENT_TCP_PORT); + exit(1); + } + + // + // Source Server + // + rlm_source_ready_mapper=new QSignalMapper(this); + connect(rlm_source_ready_mapper,SIGNAL(mapped(int)), + this,SLOT(sourceReadyReadData(int))); + + rlm_source_disconnect_mapper=new QSignalMapper(this); + connect(rlm_source_disconnect_mapper,SIGNAL(mapped(int)), + this,SLOT(sourceDisconnected(int))); + + rlm_source_server=new RDUnixServer(this); + connect(rlm_source_server,SIGNAL(newConnection()), + this,SLOT(newSourceConnectionData())); + if(!rlm_source_server->listenToAbstract(RD_RLM2_SOURCE_UNIX_ADDRESS)) { + fprintf(stderr,"rdrlmd: unable to bind source socket [%s]\n", + (const char *)rlm_source_server->errorString().toUtf8()); + exit(1); + } +} + + +void MainObject::newClientConnectionData() +{ + QTcpSocket *sock=rlm_client_server->nextPendingConnection(); + connect(sock,SIGNAL(disconnected()),rlm_client_disconnect_mapper,SLOT(map())); + rlm_client_disconnect_mapper->setMapping(sock,sock->socketDescriptor()); + rlm_client_sockets[sock->socketDescriptor()]=sock; + + SendState(sock->socketDescriptor()); + // printf("client connection %d opened\n",sock->socketDescriptor()); +} + + +void MainObject::clientDisconnected(int id) +{ + QTcpSocket *sock=NULL; + + if((sock=rlm_client_sockets.value(id))!=NULL) { + sock->deleteLater(); + rlm_client_sockets.remove(id); + // printf("client connection %d closed\n",id); + } + else { + fprintf(stderr,"unknown client connection %d attempted to close\n",id); + } +} + + +void MainObject::newSourceConnectionData() +{ + QTcpSocket *sock=rlm_source_server->nextPendingConnection(); + if(sock==NULL) { + fprintf(stderr,"rdrlmd: UNIX socket error [%s]\n", + (const char *)rlm_source_server->errorString().toUtf8()); + exit(1); + } + connect(sock,SIGNAL(readyRead()),rlm_source_ready_mapper,SLOT(map())); + rlm_source_ready_mapper->setMapping(sock,sock->socketDescriptor()); + + connect(sock,SIGNAL(disconnected()),rlm_source_disconnect_mapper,SLOT(map())); + rlm_source_disconnect_mapper->setMapping(sock,sock->socketDescriptor()); + + rlm_sources[sock->socketDescriptor()]=new MetadataSource(sock); + + // printf("source connection %d opened\n",sock->socketDescriptor()); +} + + +void MainObject::sourceReadyReadData(int id) +{ + if(rlm_sources[id]!=NULL) { + if(rlm_sources[id]->appendBuffer(rlm_sources[id]->socket()->readAll())) { + for(QMap::const_iterator it=rlm_client_sockets.begin(); + it!=rlm_client_sockets.end();it++) { + it.value()->write(rlm_sources[id]->buffer()); + } + } + } +} + + +void MainObject::sourceDisconnected(int id) +{ + if(rlm_sources.value(id)!=NULL) { + rlm_sources.value(id)->socket()->deleteLater(); + delete rlm_sources.value(id); + rlm_sources.remove(id); + // printf("source connection %d closed\n",id); + } + else { + fprintf(stderr,"unknown source connection %d attempted to close\n",id); + } +} + + +void MainObject::SendState(int id) +{ + for(QMap::const_iterator it=rlm_sources.begin(); + it!=rlm_sources.end();it++) { + if(it.value()->isCommitted()) { + rlm_client_sockets.value(id)->write(it.value()->buffer()); + } + } +} + + +int main(int argc,char *argv[]) +{ + QCoreApplication a(argc,argv); + + new MainObject(); + return a.exec(); +} diff --git a/rdrlmd/rdrlmd.h b/rdrlmd/rdrlmd.h new file mode 100644 index 00000000..11a046c6 --- /dev/null +++ b/rdrlmd/rdrlmd.h @@ -0,0 +1,78 @@ +// rdrlmd.h +// +// Rivendell RLM Consolidation Server +// +// (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. +// + +#ifndef RDRLMD_H +#define RDRLMD_H + +#include +#include +#include +#include +#include + +#include + +#define RDRLMD_USAGE "\n\n" + +class MetadataSource +{ + public: + MetadataSource(QTcpSocket *sock); + QByteArray buffer() const; + bool appendBuffer(const QByteArray &data); + bool isCommitted() const; + QTcpSocket *socket() const; + + private: + QByteArray meta_buffer; + bool meta_committed; + QTcpSocket *meta_socket; +}; + + + + +class MainObject : public QObject +{ + Q_OBJECT + public: + MainObject(QObject *parent=0); + + private slots: + void newClientConnectionData(); + void clientDisconnected(int id); + void newSourceConnectionData(); + void sourceReadyReadData(int id); + void sourceDisconnected(int id); + + private: + void SendState(int id); + QSignalMapper *rlm_client_disconnect_mapper; + QTcpServer *rlm_client_server; + QMap rlm_client_sockets; + + QSignalMapper *rlm_source_ready_mapper; + QSignalMapper *rlm_source_disconnect_mapper; + RDUnixServer *rlm_source_server; + QMap rlm_sources; +}; + + +#endif // RDRLMD_H diff --git a/rdservice/rdservice.h b/rdservice/rdservice.h index 9a524a02..d07400b8 100644 --- a/rdservice/rdservice.h +++ b/rdservice/rdservice.h @@ -31,12 +31,13 @@ #define RDSERVICE_CAED_ID 0 #define RDSERVICE_RIPCD_ID 1 #define RDSERVICE_RDCATCHD_ID 2 -#define RDSERVICE_RDVAIRPLAYD_ID 3 -#define RDSERVICE_RDREPLD_ID 4 -#define RDSERVICE_LOCALMAINT_ID 5 -#define RDSERVICE_SYSTEMMAINT_ID 6 -#define RDSERVICE_PURGECASTS_ID 7 -#define RDSERVICE_LAST_ID 8 +#define RDSERVICE_RDRLMD_ID 3 +#define RDSERVICE_RDVAIRPLAYD_ID 4 +#define RDSERVICE_RDREPLD_ID 5 +#define RDSERVICE_LOCALMAINT_ID 6 +#define RDSERVICE_SYSTEMMAINT_ID 7 +#define RDSERVICE_PURGECASTS_ID 8 +#define RDSERVICE_LAST_ID 9 #define RDSERVICE_FIRST_DROPBOX_ID 100 class MainObject : public QObject diff --git a/rdservice/startup.cpp b/rdservice/startup.cpp index d1b47c0f..e76448cc 100644 --- a/rdservice/startup.cpp +++ b/rdservice/startup.cpp @@ -41,6 +41,7 @@ bool MainObject::Startup(QString *err_msg) // KillProgram("rdrepld"); KillProgram("rdvairplayd"); + KillProgram("rdrlmd"); KillProgram("rdcatchd"); KillProgram("ripcd"); KillProgram("caed"); @@ -83,6 +84,19 @@ bool MainObject::Startup(QString *err_msg) return false; } + // + // rdrlmd(8) + // + svc_processes[RDSERVICE_RDRLMD_ID]=new Process(RDSERVICE_RDRLMD_ID,this); + args.clear(); + svc_processes[RDSERVICE_RDRLMD_ID]-> + start(QString(RD_PREFIX)+"/sbin/rdrlmd",args); + if(!svc_processes[RDSERVICE_RDRLMD_ID]->process()->waitForStarted(-1)) { + *err_msg=tr("unable to start rdrlmd(8)")+": "+ + svc_processes[RDSERVICE_RDRLMD_ID]->errorText(); + return false; + } + // // rdvairplayd(8) // diff --git a/rdvairplayd/rdvairplayd.cpp b/rdvairplayd/rdvairplayd.cpp index 63e91410..47d5f9fa 100644 --- a/rdvairplayd/rdvairplayd.cpp +++ b/rdvairplayd/rdvairplayd.cpp @@ -123,7 +123,7 @@ MainObject::MainObject(QObject *parent) QString default_svcname=rda->airplayConf()->defaultSvc(); for(int i=0;isetDefaultServiceName(default_svcname); // // FIXME: Add the ability to specify default carts for vLogs! @@ -134,12 +134,6 @@ MainObject::MainObject(QObject *parent) connect(air_logs[i],SIGNAL(reloaded()),reload_mapper,SLOT(map())); rename_mapper->setMapping(air_logs[i],i); connect(air_logs[i],SIGNAL(renamed()),rename_mapper,SLOT(map())); - // connect(air_logs[i],SIGNAL(refreshStatusChanged(bool)), - // this,SLOT(refreshStatusChangedData(bool))); - // connect(air_logs[i],SIGNAL(channelStarted(int,int,int,int)), - // this,SLOT(logChannelStartedData(int,int,int,int))); - // connect(air_logs[i],SIGNAL(channelStopped(int,int,int,int)), - // this,SLOT(logChannelStoppedData(int,int,int,int))); int cards[2]={0,0}; cards[0]=rda->airplayConf()->virtualCard(i+RD_RDVAIRPLAY_LOG_BASE); cards[1]=rda->airplayConf()->virtualCard(i+RD_RDVAIRPLAY_LOG_BASE); diff --git a/rivendell.spec.in b/rivendell.spec.in index 4dc1076f..3370a3b6 100644 --- a/rivendell.spec.in +++ b/rivendell.spec.in @@ -388,6 +388,7 @@ rm -rf $RPM_BUILD_ROOT @LOCAL_PREFIX@/sbin/rdcatchd @LOCAL_PREFIX@/sbin/rdvairplayd @LOCAL_PREFIX@/sbin/rdrepld +@LOCAL_PREFIX@/sbin/rdrlmd @LOCAL_PREFIX@/sbin/sas_shim @LOCAL_PREFIX@/sbin/rdmarkerset @LOCAL_PREFIX@/sbin/rdcleandirs