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