mirror of
https://github.com/ElvishArtisan/rivendell.git
synced 2025-04-27 06:36:37 +02:00
* 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()'.
393 lines
14 KiB
Python
393 lines
14 KiB
Python
#!/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'
|
|
|
|
#
|
|
# 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 __escape(self,string,esc):
|
|
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 __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])
|
|
#print 'pos: '+str(pattern[0])+' pattern: '+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 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 padFields(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
|
|
('PyPAD.PAD_NOW' or 'PyPAD.PAD_NEXT')
|
|
"""
|
|
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
|
|
('PyPAD.PAD_NOW' or 'PyPAD.PAD_NEXT')
|
|
"""
|
|
try:
|
|
return self.__fromIso8601(self.__fields['padUpdate'][pad_type]['startDateTime'])
|
|
except AttributeError:
|
|
return None
|
|
|
|
|
|
|
|
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=""
|
|
|
|
|