vdr  2.6.1
recording.c
Go to the documentation of this file.
1 /*
2  * recording.c: Recording file handling
3  *
4  * See the main source file 'vdr.c' for copyright information and
5  * how to reach the author.
6  *
7  * $Id: recording.c 5.14 2022/01/24 10:44:21 kls Exp $
8  */
9 
10 #include "recording.h"
11 #include <ctype.h>
12 #include <dirent.h>
13 #include <errno.h>
14 #include <fcntl.h>
15 #define __STDC_FORMAT_MACROS // Required for format specifiers
16 #include <inttypes.h>
17 #include <math.h>
18 #include <stdio.h>
19 #include <string.h>
20 #include <sys/stat.h>
21 #include <unistd.h>
22 #include "channels.h"
23 #include "cutter.h"
24 #include "i18n.h"
25 #include "interface.h"
26 #include "menu.h"
27 #include "remux.h"
28 #include "ringbuffer.h"
29 #include "skins.h"
30 #include "svdrp.h"
31 #include "tools.h"
32 #include "videodir.h"
33 
34 #define SUMMARYFALLBACK
35 
36 #define RECEXT ".rec"
37 #define DELEXT ".del"
38 /* This was the original code, which works fine in a Linux only environment.
39  Unfortunately, because of Windows and its brain dead file system, we have
40  to use a more complicated approach, in order to allow users who have enabled
41  the --vfat command line option to see their recordings even if they forget to
42  enable --vfat when restarting VDR... Gee, do I hate Windows.
43  (kls 2002-07-27)
44 #define DATAFORMAT "%4d-%02d-%02d.%02d:%02d.%02d.%02d" RECEXT
45 #define NAMEFORMAT "%s/%s/" DATAFORMAT
46 */
47 #define DATAFORMATPES "%4d-%02d-%02d.%02d%*c%02d.%02d.%02d" RECEXT
48 #define NAMEFORMATPES "%s/%s/" "%4d-%02d-%02d.%02d.%02d.%02d.%02d" RECEXT
49 #define DATAFORMATTS "%4d-%02d-%02d.%02d.%02d.%d-%d" RECEXT
50 #define NAMEFORMATTS "%s/%s/" DATAFORMATTS
51 
52 #define RESUMEFILESUFFIX "/resume%s%s"
53 #ifdef SUMMARYFALLBACK
54 #define SUMMARYFILESUFFIX "/summary.vdr"
55 #endif
56 #define INFOFILESUFFIX "/info"
57 #define MARKSFILESUFFIX "/marks"
58 
59 #define SORTMODEFILE ".sort"
60 #define TIMERRECFILE ".timer"
61 
62 #define MINDISKSPACE 1024 // MB
63 
64 #define REMOVECHECKDELTA 60 // seconds between checks for removing deleted files
65 #define DELETEDLIFETIME 300 // seconds after which a deleted recording will be actually removed
66 #define DISKCHECKDELTA 100 // seconds between checks for free disk space
67 #define REMOVELATENCY 10 // seconds to wait until next check after removing a file
68 #define MARKSUPDATEDELTA 10 // seconds between checks for updating editing marks
69 #define MININDEXAGE 3600 // seconds before an index file is considered no longer to be written
70 #define MAXREMOVETIME 10 // seconds after which to return from removing deleted recordings
71 
72 #define MAX_LINK_LEVEL 6
73 
74 #define LIMIT_SECS_PER_MB_RADIO 5 // radio recordings typically have more than this
75 
76 int DirectoryPathMax = PATH_MAX - 1;
77 int DirectoryNameMax = NAME_MAX;
78 bool DirectoryEncoding = false;
79 int InstanceId = 0;
80 
81 // --- cRemoveDeletedRecordingsThread ----------------------------------------
82 
84 protected:
85  virtual void Action(void);
86 public:
88  };
89 
91 :cThread("remove deleted recordings", true)
92 {
93 }
94 
96 {
97  // Make sure only one instance of VDR does this:
98  cLockFile LockFile(cVideoDirectory::Name());
99  if (LockFile.Lock()) {
100  time_t StartTime = time(NULL);
101  bool deleted = false;
102  bool interrupted = false;
104  for (cRecording *r = DeletedRecordings->First(); r; ) {
105  if (cIoThrottle::Engaged())
106  interrupted = true;
107  else if (time(NULL) - StartTime > MAXREMOVETIME)
108  interrupted = true; // don't stay here too long
109  else if (cRemote::HasKeys())
110  interrupted = true; // react immediately on user input
111  if (interrupted)
112  break;
113  if (r->Deleted() && time(NULL) - r->Deleted() > DELETEDLIFETIME) {
114  cRecording *next = DeletedRecordings->Next(r);
115  r->Remove();
116  DeletedRecordings->Del(r);
117  r = next;
118  deleted = true;
119  }
120  else
121  r = DeletedRecordings->Next(r);
122  }
123  if (deleted) {
125  if (!interrupted) {
126  const char *IgnoreFiles[] = { SORTMODEFILE, TIMERRECFILE, NULL };
128  }
129  }
130  }
131 }
132 
134 
135 // ---
136 
138 {
139  static time_t LastRemoveCheck = 0;
140  if (time(NULL) - LastRemoveCheck > REMOVECHECKDELTA) {
143  for (const cRecording *r = DeletedRecordings->First(); r; r = DeletedRecordings->Next(r)) {
144  if (r->Deleted() && time(NULL) - r->Deleted() > DELETEDLIFETIME) {
146  break;
147  }
148  }
149  }
150  LastRemoveCheck = time(NULL);
151  }
152 }
153 
154 void AssertFreeDiskSpace(int Priority, bool Force)
155 {
156  static cMutex Mutex;
157  cMutexLock MutexLock(&Mutex);
158  // With every call to this function we try to actually remove
159  // a file, or mark a file for removal ("delete" it), so that
160  // it will get removed during the next call.
161  static time_t LastFreeDiskCheck = 0;
162  int Factor = (Priority == -1) ? 10 : 1;
163  if (Force || time(NULL) - LastFreeDiskCheck > DISKCHECKDELTA / Factor) {
165  // Make sure only one instance of VDR does this:
166  cLockFile LockFile(cVideoDirectory::Name());
167  if (!LockFile.Lock())
168  return;
169  // Remove the oldest file that has been "deleted":
170  isyslog("low disk space while recording, trying to remove a deleted recording...");
171  int NumDeletedRecordings = 0;
172  {
174  NumDeletedRecordings = DeletedRecordings->Count();
175  if (NumDeletedRecordings) {
176  cRecording *r = DeletedRecordings->First();
177  cRecording *r0 = NULL;
178  while (r) {
179  if (r->IsOnVideoDirectoryFileSystem()) { // only remove recordings that will actually increase the free video disk space
180  if (!r0 || r->Start() < r0->Start())
181  r0 = r;
182  }
183  r = DeletedRecordings->Next(r);
184  }
185  if (r0) {
186  if (r0->Remove())
187  LastFreeDiskCheck += REMOVELATENCY / Factor;
188  DeletedRecordings->Del(r0);
189  return;
190  }
191  }
192  }
193  if (NumDeletedRecordings == 0) {
194  // DeletedRecordings was empty, so to be absolutely sure there are no
195  // deleted recordings we need to double check:
196  cRecordings::Update(true);
198  if (DeletedRecordings->Count())
199  return; // the next call will actually remove it
200  }
201  // No "deleted" files to remove, so let's see if we can delete a recording:
202  if (Priority > 0) {
203  isyslog("...no deleted recording found, trying to delete an old recording...");
205  Recordings->SetExplicitModify();
206  if (Recordings->Count()) {
207  cRecording *r = Recordings->First();
208  cRecording *r0 = NULL;
209  while (r) {
210  if (r->IsOnVideoDirectoryFileSystem()) { // only delete recordings that will actually increase the free video disk space
211  if (!r->IsEdited() && r->Lifetime() < MAXLIFETIME) { // edited recordings and recordings with MAXLIFETIME live forever
212  if ((r->Lifetime() == 0 && Priority > r->Priority()) || // the recording has no guaranteed lifetime and the new recording has higher priority
213  (r->Lifetime() > 0 && (time(NULL) - r->Start()) / SECSINDAY >= r->Lifetime())) { // the recording's guaranteed lifetime has expired
214  if (r0) {
215  if (r->Priority() < r0->Priority() || (r->Priority() == r0->Priority() && r->Start() < r0->Start()))
216  r0 = r; // in any case we delete the one with the lowest priority (or the older one in case of equal priorities)
217  }
218  else
219  r0 = r;
220  }
221  }
222  }
223  r = Recordings->Next(r);
224  }
225  if (r0 && r0->Delete()) {
226  Recordings->Del(r0);
227  Recordings->SetModified();
228  return;
229  }
230  }
231  // Unable to free disk space, but there's nothing we can do about that...
232  isyslog("...no old recording found, giving up");
233  }
234  else
235  isyslog("...no deleted recording found, priority %d too low to trigger deleting an old recording", Priority);
236  Skins.QueueMessage(mtWarning, tr("Low disk space!"), 5, -1);
237  }
238  LastFreeDiskCheck = time(NULL);
239  }
240 }
241 
242 // --- cResumeFile -----------------------------------------------------------
243 
244 cResumeFile::cResumeFile(const char *FileName, bool IsPesRecording)
245 {
246  isPesRecording = IsPesRecording;
247  const char *Suffix = isPesRecording ? RESUMEFILESUFFIX ".vdr" : RESUMEFILESUFFIX;
248  fileName = MALLOC(char, strlen(FileName) + strlen(Suffix) + 1);
249  if (fileName) {
250  strcpy(fileName, FileName);
251  sprintf(fileName + strlen(fileName), Suffix, Setup.ResumeID ? "." : "", Setup.ResumeID ? *itoa(Setup.ResumeID) : "");
252  }
253  else
254  esyslog("ERROR: can't allocate memory for resume file name");
255 }
256 
258 {
259  free(fileName);
260 }
261 
263 {
264  int resume = -1;
265  if (fileName) {
266  struct stat st;
267  if (stat(fileName, &st) == 0) {
268  if ((st.st_mode & S_IWUSR) == 0) // no write access, assume no resume
269  return -1;
270  }
271  if (isPesRecording) {
272  int f = open(fileName, O_RDONLY);
273  if (f >= 0) {
274  if (safe_read(f, &resume, sizeof(resume)) != sizeof(resume)) {
275  resume = -1;
277  }
278  close(f);
279  }
280  else if (errno != ENOENT)
282  }
283  else {
284  FILE *f = fopen(fileName, "r");
285  if (f) {
286  cReadLine ReadLine;
287  char *s;
288  int line = 0;
289  while ((s = ReadLine.Read(f)) != NULL) {
290  ++line;
291  char *t = skipspace(s + 1);
292  switch (*s) {
293  case 'I': resume = atoi(t);
294  break;
295  default: ;
296  }
297  }
298  fclose(f);
299  }
300  else if (errno != ENOENT)
302  }
303  }
304  return resume;
305 }
306 
307 bool cResumeFile::Save(int Index)
308 {
309  if (fileName) {
310  if (isPesRecording) {
311  int f = open(fileName, O_WRONLY | O_CREAT | O_TRUNC, DEFFILEMODE);
312  if (f >= 0) {
313  if (safe_write(f, &Index, sizeof(Index)) < 0)
315  close(f);
317  Recordings->ResetResume(fileName);
318  return true;
319  }
320  }
321  else {
322  FILE *f = fopen(fileName, "w");
323  if (f) {
324  fprintf(f, "I %d\n", Index);
325  fclose(f);
327  Recordings->ResetResume(fileName);
328  }
329  else
331  return true;
332  }
333  }
334  return false;
335 }
336 
338 {
339  if (fileName) {
340  if (remove(fileName) == 0) {
342  Recordings->ResetResume(fileName);
343  }
344  else if (errno != ENOENT)
346  }
347 }
348 
349 // --- cRecordingInfo --------------------------------------------------------
350 
351 cRecordingInfo::cRecordingInfo(const cChannel *Channel, const cEvent *Event)
352 {
353  channelID = Channel ? Channel->GetChannelID() : tChannelID::InvalidID;
354  channelName = Channel ? strdup(Channel->Name()) : NULL;
355  ownEvent = Event ? NULL : new cEvent(0);
356  event = ownEvent ? ownEvent : Event;
357  aux = NULL;
361  fileName = NULL;
362  errors = -1;
363  if (Channel) {
364  // Since the EPG data's component records can carry only a single
365  // language code, let's see whether the channel's PID data has
366  // more information:
368  if (!Components)
369  Components = new cComponents;
370  for (int i = 0; i < MAXAPIDS; i++) {
371  const char *s = Channel->Alang(i);
372  if (*s) {
373  tComponent *Component = Components->GetComponent(i, 2, 3);
374  if (!Component)
375  Components->SetComponent(Components->NumComponents(), 2, 3, s, NULL);
376  else if (strlen(s) > strlen(Component->language))
377  strn0cpy(Component->language, s, sizeof(Component->language));
378  }
379  }
380  // There's no "multiple languages" for Dolby Digital tracks, but
381  // we do the same procedure here, too, in case there is no component
382  // information at all:
383  for (int i = 0; i < MAXDPIDS; i++) {
384  const char *s = Channel->Dlang(i);
385  if (*s) {
386  tComponent *Component = Components->GetComponent(i, 4, 0); // AC3 component according to the DVB standard
387  if (!Component)
388  Component = Components->GetComponent(i, 2, 5); // fallback "Dolby" component according to the "Premiere pseudo standard"
389  if (!Component)
390  Components->SetComponent(Components->NumComponents(), 2, 5, s, NULL);
391  else if (strlen(s) > strlen(Component->language))
392  strn0cpy(Component->language, s, sizeof(Component->language));
393  }
394  }
395  // The same applies to subtitles:
396  for (int i = 0; i < MAXSPIDS; i++) {
397  const char *s = Channel->Slang(i);
398  if (*s) {
399  tComponent *Component = Components->GetComponent(i, 3, 3);
400  if (!Component)
401  Components->SetComponent(Components->NumComponents(), 3, 3, s, NULL);
402  else if (strlen(s) > strlen(Component->language))
403  strn0cpy(Component->language, s, sizeof(Component->language));
404  }
405  }
406  if (Components != event->Components())
407  ((cEvent *)event)->SetComponents(Components);
408  }
409 }
410 
411 cRecordingInfo::cRecordingInfo(const char *FileName)
412 {
414  channelName = NULL;
415  ownEvent = new cEvent(0);
416  event = ownEvent;
417  aux = NULL;
418  errors = -1;
422  fileName = strdup(cString::sprintf("%s%s", FileName, INFOFILESUFFIX));
423 }
424 
426 {
427  delete ownEvent;
428  free(aux);
429  free(channelName);
430  free(fileName);
431 }
432 
433 void cRecordingInfo::SetData(const char *Title, const char *ShortText, const char *Description)
434 {
435  if (Title)
436  ((cEvent *)event)->SetTitle(Title);
437  if (ShortText)
438  ((cEvent *)event)->SetShortText(ShortText);
439  if (Description)
440  ((cEvent *)event)->SetDescription(Description);
441 }
442 
443 void cRecordingInfo::SetAux(const char *Aux)
444 {
445  free(aux);
446  aux = Aux ? strdup(Aux) : NULL;
447 }
448 
449 void cRecordingInfo::SetFramesPerSecond(double FramesPerSecond)
450 {
452 }
453 
454 void cRecordingInfo::SetFileName(const char *FileName)
455 {
456  bool IsPesRecording = fileName && endswith(fileName, ".vdr");
457  free(fileName);
458  fileName = strdup(cString::sprintf("%s%s", FileName, IsPesRecording ? INFOFILESUFFIX ".vdr" : INFOFILESUFFIX));
459 }
460 
462 {
463  errors = Errors;
464 }
465 
466 bool cRecordingInfo::Read(FILE *f)
467 {
468  if (ownEvent) {
469  cReadLine ReadLine;
470  char *s;
471  int line = 0;
472  while ((s = ReadLine.Read(f)) != NULL) {
473  ++line;
474  char *t = skipspace(s + 1);
475  switch (*s) {
476  case 'C': {
477  char *p = strchr(t, ' ');
478  if (p) {
479  free(channelName);
480  channelName = strdup(compactspace(p));
481  *p = 0; // strips optional channel name
482  }
483  if (*t)
485  }
486  break;
487  case 'E': {
488  unsigned int EventID;
489  time_t StartTime;
490  int Duration;
491  unsigned int TableID = 0;
492  unsigned int Version = 0xFF;
493  int n = sscanf(t, "%u %ld %d %X %X", &EventID, &StartTime, &Duration, &TableID, &Version);
494  if (n >= 3 && n <= 5) {
495  ownEvent->SetEventID(EventID);
496  ownEvent->SetStartTime(StartTime);
497  ownEvent->SetDuration(Duration);
498  ownEvent->SetTableID(uchar(TableID));
499  ownEvent->SetVersion(uchar(Version));
500  }
501  }
502  break;
503  case 'F': framesPerSecond = atod(t);
504  break;
505  case 'L': lifetime = atoi(t);
506  break;
507  case 'P': priority = atoi(t);
508  break;
509  case 'O': errors = atoi(t);
510  break;
511  case '@': free(aux);
512  aux = strdup(t);
513  break;
514  case '#': break; // comments are ignored
515  default: if (!ownEvent->Parse(s)) {
516  esyslog("ERROR: EPG data problem in line %d", line);
517  return false;
518  }
519  break;
520  }
521  }
522  return true;
523  }
524  return false;
525 }
526 
527 bool cRecordingInfo::Write(FILE *f, const char *Prefix) const
528 {
529  if (channelID.Valid())
530  fprintf(f, "%sC %s%s%s\n", Prefix, *channelID.ToString(), channelName ? " " : "", channelName ? channelName : "");
531  event->Dump(f, Prefix, true);
532  fprintf(f, "%sF %s\n", Prefix, *dtoa(framesPerSecond, "%.10g"));
533  fprintf(f, "%sP %d\n", Prefix, priority);
534  fprintf(f, "%sL %d\n", Prefix, lifetime);
535  fprintf(f, "%sO %d\n", Prefix, errors);
536  if (aux)
537  fprintf(f, "%s@ %s\n", Prefix, aux);
538  return true;
539 }
540 
542 {
543  bool Result = false;
544  if (fileName) {
545  FILE *f = fopen(fileName, "r");
546  if (f) {
547  if (Read(f))
548  Result = true;
549  else
550  esyslog("ERROR: EPG data problem in file %s", fileName);
551  fclose(f);
552  }
553  else if (errno != ENOENT)
555  }
556  return Result;
557 }
558 
559 bool cRecordingInfo::Write(void) const
560 {
561  bool Result = false;
562  if (fileName) {
563  cSafeFile f(fileName);
564  if (f.Open()) {
565  if (Write(f))
566  Result = true;
567  f.Close();
568  }
569  else
571  }
572  return Result;
573 }
574 
575 // --- cRecording ------------------------------------------------------------
576 
577 #define RESUME_NOT_INITIALIZED (-2)
578 
579 struct tCharExchange { char a; char b; };
581  { FOLDERDELIMCHAR, '/' },
582  { '/', FOLDERDELIMCHAR },
583  { ' ', '_' },
584  // backwards compatibility:
585  { '\'', '\'' },
586  { '\'', '\x01' },
587  { '/', '\x02' },
588  { 0, 0 }
589  };
590 
591 const char *InvalidChars = "\"\\/:*?|<>#";
592 
593 bool NeedsConversion(const char *p)
594 {
595  return DirectoryEncoding &&
596  (strchr(InvalidChars, *p) // characters that can't be part of a Windows file/directory name
597  || *p == '.' && (!*(p + 1) || *(p + 1) == FOLDERDELIMCHAR)); // Windows can't handle '.' at the end of file/directory names
598 }
599 
600 char *ExchangeChars(char *s, bool ToFileSystem)
601 {
602  char *p = s;
603  while (*p) {
604  if (DirectoryEncoding) {
605  // Some file systems can't handle all characters, so we
606  // have to take extra efforts to encode/decode them:
607  if (ToFileSystem) {
608  switch (*p) {
609  // characters that can be mapped to other characters:
610  case ' ': *p = '_'; break;
611  case FOLDERDELIMCHAR: *p = '/'; break;
612  case '/': *p = FOLDERDELIMCHAR; break;
613  // characters that have to be encoded:
614  default:
615  if (NeedsConversion(p)) {
616  int l = p - s;
617  if (char *NewBuffer = (char *)realloc(s, strlen(s) + 10)) {
618  s = NewBuffer;
619  p = s + l;
620  char buf[4];
621  sprintf(buf, "#%02X", (unsigned char)*p);
622  memmove(p + 2, p, strlen(p) + 1);
623  memcpy(p, buf, 3);
624  p += 2;
625  }
626  else
627  esyslog("ERROR: out of memory");
628  }
629  }
630  }
631  else {
632  switch (*p) {
633  // mapped characters:
634  case '_': *p = ' '; break;
635  case FOLDERDELIMCHAR: *p = '/'; break;
636  case '/': *p = FOLDERDELIMCHAR; break;
637  // encoded characters:
638  case '#': {
639  if (strlen(p) > 2 && isxdigit(*(p + 1)) && isxdigit(*(p + 2))) {
640  char buf[3];
641  sprintf(buf, "%c%c", *(p + 1), *(p + 2));
642  uchar c = uchar(strtol(buf, NULL, 16));
643  if (c) {
644  *p = c;
645  memmove(p + 1, p + 3, strlen(p) - 2);
646  }
647  }
648  }
649  break;
650  // backwards compatibility:
651  case '\x01': *p = '\''; break;
652  case '\x02': *p = '/'; break;
653  case '\x03': *p = ':'; break;
654  default: ;
655  }
656  }
657  }
658  else {
659  for (struct tCharExchange *ce = CharExchange; ce->a && ce->b; ce++) {
660  if (*p == (ToFileSystem ? ce->a : ce->b)) {
661  *p = ToFileSystem ? ce->b : ce->a;
662  break;
663  }
664  }
665  }
666  p++;
667  }
668  return s;
669 }
670 
671 char *LimitNameLengths(char *s, int PathMax, int NameMax)
672 {
673  // Limits the total length of the directory path in 's' to PathMax, and each
674  // individual directory name to NameMax. The lengths of characters that need
675  // conversion when using 's' as a file name are taken into account accordingly.
676  // If a directory name exceeds NameMax, it will be truncated. If the whole
677  // directory path exceeds PathMax, individual directory names will be shortened
678  // (from right to left) until the limit is met, or until the currently handled
679  // directory name consists of only a single character. All operations are performed
680  // directly on the given 's', which may become shorter (but never longer) than
681  // the original value.
682  // Returns a pointer to 's'.
683  int Length = strlen(s);
684  int PathLength = 0;
685  // Collect the resulting lengths of each character:
686  bool NameTooLong = false;
687  int8_t a[Length];
688  int n = 0;
689  int NameLength = 0;
690  for (char *p = s; *p; p++) {
691  if (*p == FOLDERDELIMCHAR) {
692  a[n] = -1; // FOLDERDELIMCHAR is a single character, neg. sign marks it
693  NameTooLong |= NameLength > NameMax;
694  NameLength = 0;
695  PathLength += 1;
696  }
697  else if (NeedsConversion(p)) {
698  a[n] = 3; // "#xx"
699  NameLength += 3;
700  PathLength += 3;
701  }
702  else {
703  int8_t l = Utf8CharLen(p);
704  a[n] = l;
705  NameLength += l;
706  PathLength += l;
707  while (l-- > 1) {
708  a[++n] = 0;
709  p++;
710  }
711  }
712  n++;
713  }
714  NameTooLong |= NameLength > NameMax;
715  // Limit names to NameMax:
716  if (NameTooLong) {
717  while (n > 0) {
718  // Calculate the length of the current name:
719  int NameLength = 0;
720  int i = n;
721  int b = i;
722  while (i-- > 0 && a[i] >= 0) {
723  NameLength += a[i];
724  b = i;
725  }
726  // Shorten the name if necessary:
727  if (NameLength > NameMax) {
728  int l = 0;
729  i = n;
730  while (i-- > 0 && a[i] >= 0) {
731  l += a[i];
732  if (NameLength - l <= NameMax) {
733  memmove(s + i, s + n, Length - n + 1);
734  memmove(a + i, a + n, Length - n + 1);
735  Length -= n - i;
736  PathLength -= l;
737  break;
738  }
739  }
740  }
741  // Switch to the next name:
742  n = b - 1;
743  }
744  }
745  // Limit path to PathMax:
746  n = Length;
747  while (PathLength > PathMax && n > 0) {
748  // Calculate how much to cut off the current name:
749  int i = n;
750  int b = i;
751  int l = 0;
752  while (--i > 0 && a[i - 1] >= 0) {
753  if (a[i] > 0) {
754  l += a[i];
755  b = i;
756  if (PathLength - l <= PathMax)
757  break;
758  }
759  }
760  // Shorten the name if necessary:
761  if (l > 0) {
762  memmove(s + b, s + n, Length - n + 1);
763  Length -= n - b;
764  PathLength -= l;
765  }
766  // Switch to the next name:
767  n = i - 1;
768  }
769  return s;
770 }
771 
772 cRecording::cRecording(cTimer *Timer, const cEvent *Event)
773 {
774  id = 0;
776  titleBuffer = NULL;
778  fileName = NULL;
779  name = NULL;
780  fileSizeMB = -1; // unknown
781  channel = Timer->Channel()->Number();
783  isPesRecording = false;
784  isOnVideoDirectoryFileSystem = -1; // unknown
786  numFrames = -1;
787  deleted = 0;
788  // set up the actual name:
789  const char *Title = Event ? Event->Title() : NULL;
790  const char *Subtitle = Event ? Event->ShortText() : NULL;
791  if (isempty(Title))
792  Title = Timer->Channel()->Name();
793  if (isempty(Subtitle))
794  Subtitle = " ";
795  const char *macroTITLE = strstr(Timer->File(), TIMERMACRO_TITLE);
796  const char *macroEPISODE = strstr(Timer->File(), TIMERMACRO_EPISODE);
797  if (macroTITLE || macroEPISODE) {
798  name = strdup(Timer->File());
800  name = strreplace(name, TIMERMACRO_EPISODE, Subtitle);
801  // avoid blanks at the end:
802  int l = strlen(name);
803  while (l-- > 2) {
804  if (name[l] == ' ' && name[l - 1] != FOLDERDELIMCHAR)
805  name[l] = 0;
806  else
807  break;
808  }
809  if (Timer->IsSingleEvent())
810  Timer->SetFile(name); // this was an instant recording, so let's set the actual data
811  }
812  else if (Timer->IsSingleEvent() || !Setup.UseSubtitle)
813  name = strdup(Timer->File());
814  else
815  name = strdup(cString::sprintf("%s%c%s", Timer->File(), FOLDERDELIMCHAR, Subtitle));
816  // substitute characters that would cause problems in file names:
817  strreplace(name, '\n', ' ');
818  start = Timer->StartTime();
819  priority = Timer->Priority();
820  lifetime = Timer->Lifetime();
821  // handle info:
822  info = new cRecordingInfo(Timer->Channel(), Event);
823  info->SetAux(Timer->Aux());
826 }
827 
828 cRecording::cRecording(const char *FileName)
829 {
830  id = 0;
832  fileSizeMB = -1; // unknown
833  channel = -1;
834  instanceId = -1;
835  priority = MAXPRIORITY; // assume maximum in case there is no info file
837  isPesRecording = false;
838  isOnVideoDirectoryFileSystem = -1; // unknown
840  numFrames = -1;
841  deleted = 0;
842  titleBuffer = NULL;
844  FileName = fileName = strdup(FileName);
845  if (*(fileName + strlen(fileName) - 1) == '/')
846  *(fileName + strlen(fileName) - 1) = 0;
847  if (strstr(FileName, cVideoDirectory::Name()) == FileName)
848  FileName += strlen(cVideoDirectory::Name()) + 1;
849  const char *p = strrchr(FileName, '/');
850 
851  name = NULL;
853  if (p) {
854  time_t now = time(NULL);
855  struct tm tm_r;
856  struct tm t = *localtime_r(&now, &tm_r); // this initializes the time zone in 't'
857  t.tm_isdst = -1; // makes sure mktime() will determine the correct DST setting
858  if (7 == sscanf(p + 1, DATAFORMATTS, &t.tm_year, &t.tm_mon, &t.tm_mday, &t.tm_hour, &t.tm_min, &channel, &instanceId)
859  || 7 == sscanf(p + 1, DATAFORMATPES, &t.tm_year, &t.tm_mon, &t.tm_mday, &t.tm_hour, &t.tm_min, &priority, &lifetime)) {
860  t.tm_year -= 1900;
861  t.tm_mon--;
862  t.tm_sec = 0;
863  start = mktime(&t);
864  name = MALLOC(char, p - FileName + 1);
865  strncpy(name, FileName, p - FileName);
866  name[p - FileName] = 0;
867  name = ExchangeChars(name, false);
869  }
870  else
871  return;
872  GetResume();
873  // read an optional info file:
874  cString InfoFileName = cString::sprintf("%s%s", fileName, isPesRecording ? INFOFILESUFFIX ".vdr" : INFOFILESUFFIX);
875  FILE *f = fopen(InfoFileName, "r");
876  if (f) {
877  if (!info->Read(f))
878  esyslog("ERROR: EPG data problem in file %s", *InfoFileName);
879  else if (!isPesRecording) {
883  }
884  fclose(f);
885  }
886  else if (errno != ENOENT)
887  LOG_ERROR_STR(*InfoFileName);
888 #ifdef SUMMARYFALLBACK
889  // fall back to the old 'summary.vdr' if there was no 'info.vdr':
890  if (isempty(info->Title())) {
891  cString SummaryFileName = cString::sprintf("%s%s", fileName, SUMMARYFILESUFFIX);
892  FILE *f = fopen(SummaryFileName, "r");
893  if (f) {
894  int line = 0;
895  char *data[3] = { NULL };
896  cReadLine ReadLine;
897  char *s;
898  while ((s = ReadLine.Read(f)) != NULL) {
899  if (*s || line > 1) {
900  if (data[line]) {
901  int len = strlen(s);
902  len += strlen(data[line]) + 1;
903  if (char *NewBuffer = (char *)realloc(data[line], len + 1)) {
904  data[line] = NewBuffer;
905  strcat(data[line], "\n");
906  strcat(data[line], s);
907  }
908  else
909  esyslog("ERROR: out of memory");
910  }
911  else
912  data[line] = strdup(s);
913  }
914  else
915  line++;
916  }
917  fclose(f);
918  if (!data[2]) {
919  data[2] = data[1];
920  data[1] = NULL;
921  }
922  else if (data[1] && data[2]) {
923  // if line 1 is too long, it can't be the short text,
924  // so assume the short text is missing and concatenate
925  // line 1 and line 2 to be the long text:
926  int len = strlen(data[1]);
927  if (len > 80) {
928  if (char *NewBuffer = (char *)realloc(data[1], len + 1 + strlen(data[2]) + 1)) {
929  data[1] = NewBuffer;
930  strcat(data[1], "\n");
931  strcat(data[1], data[2]);
932  free(data[2]);
933  data[2] = data[1];
934  data[1] = NULL;
935  }
936  else
937  esyslog("ERROR: out of memory");
938  }
939  }
940  info->SetData(data[0], data[1], data[2]);
941  for (int i = 0; i < 3; i ++)
942  free(data[i]);
943  }
944  else if (errno != ENOENT)
945  LOG_ERROR_STR(*SummaryFileName);
946  }
947 #endif
948  if (isempty(info->Title()))
950  }
951 }
952 
954 {
955  free(titleBuffer);
956  free(sortBufferName);
957  free(sortBufferTime);
958  free(fileName);
959  free(name);
960  delete info;
961 }
962 
963 char *cRecording::StripEpisodeName(char *s, bool Strip)
964 {
965  char *t = s, *s1 = NULL, *s2 = NULL;
966  while (*t) {
967  if (*t == '/') {
968  if (s1) {
969  if (s2)
970  s1 = s2;
971  s2 = t;
972  }
973  else
974  s1 = t;
975  }
976  t++;
977  }
978  if (s1 && s2) {
979  // To have folders sorted before plain recordings, the '/' s1 points to
980  // is replaced by the character '1'. All other slashes will be replaced
981  // by '0' in SortName() (see below), which will result in the desired
982  // sequence ('0' and '1' are reversed in case of rsdDescending):
983  *s1 = (Setup.RecSortingDirection == rsdAscending) ? '1' : '0';
984  if (Strip) {
985  s1++;
986  memmove(s1, s2, t - s2 + 1);
987  }
988  }
989  return s;
990 }
991 
992 char *cRecording::SortName(void) const
993 {
995  if (!*sb) {
997  char buf[32];
998  struct tm tm_r;
999  strftime(buf, sizeof(buf), "%Y%m%d%H%I", localtime_r(&start, &tm_r));
1000  *sb = strdup(buf);
1001  }
1002  else {
1003  char *s = strdup(FileName() + strlen(cVideoDirectory::Name()));
1006  strreplace(s, '/', (Setup.RecSortingDirection == rsdAscending) ? '0' : '1'); // some locales ignore '/' when sorting
1007  int l = strxfrm(NULL, s, 0) + 1;
1008  *sb = MALLOC(char, l);
1009  strxfrm(*sb, s, l);
1010  free(s);
1011  }
1012  }
1013  return *sb;
1014 }
1015 
1017 {
1018  free(sortBufferName);
1019  free(sortBufferTime);
1020  sortBufferName = sortBufferTime = NULL;
1021 }
1022 
1023 void cRecording::SetId(int Id)
1024 {
1025  id = Id;
1026 }
1027 
1028 int cRecording::GetResume(void) const
1029 {
1030  if (resume == RESUME_NOT_INITIALIZED) {
1031  cResumeFile ResumeFile(FileName(), isPesRecording);
1032  resume = ResumeFile.Read();
1033  }
1034  return resume;
1035 }
1036 
1037 int cRecording::Compare(const cListObject &ListObject) const
1038 {
1039  cRecording *r = (cRecording *)&ListObject;
1041  return strcmp(SortName(), r->SortName());
1042  else
1043  return strcmp(r->SortName(), SortName());
1044 }
1045 
1046 bool cRecording::IsInPath(const char *Path) const
1047 {
1048  if (isempty(Path))
1049  return true;
1050  int l = strlen(Path);
1051  return strncmp(Path, name, l) == 0 && (name[l] == FOLDERDELIMCHAR);
1052 }
1053 
1055 {
1056  if (char *s = strrchr(name, FOLDERDELIMCHAR))
1057  return cString(name, s);
1058  return "";
1059 }
1060 
1062 {
1063  return strgetlast(name, FOLDERDELIMCHAR);
1064 }
1065 
1066 const char *cRecording::FileName(void) const
1067 {
1068  if (!fileName) {
1069  struct tm tm_r;
1070  struct tm *t = localtime_r(&start, &tm_r);
1071  const char *fmt = isPesRecording ? NAMEFORMATPES : NAMEFORMATTS;
1072  int ch = isPesRecording ? priority : channel;
1073  int ri = isPesRecording ? lifetime : instanceId;
1074  char *Name = LimitNameLengths(strdup(name), DirectoryPathMax - strlen(cVideoDirectory::Name()) - 1 - 42, DirectoryNameMax); // 42 = length of an actual recording directory name (generated with DATAFORMATTS) plus some reserve
1075  if (strcmp(Name, name) != 0)
1076  dsyslog("recording file name '%s' truncated to '%s'", name, Name);
1077  Name = ExchangeChars(Name, true);
1078  fileName = strdup(cString::sprintf(fmt, cVideoDirectory::Name(), Name, t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, ch, ri));
1079  free(Name);
1080  }
1081  return fileName;
1082 }
1083 
1084 const char *cRecording::Title(char Delimiter, bool NewIndicator, int Level) const
1085 {
1086  const char *New = NewIndicator && IsNew() ? "*" : "";
1087  const char *Err = NewIndicator && (info->Errors() > 0) ? "!" : "";
1088  free(titleBuffer);
1089  titleBuffer = NULL;
1090  if (Level < 0 || Level == HierarchyLevels()) {
1091  struct tm tm_r;
1092  struct tm *t = localtime_r(&start, &tm_r);
1093  char *s;
1094  if (Level > 0 && (s = strrchr(name, FOLDERDELIMCHAR)) != NULL)
1095  s++;
1096  else
1097  s = name;
1098  cString Length("");
1099  if (NewIndicator) {
1100  int Minutes = max(0, (LengthInSeconds() + 30) / 60);
1101  Length = cString::sprintf("%c%d:%02d",
1102  Delimiter,
1103  Minutes / 60,
1104  Minutes % 60
1105  );
1106  }
1107  titleBuffer = strdup(cString::sprintf("%02d.%02d.%02d%c%02d:%02d%s%s%s%c%s",
1108  t->tm_mday,
1109  t->tm_mon + 1,
1110  t->tm_year % 100,
1111  Delimiter,
1112  t->tm_hour,
1113  t->tm_min,
1114  *Length,
1115  New,
1116  Err,
1117  Delimiter,
1118  s));
1119  // let's not display a trailing FOLDERDELIMCHAR:
1120  if (!NewIndicator)
1122  s = &titleBuffer[strlen(titleBuffer) - 1];
1123  if (*s == FOLDERDELIMCHAR)
1124  *s = 0;
1125  }
1126  else if (Level < HierarchyLevels()) {
1127  const char *s = name;
1128  const char *p = s;
1129  while (*++s) {
1130  if (*s == FOLDERDELIMCHAR) {
1131  if (Level--)
1132  p = s + 1;
1133  else
1134  break;
1135  }
1136  }
1137  titleBuffer = MALLOC(char, s - p + 3);
1138  *titleBuffer = Delimiter;
1139  *(titleBuffer + 1) = Delimiter;
1140  strn0cpy(titleBuffer + 2, p, s - p + 1);
1141  }
1142  else
1143  return "";
1144  return titleBuffer;
1145 }
1146 
1147 const char *cRecording::PrefixFileName(char Prefix)
1148 {
1150  if (*p) {
1151  free(fileName);
1152  fileName = strdup(p);
1153  return fileName;
1154  }
1155  return NULL;
1156 }
1157 
1159 {
1160  const char *s = name;
1161  int level = 0;
1162  while (*++s) {
1163  if (*s == FOLDERDELIMCHAR)
1164  level++;
1165  }
1166  return level;
1167 }
1168 
1169 bool cRecording::IsEdited(void) const
1170 {
1171  const char *s = strgetlast(name, FOLDERDELIMCHAR);
1172  return *s == '%';
1173 }
1174 
1176 {
1180 }
1181 
1182 bool cRecording::HasMarks(void) const
1183 {
1184  return access(cMarks::MarksFileName(this), F_OK) == 0;
1185 }
1186 
1188 {
1189  return cMarks::DeleteMarksFile(this);
1190 }
1191 
1193 {
1194  info->Read();
1195  priority = info->priority;
1196  lifetime = info->lifetime;
1198 }
1199 
1200 bool cRecording::WriteInfo(const char *OtherFileName)
1201 {
1202  cString InfoFileName = cString::sprintf("%s%s", OtherFileName ? OtherFileName : FileName(), isPesRecording ? INFOFILESUFFIX ".vdr" : INFOFILESUFFIX);
1203  if (!OtherFileName) {
1204  // Let's keep the error counter if this is a re-started recording:
1205  cRecordingInfo ExistingInfo(FileName());
1206  if (ExistingInfo.Read())
1207  info->SetErrors(max(0, ExistingInfo.Errors()));
1208  else
1209  info->SetErrors(0);
1210  }
1211  cSafeFile f(InfoFileName);
1212  if (f.Open()) {
1213  info->Write(f);
1214  f.Close();
1215  }
1216  else
1217  LOG_ERROR_STR(*InfoFileName);
1218  return true;
1219 }
1220 
1221 void cRecording::SetStartTime(time_t Start)
1222 {
1223  start = Start;
1224  free(fileName);
1225  fileName = NULL;
1226 }
1227 
1228 bool cRecording::ChangePriorityLifetime(int NewPriority, int NewLifetime)
1229 {
1230  if (NewPriority != Priority() || NewLifetime != Lifetime()) {
1231  dsyslog("changing priority/lifetime of '%s' to %d/%d", Name(), NewPriority, NewLifetime);
1232  if (IsPesRecording()) {
1233  cString OldFileName = FileName();
1234  priority = NewPriority;
1235  lifetime = NewLifetime;
1236  free(fileName);
1237  fileName = NULL;
1238  cString NewFileName = FileName();
1239  if (!cVideoDirectory::RenameVideoFile(OldFileName, NewFileName))
1240  return false;
1241  info->SetFileName(NewFileName);
1242  }
1243  else {
1244  priority = info->priority = NewPriority;
1245  lifetime = info->lifetime = NewLifetime;
1246  if (!WriteInfo())
1247  return false;
1248  }
1249  }
1250  return true;
1251 }
1252 
1253 bool cRecording::ChangeName(const char *NewName)
1254 {
1255  if (strcmp(NewName, Name())) {
1256  dsyslog("changing name of '%s' to '%s'", Name(), NewName);
1257  cString OldName = Name();
1258  cString OldFileName = FileName();
1259  free(fileName);
1260  fileName = NULL;
1261  free(name);
1262  name = strdup(NewName);
1263  cString NewFileName = FileName();
1264  bool Exists = access(NewFileName, F_OK) == 0;
1265  if (Exists)
1266  esyslog("ERROR: recording '%s' already exists", NewName);
1267  if (Exists || !(MakeDirs(NewFileName, true) && cVideoDirectory::MoveVideoFile(OldFileName, NewFileName))) {
1268  free(name);
1269  name = strdup(OldName);
1270  free(fileName);
1271  fileName = strdup(OldFileName);
1272  return false;
1273  }
1274  isOnVideoDirectoryFileSystem = -1; // it might have been moved to a different file system
1275  ClearSortName();
1276  }
1277  return true;
1278 }
1279 
1281 {
1282  bool result = true;
1283  char *NewName = strdup(FileName());
1284  char *ext = strrchr(NewName, '.');
1285  if (ext && strcmp(ext, RECEXT) == 0) {
1286  strncpy(ext, DELEXT, strlen(ext));
1287  if (access(NewName, F_OK) == 0) {
1288  // the new name already exists, so let's remove that one first:
1289  isyslog("removing recording '%s'", NewName);
1291  }
1292  isyslog("deleting recording '%s'", FileName());
1293  if (access(FileName(), F_OK) == 0) {
1294  result = cVideoDirectory::RenameVideoFile(FileName(), NewName);
1296  }
1297  else {
1298  isyslog("recording '%s' vanished", FileName());
1299  result = true; // well, we were going to delete it, anyway
1300  }
1301  }
1302  free(NewName);
1303  return result;
1304 }
1305 
1307 {
1308  // let's do a final safety check here:
1309  if (!endswith(FileName(), DELEXT)) {
1310  esyslog("attempt to remove recording %s", FileName());
1311  return false;
1312  }
1313  isyslog("removing recording %s", FileName());
1315 }
1316 
1318 {
1319  bool result = true;
1320  char *NewName = strdup(FileName());
1321  char *ext = strrchr(NewName, '.');
1322  if (ext && strcmp(ext, DELEXT) == 0) {
1323  strncpy(ext, RECEXT, strlen(ext));
1324  if (access(NewName, F_OK) == 0) {
1325  // the new name already exists, so let's not remove that one:
1326  esyslog("ERROR: attempt to undelete '%s', while recording '%s' exists", FileName(), NewName);
1327  result = false;
1328  }
1329  else {
1330  isyslog("undeleting recording '%s'", FileName());
1331  if (access(FileName(), F_OK) == 0)
1332  result = cVideoDirectory::RenameVideoFile(FileName(), NewName);
1333  else {
1334  isyslog("deleted recording '%s' vanished", FileName());
1335  result = false;
1336  }
1337  }
1338  }
1339  free(NewName);
1340  return result;
1341 }
1342 
1343 int cRecording::IsInUse(void) const
1344 {
1345  int Use = ruNone;
1347  Use |= ruTimer;
1349  Use |= ruReplay;
1351  return Use;
1352 }
1353 
1354 void cRecording::ResetResume(void) const
1355 {
1357 }
1358 
1359 int cRecording::NumFrames(void) const
1360 {
1361  if (numFrames < 0) {
1364  return nf; // check again later for ongoing recordings
1365  numFrames = nf;
1366  }
1367  return numFrames;
1368 }
1369 
1371 {
1372  int nf = NumFrames();
1373  if (nf >= 0)
1374  return int(nf / FramesPerSecond());
1375  return -1;
1376 }
1377 
1378 int cRecording::FileSizeMB(void) const
1379 {
1380  if (fileSizeMB < 0) {
1381  int fs = DirSizeMB(FileName());
1383  return fs; // check again later for ongoing recordings
1384  fileSizeMB = fs;
1385  }
1386  return fileSizeMB;
1387 }
1388 
1389 // --- cVideoDirectoryScannerThread ------------------------------------------
1390 
1392 private:
1395  int count;
1396  bool initial;
1397  void ScanVideoDir(const char *DirName, int LinkLevel = 0, int DirLevel = 0);
1398 protected:
1399  virtual void Action(void);
1400 public:
1401  cVideoDirectoryScannerThread(cRecordings *Recordings, cRecordings *DeletedRecordings);
1403  };
1404 
1406 :cThread("video directory scanner", true)
1407 {
1408  recordings = Recordings;
1409  deletedRecordings = DeletedRecordings;
1410  count = 0;
1411  initial = true;
1412 }
1413 
1415 {
1416  Cancel(3);
1417 }
1418 
1420 {
1421  cStateKey StateKey;
1422  recordings->Lock(StateKey);
1423  count = recordings->Count();
1424  initial = count == 0; // no name checking if the list is initially empty
1425  StateKey.Remove();
1426  deletedRecordings->Lock(StateKey, true);
1428  StateKey.Remove();
1430 }
1431 
1432 void cVideoDirectoryScannerThread::ScanVideoDir(const char *DirName, int LinkLevel, int DirLevel)
1433 {
1434  // Find any new recordings:
1435  cReadDir d(DirName);
1436  struct dirent *e;
1437  while (Running() && (e = d.Next()) != NULL) {
1438  if (cIoThrottle::Engaged())
1439  cCondWait::SleepMs(100);
1440  cString buffer = AddDirectory(DirName, e->d_name);
1441  struct stat st;
1442  if (lstat(buffer, &st) == 0) {
1443  int Link = 0;
1444  if (S_ISLNK(st.st_mode)) {
1445  if (LinkLevel > MAX_LINK_LEVEL) {
1446  isyslog("max link level exceeded - not scanning %s", *buffer);
1447  continue;
1448  }
1449  Link = 1;
1450  if (stat(buffer, &st) != 0)
1451  continue;
1452  }
1453  if (S_ISDIR(st.st_mode)) {
1454  cRecordings *Recordings = NULL;
1455  if (endswith(buffer, RECEXT))
1456  Recordings = recordings;
1457  else if (endswith(buffer, DELEXT))
1458  Recordings = deletedRecordings;
1459  if (Recordings) {
1460  cStateKey StateKey;
1461  Recordings->Lock(StateKey, true);
1462  if (initial && count != recordings->Count()) {
1463  dsyslog("activated name checking for initial read of video directory");
1464  initial = false;
1465  }
1466  if (Recordings == deletedRecordings || initial || !Recordings->GetByName(buffer)) {
1467  cRecording *r = new cRecording(buffer);
1468  if (r->Name()) {
1469  r->NumFrames(); // initializes the numFrames member
1470  r->FileSizeMB(); // initializes the fileSizeMB member
1471  r->IsOnVideoDirectoryFileSystem(); // initializes the isOnVideoDirectoryFileSystem member
1472  if (Recordings == deletedRecordings)
1473  r->SetDeleted();
1474  Recordings->Add(r);
1475  count = recordings->Count();
1476  }
1477  else
1478  delete r;
1479  }
1480  StateKey.Remove();
1481  }
1482  else
1483  ScanVideoDir(buffer, LinkLevel + Link, DirLevel + 1);
1484  }
1485  }
1486  }
1487  // Handle any vanished recordings:
1488  if (!initial && DirLevel == 0) {
1489  cStateKey StateKey;
1490  recordings->Lock(StateKey, true);
1491  for (cRecording *Recording = recordings->First(); Recording; ) {
1492  cRecording *r = Recording;
1493  Recording = recordings->Next(Recording);
1494  if (access(r->FileName(), F_OK) != 0)
1495  recordings->Del(r);
1496  }
1497  StateKey.Remove();
1498  }
1499 }
1500 
1501 // --- cRecordings -----------------------------------------------------------
1502 
1506 char *cRecordings::updateFileName = NULL;
1508 time_t cRecordings::lastUpdate = 0;
1509 
1511 :cList<cRecording>(Deleted ? "4 DelRecs" : "3 Recordings")
1512 {
1513 }
1514 
1516 {
1517  // The first one to be destructed deletes it:
1520 }
1521 
1523 {
1524  if (!updateFileName)
1525  updateFileName = strdup(AddDirectory(cVideoDirectory::Name(), ".update"));
1526  return updateFileName;
1527 }
1528 
1530 {
1531  bool needsUpdate = NeedsUpdate();
1533  if (!needsUpdate)
1534  lastUpdate = time(NULL); // make sure we don't trigger ourselves
1535 }
1536 
1538 {
1539  time_t lastModified = LastModifiedTime(UpdateFileName());
1540  if (lastModified > time(NULL))
1541  return false; // somebody's clock isn't running correctly
1542  return lastUpdate < lastModified;
1543 }
1544 
1545 void cRecordings::Update(bool Wait)
1546 {
1549  lastUpdate = time(NULL); // doing this first to make sure we don't miss anything
1551  if (Wait) {
1553  cCondWait::SleepMs(100);
1554  }
1555 }
1556 
1557 const cRecording *cRecordings::GetById(int Id) const
1558 {
1559  for (const cRecording *Recording = First(); Recording; Recording = Next(Recording)) {
1560  if (Recording->Id() == Id)
1561  return Recording;
1562  }
1563  return NULL;
1564 }
1565 
1566 const cRecording *cRecordings::GetByName(const char *FileName) const
1567 {
1568  if (FileName) {
1569  for (const cRecording *Recording = First(); Recording; Recording = Next(Recording)) {
1570  if (strcmp(Recording->FileName(), FileName) == 0)
1571  return Recording;
1572  }
1573  }
1574  return NULL;
1575 }
1576 
1578 {
1579  Recording->SetId(++lastRecordingId);
1580  cList<cRecording>::Add(Recording);
1581 }
1582 
1583 void cRecordings::AddByName(const char *FileName, bool TriggerUpdate)
1584 {
1585  if (!GetByName(FileName)) {
1586  Add(new cRecording(FileName));
1587  if (TriggerUpdate)
1588  TouchUpdate();
1589  }
1590 }
1591 
1592 void cRecordings::DelByName(const char *FileName)
1593 {
1594  cRecording *Recording = GetByName(FileName);
1595  cRecording *dummy = NULL;
1596  if (!Recording)
1597  Recording = dummy = new cRecording(FileName); // allows us to use a FileName that is not in the Recordings list
1599  if (!dummy)
1600  Del(Recording, false);
1601  char *ext = strrchr(Recording->fileName, '.');
1602  if (ext) {
1603  strncpy(ext, DELEXT, strlen(ext));
1604  if (access(Recording->FileName(), F_OK) == 0) {
1605  Recording->SetDeleted();
1606  DeletedRecordings->Add(Recording);
1607  Recording = NULL; // to prevent it from being deleted below
1608  }
1609  }
1610  delete Recording;
1611  TouchUpdate();
1612 }
1613 
1614 void cRecordings::UpdateByName(const char *FileName)
1615 {
1616  if (cRecording *Recording = GetByName(FileName))
1617  Recording->ReadInfo();
1618 }
1619 
1621 {
1622  int size = 0;
1623  for (const cRecording *Recording = First(); Recording; Recording = Next(Recording)) {
1624  int FileSizeMB = Recording->FileSizeMB();
1625  if (FileSizeMB > 0 && Recording->IsOnVideoDirectoryFileSystem())
1626  size += FileSizeMB;
1627  }
1628  return size;
1629 }
1630 
1631 double cRecordings::MBperMinute(void) const
1632 {
1633  int size = 0;
1634  int length = 0;
1635  for (const cRecording *Recording = First(); Recording; Recording = Next(Recording)) {
1636  if (Recording->IsOnVideoDirectoryFileSystem()) {
1637  int FileSizeMB = Recording->FileSizeMB();
1638  if (FileSizeMB > 0) {
1639  int LengthInSeconds = Recording->LengthInSeconds();
1640  if (LengthInSeconds > 0) {
1641  if (LengthInSeconds / FileSizeMB < LIMIT_SECS_PER_MB_RADIO) { // don't count radio recordings
1642  size += FileSizeMB;
1643  length += LengthInSeconds;
1644  }
1645  }
1646  }
1647  }
1648  }
1649  return (size && length) ? double(size) * 60 / length : -1;
1650 }
1651 
1652 int cRecordings::PathIsInUse(const char *Path) const
1653 {
1654  int Use = ruNone;
1655  for (const cRecording *Recording = First(); Recording; Recording = Next(Recording)) {
1656  if (Recording->IsInPath(Path))
1657  Use |= Recording->IsInUse();
1658  }
1659  return Use;
1660 }
1661 
1662 int cRecordings::GetNumRecordingsInPath(const char *Path) const
1663 {
1664  int n = 0;
1665  for (const cRecording *Recording = First(); Recording; Recording = Next(Recording)) {
1666  if (Recording->IsInPath(Path))
1667  n++;
1668  }
1669  return n;
1670 }
1671 
1672 bool cRecordings::MoveRecordings(const char *OldPath, const char *NewPath)
1673 {
1674  if (OldPath && NewPath && strcmp(OldPath, NewPath)) {
1675  dsyslog("moving '%s' to '%s'", OldPath, NewPath);
1676  bool Moved = false;
1677  for (cRecording *Recording = First(); Recording; Recording = Next(Recording)) {
1678  if (Recording->IsInPath(OldPath)) {
1679  const char *p = Recording->Name() + strlen(OldPath);
1680  cString NewName = cString::sprintf("%s%s", NewPath, p);
1681  if (!Recording->ChangeName(NewName))
1682  return false;
1683  Moved = true;
1684  }
1685  }
1686  if (Moved)
1687  TouchUpdate();
1688  }
1689  return true;
1690 }
1691 
1692 void cRecordings::ResetResume(const char *ResumeFileName)
1693 {
1694  for (cRecording *Recording = First(); Recording; Recording = Next(Recording)) {
1695  if (!ResumeFileName || strncmp(ResumeFileName, Recording->FileName(), strlen(Recording->FileName())) == 0)
1696  Recording->ResetResume();
1697  }
1698 }
1699 
1701 {
1702  for (cRecording *Recording = First(); Recording; Recording = Next(Recording))
1703  Recording->ClearSortName();
1704 }
1705 
1706 // --- cDirCopier ------------------------------------------------------------
1707 
1708 class cDirCopier : public cThread {
1709 private:
1712  bool error;
1714  bool Throttled(void);
1715  virtual void Action(void);
1716 public:
1717  cDirCopier(const char *DirNameSrc, const char *DirNameDst);
1718  virtual ~cDirCopier();
1719  bool Error(void) { return error; }
1720  };
1721 
1722 cDirCopier::cDirCopier(const char *DirNameSrc, const char *DirNameDst)
1723 :cThread("file copier", true)
1724 {
1725  dirNameSrc = DirNameSrc;
1726  dirNameDst = DirNameDst;
1727  error = true; // prepare for the worst!
1728  suspensionLogged = false;
1729 }
1730 
1732 {
1733  Cancel(3);
1734 }
1735 
1737 {
1738  if (cIoThrottle::Engaged()) {
1739  if (!suspensionLogged) {
1740  dsyslog("suspending copy thread");
1741  suspensionLogged = true;
1742  }
1743  return true;
1744  }
1745  else if (suspensionLogged) {
1746  dsyslog("resuming copy thread");
1747  suspensionLogged = false;
1748  }
1749  return false;
1750 }
1751 
1753 {
1754  if (DirectoryOk(dirNameDst, true)) {
1755  cReadDir d(dirNameSrc);
1756  if (d.Ok()) {
1757  dsyslog("copying directory '%s' to '%s'", *dirNameSrc, *dirNameDst);
1758  dirent *e = NULL;
1759  cString FileNameSrc;
1760  cString FileNameDst;
1761  int From = -1;
1762  int To = -1;
1763  size_t BufferSize = BUFSIZ;
1764  uchar *Buffer = NULL;
1765  while (Running()) {
1766  // Suspend copying if we have severe throughput problems:
1767  if (Throttled()) {
1768  cCondWait::SleepMs(100);
1769  continue;
1770  }
1771  // Copy all files in the source directory to the destination directory:
1772  if (e) {
1773  // We're currently copying a file:
1774  if (!Buffer) {
1775  esyslog("ERROR: no buffer");
1776  break;
1777  }
1778  size_t Read = safe_read(From, Buffer, BufferSize);
1779  if (Read > 0) {
1780  size_t Written = safe_write(To, Buffer, Read);
1781  if (Written != Read) {
1782  esyslog("ERROR: can't write to destination file '%s': %m", *FileNameDst);
1783  break;
1784  }
1785  }
1786  else if (Read == 0) { // EOF on From
1787  e = NULL; // triggers switch to next entry
1788  if (fsync(To) < 0) {
1789  esyslog("ERROR: can't sync destination file '%s': %m", *FileNameDst);
1790  break;
1791  }
1792  if (close(From) < 0) {
1793  esyslog("ERROR: can't close source file '%s': %m", *FileNameSrc);
1794  break;
1795  }
1796  if (close(To) < 0) {
1797  esyslog("ERROR: can't close destination file '%s': %m", *FileNameDst);
1798  break;
1799  }
1800  // Plausibility check:
1801  off_t FileSizeSrc = FileSize(FileNameSrc);
1802  off_t FileSizeDst = FileSize(FileNameDst);
1803  if (FileSizeSrc != FileSizeDst) {
1804  esyslog("ERROR: file size discrepancy: %" PRId64 " != %" PRId64, FileSizeSrc, FileSizeDst);
1805  break;
1806  }
1807  }
1808  else {
1809  esyslog("ERROR: can't read from source file '%s': %m", *FileNameSrc);
1810  break;
1811  }
1812  }
1813  else if ((e = d.Next()) != NULL) {
1814  // We're switching to the next directory entry:
1815  FileNameSrc = AddDirectory(dirNameSrc, e->d_name);
1816  FileNameDst = AddDirectory(dirNameDst, e->d_name);
1817  struct stat st;
1818  if (stat(FileNameSrc, &st) < 0) {
1819  esyslog("ERROR: can't access source file '%s': %m", *FileNameSrc);
1820  break;
1821  }
1822  if (!(S_ISREG(st.st_mode) || S_ISLNK(st.st_mode))) {
1823  esyslog("ERROR: source file '%s' is neither a regular file nor a symbolic link", *FileNameSrc);
1824  break;
1825  }
1826  dsyslog("copying file '%s' to '%s'", *FileNameSrc, *FileNameDst);
1827  if (!Buffer) {
1828  BufferSize = max(size_t(st.st_blksize * 10), size_t(BUFSIZ));
1829  Buffer = MALLOC(uchar, BufferSize);
1830  if (!Buffer) {
1831  esyslog("ERROR: out of memory");
1832  break;
1833  }
1834  }
1835  if (access(FileNameDst, F_OK) == 0) {
1836  esyslog("ERROR: destination file '%s' already exists", *FileNameDst);
1837  break;
1838  }
1839  if ((From = open(FileNameSrc, O_RDONLY)) < 0) {
1840  esyslog("ERROR: can't open source file '%s': %m", *FileNameSrc);
1841  break;
1842  }
1843  if ((To = open(FileNameDst, O_WRONLY | O_CREAT | O_EXCL, DEFFILEMODE)) < 0) {
1844  esyslog("ERROR: can't open destination file '%s': %m", *FileNameDst);
1845  close(From);
1846  break;
1847  }
1848  }
1849  else {
1850  // We're done:
1851  free(Buffer);
1852  dsyslog("done copying directory '%s' to '%s'", *dirNameSrc, *dirNameDst);
1853  error = false;
1854  return;
1855  }
1856  }
1857  free(Buffer);
1858  close(From); // just to be absolutely sure
1859  close(To);
1860  isyslog("copying directory '%s' to '%s' ended prematurely", *dirNameSrc, *dirNameDst);
1861  }
1862  else
1863  esyslog("ERROR: can't open '%s'", *dirNameSrc);
1864  }
1865  else
1866  esyslog("ERROR: can't access '%s'", *dirNameDst);
1867 }
1868 
1869 // --- cRecordingsHandlerEntry -----------------------------------------------
1870 
1872 private:
1873  int usage;
1878  bool error;
1879  void ClearPending(void) { usage &= ~ruPending; }
1880 public:
1881  cRecordingsHandlerEntry(int Usage, const char *FileNameSrc, const char *FileNameDst);
1883  int Usage(const char *FileName = NULL) const;
1884  bool Error(void) const { return error; }
1885  void SetCanceled(void) { usage |= ruCanceled; }
1886  const char *FileNameSrc(void) const { return fileNameSrc; }
1887  const char *FileNameDst(void) const { return fileNameDst; }
1888  bool Active(cRecordings *Recordings);
1889  void Cleanup(cRecordings *Recordings);
1890  };
1891 
1892 cRecordingsHandlerEntry::cRecordingsHandlerEntry(int Usage, const char *FileNameSrc, const char *FileNameDst)
1893 {
1894  usage = Usage;
1897  cutter = NULL;
1898  copier = NULL;
1899  error = false;
1900 }
1901 
1903 {
1904  delete cutter;
1905  delete copier;
1906 }
1907 
1908 int cRecordingsHandlerEntry::Usage(const char *FileName) const
1909 {
1910  int u = usage;
1911  if (FileName && *FileName) {
1912  if (strcmp(FileName, fileNameSrc) == 0)
1913  u |= ruSrc;
1914  else if (strcmp(FileName, fileNameDst) == 0)
1915  u |= ruDst;
1916  }
1917  return u;
1918 }
1919 
1921 {
1922  if ((usage & ruCanceled) != 0)
1923  return false;
1924  // First test whether there is an ongoing operation:
1925  if (cutter) {
1926  if (cutter->Active())
1927  return true;
1928  error = cutter->Error();
1929  delete cutter;
1930  cutter = NULL;
1931  }
1932  else if (copier) {
1933  if (copier->Active())
1934  return true;
1935  error = copier->Error();
1936  delete copier;
1937  copier = NULL;
1938  }
1939  // Now check if there is something to start:
1940  if ((Usage() & ruPending) != 0) {
1941  if ((Usage() & ruCut) != 0) {
1942  cutter = new cCutter(FileNameSrc());
1943  cutter->Start();
1944  Recordings->AddByName(FileNameDst(), false);
1945  }
1946  else if ((Usage() & (ruMove | ruCopy)) != 0) {
1949  copier->Start();
1950  }
1951  ClearPending();
1952  Recordings->SetModified(); // to trigger a state change
1953  return true;
1954  }
1955  // We're done:
1956  if (!error && (usage & (ruMove | ruCopy)) != 0)
1958  if (!error && (usage & ruMove) != 0) {
1959  cRecording Recording(FileNameSrc());
1960  if (Recording.Delete()) {
1962  Recordings->DelByName(Recording.FileName());
1963  }
1964  }
1965  Recordings->SetModified(); // to trigger a state change
1966  Recordings->TouchUpdate();
1967  return false;
1968 }
1969 
1971 {
1972  if ((usage & ruCut)) { // this was a cut operation...
1973  if (cutter // ...which had not yet ended...
1974  || error) { // ...or finished with error
1975  if (cutter) {
1976  delete cutter;
1977  cutter = NULL;
1978  }
1980  Recordings->DelByName(fileNameDst);
1981  }
1982  }
1983  if ((usage & (ruMove | ruCopy)) // this was a move/copy operation...
1984  && ((usage & ruPending) // ...which had not yet started...
1985  || copier // ...or not yet finished...
1986  || error)) { // ...or finished with error
1987  if (copier) {
1988  delete copier;
1989  copier = NULL;
1990  }
1992  if ((usage & ruMove) != 0)
1993  Recordings->AddByName(fileNameSrc);
1994  Recordings->DelByName(fileNameDst);
1995  }
1996 }
1997 
1998 // --- cRecordingsHandler ----------------------------------------------------
1999 
2001 
2003 :cThread("recordings handler")
2004 {
2005  finished = true;
2006  error = false;
2007 }
2008 
2010 {
2011  Cancel(3);
2012 }
2013 
2015 {
2016  while (Running()) {
2017  bool Sleep = false;
2018  {
2020  Recordings->SetExplicitModify();
2021  cMutexLock MutexLock(&mutex);
2023  if (!r->Active(Recordings)) {
2024  error |= r->Error();
2025  r->Cleanup(Recordings);
2026  operations.Del(r);
2027  }
2028  else
2029  Sleep = true;
2030  }
2031  else
2032  break;
2033  }
2034  if (Sleep)
2035  cCondWait::SleepMs(100);
2036  }
2037 }
2038 
2040 {
2041  if (FileName && *FileName) {
2042  for (cRecordingsHandlerEntry *r = operations.First(); r; r = operations.Next(r)) {
2043  if ((r->Usage() & ruCanceled) != 0)
2044  continue;
2045  if (strcmp(FileName, r->FileNameSrc()) == 0 || strcmp(FileName, r->FileNameDst()) == 0)
2046  return r;
2047  }
2048  }
2049  return NULL;
2050 }
2051 
2052 bool cRecordingsHandler::Add(int Usage, const char *FileNameSrc, const char *FileNameDst)
2053 {
2054  dsyslog("recordings handler add %d '%s' '%s'", Usage, FileNameSrc, FileNameDst);
2055  cMutexLock MutexLock(&mutex);
2056  if (Usage == ruCut || Usage == ruMove || Usage == ruCopy) {
2057  if (FileNameSrc && *FileNameSrc) {
2058  if (Usage == ruCut || FileNameDst && *FileNameDst) {
2059  cString fnd;
2060  if (Usage == ruCut && !FileNameDst)
2061  FileNameDst = fnd = cCutter::EditedFileName(FileNameSrc);
2062  if (!Get(FileNameSrc) && !Get(FileNameDst)) {
2063  Usage |= ruPending;
2064  operations.Add(new cRecordingsHandlerEntry(Usage, FileNameSrc, FileNameDst));
2065  finished = false;
2066  Start();
2067  return true;
2068  }
2069  else
2070  esyslog("ERROR: file name already present in recordings handler add %d '%s' '%s'", Usage, FileNameSrc, FileNameDst);
2071  }
2072  else
2073  esyslog("ERROR: missing dst file name in recordings handler add %d '%s' '%s'", Usage, FileNameSrc, FileNameDst);
2074  }
2075  else
2076  esyslog("ERROR: missing src file name in recordings handler add %d '%s' '%s'", Usage, FileNameSrc, FileNameDst);
2077  }
2078  else
2079  esyslog("ERROR: invalid usage in recordings handler add %d '%s' '%s'", Usage, FileNameSrc, FileNameDst);
2080  return false;
2081 }
2082 
2083 void cRecordingsHandler::Del(const char *FileName)
2084 {
2085  cMutexLock MutexLock(&mutex);
2086  if (cRecordingsHandlerEntry *r = Get(FileName))
2087  r->SetCanceled();
2088 }
2089 
2091 {
2092  cMutexLock MutexLock(&mutex);
2093  for (cRecordingsHandlerEntry *r = operations.First(); r; r = operations.Next(r))
2094  r->SetCanceled();
2095 }
2096 
2097 int cRecordingsHandler::GetUsage(const char *FileName)
2098 {
2099  cMutexLock MutexLock(&mutex);
2100  if (cRecordingsHandlerEntry *r = Get(FileName))
2101  return r->Usage(FileName);
2102  return ruNone;
2103 }
2104 
2106 {
2107  cMutexLock MutexLock(&mutex);
2108  if (!finished && operations.Count() == 0) {
2109  finished = true;
2110  Error = error;
2111  error = false;
2112  return true;
2113  }
2114  return false;
2115 }
2116 
2117 // --- cMark -----------------------------------------------------------------
2118 
2121 
2122 cMark::cMark(int Position, const char *Comment, double FramesPerSecond)
2123 {
2124  position = Position;
2125  comment = Comment;
2126  framesPerSecond = FramesPerSecond;
2127 }
2128 
2130 {
2131 }
2132 
2134 {
2135  return cString::sprintf("%s%s%s", *IndexToHMSF(position, true, framesPerSecond), Comment() ? " " : "", Comment() ? Comment() : "");
2136 }
2137 
2138 bool cMark::Parse(const char *s)
2139 {
2140  comment = NULL;
2143  const char *p = strchr(s, ' ');
2144  if (p) {
2145  p = skipspace(p);
2146  if (*p)
2147  comment = strdup(p);
2148  }
2149  return true;
2150 }
2151 
2152 bool cMark::Save(FILE *f)
2153 {
2154  return fprintf(f, "%s\n", *ToText()) > 0;
2155 }
2156 
2157 // --- cMarks ----------------------------------------------------------------
2158 
2160 {
2161  return AddDirectory(Recording->FileName(), Recording->IsPesRecording() ? MARKSFILESUFFIX ".vdr" : MARKSFILESUFFIX);
2162 }
2163 
2164 bool cMarks::DeleteMarksFile(const cRecording *Recording)
2165 {
2166  if (remove(cMarks::MarksFileName(Recording)) < 0) {
2167  if (errno != ENOENT) {
2168  LOG_ERROR_STR(Recording->FileName());
2169  return false;
2170  }
2171  }
2172  return true;
2173 }
2174 
2175 bool cMarks::Load(const char *RecordingFileName, double FramesPerSecond, bool IsPesRecording)
2176 {
2177  recordingFileName = RecordingFileName;
2178  fileName = AddDirectory(RecordingFileName, IsPesRecording ? MARKSFILESUFFIX ".vdr" : MARKSFILESUFFIX);
2179  framesPerSecond = FramesPerSecond;
2180  isPesRecording = IsPesRecording;
2181  nextUpdate = 0;
2182  lastFileTime = -1; // the first call to Load() must take place!
2183  lastChange = 0;
2184  return Update();
2185 }
2186 
2187 bool cMarks::Update(void)
2188 {
2189  time_t t = time(NULL);
2190  if (t > nextUpdate && *fileName) {
2191  time_t LastModified = LastModifiedTime(fileName);
2192  if (LastModified != lastFileTime) // change detected, or first run
2193  lastChange = LastModified > 0 ? LastModified : t;
2194  int d = t - lastChange;
2195  if (d < 60)
2196  d = 1; // check frequently if the file has just been modified
2197  else if (d < 3600)
2198  d = 10; // older files are checked less frequently
2199  else
2200  d /= 360; // phase out checking for very old files
2201  nextUpdate = t + d;
2202  if (LastModified != lastFileTime) { // change detected, or first run
2203  lastFileTime = LastModified;
2204  if (lastFileTime == t)
2205  lastFileTime--; // make sure we don't miss updates in the remaining second
2209  Align();
2210  Sort();
2211  return true;
2212  }
2213  }
2214  }
2215  return false;
2216 }
2217 
2218 bool cMarks::Save(void)
2219 {
2220  if (cConfig<cMark>::Save()) {
2222  return true;
2223  }
2224  return false;
2225 }
2226 
2227 void cMarks::Align(void)
2228 {
2229  cIndexFile IndexFile(recordingFileName, false, isPesRecording);
2230  for (cMark *m = First(); m; m = Next(m)) {
2231  int p = IndexFile.GetClosestIFrame(m->Position());
2232  if (m->Position() - p) {
2233  //isyslog("aligned editing mark %s to %s (off by %d frame%s)", *IndexToHMSF(m->Position(), true, framesPerSecond), *IndexToHMSF(p, true, framesPerSecond), m->Position() - p, abs(m->Position() - p) > 1 ? "s" : "");
2234  m->SetPosition(p);
2235  }
2236  }
2237 }
2238 
2239 void cMarks::Sort(void)
2240 {
2241  for (cMark *m1 = First(); m1; m1 = Next(m1)) {
2242  for (cMark *m2 = Next(m1); m2; m2 = Next(m2)) {
2243  if (m2->Position() < m1->Position()) {
2244  swap(m1->position, m2->position);
2245  swap(m1->comment, m2->comment);
2246  }
2247  }
2248  }
2249 }
2250 
2251 void cMarks::Add(int Position)
2252 {
2253  cConfig<cMark>::Add(new cMark(Position, NULL, framesPerSecond));
2254  Sort();
2255 }
2256 
2257 const cMark *cMarks::Get(int Position) const
2258 {
2259  for (const cMark *mi = First(); mi; mi = Next(mi)) {
2260  if (mi->Position() == Position)
2261  return mi;
2262  }
2263  return NULL;
2264 }
2265 
2266 const cMark *cMarks::GetPrev(int Position) const
2267 {
2268  for (const cMark *mi = Last(); mi; mi = Prev(mi)) {
2269  if (mi->Position() < Position)
2270  return mi;
2271  }
2272  return NULL;
2273 }
2274 
2275 const cMark *cMarks::GetNext(int Position) const
2276 {
2277  for (const cMark *mi = First(); mi; mi = Next(mi)) {
2278  if (mi->Position() > Position)
2279  return mi;
2280  }
2281  return NULL;
2282 }
2283 
2284 const cMark *cMarks::GetNextBegin(const cMark *EndMark) const
2285 {
2286  const cMark *BeginMark = EndMark ? Next(EndMark) : First();
2287  if (BeginMark && EndMark && BeginMark->Position() == EndMark->Position()) {
2288  while (const cMark *NextMark = Next(BeginMark)) {
2289  if (BeginMark->Position() == NextMark->Position()) { // skip Begin/End at the same position
2290  if (!(BeginMark = Next(NextMark)))
2291  break;
2292  }
2293  else
2294  break;
2295  }
2296  }
2297  return BeginMark;
2298 }
2299 
2300 const cMark *cMarks::GetNextEnd(const cMark *BeginMark) const
2301 {
2302  if (!BeginMark)
2303  return NULL;
2304  const cMark *EndMark = Next(BeginMark);
2305  if (EndMark && BeginMark && BeginMark->Position() == EndMark->Position()) {
2306  while (const cMark *NextMark = Next(EndMark)) {
2307  if (EndMark->Position() == NextMark->Position()) { // skip End/Begin at the same position
2308  if (!(EndMark = Next(NextMark)))
2309  break;
2310  }
2311  else
2312  break;
2313  }
2314  }
2315  return EndMark;
2316 }
2317 
2319 {
2320  int NumSequences = 0;
2321  if (const cMark *BeginMark = GetNextBegin()) {
2322  while (const cMark *EndMark = GetNextEnd(BeginMark)) {
2323  NumSequences++;
2324  BeginMark = GetNextBegin(EndMark);
2325  }
2326  if (BeginMark) {
2327  NumSequences++; // the last sequence had no actual "end" mark
2328  if (NumSequences == 1 && BeginMark->Position() == 0)
2329  NumSequences = 0; // there is only one actual "begin" mark at offset zero, and no actual "end" mark
2330  }
2331  }
2332  return NumSequences;
2333 }
2334 
2335 // --- cRecordingUserCommand -------------------------------------------------
2336 
2337 const char *cRecordingUserCommand::command = NULL;
2338 
2339 void cRecordingUserCommand::InvokeCommand(const char *State, const char *RecordingFileName, const char *SourceFileName)
2340 {
2341  if (command) {
2342  cString cmd;
2343  if (SourceFileName)
2344  cmd = cString::sprintf("%s %s \"%s\" \"%s\"", command, State, *strescape(RecordingFileName, "\\\"$"), *strescape(SourceFileName, "\\\"$"));
2345  else
2346  cmd = cString::sprintf("%s %s \"%s\"", command, State, *strescape(RecordingFileName, "\\\"$"));
2347  isyslog("executing '%s'", *cmd);
2348  SystemExec(cmd);
2349  }
2350 }
2351 
2352 // --- cIndexFileGenerator ---------------------------------------------------
2353 
2354 #define IFG_BUFFER_SIZE KILOBYTE(100)
2355 
2357 private:
2359  bool update;
2360 protected:
2361  virtual void Action(void);
2362 public:
2363  cIndexFileGenerator(const char *RecordingName, bool Update = false);
2365  };
2366 
2367 cIndexFileGenerator::cIndexFileGenerator(const char *RecordingName, bool Update)
2368 :cThread("index file generator")
2369 ,recordingName(RecordingName)
2370 {
2371  update = Update;
2372  Start();
2373 }
2374 
2376 {
2377  Cancel(3);
2378 }
2379 
2381 {
2382  bool IndexFileComplete = false;
2383  bool IndexFileWritten = false;
2384  bool Rewind = false;
2385  cFileName FileName(recordingName, false);
2386  cUnbufferedFile *ReplayFile = FileName.Open();
2388  cPatPmtParser PatPmtParser;
2389  cFrameDetector FrameDetector;
2390  cIndexFile IndexFile(recordingName, true, false, false, true);
2391  int BufferChunks = KILOBYTE(1); // no need to read a lot at the beginning when parsing PAT/PMT
2392  off_t FileSize = 0;
2393  off_t FrameOffset = -1;
2394  uint16_t FileNumber = 1;
2395  off_t FileOffset = 0;
2396  int Last = -1;
2397  if (update) {
2398  // Look for current index and position to end of it if present:
2399  bool Independent;
2400  int Length;
2401  Last = IndexFile.Last();
2402  if (Last >= 0 && !IndexFile.Get(Last, &FileNumber, &FileOffset, &Independent, &Length))
2403  Last = -1; // reset Last if an error occurred
2404  if (Last >= 0) {
2405  Rewind = true;
2406  isyslog("updating index file");
2407  }
2408  else
2409  isyslog("generating index file");
2410  }
2411  Skins.QueueMessage(mtInfo, tr("Regenerating index file"));
2412  bool Stuffed = false;
2413  while (Running()) {
2414  // Rewind input file:
2415  if (Rewind) {
2416  ReplayFile = FileName.SetOffset(FileNumber, FileOffset);
2417  FileSize = FileOffset;
2418  Buffer.Clear();
2419  Rewind = false;
2420  }
2421  // Process data:
2422  int Length;
2423  uchar *Data = Buffer.Get(Length);
2424  if (Data) {
2425  if (FrameDetector.Synced()) {
2426  // Step 3 - generate the index:
2427  if (TsPid(Data) == PATPID)
2428  FrameOffset = FileSize; // the PAT/PMT is at the beginning of an I-frame
2429  int Processed = FrameDetector.Analyze(Data, Length);
2430  if (Processed > 0) {
2431  if (FrameDetector.NewFrame()) {
2432  if (IndexFileWritten || Last < 0) // check for first frame and do not write if in update mode
2433  IndexFile.Write(FrameDetector.IndependentFrame(), FileName.Number(), FrameOffset >= 0 ? FrameOffset : FileSize);
2434  FrameOffset = -1;
2435  IndexFileWritten = true;
2436  }
2437  FileSize += Processed;
2438  Buffer.Del(Processed);
2439  }
2440  }
2441  else if (PatPmtParser.Completed()) {
2442  // Step 2 - sync FrameDetector:
2443  int Processed = FrameDetector.Analyze(Data, Length);
2444  if (Processed > 0) {
2445  if (FrameDetector.Synced()) {
2446  // Synced FrameDetector, so rewind for actual processing:
2447  Rewind = true;
2448  }
2449  Buffer.Del(Processed);
2450  }
2451  }
2452  else {
2453  // Step 1 - parse PAT/PMT:
2454  uchar *p = Data;
2455  while (Length >= TS_SIZE) {
2456  int Pid = TsPid(p);
2457  if (Pid == PATPID)
2458  PatPmtParser.ParsePat(p, TS_SIZE);
2459  else if (PatPmtParser.IsPmtPid(Pid))
2460  PatPmtParser.ParsePmt(p, TS_SIZE);
2461  Length -= TS_SIZE;
2462  p += TS_SIZE;
2463  if (PatPmtParser.Completed()) {
2464  // Found pid, so rewind to sync FrameDetector:
2465  FrameDetector.SetPid(PatPmtParser.Vpid() ? PatPmtParser.Vpid() : PatPmtParser.Apid(0), PatPmtParser.Vpid() ? PatPmtParser.Vtype() : PatPmtParser.Atype(0));
2466  BufferChunks = IFG_BUFFER_SIZE;
2467  Rewind = true;
2468  break;
2469  }
2470  }
2471  Buffer.Del(p - Data);
2472  }
2473  }
2474  // Read data:
2475  else if (ReplayFile) {
2476  int Result = Buffer.Read(ReplayFile, BufferChunks);
2477  if (Result == 0) { // EOF
2478  if (Buffer.Available() > 0 && !Stuffed) {
2479  // So the last call to Buffer.Get() returned NULL, but there is still
2480  // data in the buffer, and we're at the end of the current TS file.
2481  // The remaining data in the buffer is less than what's needed for the
2482  // frame detector to analyze frames, so we need to put some stuffing
2483  // packets into the buffer to flush out the rest of the data (otherwise
2484  // any frames within the remaining data would not be seen here):
2485  uchar StuffingPacket[TS_SIZE] = { TS_SYNC_BYTE, 0xFF };
2486  for (int i = 0; i <= MIN_TS_PACKETS_FOR_FRAME_DETECTOR; i++)
2487  Buffer.Put(StuffingPacket, sizeof(StuffingPacket));
2488  Stuffed = true;
2489  }
2490  else {
2491  ReplayFile = FileName.NextFile();
2492  FileSize = 0;
2493  FrameOffset = -1;
2494  Buffer.Clear();
2495  Stuffed = false;
2496  }
2497  }
2498  }
2499  // Recording has been processed:
2500  else {
2501  IndexFileComplete = true;
2502  break;
2503  }
2504  }
2505  if (IndexFileComplete) {
2506  if (IndexFileWritten) {
2507  cRecordingInfo RecordingInfo(recordingName);
2508  if (RecordingInfo.Read()) {
2509  if (FrameDetector.FramesPerSecond() > 0 && !DoubleEqual(RecordingInfo.FramesPerSecond(), FrameDetector.FramesPerSecond())) {
2510  RecordingInfo.SetFramesPerSecond(FrameDetector.FramesPerSecond());
2511  RecordingInfo.Write();
2513  Recordings->UpdateByName(recordingName);
2514  }
2515  }
2516  Skins.QueueMessage(mtInfo, tr("Index file regeneration complete"));
2517  return;
2518  }
2519  else
2520  Skins.QueueMessage(mtError, tr("Index file regeneration failed!"));
2521  }
2522  // Delete the index file if the recording has not been processed entirely:
2523  IndexFile.Delete();
2524 }
2525 
2526 // --- cIndexFile ------------------------------------------------------------
2527 
2528 #define INDEXFILESUFFIX "/index"
2529 
2530 // The maximum time to wait before giving up while catching up on an index file:
2531 #define MAXINDEXCATCHUP 8 // number of retries
2532 #define INDEXCATCHUPWAIT 100 // milliseconds
2533 
2534 struct __attribute__((packed)) tIndexPes {
2535  uint32_t offset;
2536  uchar type;
2537  uchar number;
2538  uint16_t reserved;
2539  };
2540 
2541 struct __attribute__((packed)) tIndexTs {
2542  uint64_t offset:40; // up to 1TB per file (not using off_t here - must definitely be exactly 64 bit!)
2543  int reserved:7; // reserved for future use
2544  int independent:1; // marks frames that can be displayed by themselves (for trick modes)
2545  uint16_t number:16; // up to 64K files per recording
2546  tIndexTs(off_t Offset, bool Independent, uint16_t Number)
2547  {
2548  offset = Offset;
2549  reserved = 0;
2550  independent = Independent;
2551  number = Number;
2552  }
2553  };
2554 
2555 #define MAXWAITFORINDEXFILE 10 // max. time to wait for the regenerated index file (seconds)
2556 #define INDEXFILECHECKINTERVAL 500 // ms between checks for existence of the regenerated index file
2557 #define INDEXFILETESTINTERVAL 10 // ms between tests for the size of the index file in case of pausing live video
2558 
2559 cIndexFile::cIndexFile(const char *FileName, bool Record, bool IsPesRecording, bool PauseLive, bool Update)
2560 :resumeFile(FileName, IsPesRecording)
2561 {
2562  f = -1;
2563  size = 0;
2564  last = -1;
2565  index = NULL;
2566  isPesRecording = IsPesRecording;
2567  indexFileGenerator = NULL;
2568  if (FileName) {
2569  fileName = IndexFileName(FileName, isPesRecording);
2570  if (!Record && PauseLive) {
2571  // Wait until the index file contains at least two frames:
2572  time_t tmax = time(NULL) + MAXWAITFORINDEXFILE;
2573  while (time(NULL) < tmax && FileSize(fileName) < off_t(2 * sizeof(tIndexTs)))
2575  }
2576  int delta = 0;
2577  if (!Record && access(fileName, R_OK) != 0) {
2578  // Index file doesn't exist, so try to regenerate it:
2579  if (!isPesRecording) { // sorry, can only do this for TS recordings
2580  resumeFile.Delete(); // just in case
2581  indexFileGenerator = new cIndexFileGenerator(FileName);
2582  // Wait until the index file exists:
2583  time_t tmax = time(NULL) + MAXWAITFORINDEXFILE;
2584  do {
2585  cCondWait::SleepMs(INDEXFILECHECKINTERVAL); // start with a sleep, to give it a head start
2586  } while (access(fileName, R_OK) != 0 && time(NULL) < tmax);
2587  }
2588  }
2589  if (access(fileName, R_OK) == 0) {
2590  struct stat buf;
2591  if (stat(fileName, &buf) == 0) {
2592  delta = int(buf.st_size % sizeof(tIndexTs));
2593  if (delta) {
2594  delta = sizeof(tIndexTs) - delta;
2595  esyslog("ERROR: invalid file size (%" PRId64 ") in '%s'", buf.st_size, *fileName);
2596  }
2597  last = int((buf.st_size + delta) / sizeof(tIndexTs) - 1);
2598  if ((!Record || Update) && last >= 0) {
2599  size = last + 1;
2600  index = MALLOC(tIndexTs, size);
2601  if (index) {
2602  f = open(fileName, O_RDONLY);
2603  if (f >= 0) {
2604  if (safe_read(f, index, size_t(buf.st_size)) != buf.st_size) {
2605  esyslog("ERROR: can't read from file '%s'", *fileName);
2606  free(index);
2607  size = 0;
2608  last = -1;
2609  index = NULL;
2610  }
2611  else if (isPesRecording)
2613  if (!index || time(NULL) - buf.st_mtime >= MININDEXAGE) {
2614  close(f);
2615  f = -1;
2616  }
2617  // otherwise we don't close f here, see CatchUp()!
2618  }
2619  else
2621  }
2622  else {
2623  esyslog("ERROR: can't allocate %zd bytes for index '%s'", size * sizeof(tIndexTs), *fileName);
2624  size = 0;
2625  last = -1;
2626  }
2627  }
2628  }
2629  else
2630  LOG_ERROR;
2631  }
2632  else if (!Record)
2633  isyslog("missing index file %s", *fileName);
2634  if (Record) {
2635  if ((f = open(fileName, O_WRONLY | O_CREAT | O_APPEND, DEFFILEMODE)) >= 0) {
2636  if (delta) {
2637  esyslog("ERROR: padding index file with %d '0' bytes", delta);
2638  while (delta--)
2639  writechar(f, 0);
2640  }
2641  }
2642  else
2644  }
2645  }
2646 }
2647 
2649 {
2650  if (f >= 0)
2651  close(f);
2652  free(index);
2653  delete indexFileGenerator;
2654 }
2655 
2656 cString cIndexFile::IndexFileName(const char *FileName, bool IsPesRecording)
2657 {
2658  return cString::sprintf("%s%s", FileName, IsPesRecording ? INDEXFILESUFFIX ".vdr" : INDEXFILESUFFIX);
2659 }
2660 
2661 void cIndexFile::ConvertFromPes(tIndexTs *IndexTs, int Count)
2662 {
2663  tIndexPes IndexPes;
2664  while (Count-- > 0) {
2665  memcpy(&IndexPes, IndexTs, sizeof(IndexPes));
2666  IndexTs->offset = IndexPes.offset;
2667  IndexTs->independent = IndexPes.type == 1; // I_FRAME
2668  IndexTs->number = IndexPes.number;
2669  IndexTs++;
2670  }
2671 }
2672 
2673 void cIndexFile::ConvertToPes(tIndexTs *IndexTs, int Count)
2674 {
2675  tIndexPes IndexPes;
2676  while (Count-- > 0) {
2677  IndexPes.offset = uint32_t(IndexTs->offset);
2678  IndexPes.type = uchar(IndexTs->independent ? 1 : 2); // I_FRAME : "not I_FRAME" (exact frame type doesn't matter)
2679  IndexPes.number = uchar(IndexTs->number);
2680  IndexPes.reserved = 0;
2681  memcpy((void *)IndexTs, &IndexPes, sizeof(*IndexTs));
2682  IndexTs++;
2683  }
2684 }
2685 
2686 bool cIndexFile::CatchUp(int Index)
2687 {
2688  // returns true unless something really goes wrong, so that 'index' becomes NULL
2689  if (index && f >= 0) {
2690  cMutexLock MutexLock(&mutex);
2691  // Note that CatchUp() is triggered even if Index is 'last' (and thus valid).
2692  // This is done to make absolutely sure we don't miss any data at the very end.
2693  for (int i = 0; i <= MAXINDEXCATCHUP && (Index < 0 || Index >= last); i++) {
2694  struct stat buf;
2695  if (fstat(f, &buf) == 0) {
2696  int newLast = int(buf.st_size / sizeof(tIndexTs) - 1);
2697  if (newLast > last) {
2698  int NewSize = size;
2699  if (NewSize <= newLast) {
2700  NewSize *= 2;
2701  if (NewSize <= newLast)
2702  NewSize = newLast + 1;
2703  }
2704  if (tIndexTs *NewBuffer = (tIndexTs *)realloc(index, NewSize * sizeof(tIndexTs))) {
2705  size = NewSize;
2706  index = NewBuffer;
2707  int offset = (last + 1) * sizeof(tIndexTs);
2708  int delta = (newLast - last) * sizeof(tIndexTs);
2709  if (lseek(f, offset, SEEK_SET) == offset) {
2710  if (safe_read(f, &index[last + 1], delta) != delta) {
2711  esyslog("ERROR: can't read from index");
2712  free(index);
2713  index = NULL;
2714  close(f);
2715  f = -1;
2716  break;
2717  }
2718  if (isPesRecording)
2719  ConvertFromPes(&index[last + 1], newLast - last);
2720  last = newLast;
2721  }
2722  else
2724  }
2725  else {
2726  esyslog("ERROR: can't realloc() index");
2727  break;
2728  }
2729  }
2730  }
2731  else
2733  if (Index < last)
2734  break;
2735  cCondVar CondVar;
2736  CondVar.TimedWait(mutex, INDEXCATCHUPWAIT);
2737  }
2738  }
2739  return index != NULL;
2740 }
2741 
2742 bool cIndexFile::Write(bool Independent, uint16_t FileNumber, off_t FileOffset)
2743 {
2744  if (f >= 0) {
2745  tIndexTs i(FileOffset, Independent, FileNumber);
2746  if (isPesRecording)
2747  ConvertToPes(&i, 1);
2748  if (safe_write(f, &i, sizeof(i)) < 0) {
2750  close(f);
2751  f = -1;
2752  return false;
2753  }
2754  last++;
2755  }
2756  return f >= 0;
2757 }
2758 
2759 bool cIndexFile::Get(int Index, uint16_t *FileNumber, off_t *FileOffset, bool *Independent, int *Length)
2760 {
2761  if (CatchUp(Index)) {
2762  if (Index >= 0 && Index <= last) {
2763  *FileNumber = index[Index].number;
2764  *FileOffset = index[Index].offset;
2765  if (Independent)
2766  *Independent = index[Index].independent;
2767  if (Length) {
2768  if (Index < last) {
2769  uint16_t fn = index[Index + 1].number;
2770  off_t fo = index[Index + 1].offset;
2771  if (fn == *FileNumber)
2772  *Length = int(fo - *FileOffset);
2773  else
2774  *Length = -1; // this means "everything up to EOF" (the buffer's Read function will act accordingly)
2775  }
2776  else
2777  *Length = -1;
2778  }
2779  return true;
2780  }
2781  }
2782  return false;
2783 }
2784 
2785 int cIndexFile::GetNextIFrame(int Index, bool Forward, uint16_t *FileNumber, off_t *FileOffset, int *Length)
2786 {
2787  if (CatchUp()) {
2788  int d = Forward ? 1 : -1;
2789  for (;;) {
2790  Index += d;
2791  if (Index >= 0 && Index <= last) {
2792  if (index[Index].independent) {
2793  uint16_t fn;
2794  if (!FileNumber)
2795  FileNumber = &fn;
2796  off_t fo;
2797  if (!FileOffset)
2798  FileOffset = &fo;
2799  *FileNumber = index[Index].number;
2800  *FileOffset = index[Index].offset;
2801  if (Length) {
2802  if (Index < last) {
2803  uint16_t fn = index[Index + 1].number;
2804  off_t fo = index[Index + 1].offset;
2805  if (fn == *FileNumber)
2806  *Length = int(fo - *FileOffset);
2807  else
2808  *Length = -1; // this means "everything up to EOF" (the buffer's Read function will act accordingly)
2809  }
2810  else
2811  *Length = -1;
2812  }
2813  return Index;
2814  }
2815  }
2816  else
2817  break;
2818  }
2819  }
2820  return -1;
2821 }
2822 
2824 {
2825  if (index && last > 0) {
2826  Index = constrain(Index, 0, last);
2827  if (index[Index].independent)
2828  return Index;
2829  int il = Index - 1;
2830  int ih = Index + 1;
2831  for (;;) {
2832  if (il >= 0) {
2833  if (index[il].independent)
2834  return il;
2835  il--;
2836  }
2837  else if (ih > last)
2838  break;
2839  if (ih <= last) {
2840  if (index[ih].independent)
2841  return ih;
2842  ih++;
2843  }
2844  else if (il < 0)
2845  break;
2846  }
2847  }
2848  return 0;
2849 }
2850 
2851 int cIndexFile::Get(uint16_t FileNumber, off_t FileOffset)
2852 {
2853  if (CatchUp()) {
2854  //TODO implement binary search!
2855  int i;
2856  for (i = 0; i <= last; i++) {
2857  if (index[i].number > FileNumber || (index[i].number == FileNumber) && off_t(index[i].offset) >= FileOffset)
2858  break;
2859  }
2860  return i;
2861  }
2862  return -1;
2863 }
2864 
2866 {
2867  return f >= 0;
2868 }
2869 
2871 {
2872  if (*fileName) {
2873  dsyslog("deleting index file '%s'", *fileName);
2874  if (f >= 0) {
2875  close(f);
2876  f = -1;
2877  }
2878  unlink(fileName);
2879  }
2880 }
2881 
2882 int cIndexFile::GetLength(const char *FileName, bool IsPesRecording)
2883 {
2884  struct stat buf;
2885  cString s = IndexFileName(FileName, IsPesRecording);
2886  if (*s && stat(s, &buf) == 0)
2887  return buf.st_size / (IsPesRecording ? sizeof(tIndexTs) : sizeof(tIndexPes));
2888  return -1;
2889 }
2890 
2891 bool GenerateIndex(const char *FileName, bool Update)
2892 {
2893  if (DirectoryOk(FileName)) {
2894  cRecording Recording(FileName);
2895  if (Recording.Name()) {
2896  if (!Recording.IsPesRecording()) {
2897  cString IndexFileName = AddDirectory(FileName, INDEXFILESUFFIX);
2898  if (!Update)
2899  unlink(IndexFileName);
2900  cIndexFileGenerator *IndexFileGenerator = new cIndexFileGenerator(FileName, Update);
2901  while (IndexFileGenerator->Active())
2903  if (access(IndexFileName, R_OK) == 0)
2904  return true;
2905  else
2906  fprintf(stderr, "cannot create '%s'\n", *IndexFileName);
2907  }
2908  else
2909  fprintf(stderr, "'%s' is not a TS recording\n", FileName);
2910  }
2911  else
2912  fprintf(stderr, "'%s' is not a recording\n", FileName);
2913  }
2914  else
2915  fprintf(stderr, "'%s' is not a directory\n", FileName);
2916  return false;
2917 }
2918 
2919 // --- cFileName -------------------------------------------------------------
2920 
2921 #define MAXFILESPERRECORDINGPES 255
2922 #define RECORDFILESUFFIXPES "/%03d.vdr"
2923 #define MAXFILESPERRECORDINGTS 65535
2924 #define RECORDFILESUFFIXTS "/%05d.ts"
2925 #define RECORDFILESUFFIXLEN 20 // some additional bytes for safety...
2926 
2927 cFileName::cFileName(const char *FileName, bool Record, bool Blocking, bool IsPesRecording)
2928 {
2929  file = NULL;
2930  fileNumber = 0;
2931  record = Record;
2932  blocking = Blocking;
2933  isPesRecording = IsPesRecording;
2934  // Prepare the file name:
2935  fileName = MALLOC(char, strlen(FileName) + RECORDFILESUFFIXLEN);
2936  if (!fileName) {
2937  esyslog("ERROR: can't copy file name '%s'", FileName);
2938  return;
2939  }
2940  strcpy(fileName, FileName);
2941  pFileNumber = fileName + strlen(fileName);
2942  SetOffset(1);
2943 }
2944 
2946 {
2947  Close();
2948  free(fileName);
2949 }
2950 
2951 bool cFileName::GetLastPatPmtVersions(int &PatVersion, int &PmtVersion)
2952 {
2953  if (fileName && !isPesRecording) {
2954  // Find the last recording file:
2955  int Number = 1;
2956  for (; Number <= MAXFILESPERRECORDINGTS + 1; Number++) { // +1 to correctly set Number in case there actually are that many files
2958  if (access(fileName, F_OK) != 0) { // file doesn't exist
2959  Number--;
2960  break;
2961  }
2962  }
2963  for (; Number > 0; Number--) {
2964  // Search for a PAT packet from the end of the file:
2965  cPatPmtParser PatPmtParser;
2967  int fd = open(fileName, O_RDONLY | O_LARGEFILE, DEFFILEMODE);
2968  if (fd >= 0) {
2969  off_t pos = lseek(fd, -TS_SIZE, SEEK_END);
2970  while (pos >= 0) {
2971  // Read and parse the PAT/PMT:
2972  uchar buf[TS_SIZE];
2973  while (read(fd, buf, sizeof(buf)) == sizeof(buf)) {
2974  if (buf[0] == TS_SYNC_BYTE) {
2975  int Pid = TsPid(buf);
2976  if (Pid == PATPID)
2977  PatPmtParser.ParsePat(buf, sizeof(buf));
2978  else if (PatPmtParser.IsPmtPid(Pid)) {
2979  PatPmtParser.ParsePmt(buf, sizeof(buf));
2980  if (PatPmtParser.GetVersions(PatVersion, PmtVersion)) {
2981  close(fd);
2982  return true;
2983  }
2984  }
2985  else
2986  break; // PAT/PMT is always in one sequence
2987  }
2988  else
2989  return false;
2990  }
2991  pos = lseek(fd, pos - TS_SIZE, SEEK_SET);
2992  }
2993  close(fd);
2994  }
2995  else
2996  break;
2997  }
2998  }
2999  return false;
3000 }
3001 
3003 {
3004  if (!file) {
3005  int BlockingFlag = blocking ? 0 : O_NONBLOCK;
3006  if (record) {
3007  dsyslog("recording to '%s'", fileName);
3008  file = cVideoDirectory::OpenVideoFile(fileName, O_RDWR | O_CREAT | O_LARGEFILE | BlockingFlag);
3009  if (!file)
3011  }
3012  else {
3013  if (access(fileName, R_OK) == 0) {
3014  dsyslog("playing '%s'", fileName);
3015  file = cUnbufferedFile::Create(fileName, O_RDONLY | O_LARGEFILE | BlockingFlag);
3016  if (!file)
3018  }
3019  else if (errno != ENOENT)
3021  }
3022  }
3023  return file;
3024 }
3025 
3027 {
3028  if (file) {
3029  if (file->Close() < 0)
3031  delete file;
3032  file = NULL;
3033  }
3034 }
3035 
3036 cUnbufferedFile *cFileName::SetOffset(int Number, off_t Offset)
3037 {
3038  if (fileNumber != Number)
3039  Close();
3040  int MaxFilesPerRecording = isPesRecording ? MAXFILESPERRECORDINGPES : MAXFILESPERRECORDINGTS;
3041  if (0 < Number && Number <= MaxFilesPerRecording) {
3042  fileNumber = uint16_t(Number);
3044  if (record) {
3045  if (access(fileName, F_OK) == 0) {
3046  // file exists, check if it has non-zero size
3047  struct stat buf;
3048  if (stat(fileName, &buf) == 0) {
3049  if (buf.st_size != 0)
3050  return SetOffset(Number + 1); // file exists and has non zero size, let's try next suffix
3051  else {
3052  // zero size file, remove it
3053  dsyslog("cFileName::SetOffset: removing zero-sized file %s", fileName);
3054  unlink(fileName);
3055  }
3056  }
3057  else
3058  return SetOffset(Number + 1); // error with fstat - should not happen, just to be on the safe side
3059  }
3060  else if (errno != ENOENT) { // something serious has happened
3062  return NULL;
3063  }
3064  // found a non existing file suffix
3065  }
3066  if (Open()) {
3067  if (!record && Offset >= 0 && file->Seek(Offset, SEEK_SET) != Offset) {
3069  return NULL;
3070  }
3071  }
3072  return file;
3073  }
3074  esyslog("ERROR: max number of files (%d) exceeded", MaxFilesPerRecording);
3075  return NULL;
3076 }
3077 
3079 {
3080  return SetOffset(fileNumber + 1);
3081 }
3082 
3083 // --- cDoneRecordings -------------------------------------------------------
3084 
3086 
3087 bool cDoneRecordings::Load(const char *FileName)
3088 {
3089  fileName = FileName;
3090  if (*fileName && access(fileName, F_OK) == 0) {
3091  isyslog("loading %s", *fileName);
3092  FILE *f = fopen(fileName, "r");
3093  if (f) {
3094  char *s;
3095  cReadLine ReadLine;
3096  while ((s = ReadLine.Read(f)) != NULL)
3097  Add(s);
3098  fclose(f);
3099  }
3100  else {
3102  return false;
3103  }
3104  }
3105  return true;
3106 }
3107 
3108 bool cDoneRecordings::Save(void) const
3109 {
3110  bool result = true;
3111  cSafeFile f(fileName);
3112  if (f.Open()) {
3113  for (int i = 0; i < doneRecordings.Size(); i++) {
3114  if (fputs(doneRecordings[i], f) == EOF || fputc('\n', f) == EOF) {
3115  result = false;
3116  break;
3117  }
3118  }
3119  if (!f.Close())
3120  result = false;
3121  }
3122  else
3123  result = false;
3124  return result;
3125 }
3126 
3127 void cDoneRecordings::Add(const char *Title)
3128 {
3129  doneRecordings.Append(strdup(Title));
3130 }
3131 
3132 void cDoneRecordings::Append(const char *Title)
3133 {
3134  if (!Contains(Title)) {
3135  Add(Title);
3136  if (FILE *f = fopen(fileName, "a")) {
3137  fputs(Title, f);
3138  fputc('\n', f);
3139  fclose(f);
3140  }
3141  else
3142  esyslog("ERROR: can't open '%s' for appending '%s'", *fileName, Title);
3143  }
3144 }
3145 
3146 static const char *FuzzyChars = " -:";
3147 
3148 static const char *SkipFuzzyChars(const char *s)
3149 {
3150  while (*s && strchr(FuzzyChars, *s))
3151  s++;
3152  return s;
3153 }
3154 
3155 bool cDoneRecordings::Contains(const char *Title) const
3156 {
3157  for (int i = 0; i < doneRecordings.Size(); i++) {
3158  const char *s = doneRecordings[i];
3159  const char *t = Title;
3160  while (*s && *t) {
3161  s = SkipFuzzyChars(s);
3162  t = SkipFuzzyChars(t);
3163  if (!*s || !*t)
3164  break;
3165  if (toupper(uchar(*s)) != toupper(uchar(*t)))
3166  break;
3167  s++;
3168  t++;
3169  }
3170  if (!*s && !*t)
3171  return true;
3172  }
3173  return false;
3174 }
3175 
3176 // --- Index stuff -----------------------------------------------------------
3177 
3178 cString IndexToHMSF(int Index, bool WithFrame, double FramesPerSecond)
3179 {
3180  const char *Sign = "";
3181  if (Index < 0) {
3182  Index = -Index;
3183  Sign = "-";
3184  }
3185  double Seconds;
3186  int f = int(modf((Index + 0.5) / FramesPerSecond, &Seconds) * FramesPerSecond);
3187  int s = int(Seconds);
3188  int m = s / 60 % 60;
3189  int h = s / 3600;
3190  s %= 60;
3191  return cString::sprintf(WithFrame ? "%s%d:%02d:%02d.%02d" : "%s%d:%02d:%02d", Sign, h, m, s, f);
3192 }
3193 
3194 int HMSFToIndex(const char *HMSF, double FramesPerSecond)
3195 {
3196  int h, m, s, f = 0;
3197  int n = sscanf(HMSF, "%d:%d:%d.%d", &h, &m, &s, &f);
3198  if (n == 1)
3199  return h; // plain frame number
3200  if (n >= 3)
3201  return int(round((h * 3600 + m * 60 + s) * FramesPerSecond)) + f;
3202  return 0;
3203 }
3204 
3205 int SecondsToFrames(int Seconds, double FramesPerSecond)
3206 {
3207  return int(round(Seconds * FramesPerSecond));
3208 }
3209 
3210 // --- ReadFrame -------------------------------------------------------------
3211 
3212 int ReadFrame(cUnbufferedFile *f, uchar *b, int Length, int Max)
3213 {
3214  if (Length == -1)
3215  Length = Max; // this means we read up to EOF (see cIndex)
3216  else if (Length > Max) {
3217  esyslog("ERROR: frame larger than buffer (%d > %d)", Length, Max);
3218  Length = Max;
3219  }
3220  int r = f->Read(b, Length);
3221  if (r < 0)
3222  LOG_ERROR;
3223  return r;
3224 }
3225 
3226 // --- Recordings Sort Mode --------------------------------------------------
3227 
3229 
3230 bool HasRecordingsSortMode(const char *Directory)
3231 {
3232  return access(AddDirectory(Directory, SORTMODEFILE), R_OK) == 0;
3233 }
3234 
3235 void GetRecordingsSortMode(const char *Directory)
3236 {
3238  if (FILE *f = fopen(AddDirectory(Directory, SORTMODEFILE), "r")) {
3239  char buf[8];
3240  if (fgets(buf, sizeof(buf), f))
3242  fclose(f);
3243  }
3244 }
3245 
3246 void SetRecordingsSortMode(const char *Directory, eRecordingsSortMode SortMode)
3247 {
3248  if (FILE *f = fopen(AddDirectory(Directory, SORTMODEFILE), "w")) {
3249  fputs(cString::sprintf("%d\n", SortMode), f);
3250  fclose(f);
3251  }
3252 }
3253 
3254 void IncRecordingsSortMode(const char *Directory)
3255 {
3256  GetRecordingsSortMode(Directory);
3261 }
3262 
3263 // --- Recording Timer Indicator ---------------------------------------------
3264 
3265 void SetRecordingTimerId(const char *Directory, const char *TimerId)
3266 {
3267  cString FileName = AddDirectory(Directory, TIMERRECFILE);
3268  if (TimerId) {
3269  dsyslog("writing timer id '%s' to %s", TimerId, *FileName);
3270  if (FILE *f = fopen(FileName, "w")) {
3271  fprintf(f, "%s\n", TimerId);
3272  fclose(f);
3273  }
3274  else
3275  LOG_ERROR_STR(*FileName);
3276  }
3277  else {
3278  dsyslog("removing %s", *FileName);
3279  unlink(FileName);
3280  }
3281 }
3282 
3283 cString GetRecordingTimerId(const char *Directory)
3284 {
3285  cString FileName = AddDirectory(Directory, TIMERRECFILE);
3286  const char *Id = NULL;
3287  if (FILE *f = fopen(FileName, "r")) {
3288  char buf[HOST_NAME_MAX + 10]; // +10 for numeric timer id and '@'
3289  if (fgets(buf, sizeof(buf), f)) {
3290  stripspace(buf);
3291  Id = buf;
3292  }
3293  fclose(f);
3294  }
3295  return Id;
3296 }
#define MAXDPIDS
Definition: channels.h:32
#define MAXAPIDS
Definition: channels.h:31
#define MAXSPIDS
Definition: channels.h:33
const char * Alang(int i) const
Definition: channels.h:162
int Number(void) const
Definition: channels.h:178
const char * Name(void) const
Definition: channels.c:107
tChannelID GetChannelID(void) const
Definition: channels.h:190
const char * Slang(int i) const
Definition: channels.h:164
const char * Dlang(int i) const
Definition: channels.h:163
tComponent * GetComponent(int Index, uchar Stream, uchar Type)
Definition: epg.c:97
int NumComponents(void) const
Definition: epg.h:61
void SetComponent(int Index, const char *s)
Definition: epg.c:77
bool TimedWait(cMutex &Mutex, int TimeoutMs)
Definition: thread.c:132
static void SleepMs(int TimeoutMs)
Creates a cCondWait object and uses it to sleep for TimeoutMs milliseconds, immediately giving up the...
Definition: thread.c:72
Definition: cutter.h:18
bool Start(void)
Starts the actual cutting process.
Definition: cutter.c:668
bool Error(void)
Returns true if an error occurred while cutting the recording.
Definition: cutter.c:719
bool Active(void)
Returns true if the cutter is currently active.
Definition: cutter.c:706
static cString EditedFileName(const char *FileName)
Returns the full path name of the edited version of the recording with the given FileName.
Definition: cutter.c:656
cDirCopier(const char *DirNameSrc, const char *DirNameDst)
Definition: recording.c:1722
cString dirNameDst
Definition: recording.c:1711
bool suspensionLogged
Definition: recording.c:1713
virtual ~cDirCopier()
Definition: recording.c:1731
bool Throttled(void)
Definition: recording.c:1736
cString dirNameSrc
Definition: recording.c:1710
virtual void Action(void)
A derived cThread class must implement the code it wants to execute as a separate thread in this func...
Definition: recording.c:1752
bool error
Definition: recording.c:1712
bool Error(void)
Definition: recording.c:1719
cStringList doneRecordings
Definition: recording.h:517
bool Save(void) const
Definition: recording.c:3108
void Add(const char *Title)
Definition: recording.c:3127
cString fileName
Definition: recording.h:516
void Append(const char *Title)
Definition: recording.c:3132
bool Load(const char *FileName)
Definition: recording.c:3087
bool Contains(const char *Title) const
Definition: recording.c:3155
Definition: epg.h:73
bool Parse(char *s)
Definition: epg.c:490
const cComponents * Components(void) const
Definition: epg.h:108
void SetStartTime(time_t StartTime)
Definition: epg.c:216
const char * Title(void) const
Definition: epg.h:105
void SetEventID(tEventID EventID)
Definition: epg.c:156
void SetVersion(uchar Version)
Definition: epg.c:172
void SetDuration(int Duration)
Definition: epg.c:227
const char * ShortText(void) const
Definition: epg.h:106
void SetTitle(const char *Title)
Definition: epg.c:184
void SetTableID(uchar TableID)
Definition: epg.c:167
bool isPesRecording
Definition: recording.h:501
cUnbufferedFile * NextFile(void)
Definition: recording.c:3078
uint16_t Number(void)
Definition: recording.h:506
bool record
Definition: recording.h:499
void Close(void)
Definition: recording.c:3026
uint16_t fileNumber
Definition: recording.h:497
cUnbufferedFile * Open(void)
Definition: recording.c:3002
cFileName(const char *FileName, bool Record, bool Blocking=false, bool IsPesRecording=false)
Definition: recording.c:2927
char * fileName
Definition: recording.h:498
char * pFileNumber
Definition: recording.h:498
bool GetLastPatPmtVersions(int &PatVersion, int &PmtVersion)
Definition: recording.c:2951
bool blocking
Definition: recording.h:500
cUnbufferedFile * SetOffset(int Number, off_t Offset=0)
Definition: recording.c:3036
cUnbufferedFile * file
Definition: recording.h:496
bool Synced(void)
Returns true if the frame detector has synced on the data stream.
Definition: remux.h:538
bool IndependentFrame(void)
Returns true if a new frame was detected and this is an independent frame (i.e.
Definition: remux.h:543
double FramesPerSecond(void)
Returns the number of frames per second, or 0 if this information is not available.
Definition: remux.h:547
int Analyze(const uchar *Data, int Length)
Analyzes the TS packets pointed to by Data.
Definition: remux.c:1654
void SetPid(int Pid, int Type)
Sets the Pid and stream Type to detect frames for.
Definition: remux.c:1635
bool NewFrame(void)
Returns true if the data given to the last call to Analyze() started a new frame.
Definition: remux.h:540
cIndexFileGenerator(const char *RecordingName, bool Update=false)
Definition: recording.c:2367
virtual void Action(void)
A derived cThread class must implement the code it wants to execute as a separate thread in this func...
Definition: recording.c:2380
int GetNextIFrame(int Index, bool Forward, uint16_t *FileNumber=NULL, off_t *FileOffset=NULL, int *Length=NULL)
Definition: recording.c:2785
cResumeFile resumeFile
Definition: recording.h:463
bool IsStillRecording(void)
Definition: recording.c:2865
void ConvertFromPes(tIndexTs *IndexTs, int Count)
Definition: recording.c:2661
bool Write(bool Independent, uint16_t FileNumber, off_t FileOffset)
Definition: recording.c:2742
static int GetLength(const char *FileName, bool IsPesRecording=false)
Calculates the recording length (number of frames) without actually reading the index file.
Definition: recording.c:2882
bool CatchUp(int Index=-1)
Definition: recording.c:2686
void ConvertToPes(tIndexTs *IndexTs, int Count)
Definition: recording.c:2673
bool isPesRecording
Definition: recording.h:462
cString fileName
Definition: recording.h:459
cIndexFile(const char *FileName, bool Record, bool IsPesRecording=false, bool PauseLive=false, bool Update=false)
Definition: recording.c:2559
cIndexFileGenerator * indexFileGenerator
Definition: recording.h:464
static cString IndexFileName(const char *FileName, bool IsPesRecording)
Definition: recording.c:2656
bool Get(int Index, uint16_t *FileNumber, off_t *FileOffset, bool *Independent=NULL, int *Length=NULL)
Definition: recording.c:2759
int GetClosestIFrame(int Index)
Returns the index of the I-frame that is closest to the given Index (or Index itself,...
Definition: recording.c:2823
cMutex mutex
Definition: recording.h:465
void Delete(void)
Definition: recording.c:2870
int Last(void)
Returns the index of the last entry in this file, or -1 if the file is empty.
Definition: recording.h:482
tIndexTs * index
Definition: recording.h:461
static bool Engaged(void)
Returns true if any I/O throttling object is currently active.
Definition: thread.c:918
virtual void Clear(void)
Definition: tools.c:2261
void Del(cListObject *Object, bool DeleteObject=true)
Definition: tools.c:2216
void SetModified(void)
Unconditionally marks this list as modified.
Definition: tools.c:2286
bool Lock(cStateKey &StateKey, bool Write=false, int TimeoutMs=0) const
Tries to get a lock on this list and returns true if successful.
Definition: tools.c:2175
int Count(void) const
Definition: tools.h:637
void Add(cListObject *Object, cListObject *After=NULL)
Definition: tools.c:2184
cListObject * Next(void) const
Definition: tools.h:557
Definition: tools.h:641
const T * Next(const T *Object) const
< Returns the element immediately before Object in this list, or NULL if Object is the first element ...
Definition: tools.h:660
const T * Last(void) const
Returns the last element in this list, or NULL if the list is empty.
Definition: tools.h:655
const T * First(void) const
Returns the first element in this list, or NULL if the list is empty.
Definition: tools.h:653
const T * Prev(const T *Object) const
Definition: tools.h:657
bool Lock(int WaitSeconds=0)
Definition: tools.c:2027
cMark(int Position=0, const char *Comment=NULL, double FramesPerSecond=DEFAULTFRAMESPERSECOND)
Definition: recording.c:2122
cString comment
Definition: recording.h:359
int position
Definition: recording.h:358
bool Parse(const char *s)
Definition: recording.c:2138
bool Save(FILE *f)
Definition: recording.c:2152
cString ToText(void)
Definition: recording.c:2133
const char * Comment(void) const
Definition: recording.h:364
double framesPerSecond
Definition: recording.h:357
int Position(void) const
Definition: recording.h:363
virtual ~cMark()
Definition: recording.c:2129
int GetNumSequences(void) const
Returns the actual number of sequences to be cut from the recording.
Definition: recording.c:2318
double framesPerSecond
Definition: recording.h:376
void Add(int Position)
If this cMarks object is used by multiple threads, the caller must Lock() it before calling Add() and...
Definition: recording.c:2251
const cMark * GetNextBegin(const cMark *EndMark=NULL) const
Returns the next "begin" mark after EndMark, skipping any marks at the same position as EndMark.
Definition: recording.c:2284
const cMark * GetNext(int Position) const
Definition: recording.c:2275
bool Update(void)
Definition: recording.c:2187
bool Load(const char *RecordingFileName, double FramesPerSecond=DEFAULTFRAMESPERSECOND, bool IsPesRecording=false)
Definition: recording.c:2175
time_t lastFileTime
Definition: recording.h:379
const cMark * GetNextEnd(const cMark *BeginMark) const
Returns the next "end" mark after BeginMark, skipping any marks at the same position as BeginMark.
Definition: recording.c:2300
const cMark * Get(int Position) const
Definition: recording.c:2257
cString recordingFileName
Definition: recording.h:374
bool isPesRecording
Definition: recording.h:377
time_t nextUpdate
Definition: recording.h:378
cString fileName
Definition: recording.h:375
static bool DeleteMarksFile(const cRecording *Recording)
Definition: recording.c:2164
void Align(void)
Definition: recording.c:2227
void Sort(void)
Definition: recording.c:2239
static cString MarksFileName(const cRecording *Recording)
Returns the marks file name for the given Recording (regardless whether such a file actually exists).
Definition: recording.c:2159
bool Save(void)
Definition: recording.c:2218
const cMark * GetPrev(int Position) const
Definition: recording.c:2266
time_t lastChange
Definition: recording.h:380
Definition: thread.h:67
bool GetVersions(int &PatVersion, int &PmtVersion) const
Returns true if a valid PAT/PMT has been parsed and stores the current version numbers in the given v...
Definition: remux.c:938
int Vtype(void) const
Returns the video stream type as defined by the current PMT, or 0 if no video stream type has been de...
Definition: remux.h:409
void ParsePat(const uchar *Data, int Length)
Parses the PAT data from the single TS packet in Data.
Definition: remux.c:627
int Apid(int i) const
Definition: remux.h:417
void ParsePmt(const uchar *Data, int Length)
Parses the PMT data from the single TS packet in Data.
Definition: remux.c:659
bool Completed(void)
Returns true if the PMT has been completely parsed.
Definition: remux.h:412
bool IsPmtPid(int Pid) const
Returns true if Pid the one of the PMT pids as defined by the current PAT.
Definition: remux.h:400
int Atype(int i) const
Definition: remux.h:420
int Vpid(void) const
Returns the video pid as defined by the current PMT, or 0 if no video pid has been detected,...
Definition: remux.h:403
struct dirent * Next(void)
Definition: tools.c:1562
bool Ok(void)
Definition: tools.h:456
char * Read(FILE *f)
Definition: tools.c:1481
static cRecordControl * GetRecordControl(const char *FileName)
Definition: menu.c:5660
void SetFramesPerSecond(double FramesPerSecond)
Definition: recording.c:449
cEvent * ownEvent
Definition: recording.h:69
const cEvent * event
Definition: recording.h:68
int Errors(void) const
Definition: recording.h:92
cRecordingInfo(const cChannel *Channel=NULL, const cEvent *Event=NULL)
Definition: recording.c:351
bool Write(void) const
Definition: recording.c:559
bool Write(FILE *f, const char *Prefix="") const
Definition: recording.c:527
bool Read(void)
Definition: recording.c:541
char * aux
Definition: recording.h:70
const char * Title(void) const
Definition: recording.h:84
tChannelID channelID
Definition: recording.h:66
const char * Description(void) const
Definition: recording.h:86
void SetFileName(const char *FileName)
Definition: recording.c:454
const char * ShortText(void) const
Definition: recording.h:85
bool Read(FILE *f)
Definition: recording.c:466
char * channelName
Definition: recording.h:67
const char * Aux(void) const
Definition: recording.h:88
void SetErrors(int Errors)
Definition: recording.c:461
void SetAux(const char *Aux)
Definition: recording.c:443
void SetData(const char *Title, const char *ShortText, const char *Description)
Definition: recording.c:433
const cComponents * Components(void) const
Definition: recording.h:87
double framesPerSecond
Definition: recording.h:71
double FramesPerSecond(void) const
Definition: recording.h:89
char * fileName
Definition: recording.h:74
static const char * command
Definition: recording.h:434
static void InvokeCommand(const char *State, const char *RecordingFileName, const char *SourceFileName=NULL)
Definition: recording.c:2339
int isOnVideoDirectoryFileSystem
Definition: recording.h:116
virtual int Compare(const cListObject &ListObject) const
Must return 0 if this object is equal to ListObject, a positive value if it is "greater",...
Definition: recording.c:1037
time_t deleted
Definition: recording.h:128
cRecordingInfo * info
Definition: recording.h:118
bool ChangePriorityLifetime(int NewPriority, int NewLifetime)
Changes the priority and lifetime of this recording to the given values.
Definition: recording.c:1228
bool HasMarks(void) const
Returns true if this recording has any editing marks.
Definition: recording.c:1182
bool WriteInfo(const char *OtherFileName=NULL)
Writes in info file of this recording.
Definition: recording.c:1200
int resume
Definition: recording.h:105
int IsInUse(void) const
Checks whether this recording is currently in use and therefore shall not be tampered with.
Definition: recording.c:1343
bool ChangeName(const char *NewName)
Changes the name of this recording to the given value.
Definition: recording.c:1253
bool Undelete(void)
Changes the file name so that it will be visible in the "Recordings" menu again and not processed by ...
Definition: recording.c:1317
void ResetResume(void) const
Definition: recording.c:1354
bool IsNew(void) const
Definition: recording.h:172
double framesPerSecond
Definition: recording.h:117
bool Delete(void)
Changes the file name so that it will no longer be visible in the "Recordings" menu Returns false in ...
Definition: recording.c:1280
const char * Name(void) const
Returns the full name of the recording (without the video directory).
Definition: recording.h:149
cString Folder(void) const
Returns the name of the folder this recording is stored in (without the video directory).
Definition: recording.c:1054
bool isPesRecording
Definition: recording.h:115
void ClearSortName(void)
Definition: recording.c:1016
char * sortBufferName
Definition: recording.h:107
int NumFrames(void) const
Returns the number of frames in this recording.
Definition: recording.c:1359
bool IsEdited(void) const
Definition: recording.c:1169
int Id(void) const
Definition: recording.h:133
int GetResume(void) const
Returns the index of the frame where replay of this recording shall be resumed, or -1 in case of an e...
Definition: recording.c:1028
bool IsInPath(const char *Path) const
Returns true if this recording is stored anywhere under the given Path.
Definition: recording.c:1046
virtual ~cRecording()
Definition: recording.c:953
int fileSizeMB
Definition: recording.h:111
void SetId(int Id)
Definition: recording.c:1023
void SetStartTime(time_t Start)
Sets the start time of this recording to the given value.
Definition: recording.c:1221
char * SortName(void) const
Definition: recording.c:992
time_t Start(void) const
Definition: recording.h:134
int Lifetime(void) const
Definition: recording.h:136
const char * FileName(void) const
Returns the full path name to the recording directory, including the video directory and the actual '...
Definition: recording.c:1066
const char * PrefixFileName(char Prefix)
Definition: recording.c:1147
bool DeleteMarks(void)
Deletes the editing marks from this recording (if any).
Definition: recording.c:1187
int priority
Definition: recording.h:126
bool IsOnVideoDirectoryFileSystem(void) const
Definition: recording.c:1175
int HierarchyLevels(void) const
Definition: recording.c:1158
int lifetime
Definition: recording.h:127
int FileSizeMB(void) const
Returns the total file size of this recording (in MB), or -1 if the file size is unknown.
Definition: recording.c:1378
cString BaseName(void) const
Returns the base name of this recording (without the video directory and folder).
Definition: recording.c:1061
char * fileName
Definition: recording.h:109
char * titleBuffer
Definition: recording.h:106
void SetDeleted(void)
Definition: recording.h:138
int Priority(void) const
Definition: recording.h:135
void ReadInfo(void)
Definition: recording.c:1192
const char * Title(char Delimiter=' ', bool NewIndicator=false, int Level=-1) const
Definition: recording.c:1084
int instanceId
Definition: recording.h:114
bool Remove(void)
Actually removes the file from the disk Returns false in case of error.
Definition: recording.c:1306
char * name
Definition: recording.h:110
cRecording(const cRecording &)
char * sortBufferTime
Definition: recording.h:108
int channel
Definition: recording.h:113
time_t start
Definition: recording.h:125
int numFrames
Definition: recording.h:112
double FramesPerSecond(void) const
Definition: recording.h:160
bool IsPesRecording(void) const
Definition: recording.h:174
static char * StripEpisodeName(char *s, bool Strip)
Definition: recording.c:963
int LengthInSeconds(void) const
Returns the length (in seconds) of this recording, or -1 in case of error.
Definition: recording.c:1370
void Cleanup(cRecordings *Recordings)
Definition: recording.c:1970
const char * FileNameDst(void) const
Definition: recording.c:1887
int Usage(const char *FileName=NULL) const
Definition: recording.c:1908
bool Active(cRecordings *Recordings)
Definition: recording.c:1920
const char * FileNameSrc(void) const
Definition: recording.c:1886
bool Error(void) const
Definition: recording.c:1884
cRecordingsHandlerEntry(int Usage, const char *FileNameSrc, const char *FileNameDst)
Definition: recording.c:1892
void DelAll(void)
Deletes/terminates all operations.
Definition: recording.c:2090
cRecordingsHandler(void)
Definition: recording.c:2002
cRecordingsHandlerEntry * Get(const char *FileName)
Definition: recording.c:2039
bool Add(int Usage, const char *FileNameSrc, const char *FileNameDst=NULL)
Adds the given FileNameSrc to the recordings handler for (later) processing.
Definition: recording.c:2052
bool Finished(bool &Error)
Returns true if all operations in the list have been finished.
Definition: recording.c:2105
virtual void Action(void)
A derived cThread class must implement the code it wants to execute as a separate thread in this func...
Definition: recording.c:2014
int GetUsage(const char *FileName)
Returns the usage type for the given FileName.
Definition: recording.c:2097
cList< cRecordingsHandlerEntry > operations
Definition: recording.h:317
void Del(const char *FileName)
Deletes the given FileName from the list of operations.
Definition: recording.c:2083
virtual ~cRecordingsHandler()
Definition: recording.c:2009
void ResetResume(const char *ResumeFileName=NULL)
Definition: recording.c:1692
void UpdateByName(const char *FileName)
Definition: recording.c:1614
static const char * UpdateFileName(void)
Definition: recording.c:1522
virtual ~cRecordings()
Definition: recording.c:1515
double MBperMinute(void) const
Returns the average data rate (in MB/min) of all recordings, or -1 if this value is unknown.
Definition: recording.c:1631
cRecordings(bool Deleted=false)
Definition: recording.c:1510
int GetNumRecordingsInPath(const char *Path) const
Returns the total number of recordings in the given Path, including all sub-folders of Path.
Definition: recording.c:1662
const cRecording * GetById(int Id) const
Definition: recording.c:1557
static time_t lastUpdate
Definition: recording.h:234
static cRecordings deletedRecordings
Definition: recording.h:231
void AddByName(const char *FileName, bool TriggerUpdate=true)
Definition: recording.c:1583
static cRecordings recordings
Definition: recording.h:230
int TotalFileSizeMB(void) const
Definition: recording.c:1620
static void Update(bool Wait=false)
Triggers an update of the list of recordings, which will run as a separate thread if Wait is false.
Definition: recording.c:1545
static void TouchUpdate(void)
Touches the '.update' file in the video directory, so that other instances of VDR that access the sam...
Definition: recording.c:1529
void Add(cRecording *Recording)
Definition: recording.c:1577
static cVideoDirectoryScannerThread * videoDirectoryScannerThread
Definition: recording.h:235
void DelByName(const char *FileName)
Definition: recording.c:1592
bool MoveRecordings(const char *OldPath, const char *NewPath)
Moves all recordings in OldPath to NewPath.
Definition: recording.c:1672
static bool NeedsUpdate(void)
Definition: recording.c:1537
void ClearSortNames(void)
Definition: recording.c:1700
static int lastRecordingId
Definition: recording.h:232
const cRecording * GetByName(const char *FileName) const
Definition: recording.c:1566
static char * updateFileName
Definition: recording.h:233
int PathIsInUse(const char *Path) const
Checks whether any recording in the given Path is currently in use and therefore the whole Path shall...
Definition: recording.c:1652
static bool HasKeys(void)
Definition: remote.c:175
virtual void Action(void)
A derived cThread class must implement the code it wants to execute as a separate thread in this func...
Definition: recording.c:95
static const char * NowReplaying(void)
Definition: menu.c:5869
bool isPesRecording
Definition: recording.h:54
bool Save(int Index)
Definition: recording.c:307
char * fileName
Definition: recording.h:53
int Read(void)
Definition: recording.c:262
void Delete(void)
Definition: recording.c:337
cResumeFile(const char *FileName, bool IsPesRecording)
Definition: recording.c:244
void Del(int Count)
Deletes at most Count bytes from the ring buffer.
Definition: ringbuffer.c:371
int Put(const uchar *Data, int Count)
Puts at most Count bytes of Data into the ring buffer.
Definition: ringbuffer.c:306
virtual int Available(void)
Definition: ringbuffer.c:211
virtual void Clear(void)
Immediately clears the ring buffer.
Definition: ringbuffer.c:217
uchar * Get(int &Count)
Gets data from the ring buffer.
Definition: ringbuffer.c:346
int Read(int FileHandle, int Max=0)
Reads at most Max bytes from FileHandle and stores them in the ring buffer.
Definition: ringbuffer.c:230
bool Open(void)
Definition: tools.c:1772
bool Close(void)
Definition: tools.c:1782
int ResumeID
Definition: config.h:363
int AlwaysSortFoldersFirst
Definition: config.h:318
int RecSortingDirection
Definition: config.h:320
int RecordingDirs
Definition: config.h:316
int UseSubtitle
Definition: config.h:313
int DefaultSortModeRec
Definition: config.h:319
int QueueMessage(eMessageType Type, const char *s, int Seconds=0, int Timeout=0)
Like Message(), but this function may be called from a background thread.
Definition: skins.c:296
void Remove(bool IncState=true)
Removes this key from the lock it was previously used with.
Definition: thread.c:859
Definition: tools.h:178
static cString sprintf(const char *fmt,...) __attribute__((format(printf
Definition: tools.c:1149
Definition: thread.h:79
void bool Start(void)
Sets the description of this thread, which will be used when logging starting or stopping of the thre...
Definition: thread.c:304
bool Running(void)
Returns false if a derived cThread object shall leave its Action() function.
Definition: thread.h:101
void Cancel(int WaitSeconds=0)
Cancels the thread by first setting 'running' to false, so that the Action() loop can finish in an or...
Definition: thread.c:354
bool Active(void)
Checks whether the thread is still alive.
Definition: thread.c:329
Definition: timers.h:31
bool IsSingleEvent(void) const
Definition: timers.c:501
void SetFile(const char *File)
Definition: timers.c:552
const char * Aux(void) const
Definition: timers.h:77
const char * File(void) const
Definition: timers.h:75
time_t StartTime(void) const
the start time as given by the user
Definition: timers.c:705
int Priority(void) const
Definition: timers.h:72
int Lifetime(void) const
Definition: timers.h:73
const cChannel * Channel(void) const
Definition: timers.h:67
cUnbufferedFile is used for large files that are mainly written or read in a streaming manner,...
Definition: tools.h:504
static cUnbufferedFile * Create(const char *FileName, int Flags, mode_t Mode=DEFFILEMODE)
Definition: tools.c:1998
int Close(void)
Definition: tools.c:1846
ssize_t Read(void *Data, size_t Size)
Definition: tools.c:1889
off_t Seek(off_t Offset, int Whence)
Definition: tools.c:1881
int Size(void) const
Definition: tools.h:764
virtual void Append(T Data)
Definition: tools.h:784
cRecordings * deletedRecordings
Definition: recording.c:1394
void ScanVideoDir(const char *DirName, int LinkLevel=0, int DirLevel=0)
Definition: recording.c:1432
cVideoDirectoryScannerThread(cRecordings *Recordings, cRecordings *DeletedRecordings)
Definition: recording.c:1405
virtual void Action(void)
A derived cThread class must implement the code it wants to execute as a separate thread in this func...
Definition: recording.c:1419
static cString PrefixVideoFileName(const char *FileName, char Prefix)
Definition: videodir.c:169
static void RemoveEmptyVideoDirectories(const char *IgnoreFiles[]=NULL)
Definition: videodir.c:189
static bool IsOnVideoDirectoryFileSystem(const char *FileName)
Definition: videodir.c:194
static const char * Name(void)
Definition: videodir.c:60
static cUnbufferedFile * OpenVideoFile(const char *FileName, int Flags)
Definition: videodir.c:125
static bool VideoFileSpaceAvailable(int SizeMB)
Definition: videodir.c:147
static bool MoveVideoFile(const char *FromName, const char *ToName)
Definition: videodir.c:137
static bool RenameVideoFile(const char *OldName, const char *NewName)
Definition: videodir.c:132
static bool RemoveVideoFile(const char *FileName)
Definition: videodir.c:142
cSetup Setup
Definition: config.c:372
#define MAXLIFETIME
Definition: config.h:48
#define MAXPRIORITY
Definition: config.h:43
#define TIMERMACRO_EPISODE
Definition: config.h:52
#define TIMERMACRO_TITLE
Definition: config.h:51
#define tr(s)
Definition: i18n.h:85
static int Utf8CharLen(const char *s)
Definition: si.c:400
#define MAXFILESPERRECORDINGTS
Definition: recording.c:2923
#define NAMEFORMATPES
Definition: recording.c:48
int DirectoryNameMax
Definition: recording.c:77
tCharExchange CharExchange[]
Definition: recording.c:580
cString GetRecordingTimerId(const char *Directory)
Definition: recording.c:3283
bool GenerateIndex(const char *FileName, bool Update)
Generates the index of the existing recording with the given FileName.
Definition: recording.c:2891
#define REMOVELATENCY
Definition: recording.c:67
cString IndexToHMSF(int Index, bool WithFrame, double FramesPerSecond)
Definition: recording.c:3178
#define MININDEXAGE
Definition: recording.c:69
char * ExchangeChars(char *s, bool ToFileSystem)
Definition: recording.c:600
#define MINDISKSPACE
Definition: recording.c:62
#define INFOFILESUFFIX
Definition: recording.c:56
void AssertFreeDiskSpace(int Priority, bool Force)
The special Priority value -1 means that we shall get rid of any deleted recordings faster than norma...
Definition: recording.c:154
#define DELETEDLIFETIME
Definition: recording.c:65
static const char * SkipFuzzyChars(const char *s)
Definition: recording.c:3148
#define REMOVECHECKDELTA
Definition: recording.c:64
int DirectoryPathMax
Definition: recording.c:76
void GetRecordingsSortMode(const char *Directory)
Definition: recording.c:3235
#define MARKSFILESUFFIX
Definition: recording.c:57
#define MAX_LINK_LEVEL
Definition: recording.c:72
#define DATAFORMATPES
Definition: recording.c:47
static const char * FuzzyChars
Definition: recording.c:3146
bool NeedsConversion(const char *p)
Definition: recording.c:593
int SecondsToFrames(int Seconds, double FramesPerSecond)
Definition: recording.c:3205
#define MAXREMOVETIME
Definition: recording.c:70
eRecordingsSortMode RecordingsSortMode
Definition: recording.c:3228
bool HasRecordingsSortMode(const char *Directory)
Definition: recording.c:3230
#define RECEXT
Definition: recording.c:36
#define MAXFILESPERRECORDINGPES
Definition: recording.c:2921
#define INDEXCATCHUPWAIT
Definition: recording.c:2532
#define INDEXFILESUFFIX
Definition: recording.c:2528
#define IFG_BUFFER_SIZE
Definition: recording.c:2354
#define INDEXFILETESTINTERVAL
Definition: recording.c:2557
#define MAXWAITFORINDEXFILE
Definition: recording.c:2555
int InstanceId
Definition: recording.c:79
#define DELEXT
Definition: recording.c:37
#define INDEXFILECHECKINTERVAL
Definition: recording.c:2556
bool DirectoryEncoding
Definition: recording.c:78
void IncRecordingsSortMode(const char *Directory)
Definition: recording.c:3254
int HMSFToIndex(const char *HMSF, double FramesPerSecond)
Definition: recording.c:3194
#define LIMIT_SECS_PER_MB_RADIO
Definition: recording.c:74
void SetRecordingsSortMode(const char *Directory, eRecordingsSortMode SortMode)
Definition: recording.c:3246
cDoneRecordings DoneRecordingsPattern
Definition: recording.c:3085
static cRemoveDeletedRecordingsThread RemoveDeletedRecordingsThread
Definition: recording.c:133
#define DISKCHECKDELTA
Definition: recording.c:66
int ReadFrame(cUnbufferedFile *f, uchar *b, int Length, int Max)
Definition: recording.c:3212
cRecordingsHandler RecordingsHandler
Definition: recording.c:2000
cMutex MutexMarkFramesPerSecond
Definition: recording.c:2120
struct __attribute__((packed))
Definition: recording.c:2534
#define RESUME_NOT_INITIALIZED
Definition: recording.c:577
#define SORTMODEFILE
Definition: recording.c:59
#define RECORDFILESUFFIXLEN
Definition: recording.c:2925
#define MAXINDEXCATCHUP
Definition: recording.c:2531
#define NAMEFORMATTS
Definition: recording.c:50
#define DATAFORMATTS
Definition: recording.c:49
#define RECORDFILESUFFIXPES
Definition: recording.c:2922
void SetRecordingTimerId(const char *Directory, const char *TimerId)
Definition: recording.c:3265
#define TIMERRECFILE
Definition: recording.c:60
#define RECORDFILESUFFIXTS
Definition: recording.c:2924
char * LimitNameLengths(char *s, int PathMax, int NameMax)
Definition: recording.c:671
double MarkFramesPerSecond
Definition: recording.c:2119
const char * InvalidChars
Definition: recording.c:591
void RemoveDeletedRecordings(void)
Definition: recording.c:137
#define RESUMEFILESUFFIX
Definition: recording.c:52
#define SUMMARYFILESUFFIX
Definition: recording.c:54
@ ruSrc
Definition: recording.h:37
@ ruCut
Definition: recording.h:33
@ ruReplay
Definition: recording.h:31
@ ruCopy
Definition: recording.h:35
@ ruCanceled
Definition: recording.h:41
@ ruTimer
Definition: recording.h:30
@ ruDst
Definition: recording.h:38
@ ruNone
Definition: recording.h:29
@ ruMove
Definition: recording.h:34
@ ruPending
Definition: recording.h:40
eRecordingsSortMode
Definition: recording.h:550
@ rsmName
Definition: recording.h:550
@ rsmTime
Definition: recording.h:550
#define DEFAULTFRAMESPERSECOND
Definition: recording.h:352
@ rsdAscending
Definition: recording.h:549
#define RUC_COPIEDRECORDING
Definition: recording.h:430
#define LOCK_DELETEDRECORDINGS_WRITE
Definition: recording.h:310
#define FOLDERDELIMCHAR
Definition: recording.h:21
#define RUC_DELETERECORDING
Definition: recording.h:426
#define RUC_MOVEDRECORDING
Definition: recording.h:428
#define RUC_COPYINGRECORDING
Definition: recording.h:429
#define LOCK_DELETEDRECORDINGS_READ
Definition: recording.h:309
#define LOCK_RECORDINGS_WRITE
Definition: recording.h:308
int TsPid(const uchar *p)
Definition: remux.h:82
#define PATPID
Definition: remux.h:52
#define TS_SIZE
Definition: remux.h:34
#define TS_SYNC_BYTE
Definition: remux.h:33
#define MIN_TS_PACKETS_FOR_FRAME_DETECTOR
Definition: remux.h:503
cSkins Skins
Definition: skins.c:219
@ mtWarning
Definition: skins.h:37
@ mtInfo
Definition: skins.h:37
@ mtError
Definition: skins.h:37
static const tChannelID InvalidID
Definition: channels.h:68
bool Valid(void) const
Definition: channels.h:58
static tChannelID FromString(const char *s)
Definition: channels.c:23
cString ToString(void) const
Definition: channels.c:40
Definition: epg.h:44
char language[MAXLANGCODE2]
Definition: epg.h:47
int SystemExec(const char *Command, bool Detached)
Definition: thread.c:1034
const char * strgetlast(const char *s, char c)
Definition: tools.c:213
void TouchFile(const char *FileName)
Definition: tools.c:717
bool isempty(const char *s)
Definition: tools.c:349
cString strescape(const char *s, const char *chars)
Definition: tools.c:272
bool MakeDirs(const char *FileName, bool IsDirectory)
Definition: tools.c:499
cString dtoa(double d, const char *Format)
Converts the given double value to a string, making sure it uses a '.
Definition: tools.c:432
time_t LastModifiedTime(const char *FileName)
Definition: tools.c:723
double atod(const char *s)
Converts the given string, which is a floating point number using a '.
Definition: tools.c:411
char * strreplace(char *s, char c1, char c2)
Definition: tools.c:139
ssize_t safe_read(int filedes, void *buffer, size_t size)
Definition: tools.c:53
char * stripspace(char *s)
Definition: tools.c:219
ssize_t safe_write(int filedes, const void *buffer, size_t size)
Definition: tools.c:65
int DirSizeMB(const char *DirName)
returns the total size of the files in the given directory, or -1 in case of an error
Definition: tools.c:639
bool DirectoryOk(const char *DirName, bool LogErrors)
Definition: tools.c:481
char * strn0cpy(char *dest, const char *src, size_t n)
Definition: tools.c:131
char * compactspace(char *s)
Definition: tools.c:231
off_t FileSize(const char *FileName)
returns the size of the given file, or -1 in case of an error (e.g. if the file doesn't exist)
Definition: tools.c:731
bool endswith(const char *s, const char *p)
Definition: tools.c:338
cString itoa(int n)
Definition: tools.c:442
cString AddDirectory(const char *DirName, const char *FileName)
Definition: tools.c:402
void writechar(int filedes, char c)
Definition: tools.c:85
T constrain(T v, T l, T h)
Definition: tools.h:70
char * skipspace(const char *s)
Definition: tools.h:241
#define SECSINDAY
Definition: tools.h:42
#define LOG_ERROR_STR(s)
Definition: tools.h:40
unsigned char uchar
Definition: tools.h:31
#define dsyslog(a...)
Definition: tools.h:37
#define MALLOC(type, size)
Definition: tools.h:47
bool DoubleEqual(double a, double b)
Definition: tools.h:97
void swap(T &a, T &b)
Definition: tools.h:65
T max(T a, T b)
Definition: tools.h:64
#define esyslog(a...)
Definition: tools.h:35
#define LOG_ERROR
Definition: tools.h:39
#define isyslog(a...)
Definition: tools.h:36
#define KILOBYTE(n)
Definition: tools.h:44