#!/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' # # 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(
) 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 ('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=""