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

Conflicts:
	ChangeLog
This commit is contained in:
Patrick Linstruth 2018-12-10 07:18:56 -08:00
commit 1793a2474d
31 changed files with 1812 additions and 26 deletions

2
.gitignore vendored
View File

@ -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

View File

@ -18094,6 +18094,50 @@
2018-11-30 Patrick Linstruth <patrick@deltecent.com>
* Fixed regression with rdimport(1) that threw SQL errors when
importing into an existing cart.
2018-12-10 Patrick Linstruth <patrick@deltecent.com>
* Fixed a bug that was causing rdairplay(1) to segfault when no
log was loaded.
2018-12-04 Fred Gleason <fredg@paravelsystems.com>
* Added an rdrlmd(8) service.
* Implemented JSON-formatted PAD output on TCP port 34289.
2018-12-05 Fred Gleason <fredg@paravelsystems.com>
* Added a set of enclosing '{}' braces around the JSON-formatted PAD
output to make it well-formed.
2018-12-05 Fred Gleason <fredg@paravelsystems.com>
* Added a set of Python classes for processing PAD updates.
2018-12-06 Fred Gleason <fredg@paravelsystems.com>
* Added comments to the 'now_and_next.py' PyPAD script.
2018-12-06 Fred Gleason <fredg@paravelsystems.com>
* Removed support for the 'priv' argument in callbacks in PyPAD
scripts.
2018-12-06 Fred Gleason <fredg@paravelsystems.com>
* Added support for '\b', '\f', '\n' '\r' and '\t' control escapes
in the 'PyPADUpdate::padFields()' method.
2018-12-06 Fred Gleason <fredg@paravelsystems.com>
* Added a 'pypad_udp.py' PyPAD script.
2018-12-07 Fred Gleason <fredg@paravelsystems.com>
* 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 <fredg@paravelsystems.com>
* 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(<dt>)' and '%D(<dt>)' wildcards in
'PyPAD.Update::padFields()'.
2018-12-09 Fred Gleason <fredg@paravelsystems.com>
* Renamed the 'PyPAD.Update::padFields()' method to
'PyPAD.Update::resolvePadFields()'.
* Added a 'PyPAD.Update::padField()' method.
* Added a 'PyPAD.Update::escape()' method.

View File

@ -50,6 +50,7 @@ SUBDIRS = icons\
rdmonitor\
rdpanel\
rdrepld\
rdrlmd\
rdselect\
rdservice\
rdvairplayd\

View File

@ -20,7 +20,8 @@
##
## Use automake to process this into a Makefile.in
SUBDIRS = rivwebcapi\
SUBDIRS = PyPAD\
rivwebcapi\
rlm
CLEANFILES = *~\

39
apis/PyPAD/Makefile.am Normal file
View File

@ -0,0 +1,39 @@
## automake.am
##
## Automake.am for Rivendell PyPAD
##
## (C) Copyright 2018 Fred Gleason <fredg@paravelsystems.com>
##
## 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_*

View File

@ -0,0 +1,39 @@
## automake.am
##
## Automake.am for Rivendell PyPAD/api
##
## (C) Copyright 2018 Fred Gleason <fredg@paravelsystems.com>
##
## 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_*

484
apis/PyPAD/api/PyPAD.py Normal file
View File

@ -0,0 +1,484 @@
#!/usr/bin/python
# PyPAD.py
#
# PAD processor for Rivendell
#
# (C) Copyright 2018 Fred Gleason <fredg@paravelsystems.com>
#
# 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("&","&amp;")
string=string.replace("<","&lt;")
string=string.replace(">","&gt;")
string=string.replace("'","&apos;")
string=string.replace("\"","&quot;")
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(<dt>) Handler
string=self.__replaceDatetimePair(string,'D') # %D(<dt>) 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=""

View File

@ -0,0 +1,39 @@
## automake.am
##
## Automake.am for Rivendell PyPAD/examples
##
## (C) Copyright 2018 Fred Gleason <fredg@paravelsystems.com>
##
## 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_*

View File

@ -0,0 +1,63 @@
#!/usr/bin/python
# now_and_next.py
#
# Example PyPAD script for Rivendell
#
# (C) Copyright 2018 Fred Gleason <fredg@paravelsystems.com>
#
# 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)

View File

@ -0,0 +1,64 @@
#!/usr/bin/python
# pypad_udp.py
#
# Send PAD updates via UDP
#
# (C) Copyright 2018 Fred Gleason <fredg@paravelsystems.com>
#
# 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)

View File

@ -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 \

View File

@ -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

View File

@ -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

View File

@ -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;
}

View File

@ -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<RDRLMHost *> *rlm_hosts,
QObject *parent)
: QObject(parent),RDLogEvent(logname)
std::vector<RDRLMHost *> *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;i<play_rlm_hosts->size();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

View File

@ -39,6 +39,7 @@
#include <rdplay_deck.h>
#include <rdrlmhost.h>
#include <rdsimpleplayer.h>
#include <rdunixsocket.h>
//
// 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<RDRLMHost *> *rlm_hosts,QObject *parent=0);
RDLogPlay(int id,RDEventPlayer *player,Q3SocketDevice *nn_sock,
std::vector<RDRLMHost *> *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;
};

191
lib/rdunixserver.cpp Normal file
View File

@ -0,0 +1,191 @@
// rdunixserver.cpp
//
// UNIX Socket Server
//
// (C) Copyright 2018 Fred Gleason <fredg@paravelsystems.com>
//
// 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 <errno.h>
#include <linux/un.h>
#include <sys/socket.h>
#include <sys/types.h>
#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();
}

68
lib/rdunixserver.h Normal file
View File

@ -0,0 +1,68 @@
// rdunixserver.h
//
// UNIX Socket Server
//
// (C) Copyright 2018 Fred Gleason <fredg@paravelsystems.com>
//
// 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 <qobject.h>
#include <qsocketnotifier.h>
#include <qtcpsocket.h>
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

58
lib/rdunixsocket.cpp Normal file
View File

@ -0,0 +1,58 @@
// rdunixsocket.cpp
//
// UNIX Socket
//
// (C) Copyright 2018 Fred Gleason <fredg@paravelsystems.com>
//
// 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 <linux/un.h>
#include <sys/socket.h>
#include <sys/types.h>
#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;
}

43
lib/rdunixsocket.h Normal file
View File

@ -0,0 +1,43 @@
// rdunixsocket.h
//
// UNIX Socket
//
// (C) Copyright 2018 Fred Gleason <fredg@paravelsystems.com>
//
// 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 <qtcpsocket.h>
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

View File

@ -1043,6 +1043,110 @@ QString RDXmlUnescape(const QString &str)
}
QString RDJsonPadding(int padding)
{
QString ret="";
for(int i=0;i<padding;i++) {
ret+=" ";
}
return ret;
}
QString RDJsonNullField(const QString &name,int padding,bool final)
{
QString comma=",";
if(final) {
comma="";
}
return RDJsonPadding(padding)+"\""+name+"\": null"+comma+"\r\n";
}
QString RDJsonField(const QString &name,bool value,int padding,bool final)
{
QString comma=",";
if(final) {
comma="";
}
if(value) {
return RDJsonPadding(padding)+"\""+name+"\": true"+comma+"\r\n";
}
return RDJsonPadding(padding)+"\""+name+"\": false"+comma+"\r\n";
}
QString RDJsonField(const QString &name,int value,int padding,bool final)
{
QString comma=",";
if(final) {
comma="";
}
return RDJsonPadding(padding)+"\""+name+"\": "+QString().sprintf("%d",value)+
comma+"\r\n";
}
QString RDJsonField(const QString &name,unsigned value,int padding,bool final)
{
QString comma=",";
if(final) {
comma="";
}
return RDJsonPadding(padding)+"\""+name+"\": "+QString().sprintf("%u",value)+
comma+"\r\n";
}
QString RDJsonField(const QString &name,const QString &value,int padding,
bool final)
{
QString str=value;
QString comma=",";
if(final) {
comma="";
}
str.replace("\\","\\\\");
str.replace("\"","\\\"");
str.replace("/","\\/");
str.replace("\b","\\b");
str.replace("\f","\\f");
str.replace("\n","\\n");
str.replace("\r","\\r");
str.replace("\t","\\t");
return RDJsonPadding(padding)+"\""+name+"\": \""+str+"\""+comma+"\r\n";
}
QString RDJsonField(const QString &name,const QDateTime &value,int padding,
bool final)
{
QString comma=",";
if(final) {
comma="";
}
if(!value.isValid()) {
return RDJsonNullField(name,padding,final);
}
return RDJsonPadding(padding)+"\""+name+"\": \""+RDXmlDateTime(value)+"\""+
comma+"\r\n";
}
QString RDUrlEscape(const QString &str)
{
/*
@ -1098,7 +1202,18 @@ QString RDWebDateTime(const QDateTime &datetime)
if(!datetime.isValid()) {
return QString();
}
return RDLocalToUtc(datetime).toString("ddd, dd MMM yyyy hh:mm:ss")+" GMT";
int offset=RDTimeZoneOffset();
QString tzstr="-";
if(offset<0) {
tzstr="+";
}
tzstr+=QString().sprintf("%02d%02d",
offset/3600,(offset/60)-((offset/3600)*60));
if(offset==0) {
tzstr="GMT";
}
return datetime.toString("ddd, dd MMM yyyy hh:mm:ss")+" "+tzstr;
}

View File

@ -88,6 +88,21 @@ extern QString RDXmlDateTime(const QDateTime &datetime);
extern QString RDXmlTimeZoneSuffix();
extern QString RDXmlEscape(const QString &str);
extern QString RDXmlUnescape(const QString &str);
extern QString RDJsonPadding(int padding);
extern QString RDJsonNullField(const QString &name,int padding=0,
bool final=false);
extern QString RDJsonField(const QString &name,bool value,int padding=0,
bool final=false);
extern QString RDJsonField(const QString &name,int value,int padding=0,
bool final=false);
extern QString RDJsonField(const QString &name,unsigned value,int padding=0,
bool final=false);
extern QString RDJsonField(const QString &name,const QString &value,
int padding=0,bool final=false);
extern QString RDJsonField(const QString &name,const QDateTime &value,
int padding=0,bool final=false);
extern QString RDUrlEscape(const QString &str);
extern QString RDUrlUnescape(const QString &str);
extern QString RDWebDateTime(const QDateTime &datetime);

View File

@ -315,6 +315,16 @@ MainWidget::MainWidget(QWidget *parent)
//
air_nownext_socket=new Q3SocketDevice(Q3SocketDevice::Datagram);
/*
//
// RLM2 Connection
//
air_pad_socket=new RDUnixSocket(this);
if(!air_pad_socket->connectToAbstract(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;i<RDAIRPLAY_LOG_QUANTITY;i++) {
air_log[i]=new RDLogPlay(i,rdevent_player,air_nownext_socket,"",
&air_plugin_hosts);
air_log[i]=
new RDLogPlay(i,rdevent_player,air_nownext_socket,&air_plugin_hosts);
air_log[i]->setDefaultServiceName(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();

View File

@ -52,6 +52,7 @@
#include <rdsound_panel.h>
#include <rdstereometer.h>
#include <rdttydevice.h>
#include <rdunixsocket.h>
#include <rduser.h>
#include "button_log.h"

48
rdrlmd/Makefile.am Normal file
View File

@ -0,0 +1,48 @@
## Makefile.am
##
## Rivendell RLM Consolidation Server
##
## (C) Copyright 2018 Fred Gleason <fredg@paravelsystems.com>
##
## 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

207
rdrlmd/rdrlmd.cpp Normal file
View File

@ -0,0 +1,207 @@
// rdrlmd.cpp
//
// Rivendell RLM Consolidation Server
//
// (C) Copyright 2018 Fred Gleason <fredg@paravelsystems.com>
//
// 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 <stdio.h>
#include <stdlib.h>
#include <qcoreapplication.h>
#include <qhostaddress.h>
#include <rd.h>
#include <rdcmd_switch.h>
#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<int,QTcpSocket *>::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<int,MetadataSource *>::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();
}

78
rdrlmd/rdrlmd.h Normal file
View File

@ -0,0 +1,78 @@
// rdrlmd.h
//
// Rivendell RLM Consolidation Server
//
// (C) Copyright 2018 Fred Gleason <fredg@paravelsystems.com>
//
// 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 <qmap.h>
#include <qobject.h>
#include <qsignalmapper.h>
#include <qtcpserver.h>
#include <qtcpsocket.h>
#include <rdunixserver.h>
#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<int,QTcpSocket *> rlm_client_sockets;
QSignalMapper *rlm_source_ready_mapper;
QSignalMapper *rlm_source_disconnect_mapper;
RDUnixServer *rlm_source_server;
QMap<int,MetadataSource *> rlm_sources;
};
#endif // RDRLMD_H

View File

@ -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

View File

@ -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)
//

View File

@ -123,7 +123,7 @@ MainObject::MainObject(QObject *parent)
QString default_svcname=rda->airplayConf()->defaultSvc();
for(int i=0;i<RD_RDVAIRPLAY_LOG_QUAN;i++) {
air_logs[i]=new RDLogPlay(i+RD_RDVAIRPLAY_LOG_BASE,air_event_player,
air_nownext_socket,"",&air_plugin_hosts);
air_nownext_socket,&air_plugin_hosts);
air_logs[i]->setDefaultServiceName(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);

View File

@ -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