summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--dist/epgdata2xmltv/epgdata2xmltv.xsl16
-rw-r--r--extpipe.cpp117
-rw-r--r--extpipe.h13
-rw-r--r--parse.cpp40
-rw-r--r--parse.h16
-rw-r--r--po/de_DE.po9
-rw-r--r--po/it_IT.po7
-rw-r--r--setup.cpp2
-rw-r--r--xmltv2vdr.cpp181
-rw-r--r--xmltv2vdr.h3
10 files changed, 243 insertions, 161 deletions
diff --git a/dist/epgdata2xmltv/epgdata2xmltv.xsl b/dist/epgdata2xmltv/epgdata2xmltv.xsl
index a85e0c2..4f68091 100644
--- a/dist/epgdata2xmltv/epgdata2xmltv.xsl
+++ b/dist/epgdata2xmltv/epgdata2xmltv.xsl
@@ -7,21 +7,7 @@
<xsl:template match="/">
<xsl:for-each select="//data[d2=$channelnum]">
-
-<!--
-<xsl:variable name="start_xmltv">
-<xsl:value-of select="d4"/>
-</xsl:variable>
-
-<xsl:variable name="stop_xmltv">
-<xsl:value-of select="d5"/>
-</xsl:variable>
-
-<xsl:variable name="vps_xmltv">
-<xsl:value-of select="d8"/>
-</xsl:variable>
--->
-
+<xsl:sort select="d4"/>
<xsl:variable name="EVENTID">
<xsl:value-of select="d0"/>
</xsl:variable>
diff --git a/extpipe.cpp b/extpipe.cpp
index c596226..4ce2c80 100644
--- a/extpipe.cpp
+++ b/extpipe.cpp
@@ -12,7 +12,8 @@
cExtPipe::cExtPipe(void)
{
pid = -1;
- f = NULL;
+ f_stderr = -1;
+ f_stdout= -1;
}
cExtPipe::~cExtPipe()
@@ -21,68 +22,100 @@ cExtPipe::~cExtPipe()
Close(status);
}
-bool cExtPipe::Open(const char *Command, const char *Mode)
+bool cExtPipe::Open(const char *Command)
{
- int fd[2];
+ int fd_stdout[2];
+ int fd_stderr[2];
- if (pipe(fd) < 0)
+ if (pipe(fd_stdout) < 0)
{
LOG_ERROR;
return false;
}
- if ((pid = fork()) < 0) // fork failed
+ if (pipe(fd_stderr) < 0)
{
+ close(fd_stdout[0]);
+ close(fd_stdout[1]);
LOG_ERROR;
- close(fd[0]);
- close(fd[1]);
return false;
}
- const char *mode = "w";
- int iopipe = 0;
+ if ((pid = fork()) < 0) // fork failed
+ {
+ LOG_ERROR;
+ close(fd_stdout[0]);
+ close(fd_stdout[1]);
+ close(fd_stderr[0]);
+ close(fd_stderr[1]);
+ return false;
+ }
if (pid > 0) // parent process
{
- if (strcmp(Mode, "r") == 0)
- {
- mode = "r";
- iopipe = 1;
+ close(fd_stdout[1]); // close write fd, we need only read fd
+ close(fd_stderr[1]); // close write fd, we need only read fd
+ int flags=fcntl(fd_stdout[0],F_GETFL,0);
+ if (flags==-1) {
+ LOG_ERROR;
+ close(fd_stdout[0]);
+ close(fd_stderr[0]);
+ return false;
}
- close(fd[iopipe]);
- if ((f = fdopen(fd[1 - iopipe], mode)) == NULL)
- {
+ flags|=O_NONBLOCK;
+ if (fcntl(fd_stdout[0],F_SETFL,flags)==-1) {
+ LOG_ERROR;
+ close(fd_stdout[0]);
+ close(fd_stderr[0]);
+ return false;
+ }
+ flags=fcntl(fd_stderr[0],F_GETFL,0);
+ if (flags==-1) {
LOG_ERROR;
- close(fd[1 - iopipe]);
+ close(fd_stdout[0]);
+ close(fd_stderr[0]);
+ return false;
+
}
- return f != NULL;
+ if (fcntl(fd_stderr[0],F_SETFL,flags)==-1) {
+ LOG_ERROR;
+ close(fd_stdout[0]);
+ close(fd_stderr[0]);
+ return false;
+ }
+ f_stdout = fd_stdout[0];
+ f_stderr = fd_stderr[0];
+ return true;
}
else // child process
{
- int iofd = STDOUT_FILENO;
- if (strcmp(Mode, "w") == 0)
+ close(fd_stdout[0]); // close read fd, we need only write fd
+ close(fd_stderr[0]); // close read fd, we need only write fd
+
+ if (dup2(fd_stdout[1], STDOUT_FILENO) == -1) // now redirect
{
- mode = "r";
- iopipe = 1;
- iofd = STDIN_FILENO;
+ LOG_ERROR;
+ close(fd_stderr[1]);
+ close(fd_stdout[1]);
+ _exit(-1);
}
- close(fd[iopipe]);
- if (dup2(fd[1 - iopipe], iofd) == -1) // now redirect
+
+ if (dup2(fd_stderr[1], STDERR_FILENO) == -1) // now redirect
{
LOG_ERROR;
- close(fd[1 - iopipe]);
+ close(fd_stderr[1]);
+ close(fd_stdout[1]);
_exit(-1);
}
- else
+
+ int MaxPossibleFileDescriptors = getdtablesize();
+ for (int i = STDERR_FILENO + 1; i < MaxPossibleFileDescriptors; i++)
+ close(i); //close all dup'ed filedescriptors
+ if (execl("/bin/sh", "sh", "-c", Command, NULL) == -1)
{
- int MaxPossibleFileDescriptors = getdtablesize();
- for (int i = STDERR_FILENO + 1; i < MaxPossibleFileDescriptors; i++)
- close(i); //close all dup'ed filedescriptors
- if (execl("/bin/sh", "sh", "-c", Command, NULL) == -1)
- {
- LOG_ERROR_STR(Command);
- close(fd[1 - iopipe]);
- _exit(-1);
- }
+ LOG_ERROR_STR(Command);
+ close(fd_stderr[1]);
+ close(fd_stdout[1]);
+ _exit(-1);
}
_exit(0);
}
@@ -92,10 +125,16 @@ int cExtPipe::Close(int &status)
{
int ret = -1;
- if (f)
+ if (f_stderr!=-1)
+ {
+ close(f_stderr);
+ f_stderr = -1;
+ }
+
+ if (f_stdout!=-1)
{
- fclose(f);
- f = NULL;
+ close(f_stdout);
+ f_stdout=-1;
}
if (pid > 0)
diff --git a/extpipe.h b/extpipe.h
index 58a2ef1..a820cd3 100644
--- a/extpipe.h
+++ b/extpipe.h
@@ -8,19 +8,20 @@ class cExtPipe
{
private:
pid_t pid;
- FILE *f;
+ int f_stdout;
+ int f_stderr;
public:
cExtPipe(void);
~cExtPipe();
- FILE *Out()
+ int Out() const
{
- return f;
+ return f_stdout;
}
- operator FILE* () const
+ int Err() const
{
- return f;
+ return f_stderr;
}
- bool Open(const char *Command, const char *Mode);
+ bool Open(const char *Command);
int Close(int &status);
};
diff --git a/parse.cpp b/parse.cpp
index 9626bc4..8a5eb52 100644
--- a/parse.cpp
+++ b/parse.cpp
@@ -21,7 +21,9 @@ extern char *strcatrealloc(char *dest, const char *src);
void cXMLTVEvent::SetTitle(const char *Title)
{
title = strcpyrealloc(title, Title);
- if (title) title = compactspace(title);
+ if (title) {
+ title = compactspace(title);
+ }
}
void cXMLTVEvent::SetOrigTitle(const char *OrigTitle)
@@ -420,7 +422,8 @@ cEvent *cParse::SearchEvent(cSchedule* schedule, cXMLTVEvent *xevent)
return f;
}
-bool cParse::PutEvent(cSchedule* schedule, cEvent *event, cXMLTVEvent *xevent, cEPGMapping *map)
+bool cParse::PutEvent(cSchedule* schedule, cEvent *event, cXMLTVEvent *xevent, cEPGMapping *map,
+ int mapindex)
{
if (!schedule) return false;
if (!xevent) return false;
@@ -441,9 +444,11 @@ bool cParse::PutEvent(cSchedule* schedule, cEvent *event, cXMLTVEvent *xevent, c
{
esyslog("xmltv2vdr: '%s' ERROR xmltv data overlaps:",name);
time_t lstart=last->StartTime();
- esyslog("xmltv2vdr: '%s' ERROR last event '%s' @%s", name,last->Title(),
+ esyslog("xmltv2vdr: '%s' ERROR last event '%s' (%s) @%s", name,
+ *schedule->ChannelID().ToString(),last->Title(),
ctime(&lstart));
- esyslog("xmltv2vdr: '%s' ERROR next event '%s' @%s", name,xevent->Title(),
+ esyslog("xmltv2vdr: '%s' ERROR next event '%s' (%s) @%s", name,
+ *map->ChannelIDs()[mapindex].ToString(),xevent->Title(),
ctime(&start));
return false;
}
@@ -466,6 +471,7 @@ bool cParse::PutEvent(cSchedule* schedule, cEvent *event, cXMLTVEvent *xevent, c
event->SetDescription(xevent->Description());
event->SetVersion(0);
schedule->AddEvent(event);
+ schedule->Sort();
dsyslog("xmltv2vdr: '%s' adding event '%s' @%s",name,xevent->Title(),ctime(&start));
}
else
@@ -867,6 +873,14 @@ bool cParse::Process(char *buffer, int bufsize)
return false;
}
+ cSchedulesLock schedulesLock(true,25000); // wait up to 25 secs for lock!
+ const cSchedules *schedules = cSchedules::Schedules(schedulesLock);
+ if (!schedules)
+ {
+ esyslog("xmltv2vdr: '%s' cannot get schedules, try later",name);
+ return false;
+ }
+
time_t begin=time(NULL);
xmlNodePtr node=rootnode->xmlChildrenNode;
cEPGMapping *oldmap=NULL;
@@ -964,16 +978,7 @@ bool cParse::Process(char *buffer, int bufsize)
if (stoptime) xevent.SetDuration(stoptime-starttime);
if (!FetchEvent(node)) // sets xevent
{
- node=node->next;
- continue;
- }
- cSchedulesLock *schedulesLock = new cSchedulesLock(true,5000); // to be safe ;)
- const cSchedules *schedules = cSchedules::Schedules(*schedulesLock);
- if (!schedules)
- {
- if (lerr!=PARSE_NOSCHEDULES)
- esyslog("xmltv2vdr: '%s' ERROR cannot get schedules",name);
- lerr=PARSE_NOSCHEDULES;
+ dsyslog("xmltv2vdr: '%s' failed to fetch event",name);
node=node->next;
continue;
}
@@ -1003,14 +1008,14 @@ bool cParse::Process(char *buffer, int bufsize)
if (addevents)
{
cEvent *event=SearchEvent(schedule,&xevent);
- PutEvent(schedule,event,&xevent,map);
+ PutEvent(schedule,event,&xevent,map,i);
}
else
{
if (!schedule->Index())
{
if (lerr!=PARSE_EMPTYSCHEDULE)
- esyslog("xmltv2vdr: '%s' ERROR cannot merge into empty epg (%s) - try add optiopn",
+ esyslog("xmltv2vdr: '%s' ERROR cannot merge into empty epg (%s) - try add option",
name,channel->Name());
lerr=PARSE_EMPTYSCHEDULE;
}
@@ -1021,7 +1026,7 @@ bool cParse::Process(char *buffer, int bufsize)
{
if ((event=SearchEvent(schedule,&xevent)))
{
- PutEvent(schedule,event,&xevent,map);
+ PutEvent(schedule,event,&xevent,map,i);
}
else
{
@@ -1033,7 +1038,6 @@ bool cParse::Process(char *buffer, int bufsize)
}
}
}
- delete schedulesLock;
node=node->next;
}
xmlFreeDoc(xmltv);
diff --git a/parse.h b/parse.h
index 7940622..d4970b0 100644
--- a/parse.h
+++ b/parse.h
@@ -147,13 +147,12 @@ class cParse
enum
{
PARSE_NOERROR=0,
- PARSE_NOSCHEDULE=1,
- PARSE_NOCHANNEL=2,
- PARSE_NOSCHEDULES=3,
- PARSE_XMLTVERR=4,
- PARSE_NOMAPPING=5,
- PARSE_NOCHANNELID=6,
- PARSE_EMPTYSCHEDULE=7
+ PARSE_NOSCHEDULE,
+ PARSE_NOCHANNEL,
+ PARSE_XMLTVERR,
+ PARSE_NOMAPPING,
+ PARSE_NOCHANNELID,
+ PARSE_EMPTYSCHEDULE
};
private:
@@ -170,7 +169,8 @@ private:
bool FetchEvent(xmlNodePtr node);
cEPGMapping *EPGMapping(const char *ChannelName);
cTEXTMapping *TEXTMapping(const char *Name);
- bool PutEvent(cSchedule* schedule,cEvent *event,cXMLTVEvent *xevent, cEPGMapping *map);
+ bool PutEvent(cSchedule* schedule,cEvent *event,cXMLTVEvent *xevent, cEPGMapping *map,
+ int mapindex);
public:
cParse(const char *Name, cEPGMappings *Maps, cTEXTMappings *Texts);
~cParse();
diff --git a/po/de_DE.po b/po/de_DE.po
index 29db134..728c8ea 100644
--- a/po/de_DE.po
+++ b/po/de_DE.po
@@ -4,7 +4,7 @@ msgid ""
msgstr ""
"Project-Id-Version: vdr\n"
"Report-Msgid-Bugs-To: <see README>\n"
-"POT-Creation-Date: 2011-07-28 17:56+0200\n"
+"POT-Creation-Date: 2011-08-01 00:24+0200\n"
"PO-Revision-Date: 2010-12-23 23:59+0100\n"
"Last-Translator: Jochen Dolze <vdr@dolze.de>\n"
"Language-Team: <vdr@linuxtv.org>\n"
@@ -187,12 +187,11 @@ msgstr "entfernen"
msgid "Button$Map"
msgstr "hinzufügen"
-msgid "Copy settings to all channels?"
-msgstr ""
+msgid "Copy to all mapped channels?"
+msgstr "Auf zugewiesene Kanäle kopieren?"
-#, fuzzy
msgid "Reset all channel settings?"
-msgstr "EPG Quellkanal Optionen"
+msgstr "Kanaleinstellungen zurücksetzen?"
msgid "Button$Choose"
msgstr "auswählen"
diff --git a/po/it_IT.po b/po/it_IT.po
index 1c0d2e6..e9c0dac 100644
--- a/po/it_IT.po
+++ b/po/it_IT.po
@@ -4,7 +4,7 @@ msgid ""
msgstr ""
"Project-Id-Version: vdr\n"
"Report-Msgid-Bugs-To: <see README>\n"
-"POT-Creation-Date: 2011-07-28 17:56+0200\n"
+"POT-Creation-Date: 2011-08-01 00:24+0200\n"
"PO-Revision-Date: 2011-03-05 15:45+0100\n"
"Last-Translator: Diego Pierotto <vdr-italian@tiscali.it>\n"
"Language-Team: <vdr@linuxtv.org>\n"
@@ -190,12 +190,11 @@ msgstr "Non mappare"
msgid "Button$Map"
msgstr "Mappare"
-msgid "Copy settings to all channels?"
+msgid "Copy to all mapped channels?"
msgstr ""
-#, fuzzy
msgid "Reset all channel settings?"
-msgstr "Opzioni canali sorgente EPG"
+msgstr ""
msgid "Button$Choose"
msgstr "Scegli"
diff --git a/setup.cpp b/setup.cpp
index 7acdc38..606ce81 100644
--- a/setup.cpp
+++ b/setup.cpp
@@ -976,7 +976,7 @@ eOSState cMenuSetupXmltv2vdrChannelMap::ProcessKey (eKeys Key)
case kBlue: // copy
if ((Current()<cm) && (baseplugin))
{
- if (Skins.Message(mtInfo,tr("Copy settings to all channels?"))==kOk)
+ if (Skins.Message(mtInfo,tr("Copy to all mapped channels?"))==kOk)
{
const char *oldchannel=channel;
cEPGMapping *tmap=map;
diff --git a/xmltv2vdr.cpp b/xmltv2vdr.cpp
index bc52e8b..533bf2c 100644
--- a/xmltv2vdr.cpp
+++ b/xmltv2vdr.cpp
@@ -80,7 +80,7 @@ cEPGSource::cEPGSource(const char *Name, const char *ConfDir, cEPGMappings *Maps
name=strdup(Name);
confdir=strdup(ConfDir);
pin=NULL;
- pipe=false;
+ usepipe=false;
needpin=false;
daysinadvance=0;
ready2parse=ReadConfig();
@@ -123,12 +123,12 @@ bool cEPGSource::ReadConfig()
if (!strncmp(line,"pipe",4))
{
dsyslog("xmltv2vdr: '%s' is providing data through a pipe",name);
- pipe=true;
+ usepipe=true;
}
else
{
dsyslog("xmltv2vdr: '%s' is providing data through a file",name);
- pipe=false;
+ usepipe=false;
}
char *ndt=strchr(line,';');
if (ndt)
@@ -262,15 +262,66 @@ bool cEPGSource::ReadConfig()
return true;
}
+int cEPGSource::ReadOutput(char *&result, size_t &l)
+{
+ int ret=0;
+ char *fname=NULL;
+ if (asprintf(&fname,"%s/%s.xmltv",EPGSOURCES,name)==-1)
+ {
+ esyslog("xmltv2vdr: '%s' ERROR out of memory",name);
+ return 134;
+ }
+ dsyslog("xmltv2vdr: '%s' reading from '%s'",name,fname);
+
+ int fd=open(fname,O_RDONLY);
+ if (fd==-1)
+ {
+ esyslog("xmltv2vdr: '%s' ERROR failed to open '%s'",name,fname);
+ free(fname);
+ return 157;
+ }
+
+ struct stat statbuf;
+ if (fstat(fd,&statbuf)==-1)
+ {
+ esyslog("xmltv2vdr: '%s' ERROR failed to stat '%s'",name,fname);
+ close(fd);
+ free(fname);
+ return 157;
+ }
+ l=statbuf.st_size;
+ result=(char *) malloc(l+1);
+ if (!result)
+ {
+ close(fd);
+ free(fname);
+ esyslog("xmltv2vdr: '%s' ERROR out of memory",name);
+ return 134;
+ }
+ if (read(fd,result,statbuf.st_size)==statbuf.st_size)
+ {
+ }
+ else
+ {
+ esyslog("xmltv2vdr: '%s' ERROR failed to read '%s'",name,fname);
+ ret=149;
+ }
+ free(result);
+ close(fd);
+ free(fname);
+ return ret;
+}
+
int cEPGSource::Execute()
{
if (!ready2parse) return false;
if (!parse) return false;
- char *result=NULL;
- int l=0;
+ char *r_out=NULL;
+ char *r_err=NULL;
+ int l_out=0;
+ int l_err=0;
int ret=0;
- cExtPipe p;
char *cmd=NULL;
if (asprintf(&cmd,"%s %i '%s'",name,daysinadvance,pin ? pin : "")==-1)
@@ -298,8 +349,9 @@ int cEPGSource::Execute()
strcat(cmd," ");
}
}
- dsyslog("xmltv2vdr: '%s' %s",name,cmd);
- if (!p.Open(cmd,"r"))
+ //dsyslog("xmltv2vdr: '%s' %s",name,cmd);
+ cExtPipe p;
+ if (!p.Open(cmd))
{
free(cmd);
esyslog("xmltv2vdr: '%s' ERROR failed to open pipe",name);
@@ -307,23 +359,55 @@ int cEPGSource::Execute()
}
free(cmd);
dsyslog("xmltv2vdr: '%s' executing epgsource",name);
- if (pipe)
- {
- int c;
- while ((c=fgetc(p.Out()))!=EOF)
- {
- if (l%20==0) result=(char *) realloc(result, l+21);
- result[l++]=c;
+
+ int fdsopen=2;
+ while (fdsopen>0) {
+ struct pollfd fds[2];
+ fds[0].fd=p.Out();
+ fds[0].events=POLLIN;
+ fds[1].fd=p.Err();
+ fds[1].events=POLLIN;
+ if (poll(fds,2,500)>=0) {
+ if (fds[0].revents & POLLIN) {
+ unsigned char c;
+ if (read(p.Out(),&c,1)==1)
+ {
+ if (l_out%20==0) r_out=(char *) realloc(r_out, l_out+21);
+ r_out[l_out++]=c;
+ }
+ }
+ if (fds[1].revents & POLLIN) {
+ unsigned char c;
+ if (read(p.Err(),&c,1)==1)
+ {
+ if (l_err%20==0) r_err=(char *) realloc(r_err, l_err+21);
+ r_err[l_err++]=c;
+ }
+ }
+ if (fds[0].revents & POLLHUP) {
+ fdsopen--;
+ }
+ if (fds[1].revents & POLLHUP) {
+ fdsopen--;
+ }
+ } else {
+ esyslog("xmltv2vdr: '%s' ERROR polling",name);
+ break;
}
+ }
+ if (r_out) r_out[l_out]=0;
+ if (r_err) r_err[l_err]=0;
+
+ if (usepipe)
+ {
int status;
if (p.Close(status)>0)
{
int returncode=WEXITSTATUS(status);
- if ((!returncode) && (result))
+ if ((!returncode) && (r_out))
{
dsyslog("xmltv2vdr: '%s' parsing output",name);
- result[l]=0;
- if (!parse->Process(result,l))
+ if (!parse->Process(r_out,l_out))
{
esyslog("xmltv2vdr: '%s' ERROR failed to parse output",name);
ret=141;
@@ -340,67 +424,26 @@ int cEPGSource::Execute()
esyslog("xmltv2vdr: '%s' ERROR failed to execute",name);
ret=126;
}
- if (result) free(result);
}
else
{
- while ((fgetc(p.Out()))!=EOF) { }
-
int status;
if (p.Close(status)>0)
{
int returncode=WEXITSTATUS(status);
if (!returncode)
{
- char *fname=NULL;
- if (asprintf(&fname,"%s/%s.xmltv",EPGSOURCES,name)==-1)
- {
- esyslog("xmltv2vdr: '%s' ERROR out of memory",name);
- return 134;
- }
- dsyslog("xmltv2vdr: '%s' reading from '%s'",name,fname);
-
- int fd=open(fname,O_RDONLY);
- if (fd==-1)
- {
- esyslog("xmltv2vdr: '%s' ERROR failed to open '%s'",name,fname);
- free(fname);
- return 157;
- }
-
- struct stat statbuf;
- if (fstat(fd,&statbuf)==-1)
- {
- esyslog("xmltv2vdr: '%s' ERROR failed to stat '%s'",name,fname);
- close(fd);
- free(fname);
- return 157;
- }
- l=statbuf.st_size;
- result=(char *) malloc(l+1);
- if (!result)
- {
- close(fd);
- free(fname);
- esyslog("xmltv2vdr: '%s' ERROR out of memory",name);
- return 134;
- }
- if (read(fd,result,statbuf.st_size)==statbuf.st_size)
- {
+ size_t l;
+ char *result=NULL;
+ ret=ReadOutput(result,l);
+ if ((!ret) && (result)) {
if (!parse->Process(result,l))
{
esyslog("xmltv2vdr: '%s' failed to parse output",name);
ret=149;
}
}
- else
- {
- esyslog("xmltv2vdr: '%s' ERROR failed to read '%s'",name,fname);
- ret=149;
- }
- free(result);
- close(fd);
- free(fname);
+ if (result) free(result);
}
else
{
@@ -409,6 +452,16 @@ int cEPGSource::Execute()
}
}
}
+ if (r_out) free(r_out);
+ if (r_err) {
+ char *pch=strtok(r_err,"\n");
+ while (pch) {
+ esyslog("xmltv2vdr: '%s' ERROR %s",name,pch);
+ pch=strtok(NULL,"\n");
+ }
+ free(r_err);
+ }
+
return ret;
}
diff --git a/xmltv2vdr.h b/xmltv2vdr.h
index 1b63eab..1d05782 100644
--- a/xmltv2vdr.h
+++ b/xmltv2vdr.h
@@ -50,11 +50,12 @@ private:
const char *pin;
cParse *parse;
bool ready2parse;
- bool pipe;
+ bool usepipe;
bool needpin;
int daysinadvance;
int daysmax;
bool ReadConfig();
+ int ReadOutput(char *&result, size_t &l);
cEPGChannels channels;
public:
cEPGSource(const char *Name,const char *ConfDir,cEPGMappings *Maps,cTEXTMappings *Texts);