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