From 9e6fb9f3c07375123ace6f58b392d3cb8d5a5fc0 Mon Sep 17 00:00:00 2001 From: Fred Gleason Date: Tue, 4 Dec 2018 18:06:13 -0500 Subject: [PATCH 01/11] 2018-12-04 Fred Gleason * Added an rdrlmd(8) service. * Implemented JSON-formatted PAD output on TCP port 34289. --- .gitignore | 1 + ChangeLog | 3 + Makefile.am | 1 + configure.ac | 1 + lib/Makefile.am | 4 + lib/rd.h | 6 ++ lib/rdlogplay.cpp | 119 ++++++++++++++++++++- lib/rdlogplay.h | 8 +- lib/rdunixserver.cpp | 191 +++++++++++++++++++++++++++++++++ lib/rdunixserver.h | 68 ++++++++++++ lib/rdunixsocket.cpp | 58 ++++++++++ lib/rdunixsocket.h | 43 ++++++++ lib/rdweb.cpp | 117 +++++++++++++++++++- lib/rdweb.h | 15 +++ rdairplay/rdairplay.cpp | 15 ++- rdairplay/rdairplay.h | 1 + rdrlmd/Makefile.am | 48 +++++++++ rdrlmd/rdrlmd.cpp | 207 ++++++++++++++++++++++++++++++++++++ rdrlmd/rdrlmd.h | 78 ++++++++++++++ rdservice/rdservice.h | 13 +-- rdservice/startup.cpp | 14 +++ rdvairplayd/rdvairplayd.cpp | 8 +- rivendell.spec.in | 1 + 23 files changed, 999 insertions(+), 21 deletions(-) create mode 100644 lib/rdunixserver.cpp create mode 100644 lib/rdunixserver.h create mode 100644 lib/rdunixsocket.cpp create mode 100644 lib/rdunixsocket.h create mode 100644 rdrlmd/Makefile.am create mode 100644 rdrlmd/rdrlmd.cpp create mode 100644 rdrlmd/rdrlmd.h diff --git a/.gitignore b/.gitignore index df9345b4..f4b33ae4 100644 --- a/.gitignore +++ b/.gitignore @@ -83,6 +83,7 @@ rdmonitor/rdmonitor rdpanel/rdpanel rdrepld/rdrepld rdrepld-suse +rdrlmd/rdrlmd rdselect/rdselect rdservice/rdservice rdvairplayd/rdvairplayd diff --git a/ChangeLog b/ChangeLog index 1e71c8cd..a95ca202 100644 --- a/ChangeLog +++ b/ChangeLog @@ -18094,3 +18094,6 @@ 2018-11-30 Patrick Linstruth * Fixed regression with rdimport(1) that threw SQL errors when importing into an existing cart. +2018-12-04 Fred Gleason + * Added an rdrlmd(8) service. + * Implemented JSON-formatted PAD output on TCP port 34289. 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/configure.ac b/configure.ac index 7a30ce22..456724ff 100644 --- a/configure.ac +++ b/configure.ac @@ -507,6 +507,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/rdlogplay.cpp b/lib/rdlogplay.cpp index 53f27d9f..46369502 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,62 @@ void RDLogPlay::SendNowNext() if(svcname.isEmpty()) { svcname=play_defaultsvc_name; } + + // + // RLM2 + // + play_pad_socket->write(QString("\"padUpdate\": {\r\n").toUtf8()); + play_pad_socket->write(RDJsonField("dateTime",QDateTime::currentDateTime(),4).toUtf8()); + play_pad_socket->write(RDJsonField("logMachine",play_id,4)); + play_pad_socket->write(RDJsonField("onairFlag",play_onair_flag,4)); + play_pad_socket->write(RDJsonField("logMode",RDAirPlayConf::logModeText(play_op_mode),4)); + + // + // 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,8).toUtf8()); + play_pad_socket-> + write(RDJsonField("description",svc->description(),8).toUtf8()); + play_pad_socket-> + write(RDJsonField("programCode",svc->programCode(),8,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(),8,true).toUtf8()); + play_pad_socket->write(QString(" },\r\n").toUtf8()); + + // + // Now + // + play_pad_socket->write(GetPadJson("now",logline[0],4,false).toUtf8()); + + // + // Next + // + play_pad_socket->write(GetPadJson("next",logline[1],4,true).toUtf8()); + + // + // Commit the update + // + play_pad_socket->write("}\r\n\r\n",5); + + // + // 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 +3033,56 @@ void RDLogPlay::SendNowNext() } +QString RDLogPlay::GetPadJson(const QString &name,RDLogLine *ll, + int padding,bool final) const +{ + QString ret; + + if(ll==NULL) { + ret=RDJsonNullField(name,4,final); + } + else { + ret+=RDJsonPadding(padding)+"\""+name+"\": {\r\n"; + 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..1e56061b 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,8 @@ 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,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 +249,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..c255fb83 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 From fc0023a0cd5a3a8227512cc6159ae070df71111c Mon Sep 17 00:00:00 2001 From: Fred Gleason Date: Wed, 5 Dec 2018 11:33:35 -0500 Subject: [PATCH 02/11] 2018-12-05 Fred Gleason * Added a set of enclosing '{}' braces around the JSON-formatted PAD output to make it well-formed. --- ChangeLog | 3 +++ lib/rdlogplay.cpp | 36 +++++++++++++++++++----------------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/ChangeLog b/ChangeLog index a95ca202..6f22c37e 100644 --- a/ChangeLog +++ b/ChangeLog @@ -18097,3 +18097,6 @@ 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. diff --git a/lib/rdlogplay.cpp b/lib/rdlogplay.cpp index 46369502..6259de4e 100644 --- a/lib/rdlogplay.cpp +++ b/lib/rdlogplay.cpp @@ -2961,46 +2961,48 @@ void RDLogPlay::SendNowNext() // // RLM2 // - play_pad_socket->write(QString("\"padUpdate\": {\r\n").toUtf8()); - play_pad_socket->write(RDJsonField("dateTime",QDateTime::currentDateTime(),4).toUtf8()); - play_pad_socket->write(RDJsonField("logMachine",play_id,4)); - play_pad_socket->write(RDJsonField("onairFlag",play_onair_flag,4)); - play_pad_socket->write(RDJsonField("logMode",RDAirPlayConf::logModeText(play_op_mode),4)); + 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,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,8).toUtf8()); + 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(),8).toUtf8()); + write(RDJsonField("description",svc->description(),12).toUtf8()); play_pad_socket-> - write(RDJsonField("programCode",svc->programCode(),8,true).toUtf8()); - play_pad_socket->write(QString(" },\r\n").toUtf8()); + 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(),8,true).toUtf8()); - play_pad_socket->write(QString(" },\r\n").toUtf8()); + 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 // - play_pad_socket->write(GetPadJson("now",logline[0],4,false).toUtf8()); + play_pad_socket->write(GetPadJson("now",logline[0],8,false).toUtf8()); // // Next // - play_pad_socket->write(GetPadJson("next",logline[1],4,true).toUtf8()); + play_pad_socket->write(GetPadJson("next",logline[1],8,true).toUtf8()); // // Commit the update // - play_pad_socket->write("}\r\n\r\n",5); + play_pad_socket->write(QString(" }\r\n").toUtf8()); + play_pad_socket->write(QString("}\r\n\r\n").toUtf8()); // // Old-style RLM Hosts @@ -3039,7 +3041,7 @@ QString RDLogPlay::GetPadJson(const QString &name,RDLogLine *ll, QString ret; if(ll==NULL) { - ret=RDJsonNullField(name,4,final); + ret=RDJsonNullField(name,padding,final); } else { ret+=RDJsonPadding(padding)+"\""+name+"\": {\r\n"; From 1d8e30310170eaad3c64fda4b452adfa043b5886 Mon Sep 17 00:00:00 2001 From: Fred Gleason Date: Wed, 5 Dec 2018 20:15:10 -0500 Subject: [PATCH 03/11] 2018-12-05 Fred Gleason * Added a set of Python classes for processing PAD updates. --- ChangeLog | 2 + apis/Makefile.am | 3 +- apis/PyPAD/Makefile.am | 39 ++++++ apis/PyPAD/api/Makefile.am | 40 ++++++ apis/PyPAD/api/PyPAD.py | 198 ++++++++++++++++++++++++++++ apis/PyPAD/api/__init__.py | 0 apis/PyPAD/examples/Makefile.am | 38 ++++++ apis/PyPAD/examples/now_and_next.py | 18 +++ configure.ac | 8 ++ lib/rdconf.cpp | 10 +- lib/rdlogplay.cpp | 2 +- lib/rdweb.cpp | 2 +- 12 files changed, 356 insertions(+), 4 deletions(-) create mode 100644 apis/PyPAD/Makefile.am create mode 100644 apis/PyPAD/api/Makefile.am create mode 100644 apis/PyPAD/api/PyPAD.py create mode 100644 apis/PyPAD/api/__init__.py create mode 100644 apis/PyPAD/examples/Makefile.am create mode 100755 apis/PyPAD/examples/now_and_next.py diff --git a/ChangeLog b/ChangeLog index 6f22c37e..f328481e 100644 --- a/ChangeLog +++ b/ChangeLog @@ -18100,3 +18100,5 @@ 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. 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..a3e3ced0 --- /dev/null +++ b/apis/PyPAD/api/Makefile.am @@ -0,0 +1,40 @@ +## 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 = $(pkgpyexecdir) +rivendell_PYTHON = __init__.py\ + 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..75be895e --- /dev/null +++ b/apis/PyPAD/api/PyPAD.py @@ -0,0 +1,198 @@ +#!/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 socket +import json + +class PyPADUpdate(object): + def __init__(self,pad_data): + self.__fields=pad_data; + + def __replace(self,wildcard,string): + stype='now' + if wildcard[1].isupper(): + stype='next' + sfields={'a':'artist','b':'label','c':'client','d':'','e':'agency', + 'f':'','g':'groupName','h':'length','i':'description', + 'j':'cutNumber','k':'','l':'album','m': 'composer', + 'n':'cartNumber','o':'outcue','p':'publisher','q': '', + 'r':'conductor','s':'songId','t':'title','u':'userDefined', + 'v':'','w':'','x':'','y':'year', + 'z':''} + sfield=sfields[wildcard[1].lower()] + + try: + string=string.replace(wildcard,self.__fields['padUpdate'][stype][sfield]) + except TypeError: + string=string.replace(wildcard,'') + except KeyError: + string=string.replace(wildcard,'') + return string + + def dateTime(self): + """ + Returns the date-time stamp of the update in RFC-822 format (string). + """ + return self.__fields['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): + """ + Takes an argument of 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. + """ + for i in range(65,68): + string=self.__replace('%'+chr(i),string) + for i in range(69,91): + string=self.__replace('%'+chr(i),string) + for i in range(97,100): + string=self.__replace('%'+chr(i),string) + for i in range(101,123): + string=self.__replace('%'+chr(i),string) + return string + + def hasNowPad(self): + """ + Indicates if this update include 'Now' playing PAD. + """ + try: + return self.__fields['padUpdate']['now']!=None + except TypeError: + return False; + + def hasNextPad(self): + """ + Indicates if this update include 'Next' playing PAD. + """ + try: + return self.__fields['padUpdate']['next']!=None + except TypeError: + return False; + + +class PyPADReceiver(object): + def __init__(self): + self.__callback=None + self.__priv=None + + def __PyPAD_Process(self,pad): + self.__callback(pad,self.__priv) + + def setCallback(self,cb): + """ + Set the processing callback. + """ + self.__callback=cb + + def setPrivateObject(self,obj): + """ + Set the 'private' object. This object will be passed to the + callback in its second argument. + """ + self.priv=obj + + def start(self,hostname): + """ + 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 argument: + + hostname: The hostname or IP address of the Rivendell system. + """ + sock=socket.socket(socket.AF_INET) + conn=sock.connect((hostname,34289)) + 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(PyPADUpdate(json.loads(msg))) + msg="" + line="" + + diff --git a/apis/PyPAD/api/__init__.py b/apis/PyPAD/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apis/PyPAD/examples/Makefile.am b/apis/PyPAD/examples/Makefile.am new file mode 100644 index 00000000..2c62dab5 --- /dev/null +++ b/apis/PyPAD/examples/Makefile.am @@ -0,0 +1,38 @@ +## 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 + +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..dca86d35 --- /dev/null +++ b/apis/PyPAD/examples/now_and_next.py @@ -0,0 +1,18 @@ +#!/usr/bin/python + +import rivendell.PyPAD + +def ProcessPad(update,priv): + print + if update.hasNowPad(): + print update.padFields("NOW: %a - %t") + else: + print "NOW: [none]" + if update.hasNextPad(): + print update.padFields("NEXT: %A - %T") + else: + print "NEXT: [none]" + +rcvr=rivendell.PyPAD.PyPADReceiver() +rcvr.setCallback(ProcessPad) +rcvr.start("localhost") diff --git a/configure.ac b/configure.ac index 456724ff..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 \ 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 6259de4e..2e2b84f1 100644 --- a/lib/rdlogplay.cpp +++ b/lib/rdlogplay.cpp @@ -2964,7 +2964,7 @@ void RDLogPlay::SendNowNext() play_pad_socket->write(QString("{\r\n").toUtf8()); play_pad_socket->write(QString(" \"padUpdate\": {\r\n").toUtf8()); play_pad_socket->write(RDJsonField("dateTime",QDateTime::currentDateTime(),8).toUtf8()); - play_pad_socket->write(RDJsonField("logMachine",play_id,8)); + 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)); diff --git a/lib/rdweb.cpp b/lib/rdweb.cpp index c255fb83..bcc991d1 100644 --- a/lib/rdweb.cpp +++ b/lib/rdweb.cpp @@ -1213,7 +1213,7 @@ QString RDWebDateTime(const QDateTime &datetime) tzstr="GMT"; } - return RDLocalToUtc(datetime).toString("ddd, dd MMM yyyy hh:mm:ss")+" "+tzstr; + return datetime.toString("ddd, dd MMM yyyy hh:mm:ss")+" "+tzstr; } From c80f67cfd0a753440f74dee1b9b9c66b33269759 Mon Sep 17 00:00:00 2001 From: Fred Gleason Date: Thu, 6 Dec 2018 13:51:43 -0500 Subject: [PATCH 04/11] 2018-12-06 Fred Gleason * Added comments to the 'now_and_next.py' PyPAD script. --- ChangeLog | 2 + apis/PyPAD/examples/now_and_next.py | 59 +++++++++++++++++++++++++++-- 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/ChangeLog b/ChangeLog index f328481e..59f83084 100644 --- a/ChangeLog +++ b/ChangeLog @@ -18102,3 +18102,5 @@ 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. diff --git a/apis/PyPAD/examples/now_and_next.py b/apis/PyPAD/examples/now_and_next.py index dca86d35..efdfdb22 100755 --- a/apis/PyPAD/examples/now_and_next.py +++ b/apis/PyPAD/examples/now_and_next.py @@ -1,18 +1,69 @@ #!/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 rivendell.PyPAD +# help(rivendell.PyPAD) +# import rivendell.PyPAD +# +# First, we create a callback method, that will be called every time a +# log machine updates its PAD. Two arguments are supplied: +# +# update - An instance of 'PyPADUpdate' that contains the PAD information. +# +# priv - An arbitrary object that was passed to 'PyPADReceiver' using +# its 'setPrivateObject()' method. This allows initialiation to +# be done (create sockets, file handles, etc) before starting the +# receiver and then passing those objects to the processing callback. +# def ProcessPad(update,priv): print if update.hasNowPad(): - print update.padFields("NOW: %a - %t") + print "Log %03d NOW: " % update.logMachine()+update.padFields("%a - %t") else: - print "NOW: [none]" + print "Log %03d NOW: [none]" % update.logMachine() if update.hasNextPad(): - print update.padFields("NEXT: %A - %T") + print "Log %03d NEXT: " % update.logMachine()+update.padFields("%A - %T") else: - print "NEXT: [none]" + print "Log %03d NEXT: [none]" % update.logMachine() +# +# Create an instance of 'PyPADReceiver' +# rcvr=rivendell.PyPAD.PyPADReceiver() + +# +# Tell it to use the callback +# rcvr.setCallback(ProcessPad) + +# +# Start the receiver, giving it the hostname or IP address of the target +# Rivendell system. Once started, all further processing can only be done +# in the callback method! +# rcvr.start("localhost") From 340f83483975b0e62779741bd4e41fdebd112625 Mon Sep 17 00:00:00 2001 From: Fred Gleason Date: Thu, 6 Dec 2018 15:54:19 -0500 Subject: [PATCH 05/11] 2018-12-06 Fred Gleason * Removed support for the 'priv' argument in callbacks in PyPAD scripts. --- ChangeLog | 3 +++ apis/PyPAD/api/PyPAD.py | 10 +--------- apis/PyPAD/examples/now_and_next.py | 12 +++--------- 3 files changed, 7 insertions(+), 18 deletions(-) diff --git a/ChangeLog b/ChangeLog index 59f83084..b899f317 100644 --- a/ChangeLog +++ b/ChangeLog @@ -18104,3 +18104,6 @@ * 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. diff --git a/apis/PyPAD/api/PyPAD.py b/apis/PyPAD/api/PyPAD.py index 75be895e..187905aa 100644 --- a/apis/PyPAD/api/PyPAD.py +++ b/apis/PyPAD/api/PyPAD.py @@ -152,10 +152,9 @@ class PyPADUpdate(object): class PyPADReceiver(object): def __init__(self): self.__callback=None - self.__priv=None def __PyPAD_Process(self,pad): - self.__callback(pad,self.__priv) + self.__callback(pad) def setCallback(self,cb): """ @@ -163,13 +162,6 @@ class PyPADReceiver(object): """ self.__callback=cb - def setPrivateObject(self,obj): - """ - Set the 'private' object. This object will be passed to the - callback in its second argument. - """ - self.priv=obj - def start(self,hostname): """ Connect to a Rivendell system and begin processing PAD events. diff --git a/apis/PyPAD/examples/now_and_next.py b/apis/PyPAD/examples/now_and_next.py index efdfdb22..ecae1399 100755 --- a/apis/PyPAD/examples/now_and_next.py +++ b/apis/PyPAD/examples/now_and_next.py @@ -31,16 +31,10 @@ import rivendell.PyPAD # # First, we create a callback method, that will be called every time a -# log machine updates its PAD. Two arguments are supplied: +# log machine updates its PAD. An instance of 'PyPADUpdate' that contains +# the PAD information is supplied as the single argument. # -# update - An instance of 'PyPADUpdate' that contains the PAD information. -# -# priv - An arbitrary object that was passed to 'PyPADReceiver' using -# its 'setPrivateObject()' method. This allows initialiation to -# be done (create sockets, file handles, etc) before starting the -# receiver and then passing those objects to the processing callback. -# -def ProcessPad(update,priv): +def ProcessPad(update): print if update.hasNowPad(): print "Log %03d NOW: " % update.logMachine()+update.padFields("%a - %t") From d2faec8c7d57818a4046a72512a6afeab028da30 Mon Sep 17 00:00:00 2001 From: Fred Gleason Date: Thu, 6 Dec 2018 16:10:49 -0500 Subject: [PATCH 06/11] Updated '.gitignore' --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index f4b33ae4..2403beda 100644 --- a/.gitignore +++ b/.gitignore @@ -69,6 +69,7 @@ missing Makefile.in Makefile moc_* +py-compile rdadmin/rdadmin rdairplay/rdairplay rdcastmanager/rdcastmanager From 03141e64212b70a43c329f9085d956b4ef0eb380 Mon Sep 17 00:00:00 2001 From: Fred Gleason Date: Thu, 6 Dec 2018 17:49:00 -0500 Subject: [PATCH 07/11] 2018-12-06 Fred Gleason * Added support for '\b', '\f', '\n' '\r' and '\t' control escapes in the 'PyPADUpdate::padFields()' method. --- ChangeLog | 3 +++ apis/PyPAD/api/PyPAD.py | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/ChangeLog b/ChangeLog index b899f317..6e8e928c 100644 --- a/ChangeLog +++ b/ChangeLog @@ -18107,3 +18107,6 @@ 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. diff --git a/apis/PyPAD/api/PyPAD.py b/apis/PyPAD/api/PyPAD.py index 187905aa..49d62924 100644 --- a/apis/PyPAD/api/PyPAD.py +++ b/apis/PyPAD/api/PyPAD.py @@ -128,6 +128,11 @@ class PyPADUpdate(object): string=self.__replace('%'+chr(i),string) for i in range(101,123): string=self.__replace('%'+chr(i),string) + 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 hasNowPad(self): From 3a65043bd602cf5ed7ac6d592bcca5083239e941 Mon Sep 17 00:00:00 2001 From: Fred Gleason Date: Thu, 6 Dec 2018 18:40:56 -0500 Subject: [PATCH 08/11] 2018-12-06 Fred Gleason * Added a 'pypad_udp.py' PyPAD script. --- ChangeLog | 2 + apis/PyPAD/examples/Makefile.am | 3 +- apis/PyPAD/examples/pypad_udp.py | 64 ++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) create mode 100755 apis/PyPAD/examples/pypad_udp.py diff --git a/ChangeLog b/ChangeLog index 6e8e928c..ca3bb111 100644 --- a/ChangeLog +++ b/ChangeLog @@ -18110,3 +18110,5 @@ 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. diff --git a/apis/PyPAD/examples/Makefile.am b/apis/PyPAD/examples/Makefile.am index 2c62dab5..f6efe3a3 100644 --- a/apis/PyPAD/examples/Makefile.am +++ b/apis/PyPAD/examples/Makefile.am @@ -20,7 +20,8 @@ ## ## Use automake to process this into a Makefile.in -EXTRA_DIST = now_and_next.py +EXTRA_DIST = now_and_next.py\ + pypad_udp.py CLEANFILES = *~\ *.idb\ diff --git a/apis/PyPAD/examples/pypad_udp.py b/apis/PyPAD/examples/pypad_udp.py new file mode 100755 index 00000000..0bda689e --- /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 rivendell.PyPAD + +def eprint(*args,**kwargs): + print(*args,file=sys.stderr,**kwargs) + +def ProcessPad(update): + n=1 + while(True): + section='Udp'+str(n) + try: + format=config.get(section,'FormatString') + send_sock.sendto(update.padFields(format), + (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=rivendell.PyPAD.PyPADReceiver() +rcvr.setCallback(ProcessPad) +rcvr.start("localhost") From 265146f01e2b801cb28fa04954fba60655cad1d8 Mon Sep 17 00:00:00 2001 From: Fred Gleason Date: Fri, 7 Dec 2018 13:52:57 -0500 Subject: [PATCH 09/11] 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. --- ChangeLog | 10 ++ apis/PyPAD/api/Makefile.am | 5 +- apis/PyPAD/api/PyPAD.py | 180 ++++++++++++++++++++++------ apis/PyPAD/api/__init__.py | 0 apis/PyPAD/examples/now_and_next.py | 22 ++-- apis/PyPAD/examples/pypad_udp.py | 8 +- 6 files changed, 168 insertions(+), 57 deletions(-) delete mode 100644 apis/PyPAD/api/__init__.py diff --git a/ChangeLog b/ChangeLog index ca3bb111..12ca34dd 100644 --- a/ChangeLog +++ b/ChangeLog @@ -18112,3 +18112,13 @@ 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. diff --git a/apis/PyPAD/api/Makefile.am b/apis/PyPAD/api/Makefile.am index a3e3ced0..94181f77 100644 --- a/apis/PyPAD/api/Makefile.am +++ b/apis/PyPAD/api/Makefile.am @@ -20,9 +20,8 @@ ## ## Use automake to process this into a Makefile.in -rivendelldir = $(pkgpyexecdir) -rivendell_PYTHON = __init__.py\ - PyPAD.py +rivendelldir = $(pyexecdir) +rivendell_PYTHON = PyPAD.py CLEANFILES = *~\ *.idb\ diff --git a/apis/PyPAD/api/PyPAD.py b/apis/PyPAD/api/PyPAD.py index 49d62924..88858979 100644 --- a/apis/PyPAD/api/PyPAD.py +++ b/apis/PyPAD/api/PyPAD.py @@ -23,31 +23,96 @@ import socket import json -class PyPADUpdate(object): +# +# Enumerated Constants (sort of) +# +# Escape types +# +ESCAPE_NONE=0 +ESCAPE_XML=1 +ESCAPE_URL=2 +ESCAPE_JSON=3 + +# +# 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 __replace(self,wildcard,string): - stype='now' - if wildcard[1].isupper(): - stype='next' - sfields={'a':'artist','b':'label','c':'client','d':'','e':'agency', - 'f':'','g':'groupName','h':'length','i':'description', - 'j':'cutNumber','k':'','l':'album','m': 'composer', - 'n':'cartNumber','o':'outcue','p':'publisher','q': '', - 'r':'conductor','s':'songId','t':'title','u':'userDefined', - 'v':'','w':'','x':'','y':'year', - 'z':''} - sfield=sfields[wildcard[1].lower()] - - try: - string=string.replace(wildcard,self.__fields['padUpdate'][stype][sfield]) - except TypeError: - string=string.replace(wildcard,'') - except KeyError: - string=string.replace(wildcard,'') + 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,escaping): + if(escaping==0): + return string + if(escaping==1): + return self.__escapeXml(string) + if(escaping==2): + return self.__escapeWeb(string) + if(escaping==3): + return self.__escapeJson(string) + raise ValueError('invalid escaping value') + + def __replaceWildcard(self,wildcard,sfield,stype,string,escaping): + try: + if isinstance(self.__fields['padUpdate'][stype][sfield],unicode): + string=string.replace('%'+wildcard,self.__escape(self.__fields['padUpdate'][stype][sfield],escaping)) + 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,escaping): + string=self.__replaceWildcard(wildcard,sfield,'now',string,escaping); + string=self.__replaceWildcard(wildcard.upper(),sfield,'next',string,escaping); + return string; + def dateTime(self): """ Returns the date-time stamp of the update in RFC-822 format (string). @@ -113,21 +178,55 @@ class PyPADUpdate(object): """ return self.__fields['padUpdate']['log']['name'] - def padFields(self,string): + def padFields(self,string,escaping): """ - Takes an argument of 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. + 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. + + escaping - 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. """ - for i in range(65,68): - string=self.__replace('%'+chr(i),string) - for i in range(69,91): - string=self.__replace('%'+chr(i),string) - for i in range(97,100): - string=self.__replace('%'+chr(i),string) - for i in range(101,123): - string=self.__replace('%'+chr(i),string) + string=self.__replaceWildcardPair('a','artist',string,escaping) + string=self.__replaceWildcardPair('b','label',string,escaping) + string=self.__replaceWildcardPair('c','client',string,escaping) + # DateTime + #string=self.__replaceWildcardPair('d',sfield,string,escaping) + string=self.__replaceWildcardPair('e','agency',string,escaping) + # Unassigned + #string=self.__replaceWildcardPair('f',sfield,string,escaping) # Unassigned + string=self.__replaceWildcardPair('g','groupName',string,escaping) + string=self.__replaceWildcardPair('h','length',string,escaping) + string=self.__replaceWildcardPair('i','description',string,escaping) + string=self.__replaceWildcardPair('j','cutNumber',string,escaping) + #string=self.__replaceWildcardPair('k',sfield,string,escaping) # Start time for rdimport + string=self.__replaceWildcardPair('l','album',string,escaping) + string=self.__replaceWildcardPair('m','composer',string,escaping) + string=self.__replaceWildcardPair('n','cartNumber',string,escaping) + string=self.__replaceWildcardPair('o','outcue',string,escaping) + string=self.__replaceWildcardPair('p','publisher',string,escaping) + #string=self.__replaceWildcardPair('q',sfield,string,escaping) # Start date for rdimport + string=self.__replaceWildcardPair('r','conductor',string,escaping) + string=self.__replaceWildcardPair('s','songId',string,escaping) + string=self.__replaceWildcardPair('t','title',string,escaping) + string=self.__replaceWildcardPair('u','userDefined',string,escaping) + #string=self.__replaceWildcardPair('v',sfield,string,escaping) # Length, rounded down + #string=self.__replaceWildcardPair('w',sfield,string,escaping) # Unassigned + #string=self.__replaceWildcardPair('x',sfield,string,escaping) # Unassigned + string=self.__replaceWildcardPair('y','year',string,escaping) + #string=self.__replaceWildcardPair('z',sfield,string,escaping) # Unassigned string=string.replace('\\b','\b') string=string.replace('\\f','\f') string=string.replace('\\n','\n') @@ -154,7 +253,7 @@ class PyPADUpdate(object): return False; -class PyPADReceiver(object): +class Receiver(object): def __init__(self): self.__callback=None @@ -167,17 +266,20 @@ class PyPADReceiver(object): """ self.__callback=cb - def start(self,hostname): + 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 argument: + Takes the following arguments: - hostname: The hostname or IP address of the Rivendell system. + 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,34289)) + conn=sock.connect((hostname,port)) c="" line="" msg="" @@ -188,7 +290,7 @@ class PyPADReceiver(object): if c[0]=="\n": msg+=line if line=="\r\n": - self.__PyPAD_Process(PyPADUpdate(json.loads(msg))) + self.__PyPAD_Process(Update(json.loads(msg))) msg="" line="" diff --git a/apis/PyPAD/api/__init__.py b/apis/PyPAD/api/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/apis/PyPAD/examples/now_and_next.py b/apis/PyPAD/examples/now_and_next.py index ecae1399..1debeaba 100755 --- a/apis/PyPAD/examples/now_and_next.py +++ b/apis/PyPAD/examples/now_and_next.py @@ -24,31 +24,31 @@ # To see the full documentation of these classes, enter the following at # a python interactive prompt: # -# import rivendell.PyPAD -# help(rivendell.PyPAD) +# import PyPAD +# help(PyPAD) # -import rivendell.PyPAD +import PyPAD # # First, we create a callback method, that will be called every time a -# log machine updates its PAD. An instance of 'PyPADUpdate' that contains +# 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.hasNowPad(): - print "Log %03d NOW: " % update.logMachine()+update.padFields("%a - %t") + print "Log %03d NOW: " % update.logMachine()+update.padFields("%a - %t",PyPAD.ESCAPE_NONE) else: print "Log %03d NOW: [none]" % update.logMachine() if update.hasNextPad(): - print "Log %03d NEXT: " % update.logMachine()+update.padFields("%A - %T") + print "Log %03d NEXT: " % update.logMachine()+update.padFields("%A - %T",PyPAD.ESCAPE_NONE) else: print "Log %03d NEXT: [none]" % update.logMachine() # # Create an instance of 'PyPADReceiver' # -rcvr=rivendell.PyPAD.PyPADReceiver() +rcvr=PyPAD.Receiver() # # Tell it to use the callback @@ -56,8 +56,8 @@ rcvr=rivendell.PyPAD.PyPADReceiver() rcvr.setCallback(ProcessPad) # -# Start the receiver, giving it the hostname or IP address of the target -# Rivendell system. Once started, all further processing can only be done -# in the callback method! +# 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") +rcvr.start("localhost",PyPAD.PAD_TCP_PORT) diff --git a/apis/PyPAD/examples/pypad_udp.py b/apis/PyPAD/examples/pypad_udp.py index 0bda689e..f05c9eab 100755 --- a/apis/PyPAD/examples/pypad_udp.py +++ b/apis/PyPAD/examples/pypad_udp.py @@ -25,7 +25,7 @@ from __future__ import print_function import sys import socket import ConfigParser -import rivendell.PyPAD +import PyPAD def eprint(*args,**kwargs): print(*args,file=sys.stderr,**kwargs) @@ -36,7 +36,7 @@ def ProcessPad(update): section='Udp'+str(n) try: format=config.get(section,'FormatString') - send_sock.sendto(update.padFields(format), + send_sock.sendto(update.padFields(format,int(config.get(section,'Encoding'))), (config.get(section,'IpAddress'),int(config.get(section,'UdpPort')))) n=n+1 except ConfigParser.NoSectionError: @@ -59,6 +59,6 @@ else: # send_sock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) -rcvr=rivendell.PyPAD.PyPADReceiver() +rcvr=PyPAD.Receiver() rcvr.setCallback(ProcessPad) -rcvr.start("localhost") +rcvr.start("localhost",PyPAD.PAD_TCP_PORT) From 2f4a4ada0601f36b404988cf326051e06c793ec7 Mon Sep 17 00:00:00 2001 From: Fred Gleason Date: Sat, 8 Dec 2018 21:55:48 -0500 Subject: [PATCH 10/11] 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()'. --- ChangeLog | 14 ++ apis/PyPAD/api/PyPAD.py | 219 ++++++++++++++++++++-------- apis/PyPAD/examples/now_and_next.py | 4 +- apis/PyPAD/examples/pypad_udp.py | 4 +- lib/rdlogplay.cpp | 24 ++- lib/rdlogplay.h | 3 +- lib/rdweb.cpp | 2 +- 7 files changed, 199 insertions(+), 71 deletions(-) diff --git a/ChangeLog b/ChangeLog index 12ca34dd..2c8600c1 100644 --- a/ChangeLog +++ b/ChangeLog @@ -18122,3 +18122,17 @@ 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()'. diff --git a/apis/PyPAD/api/PyPAD.py b/apis/PyPAD/api/PyPAD.py index 88858979..7dfcddf6 100644 --- a/apis/PyPAD/api/PyPAD.py +++ b/apis/PyPAD/api/PyPAD.py @@ -20,6 +20,7 @@ # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # +import datetime import socket import json @@ -33,6 +34,12 @@ 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 # @@ -42,6 +49,12 @@ 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("<","<") @@ -85,21 +98,21 @@ class Update(object): string=string.replace("\t","\\t") return string - def __escape(self,string,escaping): - if(escaping==0): + def __escape(self,string,esc): + if(esc==0): return string - if(escaping==1): + if(esc==1): return self.__escapeXml(string) - if(escaping==2): + if(esc==2): return self.__escapeWeb(string) - if(escaping==3): + if(esc==3): return self.__escapeJson(string) - raise ValueError('invalid escaping value') + raise ValueError('invalid esc value') - def __replaceWildcard(self,wildcard,sfield,stype,string,escaping): + 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],escaping)) + 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: @@ -108,16 +121,96 @@ class Update(object): string=string.replace('%'+wildcard,'') return string - def __replaceWildcardPair(self,wildcard,sfield,string,escaping): - string=self.__replaceWildcard(wildcard,sfield,'now',string,escaping); - string=self.__replaceWildcard(wildcard.upper(),sfield,'next',string,escaping); + 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 stamp of the update in RFC-822 format (string). + Returns the date-time of the PAD update (datetime) """ - return self.__fields['padUpdate']['dateTime'] + return self.__fromIso8601(pad_data['padUpdate']['dateTime']) def logMachine(self): """ @@ -178,7 +271,7 @@ class Update(object): """ return self.__fields['padUpdate']['log']['name'] - def padFields(self,string,escaping): + def padFields(self,string,esc): """ Takes two arguments: @@ -187,46 +280,45 @@ class Update(object): 'Metadata Wildcards' section of the Rivendell Operations Guide for a list of recognized wildcards. - escaping - Character escaping to be applied to the PAD fields. - Must be one of the following: + 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. + 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,escaping) - string=self.__replaceWildcardPair('b','label',string,escaping) - string=self.__replaceWildcardPair('c','client',string,escaping) - # DateTime - #string=self.__replaceWildcardPair('d',sfield,string,escaping) - string=self.__replaceWildcardPair('e','agency',string,escaping) - # Unassigned - #string=self.__replaceWildcardPair('f',sfield,string,escaping) # Unassigned - string=self.__replaceWildcardPair('g','groupName',string,escaping) - string=self.__replaceWildcardPair('h','length',string,escaping) - string=self.__replaceWildcardPair('i','description',string,escaping) - string=self.__replaceWildcardPair('j','cutNumber',string,escaping) - #string=self.__replaceWildcardPair('k',sfield,string,escaping) # Start time for rdimport - string=self.__replaceWildcardPair('l','album',string,escaping) - string=self.__replaceWildcardPair('m','composer',string,escaping) - string=self.__replaceWildcardPair('n','cartNumber',string,escaping) - string=self.__replaceWildcardPair('o','outcue',string,escaping) - string=self.__replaceWildcardPair('p','publisher',string,escaping) - #string=self.__replaceWildcardPair('q',sfield,string,escaping) # Start date for rdimport - string=self.__replaceWildcardPair('r','conductor',string,escaping) - string=self.__replaceWildcardPair('s','songId',string,escaping) - string=self.__replaceWildcardPair('t','title',string,escaping) - string=self.__replaceWildcardPair('u','userDefined',string,escaping) - #string=self.__replaceWildcardPair('v',sfield,string,escaping) # Length, rounded down - #string=self.__replaceWildcardPair('w',sfield,string,escaping) # Unassigned - #string=self.__replaceWildcardPair('x',sfield,string,escaping) # Unassigned - string=self.__replaceWildcardPair('y','year',string,escaping) - #string=self.__replaceWildcardPair('z',sfield,string,escaping) # Unassigned + 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') @@ -234,24 +326,27 @@ class Update(object): string=string.replace('\\t','\t') return string - def hasNowPad(self): + def hasPadType(self,pad_type): """ - Indicates if this update include 'Now' playing PAD. + Indicates if this update includes the specified PAD type + ('PyPAD.PAD_NOW' or 'PyPAD.PAD_NEXT') """ try: - return self.__fields['padUpdate']['now']!=None + return self.__fields['padUpdate'][pad_type]!=None except TypeError: return False; - - def hasNextPad(self): + + def startDateTime(self,pad_type): """ - Indicates if this update include 'Next' playing PAD. + Returns the start datetime of the specified PAD type + ('PyPAD.PAD_NOW' or 'PyPAD.PAD_NEXT') """ try: - return self.__fields['padUpdate']['next']!=None - except TypeError: - return False; - + return self.__fromIso8601(self.__fields['padUpdate'][pad_type]['startDateTime']) + except AttributeError: + return None + + class Receiver(object): def __init__(self): diff --git a/apis/PyPAD/examples/now_and_next.py b/apis/PyPAD/examples/now_and_next.py index 1debeaba..cc71b02f 100755 --- a/apis/PyPAD/examples/now_and_next.py +++ b/apis/PyPAD/examples/now_and_next.py @@ -36,11 +36,11 @@ import PyPAD # def ProcessPad(update): print - if update.hasNowPad(): + if update.hasPadType(PyPAD.TYPE_NOW): print "Log %03d NOW: " % update.logMachine()+update.padFields("%a - %t",PyPAD.ESCAPE_NONE) else: print "Log %03d NOW: [none]" % update.logMachine() - if update.hasNextPad(): + if update.hasPadType(PyPAD.TYPE_NEXT): print "Log %03d NEXT: " % update.logMachine()+update.padFields("%A - %T",PyPAD.ESCAPE_NONE) else: print "Log %03d NEXT: [none]" % update.logMachine() diff --git a/apis/PyPAD/examples/pypad_udp.py b/apis/PyPAD/examples/pypad_udp.py index f05c9eab..64cad64b 100755 --- a/apis/PyPAD/examples/pypad_udp.py +++ b/apis/PyPAD/examples/pypad_udp.py @@ -35,8 +35,8 @@ def ProcessPad(update): while(True): section='Udp'+str(n) try: - format=config.get(section,'FormatString') - send_sock.sendto(update.padFields(format,int(config.get(section,'Encoding'))), + fmtstr=config.get(section,'FormatString') + send_sock.sendto(update.padFields(fmtstr,int(config.get(section,'Encoding'))), (config.get(section,'IpAddress'),int(config.get(section,'UdpPort')))) n=n+1 except ConfigParser.NoSectionError: diff --git a/lib/rdlogplay.cpp b/lib/rdlogplay.cpp index 2e2b84f1..16229541 100644 --- a/lib/rdlogplay.cpp +++ b/lib/rdlogplay.cpp @@ -2991,12 +2991,23 @@ void RDLogPlay::SendNowNext() // // Now // - play_pad_socket->write(GetPadJson("now",logline[0],8,false).toUtf8()); + 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 // - play_pad_socket->write(GetPadJson("next",logline[1],8,true).toUtf8()); + 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 @@ -3036,7 +3047,8 @@ void RDLogPlay::SendNowNext() QString RDLogPlay::GetPadJson(const QString &name,RDLogLine *ll, - int padding,bool final) const + const QDateTime &start_datetime,int padding, + bool final) const { QString ret; @@ -3045,6 +3057,12 @@ QString RDLogPlay::GetPadJson(const QString &name,RDLogLine *ll, } 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); diff --git a/lib/rdlogplay.h b/lib/rdlogplay.h index 1e56061b..4cb69acd 100644 --- a/lib/rdlogplay.h +++ b/lib/rdlogplay.h @@ -189,7 +189,8 @@ 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,int padding, + 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; diff --git a/lib/rdweb.cpp b/lib/rdweb.cpp index bcc991d1..bbe881e4 100644 --- a/lib/rdweb.cpp +++ b/lib/rdweb.cpp @@ -1142,7 +1142,7 @@ QString RDJsonField(const QString &name,const QDateTime &value,int padding, if(!value.isValid()) { return RDJsonNullField(name,padding,final); } - return RDJsonPadding(padding)+"\""+name+"\": \""+RDWebDateTime(value)+"\""+ + return RDJsonPadding(padding)+"\""+name+"\": \""+RDXmlDateTime(value)+"\""+ comma+"\r\n"; } From d3e35a8cab761375469107cf74653a4c5f708d90 Mon Sep 17 00:00:00 2001 From: Fred Gleason Date: Sun, 9 Dec 2018 19:33:21 -0500 Subject: [PATCH 11/11] 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. --- ChangeLog | 5 ++ apis/PyPAD/api/PyPAD.py | 124 ++++++++++++++++++++++++---- apis/PyPAD/examples/now_and_next.py | 4 +- apis/PyPAD/examples/pypad_udp.py | 2 +- 4 files changed, 116 insertions(+), 19 deletions(-) diff --git a/ChangeLog b/ChangeLog index 2c8600c1..94615112 100644 --- a/ChangeLog +++ b/ChangeLog @@ -18136,3 +18136,8 @@ 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/apis/PyPAD/api/PyPAD.py b/apis/PyPAD/api/PyPAD.py index 7dfcddf6..2ca84962 100644 --- a/apis/PyPAD/api/PyPAD.py +++ b/apis/PyPAD/api/PyPAD.py @@ -40,6 +40,34 @@ ESCAPE_JSON=3 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 # @@ -98,21 +126,10 @@ class Update(object): 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)) + 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: @@ -197,7 +214,6 @@ class Update(object): 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): @@ -212,6 +228,30 @@ class Update(object): """ 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 @@ -271,7 +311,7 @@ class Update(object): """ return self.__fields['padUpdate']['log']['name'] - def padFields(self,string,esc): + def resolvePadFields(self,string,esc): """ Takes two arguments: @@ -329,7 +369,10 @@ class Update(object): def hasPadType(self,pad_type): """ Indicates if this update includes the specified PAD type - ('PyPAD.PAD_NOW' or 'PyPAD.PAD_NEXT') + 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 @@ -339,13 +382,62 @@ class Update(object): def startDateTime(self,pad_type): """ Returns the start datetime of the specified PAD type - ('PyPAD.PAD_NOW' or 'PyPAD.PAD_NEXT') + 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): diff --git a/apis/PyPAD/examples/now_and_next.py b/apis/PyPAD/examples/now_and_next.py index cc71b02f..3859b000 100755 --- a/apis/PyPAD/examples/now_and_next.py +++ b/apis/PyPAD/examples/now_and_next.py @@ -37,11 +37,11 @@ import PyPAD def ProcessPad(update): print if update.hasPadType(PyPAD.TYPE_NOW): - print "Log %03d NOW: " % update.logMachine()+update.padFields("%a - %t",PyPAD.ESCAPE_NONE) + 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.padFields("%A - %T",PyPAD.ESCAPE_NONE) + print "Log %03d NEXT: " % update.logMachine()+update.resolvePadFields("%A - %T",PyPAD.ESCAPE_NONE) else: print "Log %03d NEXT: [none]" % update.logMachine() diff --git a/apis/PyPAD/examples/pypad_udp.py b/apis/PyPAD/examples/pypad_udp.py index 64cad64b..e8ae425a 100755 --- a/apis/PyPAD/examples/pypad_udp.py +++ b/apis/PyPAD/examples/pypad_udp.py @@ -36,7 +36,7 @@ def ProcessPad(update): section='Udp'+str(n) try: fmtstr=config.get(section,'FormatString') - send_sock.sendto(update.padFields(fmtstr,int(config.get(section,'Encoding'))), + 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: