vdr  2.0.4
svdrp.c
Go to the documentation of this file.
1 /*
2  * svdrp.c: Simple Video Disk Recorder Protocol
3  *
4  * See the main source file 'vdr.c' for copyright information and
5  * how to reach the author.
6  *
7  * The "Simple Video Disk Recorder Protocol" (SVDRP) was inspired
8  * by the "Simple Mail Transfer Protocol" (SMTP) and is fully ASCII
9  * text based. Therefore you can simply 'telnet' to your VDR port
10  * and interact with the Video Disk Recorder - or write a full featured
11  * graphical interface that sits on top of an SVDRP connection.
12  *
13  * $Id: svdrp.c 2.24 2013/02/17 13:18:01 kls Exp $
14  */
15 
16 #include "svdrp.h"
17 #include <arpa/inet.h>
18 #include <ctype.h>
19 #include <errno.h>
20 #include <fcntl.h>
21 #include <netinet/in.h>
22 #include <stdarg.h>
23 #include <stdio.h>
24 #include <stdlib.h>
25 #include <string.h>
26 #include <sys/socket.h>
27 #include <sys/time.h>
28 #include <unistd.h>
29 #include "channels.h"
30 #include "config.h"
31 #include "cutter.h"
32 #include "device.h"
33 #include "eitscan.h"
34 #include "filetransfer.h"
35 #include "keys.h"
36 #include "menu.h"
37 #include "plugin.h"
38 #include "remote.h"
39 #include "skins.h"
40 #include "timers.h"
41 #include "tools.h"
42 #include "videodir.h"
43 
44 // --- cSocket ---------------------------------------------------------------
45 
46 cSocket::cSocket(int Port, int Queue)
47 {
48  port = Port;
49  sock = -1;
50  queue = Queue;
51 }
52 
54 {
55  Close();
56 }
57 
58 void cSocket::Close(void)
59 {
60  if (sock >= 0) {
61  close(sock);
62  sock = -1;
63  }
64 }
65 
66 bool cSocket::Open(void)
67 {
68  if (sock < 0) {
69  // create socket:
70  sock = socket(PF_INET, SOCK_STREAM, 0);
71  if (sock < 0) {
72  LOG_ERROR;
73  port = 0;
74  return false;
75  }
76  // allow it to always reuse the same port:
77  int ReUseAddr = 1;
78  setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &ReUseAddr, sizeof(ReUseAddr));
79  //
80  struct sockaddr_in name;
81  name.sin_family = AF_INET;
82  name.sin_port = htons(port);
83  name.sin_addr.s_addr = SVDRPhosts.LocalhostOnly() ? htonl(INADDR_LOOPBACK) : htonl(INADDR_ANY);
84  if (bind(sock, (struct sockaddr *)&name, sizeof(name)) < 0) {
85  LOG_ERROR;
86  Close();
87  return false;
88  }
89  // make it non-blocking:
90  int oldflags = fcntl(sock, F_GETFL, 0);
91  if (oldflags < 0) {
92  LOG_ERROR;
93  return false;
94  }
95  oldflags |= O_NONBLOCK;
96  if (fcntl(sock, F_SETFL, oldflags) < 0) {
97  LOG_ERROR;
98  return false;
99  }
100  // listen to the socket:
101  if (listen(sock, queue) < 0) {
102  LOG_ERROR;
103  return false;
104  }
105  }
106  return true;
107 }
108 
110 {
111  if (Open()) {
112  struct sockaddr_in clientname;
113  uint size = sizeof(clientname);
114  int newsock = accept(sock, (struct sockaddr *)&clientname, &size);
115  if (newsock > 0) {
116  bool accepted = SVDRPhosts.Acceptable(clientname.sin_addr.s_addr);
117  if (!accepted) {
118  const char *s = "Access denied!\n";
119  if (write(newsock, s, strlen(s)) < 0)
120  LOG_ERROR;
121  close(newsock);
122  newsock = -1;
123  }
124  isyslog("connect from %s, port %hu - %s", inet_ntoa(clientname.sin_addr), ntohs(clientname.sin_port), accepted ? "accepted" : "DENIED");
125  }
126  else if (errno != EINTR && errno != EAGAIN)
127  LOG_ERROR;
128  return newsock;
129  }
130  return -1;
131 }
132 
133 // --- cPUTEhandler ----------------------------------------------------------
134 
136 {
137  if ((f = tmpfile()) != NULL) {
138  status = 354;
139  message = "Enter EPG data, end with \".\" on a line by itself";
140  }
141  else {
142  LOG_ERROR;
143  status = 554;
144  message = "Error while opening temporary file";
145  }
146 }
147 
149 {
150  if (f)
151  fclose(f);
152 }
153 
154 bool cPUTEhandler::Process(const char *s)
155 {
156  if (f) {
157  if (strcmp(s, ".") != 0) {
158  fputs(s, f);
159  fputc('\n', f);
160  return true;
161  }
162  else {
163  rewind(f);
164  if (cSchedules::Read(f)) {
165  cSchedules::Cleanup(true);
166  status = 250;
167  message = "EPG data processed";
168  }
169  else {
170  status = 451;
171  message = "Error while processing EPG data";
172  }
173  fclose(f);
174  f = NULL;
175  }
176  }
177  return false;
178 }
179 
180 // --- cSVDRP ----------------------------------------------------------------
181 
182 #define MAXHELPTOPIC 10
183 #define EITDISABLETIME 10 // seconds until EIT processing is enabled again after a CLRE command
184  // adjust the help for CLRE accordingly if changing this!
185 
186 const char *HelpPages[] = {
187  "CHAN [ + | - | <number> | <name> | <id> ]\n"
188  " Switch channel up, down or to the given channel number, name or id.\n"
189  " Without option (or after successfully switching to the channel)\n"
190  " it returns the current channel number and name.",
191  "CLRE [ <number> | <name> | <id> ]\n"
192  " Clear the EPG list of the given channel number, name or id.\n"
193  " Without option it clears the entire EPG list.\n"
194  " After a CLRE command, no further EPG processing is done for 10\n"
195  " seconds, so that data sent with subsequent PUTE commands doesn't\n"
196  " interfere with data from the broadcasters.",
197  "CPYR <number> <new name>\n"
198  " Copy the recording with the given number. Before a recording can be\n"
199  " copied, an LSTR command must have been executed in order to retrieve\n"
200  " the recording numbers. The numbers don't change during subsequent CPYR\n"
201  " commands.",
202  "DELC <number>\n"
203  " Delete channel.",
204  "DELR <number>\n"
205  " Delete the recording with the given number. Before a recording can be\n"
206  " deleted, an LSTR command must have been executed in order to retrieve\n"
207  " the recording numbers. The numbers don't change during subsequent DELR\n"
208  " commands. CAUTION: THERE IS NO CONFIRMATION PROMPT WHEN DELETING A\n"
209  " RECORDING - BE SURE YOU KNOW WHAT YOU ARE DOING!",
210  "DELT <number>\n"
211  " Delete timer.",
212  "EDIT <number>\n"
213  " Edit the recording with the given number. Before a recording can be\n"
214  " edited, an LSTR command must have been executed in order to retrieve\n"
215  " the recording numbers.",
216  "GRAB <filename> [ <quality> [ <sizex> <sizey> ] ]\n"
217  " Grab the current frame and save it to the given file. Images can\n"
218  " be stored as JPEG or PNM, depending on the given file name extension.\n"
219  " The quality of the grabbed image can be in the range 0..100, where 100\n"
220  " (the default) means \"best\" (only applies to JPEG). The size parameters\n"
221  " define the size of the resulting image (default is full screen).\n"
222  " If the file name is just an extension (.jpg, .jpeg or .pnm) the image\n"
223  " data will be sent to the SVDRP connection encoded in base64. The same\n"
224  " happens if '-' (a minus sign) is given as file name, in which case the\n"
225  " image format defaults to JPEG.",
226  "HELP [ <topic> ]\n"
227  " The HELP command gives help info.",
228  "HITK [ <key> ... ]\n"
229  " Hit the given remote control key. Without option a list of all\n"
230  " valid key names is given. If more than one key is given, they are\n"
231  " entered into the remote control queue in the given sequence. There\n"
232  " can be up to 31 keys.",
233  "LSTC [ :groups | <number> | <name> | <id> ]\n"
234  " List channels. Without option, all channels are listed. Otherwise\n"
235  " only the given channel is listed. If a name is given, all channels\n"
236  " containing the given string as part of their name are listed.\n"
237  " If ':groups' is given, all channels are listed including group\n"
238  " separators. The channel number of a group separator is always 0.",
239  "LSTE [ <channel> ] [ now | next | at <time> ]\n"
240  " List EPG data. Without any parameters all data of all channels is\n"
241  " listed. If a channel is given (either by number or by channel ID),\n"
242  " only data for that channel is listed. 'now', 'next', or 'at <time>'\n"
243  " restricts the returned data to present events, following events, or\n"
244  " events at the given time (which must be in time_t form).",
245  "LSTR [ <number> [ path ] ]\n"
246  " List recordings. Without option, all recordings are listed. Otherwise\n"
247  " the information for the given recording is listed. If a recording\n"
248  " number and the keyword 'path' is given, the actual file name of that\n"
249  " recording's directory is listed.",
250  "LSTT [ <number> ] [ id ]\n"
251  " List timers. Without option, all timers are listed. Otherwise\n"
252  " only the given timer is listed. If the keyword 'id' is given, the\n"
253  " channels will be listed with their unique channel ids instead of\n"
254  " their numbers.",
255  "MESG <message>\n"
256  " Displays the given message on the OSD. The message will be queued\n"
257  " and displayed whenever this is suitable.\n",
258  "MODC <number> <settings>\n"
259  " Modify a channel. Settings must be in the same format as returned\n"
260  " by the LSTC command.",
261  "MODT <number> on | off | <settings>\n"
262  " Modify a timer. Settings must be in the same format as returned\n"
263  " by the LSTT command. The special keywords 'on' and 'off' can be\n"
264  " used to easily activate or deactivate a timer.",
265  "MOVC <number> <to>\n"
266  " Move a channel to a new position.",
267  "MOVR <number> <new name>\n"
268  " Move the recording with the given number. Before a recording can be\n"
269  " moved, an LSTR command must have been executed in order to retrieve\n"
270  " the recording numbers. The numbers don't change during subsequent MOVR\n"
271  " commands.",
272  "NEWC <settings>\n"
273  " Create a new channel. Settings must be in the same format as returned\n"
274  " by the LSTC command.",
275  "NEWT <settings>\n"
276  " Create a new timer. Settings must be in the same format as returned\n"
277  " by the LSTT command.",
278  "NEXT [ abs | rel ]\n"
279  " Show the next timer event. If no option is given, the output will be\n"
280  " in human readable form. With option 'abs' the absolute time of the next\n"
281  " event will be given as the number of seconds since the epoch (time_t\n"
282  " format), while with option 'rel' the relative time will be given as the\n"
283  " number of seconds from now until the event. If the absolute time given\n"
284  " is smaller than the current time, or if the relative time is less than\n"
285  " zero, this means that the timer is currently recording and has started\n"
286  " at the given time. The first value in the resulting line is the number\n"
287  " of the timer.",
288  "PLAY <number> [ begin | <position> ]\n"
289  " Play the recording with the given number. Before a recording can be\n"
290  " played, an LSTR command must have been executed in order to retrieve\n"
291  " the recording numbers.\n"
292  " The keyword 'begin' plays the recording from its very beginning, while\n"
293  " a <position> (given as hh:mm:ss[.ff] or framenumber) starts at that\n"
294  " position. If neither 'begin' nor a <position> are given, replay is resumed\n"
295  " at the position where any previous replay was stopped, or from the beginning\n"
296  " by default. To control or stop the replay session, use the usual remote\n"
297  " control keypresses via the HITK command.",
298  "PLUG <name> [ help | main ] [ <command> [ <options> ]]\n"
299  " Send a command to a plugin.\n"
300  " The PLUG command without any parameters lists all plugins.\n"
301  " If only a name is given, all commands known to that plugin are listed.\n"
302  " If a command is given (optionally followed by parameters), that command\n"
303  " is sent to the plugin, and the result will be displayed.\n"
304  " The keyword 'help' lists all the SVDRP commands known to the named plugin.\n"
305  " If 'help' is followed by a command, the detailed help for that command is\n"
306  " given. The keyword 'main' initiates a call to the main menu function of the\n"
307  " given plugin.\n",
308  "PUTE [ file ]\n"
309  " Put data into the EPG list. The data entered has to strictly follow the\n"
310  " format defined in vdr(5) for the 'epg.data' file. A '.' on a line\n"
311  " by itself terminates the input and starts processing of the data (all\n"
312  " entered data is buffered until the terminating '.' is seen).\n"
313  " If a file name is given, epg data will be read from this file (which\n"
314  " must be accessible under the given name from the machine VDR is running\n"
315  " on). In case of file input, no terminating '.' shall be given.\n",
316  "REMO [ on | off ]\n"
317  " Turns the remote control on or off. Without a parameter, the current\n"
318  " status of the remote control is reported.",
319  "SCAN\n"
320  " Forces an EPG scan. If this is a single DVB device system, the scan\n"
321  " will be done on the primary device unless it is currently recording.",
322  "STAT disk\n"
323  " Return information about disk usage (total, free, percent).",
324  "UPDT <settings>\n"
325  " Updates a timer. Settings must be in the same format as returned\n"
326  " by the LSTT command. If a timer with the same channel, day, start\n"
327  " and stop time does not yet exists, it will be created.",
328  "UPDR\n"
329  " Initiates a re-read of the recordings directory, which is the SVDRP\n"
330  " equivalent to 'touch .update'.",
331  "VOLU [ <number> | + | - | mute ]\n"
332  " Set the audio volume to the given number (which is limited to the range\n"
333  " 0...255). If the special options '+' or '-' are given, the volume will\n"
334  " be turned up or down, respectively. The option 'mute' will toggle the\n"
335  " audio muting. If no option is given, the current audio volume level will\n"
336  " be returned.",
337  "QUIT\n"
338  " Exit vdr (SVDRP).\n"
339  " You can also hit Ctrl-D to exit.",
340  NULL
341  };
342 
343 /* SVDRP Reply Codes:
344 
345  214 Help message
346  215 EPG or recording data record
347  216 Image grab data (base 64)
348  220 VDR service ready
349  221 VDR service closing transmission channel
350  250 Requested VDR action okay, completed
351  354 Start sending EPG data
352  451 Requested action aborted: local error in processing
353  500 Syntax error, command unrecognized
354  501 Syntax error in parameters or arguments
355  502 Command not implemented
356  504 Command parameter not implemented
357  550 Requested action not taken
358  554 Transaction failed
359  900 Default plugin reply code
360  901..999 Plugin specific reply codes
361 
362 */
363 
364 const char *GetHelpTopic(const char *HelpPage)
365 {
366  static char topic[MAXHELPTOPIC];
367  const char *q = HelpPage;
368  while (*q) {
369  if (isspace(*q)) {
370  uint n = q - HelpPage;
371  if (n >= sizeof(topic))
372  n = sizeof(topic) - 1;
373  strncpy(topic, HelpPage, n);
374  topic[n] = 0;
375  return topic;
376  }
377  q++;
378  }
379  return NULL;
380 }
381 
382 const char *GetHelpPage(const char *Cmd, const char **p)
383 {
384  if (p) {
385  while (*p) {
386  const char *t = GetHelpTopic(*p);
387  if (strcasecmp(Cmd, t) == 0)
388  return *p;
389  p++;
390  }
391  }
392  return NULL;
393 }
394 
395 char *cSVDRP::grabImageDir = NULL;
396 
397 cSVDRP::cSVDRP(int Port)
398 :socket(Port)
399 {
400  PUTEhandler = NULL;
401  numChars = 0;
402  length = BUFSIZ;
403  cmdLine = MALLOC(char, length);
404  lastActivity = 0;
405  isyslog("SVDRP listening on port %d", Port);
406 }
407 
409 {
410  Close(true);
411  free(cmdLine);
412 }
413 
414 void cSVDRP::Close(bool SendReply, bool Timeout)
415 {
416  if (file.IsOpen()) {
417  if (SendReply) {
418  //TODO how can we get the *full* hostname?
419  char buffer[BUFSIZ];
420  gethostname(buffer, sizeof(buffer));
421  Reply(221, "%s closing connection%s", buffer, Timeout ? " (timeout)" : "");
422  }
423  isyslog("closing SVDRP connection"); //TODO store IP#???
424  file.Close();
426  }
427 }
428 
429 bool cSVDRP::Send(const char *s, int length)
430 {
431  if (length < 0)
432  length = strlen(s);
433  if (safe_write(file, s, length) < 0) {
434  LOG_ERROR;
435  Close();
436  return false;
437  }
438  return true;
439 }
440 
441 void cSVDRP::Reply(int Code, const char *fmt, ...)
442 {
443  if (file.IsOpen()) {
444  if (Code != 0) {
445  va_list ap;
446  va_start(ap, fmt);
447  cString buffer = cString::vsprintf(fmt, ap);
448  va_end(ap);
449  const char *s = buffer;
450  while (s && *s) {
451  const char *n = strchr(s, '\n');
452  char cont = ' ';
453  if (Code < 0 || n && *(n + 1)) // trailing newlines don't count!
454  cont = '-';
455  char number[16];
456  sprintf(number, "%03d%c", abs(Code), cont);
457  if (!(Send(number) && Send(s, n ? n - s : -1) && Send("\r\n")))
458  break;
459  s = n ? n + 1 : NULL;
460  }
461  }
462  else {
463  Reply(451, "Zero return code - looks like a programming error!");
464  esyslog("SVDRP: zero return code!");
465  }
466  }
467 }
468 
469 void cSVDRP::PrintHelpTopics(const char **hp)
470 {
471  int NumPages = 0;
472  if (hp) {
473  while (*hp) {
474  NumPages++;
475  hp++;
476  }
477  hp -= NumPages;
478  }
479  const int TopicsPerLine = 5;
480  int x = 0;
481  for (int y = 0; (y * TopicsPerLine + x) < NumPages; y++) {
482  char buffer[TopicsPerLine * MAXHELPTOPIC + 5];
483  char *q = buffer;
484  q += sprintf(q, " ");
485  for (x = 0; x < TopicsPerLine && (y * TopicsPerLine + x) < NumPages; x++) {
486  const char *topic = GetHelpTopic(hp[(y * TopicsPerLine + x)]);
487  if (topic)
488  q += sprintf(q, "%*s", -MAXHELPTOPIC, topic);
489  }
490  x = 0;
491  Reply(-214, "%s", buffer);
492  }
493 }
494 
495 void cSVDRP::CmdCHAN(const char *Option)
496 {
497  if (*Option) {
498  int n = -1;
499  int d = 0;
500  if (isnumber(Option)) {
501  int o = strtol(Option, NULL, 10);
502  if (o >= 1 && o <= Channels.MaxNumber())
503  n = o;
504  }
505  else if (strcmp(Option, "-") == 0) {
507  if (n > 1) {
508  n--;
509  d = -1;
510  }
511  }
512  else if (strcmp(Option, "+") == 0) {
514  if (n < Channels.MaxNumber()) {
515  n++;
516  d = 1;
517  }
518  }
519  else {
521  if (channel)
522  n = channel->Number();
523  else {
524  for (cChannel *channel = Channels.First(); channel; channel = Channels.Next(channel)) {
525  if (!channel->GroupSep()) {
526  if (strcasecmp(channel->Name(), Option) == 0) {
527  n = channel->Number();
528  break;
529  }
530  }
531  }
532  }
533  }
534  if (n < 0) {
535  Reply(501, "Undefined channel \"%s\"", Option);
536  return;
537  }
538  if (!d) {
539  cChannel *channel = Channels.GetByNumber(n);
540  if (channel) {
541  if (!cDevice::PrimaryDevice()->SwitchChannel(channel, true)) {
542  Reply(554, "Error switching to channel \"%d\"", channel->Number());
543  return;
544  }
545  }
546  else {
547  Reply(550, "Unable to find channel \"%s\"", Option);
548  return;
549  }
550  }
551  else
553  }
555  if (channel)
556  Reply(250, "%d %s", channel->Number(), channel->Name());
557  else
558  Reply(550, "Unable to find channel \"%d\"", cDevice::CurrentChannel());
559 }
560 
561 void cSVDRP::CmdCLRE(const char *Option)
562 {
563  if (*Option) {
564  tChannelID ChannelID = tChannelID::InvalidID;
565  if (isnumber(Option)) {
566  int o = strtol(Option, NULL, 10);
567  if (o >= 1 && o <= Channels.MaxNumber())
568  ChannelID = Channels.GetByNumber(o)->GetChannelID();
569  }
570  else {
571  ChannelID = tChannelID::FromString(Option);
572  if (ChannelID == tChannelID::InvalidID) {
573  for (cChannel *Channel = Channels.First(); Channel; Channel = Channels.Next(Channel)) {
574  if (!Channel->GroupSep()) {
575  if (strcasecmp(Channel->Name(), Option) == 0) {
576  ChannelID = Channel->GetChannelID();
577  break;
578  }
579  }
580  }
581  }
582  }
583  if (!(ChannelID == tChannelID::InvalidID)) {
584  cSchedulesLock SchedulesLock(true, 1000);
585  cSchedules *s = (cSchedules *)cSchedules::Schedules(SchedulesLock);
586  if (s) {
587  cSchedule *Schedule = NULL;
588  ChannelID.ClrRid();
589  for (cSchedule *p = s->First(); p; p = s->Next(p)) {
590  if (p->ChannelID() == ChannelID) {
591  Schedule = p;
592  break;
593  }
594  }
595  if (Schedule) {
596  for (cTimer *Timer = Timers.First(); Timer; Timer = Timers.Next(Timer)) {
597  if (ChannelID == Timer->Channel()->GetChannelID().ClrRid())
598  Timer->SetEvent(NULL);
599  }
600  Schedule->Cleanup(INT_MAX);
602  Reply(250, "EPG data of channel \"%s\" cleared", Option);
603  }
604  else {
605  Reply(550, "No EPG data found for channel \"%s\"", Option);
606  return;
607  }
608  }
609  else
610  Reply(451, "Can't get EPG data");
611  }
612  else
613  Reply(501, "Undefined channel \"%s\"", Option);
614  }
615  else {
617  if (cSchedules::ClearAll()) {
618  Reply(250, "EPG data cleared");
620  }
621  else
622  Reply(451, "Error while clearing EPG data");
623  }
624 }
625 
626 void cSVDRP::CmdCPYR(const char *Option)
627 {
628  if (*Option) {
629  char *tail;
630  int n = strtol(Option, &tail, 10);
631  cRecording *recording = Recordings.Get(n - 1);
632  if (recording && tail && tail != Option) {
633  char *oldName = strdup(recording->Name());
634  tail = skipspace(tail);
635  if (!cFileTransfer::Active()) {
636  if (cFileTransfer::Start(recording, tail, true))
637  Reply(250, "Copying recording \"%s\" to \"%s\"", oldName, tail);
638  else
639  Reply(554, "Can't start file transfer");
640  }
641  else
642  Reply(554, "File transfer already active");
643  free(oldName);
644  }
645  else
646  Reply(550, "Recording \"%d\" not found%s", n, Recordings.Count() ? "" : " (use LSTR before copying)");
647  }
648  else
649  Reply(501, "Invalid Option \"%s\"", Option);
650 }
651 
652 void cSVDRP::CmdDELC(const char *Option)
653 {
654  if (*Option) {
655  if (isnumber(Option)) {
656  if (!Channels.BeingEdited()) {
657  cChannel *channel = Channels.GetByNumber(strtol(Option, NULL, 10));
658  if (channel) {
659  for (cTimer *timer = Timers.First(); timer; timer = Timers.Next(timer)) {
660  if (timer->Channel() == channel) {
661  Reply(550, "Channel \"%s\" is in use by timer %d", Option, timer->Index() + 1);
662  return;
663  }
664  }
665  int CurrentChannelNr = cDevice::CurrentChannel();
666  cChannel *CurrentChannel = Channels.GetByNumber(CurrentChannelNr);
667  if (CurrentChannel && channel == CurrentChannel) {
668  int n = Channels.GetNextNormal(CurrentChannel->Index());
669  if (n < 0)
670  n = Channels.GetPrevNormal(CurrentChannel->Index());
671  CurrentChannel = Channels.Get(n);
672  CurrentChannelNr = 0; // triggers channel switch below
673  }
674  Channels.Del(channel);
675  Channels.ReNumber();
676  Channels.SetModified(true);
677  isyslog("channel %s deleted", Option);
678  if (CurrentChannel && CurrentChannel->Number() != CurrentChannelNr) {
680  Channels.SwitchTo(CurrentChannel->Number());
681  else
682  cDevice::SetCurrentChannel(CurrentChannel);
683  }
684  Reply(250, "Channel \"%s\" deleted", Option);
685  }
686  else
687  Reply(501, "Channel \"%s\" not defined", Option);
688  }
689  else
690  Reply(550, "Channels are being edited - try again later");
691  }
692  else
693  Reply(501, "Error in channel number \"%s\"", Option);
694  }
695  else
696  Reply(501, "Missing channel number");
697 }
698 
699 void cSVDRP::CmdDELR(const char *Option)
700 {
701  if (*Option) {
702  if (isnumber(Option)) {
703  cRecording *recording = recordings.Get(strtol(Option, NULL, 10) - 1);
704  if (recording) {
706  if (!rc) {
707  if (!cCutter::Active(recording->FileName())) {
708  if (recording->Delete()) {
709  Reply(250, "Recording \"%s\" deleted", Option);
710  Recordings.DelByName(recording->FileName());
711  }
712  else
713  Reply(554, "Error while deleting recording!");
714  }
715  else
716  Reply(550, "Recording \"%s\" is being edited", Option);
717  }
718  else
719  Reply(550, "Recording \"%s\" is in use by timer %d", Option, rc->Timer()->Index() + 1);
720  }
721  else
722  Reply(550, "Recording \"%s\" not found%s", Option, recordings.Count() ? "" : " (use LSTR before deleting)");
723  }
724  else
725  Reply(501, "Error in recording number \"%s\"", Option);
726  }
727  else
728  Reply(501, "Missing recording number");
729 }
730 
731 void cSVDRP::CmdDELT(const char *Option)
732 {
733  if (*Option) {
734  if (isnumber(Option)) {
735  if (!Timers.BeingEdited()) {
736  cTimer *timer = Timers.Get(strtol(Option, NULL, 10) - 1);
737  if (timer) {
738  if (!timer->Recording()) {
739  isyslog("deleting timer %s", *timer->ToDescr());
740  Timers.Del(timer);
742  Reply(250, "Timer \"%s\" deleted", Option);
743  }
744  else
745  Reply(550, "Timer \"%s\" is recording", Option);
746  }
747  else
748  Reply(501, "Timer \"%s\" not defined", Option);
749  }
750  else
751  Reply(550, "Timers are being edited - try again later");
752  }
753  else
754  Reply(501, "Error in timer number \"%s\"", Option);
755  }
756  else
757  Reply(501, "Missing timer number");
758 }
759 
760 void cSVDRP::CmdEDIT(const char *Option)
761 {
762  if (*Option) {
763  if (isnumber(Option)) {
764  cRecording *recording = recordings.Get(strtol(Option, NULL, 10) - 1);
765  if (recording) {
766  cMarks Marks;
767  if (Marks.Load(recording->FileName(), recording->FramesPerSecond(), recording->IsPesRecording()) && Marks.Count()) {
768  if (!cCutter::Active()) {
769  if (cCutter::Start(recording->FileName()))
770  Reply(250, "Editing recording \"%s\" [%s]", Option, recording->Title());
771  else
772  Reply(554, "Can't start editing process");
773  }
774  else
775  Reply(554, "Editing process already active");
776  }
777  else
778  Reply(554, "No editing marks defined");
779  }
780  else
781  Reply(550, "Recording \"%s\" not found%s", Option, recordings.Count() ? "" : " (use LSTR before editing)");
782  }
783  else
784  Reply(501, "Error in recording number \"%s\"", Option);
785  }
786  else
787  Reply(501, "Missing recording number");
788 }
789 
790 void cSVDRP::CmdGRAB(const char *Option)
791 {
792  const char *FileName = NULL;
793  bool Jpeg = true;
794  int Quality = -1, SizeX = -1, SizeY = -1;
795  if (*Option) {
796  char buf[strlen(Option) + 1];
797  char *p = strcpy(buf, Option);
798  const char *delim = " \t";
799  char *strtok_next;
800  FileName = strtok_r(p, delim, &strtok_next);
801  // image type:
802  const char *Extension = strrchr(FileName, '.');
803  if (Extension) {
804  if (strcasecmp(Extension, ".jpg") == 0 || strcasecmp(Extension, ".jpeg") == 0)
805  Jpeg = true;
806  else if (strcasecmp(Extension, ".pnm") == 0)
807  Jpeg = false;
808  else {
809  Reply(501, "Unknown image type \"%s\"", Extension + 1);
810  return;
811  }
812  if (Extension == FileName)
813  FileName = NULL;
814  }
815  else if (strcmp(FileName, "-") == 0)
816  FileName = NULL;
817  // image quality (and obsolete type):
818  if ((p = strtok_r(NULL, delim, &strtok_next)) != NULL) {
819  if (strcasecmp(p, "JPEG") == 0 || strcasecmp(p, "PNM") == 0) {
820  // tolerate for backward compatibility
821  p = strtok_r(NULL, delim, &strtok_next);
822  }
823  if (p) {
824  if (isnumber(p))
825  Quality = atoi(p);
826  else {
827  Reply(501, "Invalid quality \"%s\"", p);
828  return;
829  }
830  }
831  }
832  // image size:
833  if ((p = strtok_r(NULL, delim, &strtok_next)) != NULL) {
834  if (isnumber(p))
835  SizeX = atoi(p);
836  else {
837  Reply(501, "Invalid sizex \"%s\"", p);
838  return;
839  }
840  if ((p = strtok_r(NULL, delim, &strtok_next)) != NULL) {
841  if (isnumber(p))
842  SizeY = atoi(p);
843  else {
844  Reply(501, "Invalid sizey \"%s\"", p);
845  return;
846  }
847  }
848  else {
849  Reply(501, "Missing sizey");
850  return;
851  }
852  }
853  if ((p = strtok_r(NULL, delim, &strtok_next)) != NULL) {
854  Reply(501, "Unexpected parameter \"%s\"", p);
855  return;
856  }
857  // canonicalize the file name:
858  char RealFileName[PATH_MAX];
859  if (FileName) {
860  if (grabImageDir) {
861  cString s(FileName);
862  FileName = s;
863  const char *slash = strrchr(FileName, '/');
864  if (!slash) {
865  s = AddDirectory(grabImageDir, FileName);
866  FileName = s;
867  }
868  slash = strrchr(FileName, '/'); // there definitely is one
869  cString t(s);
870  t.Truncate(slash - FileName);
871  char *r = realpath(t, RealFileName);
872  if (!r) {
873  LOG_ERROR_STR(FileName);
874  Reply(501, "Invalid file name \"%s\"", FileName);
875  return;
876  }
877  strcat(RealFileName, slash);
878  FileName = RealFileName;
879  if (strncmp(FileName, grabImageDir, strlen(grabImageDir)) != 0) {
880  Reply(501, "Invalid file name \"%s\"", FileName);
881  return;
882  }
883  }
884  else {
885  Reply(550, "Grabbing to file not allowed (use \"GRAB -\" instead)");
886  return;
887  }
888  }
889  // actual grabbing:
890  int ImageSize;
891  uchar *Image = cDevice::PrimaryDevice()->GrabImage(ImageSize, Jpeg, Quality, SizeX, SizeY);
892  if (Image) {
893  if (FileName) {
894  int fd = open(FileName, O_WRONLY | O_CREAT | O_NOFOLLOW | O_TRUNC, DEFFILEMODE);
895  if (fd >= 0) {
896  if (safe_write(fd, Image, ImageSize) == ImageSize) {
897  dsyslog("grabbed image to %s", FileName);
898  Reply(250, "Grabbed image %s", Option);
899  }
900  else {
901  LOG_ERROR_STR(FileName);
902  Reply(451, "Can't write to '%s'", FileName);
903  }
904  close(fd);
905  }
906  else {
907  LOG_ERROR_STR(FileName);
908  Reply(451, "Can't open '%s'", FileName);
909  }
910  }
911  else {
912  cBase64Encoder Base64(Image, ImageSize);
913  const char *s;
914  while ((s = Base64.NextLine()) != NULL)
915  Reply(-216, "%s", s);
916  Reply(216, "Grabbed image %s", Option);
917  }
918  free(Image);
919  }
920  else
921  Reply(451, "Grab image failed");
922  }
923  else
924  Reply(501, "Missing filename");
925 }
926 
927 void cSVDRP::CmdHELP(const char *Option)
928 {
929  if (*Option) {
930  const char *hp = GetHelpPage(Option, HelpPages);
931  if (hp)
932  Reply(-214, "%s", hp);
933  else {
934  Reply(504, "HELP topic \"%s\" unknown", Option);
935  return;
936  }
937  }
938  else {
939  Reply(-214, "This is VDR version %s", VDRVERSION);
940  Reply(-214, "Topics:");
942  cPlugin *plugin;
943  for (int i = 0; (plugin = cPluginManager::GetPlugin(i)) != NULL; i++) {
944  const char **hp = plugin->SVDRPHelpPages();
945  if (hp)
946  Reply(-214, "Plugin %s v%s - %s", plugin->Name(), plugin->Version(), plugin->Description());
947  PrintHelpTopics(hp);
948  }
949  Reply(-214, "To report bugs in the implementation send email to");
950  Reply(-214, " vdr-bugs@tvdr.de");
951  }
952  Reply(214, "End of HELP info");
953 }
954 
955 void cSVDRP::CmdHITK(const char *Option)
956 {
957  if (*Option) {
958  if (!cRemote::Enabled()) {
959  Reply(550, "Remote control currently disabled (key \"%s\" discarded)", Option);
960  return;
961  }
962  char buf[strlen(Option) + 1];
963  strcpy(buf, Option);
964  const char *delim = " \t";
965  char *strtok_next;
966  char *p = strtok_r(buf, delim, &strtok_next);
967  int NumKeys = 0;
968  while (p) {
969  eKeys k = cKey::FromString(p);
970  if (k != kNone) {
971  if (!cRemote::Put(k)) {
972  Reply(451, "Too many keys in \"%s\" (only %d accepted)", Option, NumKeys);
973  return;
974  }
975  }
976  else {
977  Reply(504, "Unknown key: \"%s\"", p);
978  return;
979  }
980  NumKeys++;
981  p = strtok_r(NULL, delim, &strtok_next);
982  }
983  Reply(250, "Key%s \"%s\" accepted", NumKeys > 1 ? "s" : "", Option);
984  }
985  else {
986  Reply(-214, "Valid <key> names for the HITK command:");
987  for (int i = 0; i < kNone; i++) {
988  Reply(-214, " %s", cKey::ToString(eKeys(i)));
989  }
990  Reply(214, "End of key list");
991  }
992 }
993 
994 void cSVDRP::CmdLSTC(const char *Option)
995 {
996  bool WithGroupSeps = strcasecmp(Option, ":groups") == 0;
997  if (*Option && !WithGroupSeps) {
998  if (isnumber(Option)) {
999  cChannel *channel = Channels.GetByNumber(strtol(Option, NULL, 10));
1000  if (channel)
1001  Reply(250, "%d %s", channel->Number(), *channel->ToText());
1002  else
1003  Reply(501, "Channel \"%s\" not defined", Option);
1004  }
1005  else {
1007  if (!next) {
1008  for (cChannel *channel = Channels.First(); channel; channel = Channels.Next(channel)) {
1009  if (!channel->GroupSep()) {
1010  if (strcasestr(channel->Name(), Option)) {
1011  if (next)
1012  Reply(-250, "%d %s", next->Number(), *next->ToText());
1013  next = channel;
1014  }
1015  }
1016  }
1017  }
1018  if (next)
1019  Reply(250, "%d %s", next->Number(), *next->ToText());
1020  else
1021  Reply(501, "Channel \"%s\" not defined", Option);
1022  }
1023  }
1024  else if (Channels.MaxNumber() >= 1) {
1025  for (cChannel *channel = Channels.First(); channel; channel = Channels.Next(channel)) {
1026  if (WithGroupSeps)
1027  Reply(channel->Next() ? -250: 250, "%d %s", channel->GroupSep() ? 0 : channel->Number(), *channel->ToText());
1028  else if (!channel->GroupSep())
1029  Reply(channel->Number() < Channels.MaxNumber() ? -250 : 250, "%d %s", channel->Number(), *channel->ToText());
1030  }
1031  }
1032  else
1033  Reply(550, "No channels defined");
1034 }
1035 
1036 void cSVDRP::CmdLSTE(const char *Option)
1037 {
1038  cSchedulesLock SchedulesLock;
1039  const cSchedules *Schedules = cSchedules::Schedules(SchedulesLock);
1040  if (Schedules) {
1041  const cSchedule* Schedule = NULL;
1042  eDumpMode DumpMode = dmAll;
1043  time_t AtTime = 0;
1044  if (*Option) {
1045  char buf[strlen(Option) + 1];
1046  strcpy(buf, Option);
1047  const char *delim = " \t";
1048  char *strtok_next;
1049  char *p = strtok_r(buf, delim, &strtok_next);
1050  while (p && DumpMode == dmAll) {
1051  if (strcasecmp(p, "NOW") == 0)
1052  DumpMode = dmPresent;
1053  else if (strcasecmp(p, "NEXT") == 0)
1054  DumpMode = dmFollowing;
1055  else if (strcasecmp(p, "AT") == 0) {
1056  DumpMode = dmAtTime;
1057  if ((p = strtok_r(NULL, delim, &strtok_next)) != NULL) {
1058  if (isnumber(p))
1059  AtTime = strtol(p, NULL, 10);
1060  else {
1061  Reply(501, "Invalid time");
1062  return;
1063  }
1064  }
1065  else {
1066  Reply(501, "Missing time");
1067  return;
1068  }
1069  }
1070  else if (!Schedule) {
1071  cChannel* Channel = NULL;
1072  if (isnumber(p))
1073  Channel = Channels.GetByNumber(strtol(Option, NULL, 10));
1074  else
1075  Channel = Channels.GetByChannelID(tChannelID::FromString(Option));
1076  if (Channel) {
1077  Schedule = Schedules->GetSchedule(Channel);
1078  if (!Schedule) {
1079  Reply(550, "No schedule found");
1080  return;
1081  }
1082  }
1083  else {
1084  Reply(550, "Channel \"%s\" not defined", p);
1085  return;
1086  }
1087  }
1088  else {
1089  Reply(501, "Unknown option: \"%s\"", p);
1090  return;
1091  }
1092  p = strtok_r(NULL, delim, &strtok_next);
1093  }
1094  }
1095  int fd = dup(file);
1096  if (fd) {
1097  FILE *f = fdopen(fd, "w");
1098  if (f) {
1099  if (Schedule)
1100  Schedule->Dump(f, "215-", DumpMode, AtTime);
1101  else
1102  Schedules->Dump(f, "215-", DumpMode, AtTime);
1103  fflush(f);
1104  Reply(215, "End of EPG data");
1105  fclose(f);
1106  }
1107  else {
1108  Reply(451, "Can't open file connection");
1109  close(fd);
1110  }
1111  }
1112  else
1113  Reply(451, "Can't dup stream descriptor");
1114  }
1115  else
1116  Reply(451, "Can't get EPG data");
1117 }
1118 
1119 void cSVDRP::CmdLSTR(const char *Option)
1120 {
1121  int Number = 0;
1122  bool Path = false;
1123  recordings.Update(true);
1124  if (*Option) {
1125  char buf[strlen(Option) + 1];
1126  strcpy(buf, Option);
1127  const char *delim = " \t";
1128  char *strtok_next;
1129  char *p = strtok_r(buf, delim, &strtok_next);
1130  while (p) {
1131  if (!Number) {
1132  if (isnumber(p))
1133  Number = strtol(p, NULL, 10);
1134  else {
1135  Reply(501, "Error in recording number \"%s\"", Option);
1136  return;
1137  }
1138  }
1139  else if (strcasecmp(p, "PATH") == 0)
1140  Path = true;
1141  else {
1142  Reply(501, "Unknown option: \"%s\"", p);
1143  return;
1144  }
1145  p = strtok_r(NULL, delim, &strtok_next);
1146  }
1147  if (Number) {
1148  cRecording *recording = recordings.Get(strtol(Option, NULL, 10) - 1);
1149  if (recording) {
1150  FILE *f = fdopen(file, "w");
1151  if (f) {
1152  if (Path)
1153  Reply(250, "%s", recording->FileName());
1154  else {
1155  recording->Info()->Write(f, "215-");
1156  fflush(f);
1157  Reply(215, "End of recording information");
1158  }
1159  // don't 'fclose(f)' here!
1160  }
1161  else
1162  Reply(451, "Can't open file connection");
1163  }
1164  else
1165  Reply(550, "Recording \"%s\" not found", Option);
1166  }
1167  }
1168  else if (recordings.Count()) {
1169  cRecording *recording = recordings.First();
1170  while (recording) {
1171  Reply(recording == recordings.Last() ? 250 : -250, "%d %s", recording->Index() + 1, recording->Title(' ', true));
1172  recording = recordings.Next(recording);
1173  }
1174  }
1175  else
1176  Reply(550, "No recordings available");
1177 }
1178 
1179 void cSVDRP::CmdLSTT(const char *Option)
1180 {
1181  int Number = 0;
1182  bool Id = false;
1183  if (*Option) {
1184  char buf[strlen(Option) + 1];
1185  strcpy(buf, Option);
1186  const char *delim = " \t";
1187  char *strtok_next;
1188  char *p = strtok_r(buf, delim, &strtok_next);
1189  while (p) {
1190  if (isnumber(p))
1191  Number = strtol(p, NULL, 10);
1192  else if (strcasecmp(p, "ID") == 0)
1193  Id = true;
1194  else {
1195  Reply(501, "Unknown option: \"%s\"", p);
1196  return;
1197  }
1198  p = strtok_r(NULL, delim, &strtok_next);
1199  }
1200  }
1201  if (Number) {
1202  cTimer *timer = Timers.Get(Number - 1);
1203  if (timer)
1204  Reply(250, "%d %s", timer->Index() + 1, *timer->ToText(Id));
1205  else
1206  Reply(501, "Timer \"%s\" not defined", Option);
1207  }
1208  else if (Timers.Count()) {
1209  for (int i = 0; i < Timers.Count(); i++) {
1210  cTimer *timer = Timers.Get(i);
1211  if (timer)
1212  Reply(i < Timers.Count() - 1 ? -250 : 250, "%d %s", timer->Index() + 1, *timer->ToText(Id));
1213  else
1214  Reply(501, "Timer \"%d\" not found", i + 1);
1215  }
1216  }
1217  else
1218  Reply(550, "No timers defined");
1219 }
1220 
1221 void cSVDRP::CmdMESG(const char *Option)
1222 {
1223  if (*Option) {
1224  isyslog("SVDRP message: '%s'", Option);
1225  Skins.QueueMessage(mtInfo, Option);
1226  Reply(250, "Message queued");
1227  }
1228  else
1229  Reply(501, "Missing message");
1230 }
1231 
1232 void cSVDRP::CmdMODC(const char *Option)
1233 {
1234  if (*Option) {
1235  char *tail;
1236  int n = strtol(Option, &tail, 10);
1237  if (tail && tail != Option) {
1238  tail = skipspace(tail);
1239  if (!Channels.BeingEdited()) {
1240  cChannel *channel = Channels.GetByNumber(n);
1241  if (channel) {
1242  cChannel ch;
1243  if (ch.Parse(tail)) {
1244  if (Channels.HasUniqueChannelID(&ch, channel)) {
1245  *channel = ch;
1246  Channels.ReNumber();
1247  Channels.SetModified(true);
1248  isyslog("modifed channel %d %s", channel->Number(), *channel->ToText());
1249  Reply(250, "%d %s", channel->Number(), *channel->ToText());
1250  }
1251  else
1252  Reply(501, "Channel settings are not unique");
1253  }
1254  else
1255  Reply(501, "Error in channel settings");
1256  }
1257  else
1258  Reply(501, "Channel \"%d\" not defined", n);
1259  }
1260  else
1261  Reply(550, "Channels are being edited - try again later");
1262  }
1263  else
1264  Reply(501, "Error in channel number");
1265  }
1266  else
1267  Reply(501, "Missing channel settings");
1268 }
1269 
1270 void cSVDRP::CmdMODT(const char *Option)
1271 {
1272  if (*Option) {
1273  char *tail;
1274  int n = strtol(Option, &tail, 10);
1275  if (tail && tail != Option) {
1276  tail = skipspace(tail);
1277  if (!Timers.BeingEdited()) {
1278  cTimer *timer = Timers.Get(n - 1);
1279  if (timer) {
1280  cTimer t = *timer;
1281  if (strcasecmp(tail, "ON") == 0)
1282  t.SetFlags(tfActive);
1283  else if (strcasecmp(tail, "OFF") == 0)
1284  t.ClrFlags(tfActive);
1285  else if (!t.Parse(tail)) {
1286  Reply(501, "Error in timer settings");
1287  return;
1288  }
1289  *timer = t;
1290  Timers.SetModified();
1291  isyslog("timer %s modified (%s)", *timer->ToDescr(), timer->HasFlags(tfActive) ? "active" : "inactive");
1292  Reply(250, "%d %s", timer->Index() + 1, *timer->ToText());
1293  }
1294  else
1295  Reply(501, "Timer \"%d\" not defined", n);
1296  }
1297  else
1298  Reply(550, "Timers are being edited - try again later");
1299  }
1300  else
1301  Reply(501, "Error in timer number");
1302  }
1303  else
1304  Reply(501, "Missing timer settings");
1305 }
1306 
1307 void cSVDRP::CmdMOVC(const char *Option)
1308 {
1309  if (*Option) {
1310  if (!Channels.BeingEdited() && !Timers.BeingEdited()) {
1311  char *tail;
1312  int From = strtol(Option, &tail, 10);
1313  if (tail && tail != Option) {
1314  tail = skipspace(tail);
1315  if (tail && tail != Option) {
1316  int To = strtol(tail, NULL, 10);
1317  int CurrentChannelNr = cDevice::CurrentChannel();
1318  cChannel *CurrentChannel = Channels.GetByNumber(CurrentChannelNr);
1319  cChannel *FromChannel = Channels.GetByNumber(From);
1320  if (FromChannel) {
1321  cChannel *ToChannel = Channels.GetByNumber(To);
1322  if (ToChannel) {
1323  int FromNumber = FromChannel->Number();
1324  int ToNumber = ToChannel->Number();
1325  if (FromNumber != ToNumber) {
1326  Channels.Move(FromChannel, ToChannel);
1327  Channels.ReNumber();
1328  Channels.SetModified(true);
1329  if (CurrentChannel && CurrentChannel->Number() != CurrentChannelNr) {
1331  Channels.SwitchTo(CurrentChannel->Number());
1332  else
1333  cDevice::SetCurrentChannel(CurrentChannel);
1334  }
1335  isyslog("channel %d moved to %d", FromNumber, ToNumber);
1336  Reply(250,"Channel \"%d\" moved to \"%d\"", From, To);
1337  }
1338  else
1339  Reply(501, "Can't move channel to same position");
1340  }
1341  else
1342  Reply(501, "Channel \"%d\" not defined", To);
1343  }
1344  else
1345  Reply(501, "Channel \"%d\" not defined", From);
1346  }
1347  else
1348  Reply(501, "Error in channel number");
1349  }
1350  else
1351  Reply(501, "Error in channel number");
1352  }
1353  else
1354  Reply(550, "Channels or timers are being edited - try again later");
1355  }
1356  else
1357  Reply(501, "Missing channel number");
1358 }
1359 
1360 void cSVDRP::CmdMOVR(const char *Option)
1361 {
1362  if (*Option) {
1363  char *tail;
1364  int n = strtol(Option, &tail, 10);
1365  cRecording *recording = Recordings.Get(n - 1);
1366  if (recording && tail && tail != Option) {
1367  char *oldName = strdup(recording->Name());
1368  tail = skipspace(tail);
1369  if (!cFileTransfer::Active()) {
1370  if (cFileTransfer::Start(recording, tail))
1371  Reply(250, "Moving recording \"%s\" to \"%s\"", oldName, tail);
1372  else
1373  Reply(554, "Can't start file transfer");
1374  }
1375  else
1376  Reply(554, "File transfer already active");
1377  free(oldName);
1378  }
1379  else
1380  Reply(550, "Recording \"%d\" not found%s", n, Recordings.Count() ? "" : " (use LSTR before moving)");
1381  }
1382  else
1383  Reply(501, "Invalid Option \"%s\"", Option);
1384 }
1385 
1386 void cSVDRP::CmdNEWC(const char *Option)
1387 {
1388  if (*Option) {
1389  cChannel ch;
1390  if (ch.Parse(Option)) {
1391  if (Channels.HasUniqueChannelID(&ch)) {
1392  cChannel *channel = new cChannel;
1393  *channel = ch;
1394  Channels.Add(channel);
1395  Channels.ReNumber();
1396  Channels.SetModified(true);
1397  isyslog("new channel %d %s", channel->Number(), *channel->ToText());
1398  Reply(250, "%d %s", channel->Number(), *channel->ToText());
1399  }
1400  else
1401  Reply(501, "Channel settings are not unique");
1402  }
1403  else
1404  Reply(501, "Error in channel settings");
1405  }
1406  else
1407  Reply(501, "Missing channel settings");
1408 }
1409 
1410 void cSVDRP::CmdNEWT(const char *Option)
1411 {
1412  if (*Option) {
1413  cTimer *timer = new cTimer;
1414  if (timer->Parse(Option)) {
1415  Timers.Add(timer);
1416  Timers.SetModified();
1417  isyslog("timer %s added", *timer->ToDescr());
1418  Reply(250, "%d %s", timer->Index() + 1, *timer->ToText());
1419  return;
1420  }
1421  else
1422  Reply(501, "Error in timer settings");
1423  delete timer;
1424  }
1425  else
1426  Reply(501, "Missing timer settings");
1427 }
1428 
1429 void cSVDRP::CmdNEXT(const char *Option)
1430 {
1432  if (t) {
1433  time_t Start = t->StartTime();
1434  int Number = t->Index() + 1;
1435  if (!*Option)
1436  Reply(250, "%d %s", Number, *TimeToString(Start));
1437  else if (strcasecmp(Option, "ABS") == 0)
1438  Reply(250, "%d %ld", Number, Start);
1439  else if (strcasecmp(Option, "REL") == 0)
1440  Reply(250, "%d %ld", Number, Start - time(NULL));
1441  else
1442  Reply(501, "Unknown option: \"%s\"", Option);
1443  }
1444  else
1445  Reply(550, "No active timers");
1446 }
1447 
1448 void cSVDRP::CmdPLAY(const char *Option)
1449 {
1450  if (*Option) {
1451  char *opt = strdup(Option);
1452  char *num = skipspace(opt);
1453  char *option = num;
1454  while (*option && !isspace(*option))
1455  option++;
1456  char c = *option;
1457  *option = 0;
1458  if (isnumber(num)) {
1459  cRecording *recording = recordings.Get(strtol(num, NULL, 10) - 1);
1460  if (recording) {
1461  if (c)
1462  option = skipspace(++option);
1465  if (*option) {
1466  int pos = 0;
1467  if (strcasecmp(option, "BEGIN") != 0)
1468  pos = HMSFToIndex(option, recording->FramesPerSecond());
1469  cResumeFile resume(recording->FileName(), recording->IsPesRecording());
1470  if (pos <= 0)
1471  resume.Delete();
1472  else
1473  resume.Save(pos);
1474  }
1475  cReplayControl::SetRecording(recording->FileName());
1477  cControl::Attach();
1478  Reply(250, "Playing recording \"%s\" [%s]", num, recording->Title());
1479  }
1480  else
1481  Reply(550, "Recording \"%s\" not found%s", num, recordings.Count() ? "" : " (use LSTR before playing)");
1482  }
1483  else
1484  Reply(501, "Error in recording number \"%s\"", num);
1485  free(opt);
1486  }
1487  else
1488  Reply(501, "Missing recording number");
1489 }
1490 
1491 void cSVDRP::CmdPLUG(const char *Option)
1492 {
1493  if (*Option) {
1494  char *opt = strdup(Option);
1495  char *name = skipspace(opt);
1496  char *option = name;
1497  while (*option && !isspace(*option))
1498  option++;
1499  char c = *option;
1500  *option = 0;
1501  cPlugin *plugin = cPluginManager::GetPlugin(name);
1502  if (plugin) {
1503  if (c)
1504  option = skipspace(++option);
1505  char *cmd = option;
1506  while (*option && !isspace(*option))
1507  option++;
1508  if (*option) {
1509  *option++ = 0;
1510  option = skipspace(option);
1511  }
1512  if (!*cmd || strcasecmp(cmd, "HELP") == 0) {
1513  if (*cmd && *option) {
1514  const char *hp = GetHelpPage(option, plugin->SVDRPHelpPages());
1515  if (hp) {
1516  Reply(-214, "%s", hp);
1517  Reply(214, "End of HELP info");
1518  }
1519  else
1520  Reply(504, "HELP topic \"%s\" for plugin \"%s\" unknown", option, plugin->Name());
1521  }
1522  else {
1523  Reply(-214, "Plugin %s v%s - %s", plugin->Name(), plugin->Version(), plugin->Description());
1524  const char **hp = plugin->SVDRPHelpPages();
1525  if (hp) {
1526  Reply(-214, "SVDRP commands:");
1527  PrintHelpTopics(hp);
1528  Reply(214, "End of HELP info");
1529  }
1530  else
1531  Reply(214, "This plugin has no SVDRP commands");
1532  }
1533  }
1534  else if (strcasecmp(cmd, "MAIN") == 0) {
1535  if (cRemote::CallPlugin(plugin->Name()))
1536  Reply(250, "Initiated call to main menu function of plugin \"%s\"", plugin->Name());
1537  else
1538  Reply(550, "A plugin call is already pending - please try again later");
1539  }
1540  else {
1541  int ReplyCode = 900;
1542  cString s = plugin->SVDRPCommand(cmd, option, ReplyCode);
1543  if (*s)
1544  Reply(abs(ReplyCode), "%s", *s);
1545  else
1546  Reply(500, "Command unrecognized: \"%s\"", cmd);
1547  }
1548  }
1549  else
1550  Reply(550, "Plugin \"%s\" not found (use PLUG for a list of plugins)", name);
1551  free(opt);
1552  }
1553  else {
1554  Reply(-214, "Available plugins:");
1555  cPlugin *plugin;
1556  for (int i = 0; (plugin = cPluginManager::GetPlugin(i)) != NULL; i++)
1557  Reply(-214, "%s v%s - %s", plugin->Name(), plugin->Version(), plugin->Description());
1558  Reply(214, "End of plugin list");
1559  }
1560 }
1561 
1562 void cSVDRP::CmdPUTE(const char *Option)
1563 {
1564  if (*Option) {
1565  FILE *f = fopen(Option, "r");
1566  if (f) {
1567  if (cSchedules::Read(f)) {
1568  cSchedules::Cleanup(true);
1569  Reply(250, "EPG data processed from \"%s\"", Option);
1570  }
1571  else
1572  Reply(451, "Error while processing EPG from \"%s\"", Option);
1573  fclose(f);
1574  }
1575  else
1576  Reply(501, "Cannot open file \"%s\"", Option);
1577  }
1578  else {
1579  delete PUTEhandler;
1580  PUTEhandler = new cPUTEhandler;
1581  Reply(PUTEhandler->Status(), "%s", PUTEhandler->Message());
1582  if (PUTEhandler->Status() != 354)
1584  }
1585 }
1586 
1587 void cSVDRP::CmdREMO(const char *Option)
1588 {
1589  if (*Option) {
1590  if (!strcasecmp(Option, "ON")) {
1591  cRemote::SetEnabled(true);
1592  Reply(250, "Remote control enabled");
1593  }
1594  else if (!strcasecmp(Option, "OFF")) {
1595  cRemote::SetEnabled(false);
1596  Reply(250, "Remote control disabled");
1597  }
1598  else
1599  Reply(501, "Invalid Option \"%s\"", Option);
1600  }
1601  else
1602  Reply(250, "Remote control is %s", cRemote::Enabled() ? "enabled" : "disabled");
1603 }
1604 
1605 void cSVDRP::CmdSCAN(const char *Option)
1606 {
1608  Reply(250, "EPG scan triggered");
1609 }
1610 
1611 void cSVDRP::CmdSTAT(const char *Option)
1612 {
1613  if (*Option) {
1614  if (strcasecmp(Option, "DISK") == 0) {
1615  int FreeMB, UsedMB;
1616  int Percent = VideoDiskSpace(&FreeMB, &UsedMB);
1617  Reply(250, "%dMB %dMB %d%%", FreeMB + UsedMB, FreeMB, Percent);
1618  }
1619  else
1620  Reply(501, "Invalid Option \"%s\"", Option);
1621  }
1622  else
1623  Reply(501, "No option given");
1624 }
1625 
1626 void cSVDRP::CmdUPDT(const char *Option)
1627 {
1628  if (*Option) {
1629  cTimer *timer = new cTimer;
1630  if (timer->Parse(Option)) {
1631  if (!Timers.BeingEdited()) {
1632  cTimer *t = Timers.GetTimer(timer);
1633  if (t) {
1634  t->Parse(Option);
1635  delete timer;
1636  timer = t;
1637  isyslog("timer %s updated", *timer->ToDescr());
1638  }
1639  else {
1640  Timers.Add(timer);
1641  isyslog("timer %s added", *timer->ToDescr());
1642  }
1643  Timers.SetModified();
1644  Reply(250, "%d %s", timer->Index() + 1, *timer->ToText());
1645  return;
1646  }
1647  else
1648  Reply(550, "Timers are being edited - try again later");
1649  }
1650  else
1651  Reply(501, "Error in timer settings");
1652  delete timer;
1653  }
1654  else
1655  Reply(501, "Missing timer settings");
1656 }
1657 
1658 void cSVDRP::CmdUPDR(const char *Option)
1659 {
1660  Recordings.Update(false);
1661  Reply(250, "Re-read of recordings directory triggered");
1662 }
1663 
1664 void cSVDRP::CmdVOLU(const char *Option)
1665 {
1666  if (*Option) {
1667  if (isnumber(Option))
1668  cDevice::PrimaryDevice()->SetVolume(strtol(Option, NULL, 10), true);
1669  else if (strcmp(Option, "+") == 0)
1671  else if (strcmp(Option, "-") == 0)
1673  else if (strcasecmp(Option, "MUTE") == 0)
1675  else {
1676  Reply(501, "Unknown option: \"%s\"", Option);
1677  return;
1678  }
1679  }
1680  if (cDevice::PrimaryDevice()->IsMute())
1681  Reply(250, "Audio is mute");
1682  else
1683  Reply(250, "Audio volume is %d", cDevice::CurrentVolume());
1684 }
1685 
1686 #define CMD(c) (strcasecmp(Cmd, c) == 0)
1687 
1688 void cSVDRP::Execute(char *Cmd)
1689 {
1690  // handle PUTE data:
1691  if (PUTEhandler) {
1692  if (!PUTEhandler->Process(Cmd)) {
1693  Reply(PUTEhandler->Status(), "%s", PUTEhandler->Message());
1695  }
1696  cEitFilter::SetDisableUntil(time(NULL) + EITDISABLETIME); // re-trigger the timeout, in case there is very much EPG data
1697  return;
1698  }
1699  // skip leading whitespace:
1700  Cmd = skipspace(Cmd);
1701  // find the end of the command word:
1702  char *s = Cmd;
1703  while (*s && !isspace(*s))
1704  s++;
1705  if (*s)
1706  *s++ = 0;
1707  s = skipspace(s);
1708  if (CMD("CHAN")) CmdCHAN(s);
1709  else if (CMD("CLRE")) CmdCLRE(s);
1710  else if (CMD("CPYR")) CmdCPYR(s);
1711  else if (CMD("DELC")) CmdDELC(s);
1712  else if (CMD("DELR")) CmdDELR(s);
1713  else if (CMD("DELT")) CmdDELT(s);
1714  else if (CMD("EDIT")) CmdEDIT(s);
1715  else if (CMD("GRAB")) CmdGRAB(s);
1716  else if (CMD("HELP")) CmdHELP(s);
1717  else if (CMD("HITK")) CmdHITK(s);
1718  else if (CMD("LSTC")) CmdLSTC(s);
1719  else if (CMD("LSTE")) CmdLSTE(s);
1720  else if (CMD("LSTR")) CmdLSTR(s);
1721  else if (CMD("LSTT")) CmdLSTT(s);
1722  else if (CMD("MESG")) CmdMESG(s);
1723  else if (CMD("MODC")) CmdMODC(s);
1724  else if (CMD("MODT")) CmdMODT(s);
1725  else if (CMD("MOVC")) CmdMOVC(s);
1726  else if (CMD("MOVR")) CmdMOVR(s);
1727  else if (CMD("NEWC")) CmdNEWC(s);
1728  else if (CMD("NEWT")) CmdNEWT(s);
1729  else if (CMD("NEXT")) CmdNEXT(s);
1730  else if (CMD("PLAY")) CmdPLAY(s);
1731  else if (CMD("PLUG")) CmdPLUG(s);
1732  else if (CMD("PUTE")) CmdPUTE(s);
1733  else if (CMD("REMO")) CmdREMO(s);
1734  else if (CMD("SCAN")) CmdSCAN(s);
1735  else if (CMD("STAT")) CmdSTAT(s);
1736  else if (CMD("UPDR")) CmdUPDR(s);
1737  else if (CMD("UPDT")) CmdUPDT(s);
1738  else if (CMD("VOLU")) CmdVOLU(s);
1739  else if (CMD("QUIT")) Close(true);
1740  else Reply(500, "Command unrecognized: \"%s\"", Cmd);
1741 }
1742 
1744 {
1745  bool NewConnection = !file.IsOpen();
1746  bool SendGreeting = NewConnection;
1747 
1748  if (file.IsOpen() || file.Open(socket.Accept())) {
1749  if (SendGreeting) {
1750  //TODO how can we get the *full* hostname?
1751  char buffer[BUFSIZ];
1752  gethostname(buffer, sizeof(buffer));
1753  time_t now = time(NULL);
1754  Reply(220, "%s SVDRP VideoDiskRecorder %s; %s; %s", buffer, VDRVERSION, *TimeToString(now), cCharSetConv::SystemCharacterTable() ? cCharSetConv::SystemCharacterTable() : "UTF-8");
1755  }
1756  if (NewConnection)
1757  lastActivity = time(NULL);
1758  while (file.Ready(false)) {
1759  unsigned char c;
1760  int r = safe_read(file, &c, 1);
1761  if (r > 0) {
1762  if (c == '\n' || c == 0x00) {
1763  // strip trailing whitespace:
1764  while (numChars > 0 && strchr(" \t\r\n", cmdLine[numChars - 1]))
1765  cmdLine[--numChars] = 0;
1766  // make sure the string is terminated:
1767  cmdLine[numChars] = 0;
1768  // showtime!
1769  Execute(cmdLine);
1770  numChars = 0;
1771  if (length > BUFSIZ) {
1772  free(cmdLine); // let's not tie up too much memory
1773  length = BUFSIZ;
1774  cmdLine = MALLOC(char, length);
1775  }
1776  }
1777  else if (c == 0x04 && numChars == 0) {
1778  // end of file (only at beginning of line)
1779  Close(true);
1780  }
1781  else if (c == 0x08 || c == 0x7F) {
1782  // backspace or delete (last character)
1783  if (numChars > 0)
1784  numChars--;
1785  }
1786  else if (c <= 0x03 || c == 0x0D) {
1787  // ignore control characters
1788  }
1789  else {
1790  if (numChars >= length - 1) {
1791  int NewLength = length + BUFSIZ;
1792  if (char *NewBuffer = (char *)realloc(cmdLine, NewLength)) {
1793  length = NewLength;
1794  cmdLine = NewBuffer;
1795  }
1796  else {
1797  esyslog("ERROR: out of memory");
1798  Close();
1799  break;
1800  }
1801  }
1802  cmdLine[numChars++] = c;
1803  cmdLine[numChars] = 0;
1804  }
1805  lastActivity = time(NULL);
1806  }
1807  else if (r <= 0) {
1808  isyslog("lost connection to SVDRP client");
1809  Close();
1810  }
1811  }
1812  if (Setup.SVDRPTimeout && time(NULL) - lastActivity > Setup.SVDRPTimeout) {
1813  isyslog("timeout on SVDRP connection");
1814  Close(true, true);
1815  }
1816  return true;
1817  }
1818  return false;
1819 }
1820 
1821 void cSVDRP::SetGrabImageDir(const char *GrabImageDir)
1822 {
1823  free(grabImageDir);
1824  grabImageDir = GrabImageDir ? strdup(GrabImageDir) : NULL;
1825 }
1826 
1827 //TODO more than one connection???
1828