vdr  1.7.31
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 2.64 2012/09/30 13:05:14 kls Exp $
8  */
9 
10 #include "recording.h"
11 #include <ctype.h>
12 #include <dirent.h>
13 #include <errno.h>
14 #include <fcntl.h>
15 #define __STDC_FORMAT_MACROS // Required for format specifiers
16 #include <inttypes.h>
17 #include <math.h>
18 #include <stdio.h>
19 #include <string.h>
20 #include <sys/stat.h>
21 #include <unistd.h>
22 #include "channels.h"
23 #include "i18n.h"
24 #include "interface.h"
25 #include "remux.h"
26 #include "ringbuffer.h"
27 #include "skins.h"
28 #include "tools.h"
29 #include "videodir.h"
30 
31 #define SUMMARYFALLBACK
32 
33 #define RECEXT ".rec"
34 #define DELEXT ".del"
35 /* This was the original code, which works fine in a Linux only environment.
36  Unfortunately, because of Windows and its brain dead file system, we have
37  to use a more complicated approach, in order to allow users who have enabled
38  the --vfat command line option to see their recordings even if they forget to
39  enable --vfat when restarting VDR... Gee, do I hate Windows.
40  (kls 2002-07-27)
41 #define DATAFORMAT "%4d-%02d-%02d.%02d:%02d.%02d.%02d" RECEXT
42 #define NAMEFORMAT "%s/%s/" DATAFORMAT
43 */
44 #define DATAFORMATPES "%4d-%02d-%02d.%02d%*c%02d.%02d.%02d" RECEXT
45 #define NAMEFORMATPES "%s/%s/" "%4d-%02d-%02d.%02d.%02d.%02d.%02d" RECEXT
46 #define DATAFORMATTS "%4d-%02d-%02d.%02d.%02d.%d-%d" RECEXT
47 #define NAMEFORMATTS "%s/%s/" DATAFORMATTS
48 
49 #define RESUMEFILESUFFIX "/resume%s%s"
50 #ifdef SUMMARYFALLBACK
51 #define SUMMARYFILESUFFIX "/summary.vdr"
52 #endif
53 #define INFOFILESUFFIX "/info"
54 #define MARKSFILESUFFIX "/marks"
55 
56 #define SORTMODEFILE ".sort"
57 
58 #define MINDISKSPACE 1024 // MB
59 
60 #define REMOVECHECKDELTA 60 // seconds between checks for removing deleted files
61 #define DELETEDLIFETIME 300 // seconds after which a deleted recording will be actually removed
62 #define DISKCHECKDELTA 100 // seconds between checks for free disk space
63 #define REMOVELATENCY 10 // seconds to wait until next check after removing a file
64 #define MARKSUPDATEDELTA 10 // seconds between checks for updating editing marks
65 #define MININDEXAGE 3600 // seconds before an index file is considered no longer to be written
66 
67 #define MAX_SUBTITLE_LENGTH 40
68 
69 #define MAX_LINK_LEVEL 6
70 
71 bool VfatFileSystem = false;
72 int InstanceId = 0;
73 
75 
76 // --- cRemoveDeletedRecordingsThread ----------------------------------------
77 
79 protected:
80  virtual void Action(void);
81 public:
83  };
84 
86 :cThread("remove deleted recordings")
87 {
88 }
89 
91 {
92  SetPriority(19);
93  SetIOPriority(7);
94  // Make sure only one instance of VDR does this:
95  cLockFile LockFile(VideoDirectory);
96  if (LockFile.Lock()) {
97  bool deleted = false;
98  cThreadLock DeletedRecordingsLock(&DeletedRecordings);
99  for (cRecording *r = DeletedRecordings.First(); r; ) {
100  if (r->Deleted() && time(NULL) - r->Deleted() > DELETEDLIFETIME) {
101  cRecording *next = DeletedRecordings.Next(r);
102  r->Remove();
104  r = next;
105  deleted = true;
106  continue;
107  }
108  r = DeletedRecordings.Next(r);
109  }
110  if (deleted) {
111  const char *IgnoreFiles[] = { SORTMODEFILE, NULL };
112  RemoveEmptyVideoDirectories(IgnoreFiles);
113  }
114  }
115 }
116 
118 
119 // ---
120 
122 {
123  static time_t LastRemoveCheck = 0;
124  if (time(NULL) - LastRemoveCheck > REMOVECHECKDELTA) {
125  if (!RemoveDeletedRecordingsThread.Active()) {
126  cThreadLock DeletedRecordingsLock(&DeletedRecordings);
127  for (cRecording *r = DeletedRecordings.First(); r; r = DeletedRecordings.Next(r)) {
128  if (r->Deleted() && time(NULL) - r->Deleted() > DELETEDLIFETIME) {
129  RemoveDeletedRecordingsThread.Start();
130  break;
131  }
132  }
133  }
134  LastRemoveCheck = time(NULL);
135  }
136 }
137 
138 void AssertFreeDiskSpace(int Priority, bool Force)
139 {
140  static cMutex Mutex;
141  cMutexLock MutexLock(&Mutex);
142  // With every call to this function we try to actually remove
143  // a file, or mark a file for removal ("delete" it), so that
144  // it will get removed during the next call.
145  static time_t LastFreeDiskCheck = 0;
146  int Factor = (Priority == -1) ? 10 : 1;
147  if (Force || time(NULL) - LastFreeDiskCheck > DISKCHECKDELTA / Factor) {
149  // Make sure only one instance of VDR does this:
150  cLockFile LockFile(VideoDirectory);
151  if (!LockFile.Lock())
152  return;
153  // Remove the oldest file that has been "deleted":
154  isyslog("low disk space while recording, trying to remove a deleted recording...");
155  cThreadLock DeletedRecordingsLock(&DeletedRecordings);
156  if (DeletedRecordings.Count()) {
158  cRecording *r0 = NULL;
159  while (r) {
160  if (r->IsOnVideoDirectoryFileSystem()) { // only remove recordings that will actually increase the free video disk space
161  if (!r0 || r->Start() < r0->Start())
162  r0 = r;
163  }
164  r = DeletedRecordings.Next(r);
165  }
166  if (r0) {
167  if (r0->Remove())
168  LastFreeDiskCheck += REMOVELATENCY / Factor;
170  return;
171  }
172  }
173  else {
174  // DeletedRecordings was empty, so to be absolutely sure there are no
175  // deleted recordings we need to double check:
177  if (DeletedRecordings.Count())
178  return; // the next call will actually remove it
179  }
180  // No "deleted" files to remove, so let's see if we can delete a recording:
181  isyslog("...no deleted recording found, trying to delete an old recording...");
182  cThreadLock RecordingsLock(&Recordings);
183  if (Recordings.Count()) {
184  cRecording *r = Recordings.First();
185  cRecording *r0 = NULL;
186  while (r) {
187  if (r->IsOnVideoDirectoryFileSystem()) { // only delete recordings that will actually increase the free video disk space
188  if (!r->IsEdited() && r->Lifetime() < MAXLIFETIME) { // edited recordings and recordings with MAXLIFETIME live forever
189  if ((r->Lifetime() == 0 && Priority > r->Priority()) || // the recording has no guaranteed lifetime and the new recording has higher priority
190  (r->Lifetime() > 0 && (time(NULL) - r->Start()) / SECSINDAY >= r->Lifetime())) { // the recording's guaranteed lifetime has expired
191  if (r0) {
192  if (r->Priority() < r0->Priority() || (r->Priority() == r0->Priority() && r->Start() < r0->Start()))
193  r0 = r; // in any case we delete the one with the lowest priority (or the older one in case of equal priorities)
194  }
195  else
196  r0 = r;
197  }
198  }
199  }
200  r = Recordings.Next(r);
201  }
202  if (r0 && r0->Delete()) {
203  Recordings.Del(r0);
204  return;
205  }
206  }
207  // Unable to free disk space, but there's nothing we can do about that...
208  isyslog("...no old recording found, giving up");
209  Skins.QueueMessage(mtWarning, tr("Low disk space!"), 5, -1);
210  }
211  LastFreeDiskCheck = time(NULL);
212  }
213 }
214 
215 // --- cResumeFile -----------------------------------------------------------
216 
217 cResumeFile::cResumeFile(const char *FileName, bool IsPesRecording)
218 {
219  isPesRecording = IsPesRecording;
220  const char *Suffix = isPesRecording ? RESUMEFILESUFFIX ".vdr" : RESUMEFILESUFFIX;
221  fileName = MALLOC(char, strlen(FileName) + strlen(Suffix) + 1);
222  if (fileName) {
223  strcpy(fileName, FileName);
224  sprintf(fileName + strlen(fileName), Suffix, Setup.ResumeID ? "." : "", Setup.ResumeID ? *itoa(Setup.ResumeID) : "");
225  }
226  else
227  esyslog("ERROR: can't allocate memory for resume file name");
228 }
229 
231 {
232  free(fileName);
233 }
234 
236 {
237  int resume = -1;
238  if (fileName) {
239  struct stat st;
240  if (stat(fileName, &st) == 0) {
241  if ((st.st_mode & S_IWUSR) == 0) // no write access, assume no resume
242  return -1;
243  }
244  if (isPesRecording) {
245  int f = open(fileName, O_RDONLY);
246  if (f >= 0) {
247  if (safe_read(f, &resume, sizeof(resume)) != sizeof(resume)) {
248  resume = -1;
250  }
251  close(f);
252  }
253  else if (errno != ENOENT)
255  }
256  else {
257  FILE *f = fopen(fileName, "r");
258  if (f) {
259  cReadLine ReadLine;
260  char *s;
261  int line = 0;
262  while ((s = ReadLine.Read(f)) != NULL) {
263  ++line;
264  char *t = skipspace(s + 1);
265  switch (*s) {
266  case 'I': resume = atoi(t);
267  break;
268  default: ;
269  }
270  }
271  fclose(f);
272  }
273  else if (errno != ENOENT)
275  }
276  }
277  return resume;
278 }
279 
280 bool cResumeFile::Save(int Index)
281 {
282  if (fileName) {
283  if (isPesRecording) {
284  int f = open(fileName, O_WRONLY | O_CREAT | O_TRUNC, DEFFILEMODE);
285  if (f >= 0) {
286  if (safe_write(f, &Index, sizeof(Index)) < 0)
288  close(f);
290  return true;
291  }
292  }
293  else {
294  FILE *f = fopen(fileName, "w");
295  if (f) {
296  fprintf(f, "I %d\n", Index);
297  fclose(f);
299  }
300  else
302  return true;
303  }
304  }
305  return false;
306 }
307 
309 {
310  if (fileName) {
311  if (remove(fileName) == 0)
313  else if (errno != ENOENT)
315  }
316 }
317 
318 // --- cRecordingInfo --------------------------------------------------------
319 
320 cRecordingInfo::cRecordingInfo(const cChannel *Channel, const cEvent *Event)
321 {
322  channelID = Channel ? Channel->GetChannelID() : tChannelID::InvalidID;
323  channelName = Channel ? strdup(Channel->Name()) : NULL;
324  ownEvent = Event ? NULL : new cEvent(0);
325  event = ownEvent ? ownEvent : Event;
326  aux = NULL;
330  fileName = NULL;
331  if (Channel) {
332  // Since the EPG data's component records can carry only a single
333  // language code, let's see whether the channel's PID data has
334  // more information:
336  if (!Components)
337  Components = new cComponents;
338  for (int i = 0; i < MAXAPIDS; i++) {
339  const char *s = Channel->Alang(i);
340  if (*s) {
341  tComponent *Component = Components->GetComponent(i, 2, 3);
342  if (!Component)
343  Components->SetComponent(Components->NumComponents(), 2, 3, s, NULL);
344  else if (strlen(s) > strlen(Component->language))
345  strn0cpy(Component->language, s, sizeof(Component->language));
346  }
347  }
348  // There's no "multiple languages" for Dolby Digital tracks, but
349  // we do the same procedure here, too, in case there is no component
350  // information at all:
351  for (int i = 0; i < MAXDPIDS; i++) {
352  const char *s = Channel->Dlang(i);
353  if (*s) {
354  tComponent *Component = Components->GetComponent(i, 4, 0); // AC3 component according to the DVB standard
355  if (!Component)
356  Component = Components->GetComponent(i, 2, 5); // fallback "Dolby" component according to the "Premiere pseudo standard"
357  if (!Component)
358  Components->SetComponent(Components->NumComponents(), 2, 5, s, NULL);
359  else if (strlen(s) > strlen(Component->language))
360  strn0cpy(Component->language, s, sizeof(Component->language));
361  }
362  }
363  // The same applies to subtitles:
364  for (int i = 0; i < MAXSPIDS; i++) {
365  const char *s = Channel->Slang(i);
366  if (*s) {
367  tComponent *Component = Components->GetComponent(i, 3, 3);
368  if (!Component)
369  Components->SetComponent(Components->NumComponents(), 3, 3, s, NULL);
370  else if (strlen(s) > strlen(Component->language))
371  strn0cpy(Component->language, s, sizeof(Component->language));
372  }
373  }
374  if (Components != event->Components())
375  ((cEvent *)event)->SetComponents(Components);
376  }
377 }
378 
379 cRecordingInfo::cRecordingInfo(const char *FileName)
380 {
382  channelName = NULL;
383  ownEvent = new cEvent(0);
384  event = ownEvent;
385  aux = NULL;
389  fileName = strdup(cString::sprintf("%s%s", FileName, INFOFILESUFFIX));
390 }
391 
393 {
394  delete ownEvent;
395  free(aux);
396  free(channelName);
397  free(fileName);
398 }
399 
400 void cRecordingInfo::SetData(const char *Title, const char *ShortText, const char *Description)
401 {
402  if (!isempty(Title))
403  ((cEvent *)event)->SetTitle(Title);
404  if (!isempty(ShortText))
405  ((cEvent *)event)->SetShortText(ShortText);
406  if (!isempty(Description))
407  ((cEvent *)event)->SetDescription(Description);
408 }
409 
410 void cRecordingInfo::SetAux(const char *Aux)
411 {
412  free(aux);
413  aux = Aux ? strdup(Aux) : NULL;
414 }
415 
416 void cRecordingInfo::SetFramesPerSecond(double FramesPerSecond)
417 {
419 }
420 
421 bool cRecordingInfo::Read(FILE *f)
422 {
423  if (ownEvent) {
424  cReadLine ReadLine;
425  char *s;
426  int line = 0;
427  while ((s = ReadLine.Read(f)) != NULL) {
428  ++line;
429  char *t = skipspace(s + 1);
430  switch (*s) {
431  case 'C': {
432  char *p = strchr(t, ' ');
433  if (p) {
434  free(channelName);
435  channelName = strdup(compactspace(p));
436  *p = 0; // strips optional channel name
437  }
438  if (*t)
440  }
441  break;
442  case 'E': {
443  unsigned int EventID;
444  time_t StartTime;
445  int Duration;
446  unsigned int TableID = 0;
447  unsigned int Version = 0xFF;
448  int n = sscanf(t, "%u %ld %d %X %X", &EventID, &StartTime, &Duration, &TableID, &Version);
449  if (n >= 3 && n <= 5) {
450  ownEvent->SetEventID(EventID);
451  ownEvent->SetStartTime(StartTime);
452  ownEvent->SetDuration(Duration);
453  ownEvent->SetTableID(uchar(TableID));
454  ownEvent->SetVersion(uchar(Version));
455  }
456  }
457  break;
458  case 'F': framesPerSecond = atof(t);
459  break;
460  case 'L': lifetime = atoi(t);
461  break;
462  case 'P': priority = atoi(t);
463  break;
464  case '@': free(aux);
465  aux = strdup(t);
466  break;
467  case '#': break; // comments are ignored
468  default: if (!ownEvent->Parse(s)) {
469  esyslog("ERROR: EPG data problem in line %d", line);
470  return false;
471  }
472  break;
473  }
474  }
475  return true;
476  }
477  return false;
478 }
479 
480 bool cRecordingInfo::Write(FILE *f, const char *Prefix) const
481 {
482  if (channelID.Valid())
483  fprintf(f, "%sC %s%s%s\n", Prefix, *channelID.ToString(), channelName ? " " : "", channelName ? channelName : "");
484  event->Dump(f, Prefix, true);
485  fprintf(f, "%sF %.10g\n", Prefix, framesPerSecond);
486  fprintf(f, "%sP %d\n", Prefix, priority);
487  fprintf(f, "%sL %d\n", Prefix, lifetime);
488  if (aux)
489  fprintf(f, "%s@ %s\n", Prefix, aux);
490  return true;
491 }
492 
494 {
495  bool Result = false;
496  if (fileName) {
497  FILE *f = fopen(fileName, "r");
498  if (f) {
499  if (Read(f))
500  Result = true;
501  else
502  esyslog("ERROR: EPG data problem in file %s", fileName);
503  fclose(f);
504  }
505  else if (errno != ENOENT)
507  }
508  return Result;
509 }
510 
511 bool cRecordingInfo::Write(void) const
512 {
513  bool Result = false;
514  if (fileName) {
515  cSafeFile f(fileName);
516  if (f.Open()) {
517  if (Write(f))
518  Result = true;
519  f.Close();
520  }
521  else
523  }
524  return Result;
525 }
526 
527 // --- cRecording ------------------------------------------------------------
528 
529 #define RESUME_NOT_INITIALIZED (-2)
530 
531 struct tCharExchange { char a; char b; };
533  { FOLDERDELIMCHAR, '/' },
534  { '/', FOLDERDELIMCHAR },
535  { ' ', '_' },
536  // backwards compatibility:
537  { '\'', '\'' },
538  { '\'', '\x01' },
539  { '/', '\x02' },
540  { 0, 0 }
541  };
542 
543 char *ExchangeChars(char *s, bool ToFileSystem)
544 {
545  char *p = s;
546  while (*p) {
547  if (VfatFileSystem) {
548  // The VFAT file system can't handle all characters, so we
549  // have to take extra efforts to encode/decode them:
550  if (ToFileSystem) {
551  const char *InvalidChars = "\"\\/:*?|<>#";
552  switch (*p) {
553  // characters that can be mapped to other characters:
554  case ' ': *p = '_'; break;
555  case FOLDERDELIMCHAR: *p = '/'; break;
556  // characters that have to be encoded:
557  default:
558  if (strchr(InvalidChars, *p) || *p == '.' && (!*(p + 1) || *(p + 1) == FOLDERDELIMCHAR)) { // Windows can't handle '.' at the end of file/directory names
559  int l = p - s;
560  if (char *NewBuffer = (char *)realloc(s, strlen(s) + 10)) {
561  s = NewBuffer;
562  p = s + l;
563  char buf[4];
564  sprintf(buf, "#%02X", (unsigned char)*p);
565  memmove(p + 2, p, strlen(p) + 1);
566  strncpy(p, buf, 3);
567  p += 2;
568  }
569  else
570  esyslog("ERROR: out of memory");
571  }
572  }
573  }
574  else {
575  switch (*p) {
576  // mapped characters:
577  case '_': *p = ' '; break;
578  case '/': *p = FOLDERDELIMCHAR; break;
579  // encoded characters:
580  case '#': {
581  if (strlen(p) > 2 && isxdigit(*(p + 1)) && isxdigit(*(p + 2))) {
582  char buf[3];
583  sprintf(buf, "%c%c", *(p + 1), *(p + 2));
584  uchar c = uchar(strtol(buf, NULL, 16));
585  if (c) {
586  *p = c;
587  memmove(p + 1, p + 3, strlen(p) - 2);
588  }
589  }
590  }
591  break;
592  // backwards compatibility:
593  case '\x01': *p = '\''; break;
594  case '\x02': *p = '/'; break;
595  case '\x03': *p = ':'; break;
596  default: ;
597  }
598  }
599  }
600  else {
601  for (struct tCharExchange *ce = CharExchange; ce->a && ce->b; ce++) {
602  if (*p == (ToFileSystem ? ce->a : ce->b)) {
603  *p = ToFileSystem ? ce->b : ce->a;
604  break;
605  }
606  }
607  }
608  p++;
609  }
610  return s;
611 }
612 
613 cRecording::cRecording(cTimer *Timer, const cEvent *Event)
614 {
616  titleBuffer = NULL;
618  fileName = NULL;
619  name = NULL;
620  fileSizeMB = -1; // unknown
621  channel = Timer->Channel()->Number();
623  isPesRecording = false;
624  isOnVideoDirectoryFileSystem = -1; // unknown
626  numFrames = -1;
627  deleted = 0;
628  // set up the actual name:
629  const char *Title = Event ? Event->Title() : NULL;
630  const char *Subtitle = Event ? Event->ShortText() : NULL;
631  char SubtitleBuffer[MAX_SUBTITLE_LENGTH];
632  if (isempty(Title))
633  Title = Timer->Channel()->Name();
634  if (isempty(Subtitle))
635  Subtitle = " ";
636  else if (strlen(Subtitle) > MAX_SUBTITLE_LENGTH) {
637  // let's make sure the Subtitle doesn't produce too long a file name:
638  Utf8Strn0Cpy(SubtitleBuffer, Subtitle, MAX_SUBTITLE_LENGTH);
639  Subtitle = SubtitleBuffer;
640  }
641  const char *macroTITLE = strstr(Timer->File(), TIMERMACRO_TITLE);
642  const char *macroEPISODE = strstr(Timer->File(), TIMERMACRO_EPISODE);
643  if (macroTITLE || macroEPISODE) {
644  name = strdup(Timer->File());
645  name = strreplace(name, TIMERMACRO_TITLE, Title);
646  name = strreplace(name, TIMERMACRO_EPISODE, Subtitle);
647  // avoid blanks at the end:
648  int l = strlen(name);
649  while (l-- > 2) {
650  if (name[l] == ' ' && name[l - 1] != FOLDERDELIMCHAR)
651  name[l] = 0;
652  else
653  break;
654  }
655  if (Timer->IsSingleEvent()) {
656  Timer->SetFile(name); // this was an instant recording, so let's set the actual data
658  }
659  }
660  else if (Timer->IsSingleEvent() || !Setup.UseSubtitle)
661  name = strdup(Timer->File());
662  else
663  name = strdup(cString::sprintf("%s~%s", Timer->File(), Subtitle));
664  // substitute characters that would cause problems in file names:
665  strreplace(name, '\n', ' ');
666  start = Timer->StartTime();
667  priority = Timer->Priority();
668  lifetime = Timer->Lifetime();
669  // handle info:
670  info = new cRecordingInfo(Timer->Channel(), Event);
671  info->SetAux(Timer->Aux());
674 }
675 
676 cRecording::cRecording(const char *FileName)
677 {
679  fileSizeMB = -1; // unknown
680  channel = -1;
681  instanceId = -1;
682  priority = MAXPRIORITY; // assume maximum in case there is no info file
684  isPesRecording = false;
685  isOnVideoDirectoryFileSystem = -1; // unknown
687  numFrames = -1;
688  deleted = 0;
689  titleBuffer = NULL;
691  FileName = fileName = strdup(FileName);
692  if (*(fileName + strlen(fileName) - 1) == '/')
693  *(fileName + strlen(fileName) - 1) = 0;
694  FileName += strlen(VideoDirectory) + 1;
695  const char *p = strrchr(FileName, '/');
696 
697  name = NULL;
699  if (p) {
700  time_t now = time(NULL);
701  struct tm tm_r;
702  struct tm t = *localtime_r(&now, &tm_r); // this initializes the time zone in 't'
703  t.tm_isdst = -1; // makes sure mktime() will determine the correct DST setting
704  if (7 == sscanf(p + 1, DATAFORMATTS, &t.tm_year, &t.tm_mon, &t.tm_mday, &t.tm_hour, &t.tm_min, &channel, &instanceId)
705  || 7 == sscanf(p + 1, DATAFORMATPES, &t.tm_year, &t.tm_mon, &t.tm_mday, &t.tm_hour, &t.tm_min, &priority, &lifetime)) {
706  t.tm_year -= 1900;
707  t.tm_mon--;
708  t.tm_sec = 0;
709  start = mktime(&t);
710  name = MALLOC(char, p - FileName + 1);
711  strncpy(name, FileName, p - FileName);
712  name[p - FileName] = 0;
713  name = ExchangeChars(name, false);
715  }
716  else
717  return;
718  GetResume();
719  // read an optional info file:
720  cString InfoFileName = cString::sprintf("%s%s", fileName, isPesRecording ? INFOFILESUFFIX ".vdr" : INFOFILESUFFIX);
721  FILE *f = fopen(InfoFileName, "r");
722  if (f) {
723  if (!info->Read(f))
724  esyslog("ERROR: EPG data problem in file %s", *InfoFileName);
725  else if (!isPesRecording) {
729  }
730  fclose(f);
731  }
732  else if (errno == ENOENT)
734  else
735  LOG_ERROR_STR(*InfoFileName);
736 #ifdef SUMMARYFALLBACK
737  // fall back to the old 'summary.vdr' if there was no 'info.vdr':
738  if (isempty(info->Title())) {
739  cString SummaryFileName = cString::sprintf("%s%s", fileName, SUMMARYFILESUFFIX);
740  FILE *f = fopen(SummaryFileName, "r");
741  if (f) {
742  int line = 0;
743  char *data[3] = { NULL };
744  cReadLine ReadLine;
745  char *s;
746  while ((s = ReadLine.Read(f)) != NULL) {
747  if (*s || line > 1) {
748  if (data[line]) {
749  int len = strlen(s);
750  len += strlen(data[line]) + 1;
751  if (char *NewBuffer = (char *)realloc(data[line], len + 1)) {
752  data[line] = NewBuffer;
753  strcat(data[line], "\n");
754  strcat(data[line], s);
755  }
756  else
757  esyslog("ERROR: out of memory");
758  }
759  else
760  data[line] = strdup(s);
761  }
762  else
763  line++;
764  }
765  fclose(f);
766  if (!data[2]) {
767  data[2] = data[1];
768  data[1] = NULL;
769  }
770  else if (data[1] && data[2]) {
771  // if line 1 is too long, it can't be the short text,
772  // so assume the short text is missing and concatenate
773  // line 1 and line 2 to be the long text:
774  int len = strlen(data[1]);
775  if (len > 80) {
776  if (char *NewBuffer = (char *)realloc(data[1], len + 1 + strlen(data[2]) + 1)) {
777  data[1] = NewBuffer;
778  strcat(data[1], "\n");
779  strcat(data[1], data[2]);
780  free(data[2]);
781  data[2] = data[1];
782  data[1] = NULL;
783  }
784  else
785  esyslog("ERROR: out of memory");
786  }
787  }
788  info->SetData(data[0], data[1], data[2]);
789  for (int i = 0; i < 3; i ++)
790  free(data[i]);
791  }
792  else if (errno != ENOENT)
793  LOG_ERROR_STR(*SummaryFileName);
794  }
795 #endif
796  }
797 }
798 
800 {
801  free(titleBuffer);
802  free(sortBufferName);
803  free(sortBufferTime);
804  free(fileName);
805  free(name);
806  delete info;
807 }
808 
810 {
811  char *t = s, *s1 = NULL, *s2 = NULL;
812  while (*t) {
813  if (*t == '/') {
814  if (s1) {
815  if (s2)
816  s1 = s2;
817  s2 = t;
818  }
819  else
820  s1 = t;
821  }
822  t++;
823  }
824  if (s1 && s2) {
825  // To have folders sorted before plain recordings, the '/' s1 points to
826  // is replaced by the character 'b'. All other slashes will be replaced
827  // by 'a' in SortName() (see below), which will result in the desired
828  // sequence:
829  *s1 = 'b';
830  s1++;
831  memmove(s1, s2, t - s2 + 1);
832  }
833  return s;
834 }
835 
836 char *cRecording::SortName(void) const
837 {
839  if (!*sb) {
840  char *s = (RecordingsSortMode == rsmName) ? strdup(FileName() + strlen(VideoDirectory))
841  : StripEpisodeName(strdup(FileName() + strlen(VideoDirectory)));
842  strreplace(s, '/', 'a'); // some locales ignore '/' when sorting
843  int l = strxfrm(NULL, s, 0) + 1;
844  *sb = MALLOC(char, l);
845  strxfrm(*sb, s, l);
846  free(s);
847  }
848  return *sb;
849 }
850 
851 int cRecording::GetResume(void) const
852 {
854  cResumeFile ResumeFile(FileName(), isPesRecording);
855  resume = ResumeFile.Read();
856  }
857  return resume;
858 }
859 
860 int cRecording::Compare(const cListObject &ListObject) const
861 {
862  cRecording *r = (cRecording *)&ListObject;
863  return strcasecmp(SortName(), r->SortName());
864 }
865 
866 const char *cRecording::FileName(void) const
867 {
868  if (!fileName) {
869  struct tm tm_r;
870  struct tm *t = localtime_r(&start, &tm_r);
871  const char *fmt = isPesRecording ? NAMEFORMATPES : NAMEFORMATTS;
872  int ch = isPesRecording ? priority : channel;
873  int ri = isPesRecording ? lifetime : instanceId;
874  name = ExchangeChars(name, true);
875  fileName = strdup(cString::sprintf(fmt, VideoDirectory, name, t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, ch, ri));
876  name = ExchangeChars(name, false);
877  }
878  return fileName;
879 }
880 
881 const char *cRecording::Title(char Delimiter, bool NewIndicator, int Level) const
882 {
883  char New = NewIndicator && IsNew() ? '*' : ' ';
884  free(titleBuffer);
885  titleBuffer = NULL;
886  if (Level < 0 || Level == HierarchyLevels()) {
887  struct tm tm_r;
888  struct tm *t = localtime_r(&start, &tm_r);
889  char *s;
890  if (Level > 0 && (s = strrchr(name, FOLDERDELIMCHAR)) != NULL)
891  s++;
892  else
893  s = name;
894  cString Length("");
895  if (NewIndicator) {
896  int Minutes = max(0, (LengthInSeconds() + 30) / 60);
897  Length = cString::sprintf("%c%d:%02d",
898  Delimiter,
899  Minutes / 60,
900  Minutes % 60
901  );
902  }
903  titleBuffer = strdup(cString::sprintf("%02d.%02d.%02d%c%02d:%02d%s%c%c%s",
904  t->tm_mday,
905  t->tm_mon + 1,
906  t->tm_year % 100,
907  Delimiter,
908  t->tm_hour,
909  t->tm_min,
910  *Length,
911  New,
912  Delimiter,
913  s));
914  // let's not display a trailing FOLDERDELIMCHAR:
915  if (!NewIndicator)
917  s = &titleBuffer[strlen(titleBuffer) - 1];
918  if (*s == FOLDERDELIMCHAR)
919  *s = 0;
920  }
921  else if (Level < HierarchyLevels()) {
922  const char *s = name;
923  const char *p = s;
924  while (*++s) {
925  if (*s == FOLDERDELIMCHAR) {
926  if (Level--)
927  p = s + 1;
928  else
929  break;
930  }
931  }
932  titleBuffer = MALLOC(char, s - p + 3);
933  *titleBuffer = Delimiter;
934  *(titleBuffer + 1) = Delimiter;
935  strn0cpy(titleBuffer + 2, p, s - p + 1);
936  }
937  else
938  return "";
939  return titleBuffer;
940 }
941 
942 const char *cRecording::PrefixFileName(char Prefix)
943 {
944  cString p = PrefixVideoFileName(FileName(), Prefix);
945  if (*p) {
946  free(fileName);
947  fileName = strdup(p);
948  return fileName;
949  }
950  return NULL;
951 }
952 
953 const char *cRecording::UpdateFileName(const char *FileName)
954 {
955  if (FileName && *FileName) {
956  free(fileName);
957  fileName = strdup(FileName);
958  return fileName;
959  }
960  return NULL;
961 }
962 
964 {
965  const char *s = name;
966  int level = 0;
967  while (*++s) {
968  if (*s == FOLDERDELIMCHAR)
969  level++;
970  }
971  return level;
972 }
973 
974 bool cRecording::IsEdited(void) const
975 {
976  const char *s = strrchr(name, FOLDERDELIMCHAR);
977  s = !s ? name : s + 1;
978  return *s == '%';
979 }
980 
982 {
986 }
987 
989 {
990  info->Read();
994 }
995 
997 {
998  cString InfoFileName = cString::sprintf("%s%s", fileName, isPesRecording ? INFOFILESUFFIX ".vdr" : INFOFILESUFFIX);
999  FILE *f = fopen(InfoFileName, "w");
1000  if (f) {
1001  info->Write(f);
1002  fclose(f);
1003  }
1004  else
1005  LOG_ERROR_STR(*InfoFileName);
1006  return true;
1007 }
1008 
1009 void cRecording::SetStartTime(time_t Start)
1010 {
1011  start = Start;
1012  free(fileName);
1013  fileName = NULL;
1014 }
1015 
1017 {
1018  bool result = true;
1019  char *NewName = strdup(FileName());
1020  char *ext = strrchr(NewName, '.');
1021  if (ext && strcmp(ext, RECEXT) == 0) {
1022  strncpy(ext, DELEXT, strlen(ext));
1023  if (access(NewName, F_OK) == 0) {
1024  // the new name already exists, so let's remove that one first:
1025  isyslog("removing recording '%s'", NewName);
1026  RemoveVideoFile(NewName);
1027  }
1028  isyslog("deleting recording '%s'", FileName());
1029  if (access(FileName(), F_OK) == 0) {
1030  result = RenameVideoFile(FileName(), NewName);
1032  }
1033  else {
1034  isyslog("recording '%s' vanished", FileName());
1035  result = true; // well, we were going to delete it, anyway
1036  }
1037  }
1038  free(NewName);
1039  return result;
1040 }
1041 
1043 {
1044  // let's do a final safety check here:
1045  if (!endswith(FileName(), DELEXT)) {
1046  esyslog("attempt to remove recording %s", FileName());
1047  return false;
1048  }
1049  isyslog("removing recording %s", FileName());
1050  return RemoveVideoFile(FileName());
1051 }
1052 
1054 {
1055  bool result = true;
1056  char *NewName = strdup(FileName());
1057  char *ext = strrchr(NewName, '.');
1058  if (ext && strcmp(ext, DELEXT) == 0) {
1059  strncpy(ext, RECEXT, strlen(ext));
1060  if (access(NewName, F_OK) == 0) {
1061  // the new name already exists, so let's not remove that one:
1062  esyslog("ERROR: attempt to undelete '%s', while recording '%s' exists", FileName(), NewName);
1063  result = false;
1064  }
1065  else {
1066  isyslog("undeleting recording '%s'", FileName());
1067  if (access(FileName(), F_OK) == 0)
1068  result = RenameVideoFile(FileName(), NewName);
1069  else {
1070  isyslog("deleted recording '%s' vanished", FileName());
1071  result = false;
1072  }
1073  }
1074  }
1075  free(NewName);
1076  return result;
1077 }
1078 
1079 void cRecording::ResetResume(void) const
1080 {
1082 }
1083 
1084 int cRecording::NumFrames(void) const
1085 {
1086  if (numFrames < 0) {
1089  return nf; // check again later for ongoing recordings
1090  numFrames = nf;
1091  }
1092  return numFrames;
1093 }
1094 
1096 {
1097  int nf = NumFrames();
1098  if (nf >= 0)
1099  return int(nf / FramesPerSecond());
1100  return -1;
1101 }
1102 
1103 int cRecording::FileSizeMB(void) const
1104 {
1105  if (fileSizeMB < 0) {
1106  int fs = DirSizeMB(FileName());
1108  return fs; // check again later for ongoing recordings
1109  fileSizeMB = fs;
1110  }
1111  return fileSizeMB;
1112 }
1113 
1114 // --- cRecordings -----------------------------------------------------------
1115 
1117 
1118 char *cRecordings::updateFileName = NULL;
1119 
1121 :cThread("video directory scanner")
1122 {
1123  deleted = Deleted;
1124  lastUpdate = 0;
1125  state = 0;
1126 }
1127 
1129 {
1130  Cancel(3);
1131 }
1132 
1134 {
1135  Refresh();
1136 }
1137 
1139 {
1140  if (!updateFileName)
1141  updateFileName = strdup(AddDirectory(VideoDirectory, ".update"));
1142  return updateFileName;
1143 }
1144 
1145 void cRecordings::Refresh(bool Foreground)
1146 {
1147  lastUpdate = time(NULL); // doing this first to make sure we don't miss anything
1148  Lock();
1149  Clear();
1150  ChangeState();
1151  Unlock();
1152  ScanVideoDir(VideoDirectory, Foreground);
1153 }
1154 
1155 void cRecordings::ScanVideoDir(const char *DirName, bool Foreground, int LinkLevel)
1156 {
1157  cReadDir d(DirName);
1158  struct dirent *e;
1159  while ((Foreground || Running()) && (e = d.Next()) != NULL) {
1160  cString buffer = AddDirectory(DirName, e->d_name);
1161  struct stat st;
1162  if (lstat(buffer, &st) == 0) {
1163  int Link = 0;
1164  if (S_ISLNK(st.st_mode)) {
1165  if (LinkLevel > MAX_LINK_LEVEL) {
1166  isyslog("max link level exceeded - not scanning %s", *buffer);
1167  continue;
1168  }
1169  Link = 1;
1170  if (stat(buffer, &st) != 0)
1171  continue;
1172  }
1173  if (S_ISDIR(st.st_mode)) {
1174  if (endswith(buffer, deleted ? DELEXT : RECEXT)) {
1175  cRecording *r = new cRecording(buffer);
1176  if (r->Name()) {
1177  r->NumFrames(); // initializes the numFrames member
1178  r->FileSizeMB(); // initializes the fileSizeMB member
1179  if (deleted)
1180  r->deleted = time(NULL);
1181  Lock();
1182  Add(r);
1183  ChangeState();
1184  Unlock();
1185  }
1186  else
1187  delete r;
1188  }
1189  else
1190  ScanVideoDir(buffer, Foreground, LinkLevel + Link);
1191  }
1192  }
1193  }
1194 }
1195 
1197 {
1198  int NewState = state;
1199  bool Result = State != NewState;
1200  State = state;
1201  return Result;
1202 }
1203 
1205 {
1206  bool needsUpdate = NeedsUpdate();
1208  if (!needsUpdate)
1209  lastUpdate = time(NULL); // make sure we don't trigger ourselves
1210 }
1211 
1213 {
1214  time_t lastModified = LastModifiedTime(UpdateFileName());
1215  if (lastModified > time(NULL))
1216  return false; // somebody's clock isn't running correctly
1217  return lastUpdate < lastModified;
1218 }
1219 
1220 bool cRecordings::Update(bool Wait)
1221 {
1222  if (Wait) {
1223  Refresh(true);
1224  return Count() > 0;
1225  }
1226  else
1227  Start();
1228  return false;
1229 }
1230 
1231 cRecording *cRecordings::GetByName(const char *FileName)
1232 {
1233  if (FileName) {
1234  for (cRecording *recording = First(); recording; recording = Next(recording)) {
1235  if (strcmp(recording->FileName(), FileName) == 0)
1236  return recording;
1237  }
1238  }
1239  return NULL;
1240 }
1241 
1242 void cRecordings::AddByName(const char *FileName, bool TriggerUpdate)
1243 {
1244  LOCK_THREAD;
1245  cRecording *recording = GetByName(FileName);
1246  if (!recording) {
1247  recording = new cRecording(FileName);
1248  Add(recording);
1249  ChangeState();
1250  if (TriggerUpdate)
1251  TouchUpdate();
1252  }
1253 }
1254 
1255 void cRecordings::DelByName(const char *FileName, bool RemoveRecording)
1256 {
1257  LOCK_THREAD;
1258  cRecording *recording = GetByName(FileName);
1259  if (recording) {
1260  cThreadLock DeletedRecordingsLock(&DeletedRecordings);
1261  Del(recording, false);
1262  char *ext = strrchr(recording->fileName, '.');
1263  if (ext && RemoveRecording) {
1264  strncpy(ext, DELEXT, strlen(ext));
1265  if (access(recording->FileName(), F_OK) == 0) {
1266  recording->deleted = time(NULL);
1267  DeletedRecordings.Add(recording);
1268  recording = NULL; // to prevent it from being deleted below
1269  }
1270  }
1271  delete recording;
1272  ChangeState();
1273  TouchUpdate();
1274  }
1275 }
1276 
1277 void cRecordings::UpdateByName(const char *FileName)
1278 {
1279  LOCK_THREAD;
1280  cRecording *recording = GetByName(FileName);
1281  if (recording)
1282  recording->ReadInfo();
1283 }
1284 
1286 {
1287  int size = 0;
1288  LOCK_THREAD;
1289  for (cRecording *recording = First(); recording; recording = Next(recording)) {
1290  int FileSizeMB = recording->FileSizeMB();
1291  if (FileSizeMB > 0 && recording->IsOnVideoDirectoryFileSystem())
1292  size += FileSizeMB;
1293  }
1294  return size;
1295 }
1296 
1298 {
1299  int size = 0;
1300  int length = 0;
1301  LOCK_THREAD;
1302  for (cRecording *recording = First(); recording; recording = Next(recording)) {
1303  if (recording->IsOnVideoDirectoryFileSystem()) {
1304  int FileSizeMB = recording->FileSizeMB();
1305  if (FileSizeMB > 0) {
1306  int LengthInSeconds = recording->LengthInSeconds();
1307  if (LengthInSeconds > 0) {
1308  size += FileSizeMB;
1309  length += LengthInSeconds;
1310  }
1311  }
1312  }
1313  }
1314  return (size && length) ? double(size) * 60 / length : -1;
1315 }
1316 
1317 void cRecordings::ResetResume(const char *ResumeFileName)
1318 {
1319  LOCK_THREAD;
1320  for (cRecording *recording = First(); recording; recording = Next(recording)) {
1321  if (!ResumeFileName || strncmp(ResumeFileName, recording->FileName(), strlen(recording->FileName())) == 0)
1322  recording->ResetResume();
1323  }
1324  ChangeState();
1325 }
1326 
1327 // --- cMark -----------------------------------------------------------------
1328 
1331 
1332 cMark::cMark(int Position, const char *Comment, double FramesPerSecond)
1333 {
1334  position = Position;
1335  comment = Comment;
1336  framesPerSecond = FramesPerSecond;
1337 }
1338 
1340 {
1341 }
1342 
1344 {
1345  return cString::sprintf("%s%s%s\n", *IndexToHMSF(position, true, framesPerSecond), Comment() ? " " : "", Comment() ? Comment() : "");
1346 }
1347 
1348 bool cMark::Parse(const char *s)
1349 {
1350  comment = NULL;
1353  const char *p = strchr(s, ' ');
1354  if (p) {
1355  p = skipspace(p);
1356  if (*p)
1357  comment = strdup(p);
1358  }
1359  return true;
1360 }
1361 
1362 bool cMark::Save(FILE *f)
1363 {
1364  return fprintf(f, "%s", *ToText()) > 0;
1365 }
1366 
1367 // --- cMarks ----------------------------------------------------------------
1368 
1369 bool cMarks::Load(const char *RecordingFileName, double FramesPerSecond, bool IsPesRecording)
1370 {
1371  fileName = AddDirectory(RecordingFileName, IsPesRecording ? MARKSFILESUFFIX ".vdr" : MARKSFILESUFFIX);
1372  framesPerSecond = FramesPerSecond;
1373  nextUpdate = 0;
1374  lastFileTime = -1; // the first call to Load() must take place!
1375  lastChange = 0;
1376  return Update();
1377 }
1378 
1379 bool cMarks::Update(void)
1380 {
1381  time_t t = time(NULL);
1382  if (t > nextUpdate) {
1383  time_t LastModified = LastModifiedTime(fileName);
1384  if (LastModified != lastFileTime) // change detected, or first run
1385  lastChange = LastModified > 0 ? LastModified : t;
1386  int d = t - lastChange;
1387  if (d < 60)
1388  d = 1; // check frequently if the file has just been modified
1389  else if (d < 3600)
1390  d = 10; // older files are checked less frequently
1391  else
1392  d /= 360; // phase out checking for very old files
1393  nextUpdate = t + d;
1394  if (LastModified != lastFileTime) { // change detected, or first run
1395  lastFileTime = LastModified;
1396  if (lastFileTime == t)
1397  lastFileTime--; // make sure we don't miss updates in the remaining second
1398  cMutexLock MutexLock(&MutexMarkFramesPerSecond);
1401  Sort();
1402  return true;
1403  }
1404  }
1405  }
1406  return false;
1407 }
1408 
1409 void cMarks::Sort(void)
1410 {
1411  for (cMark *m1 = First(); m1; m1 = Next(m1)) {
1412  for (cMark *m2 = Next(m1); m2; m2 = Next(m2)) {
1413  if (m2->Position() < m1->Position()) {
1414  swap(m1->position, m2->position);
1415  swap(m1->comment, m2->comment);
1416  }
1417  }
1418  }
1419 }
1420 
1421 cMark *cMarks::Add(int Position)
1422 {
1423  cMark *m = Get(Position);
1424  if (!m) {
1425  cConfig<cMark>::Add(m = new cMark(Position, NULL, framesPerSecond));
1426  Sort();
1427  }
1428  return m;
1429 }
1430 
1431 cMark *cMarks::Get(int Position)
1432 {
1433  for (cMark *mi = First(); mi; mi = Next(mi)) {
1434  if (mi->Position() == Position)
1435  return mi;
1436  }
1437  return NULL;
1438 }
1439 
1440 cMark *cMarks::GetPrev(int Position)
1441 {
1442  for (cMark *mi = Last(); mi; mi = Prev(mi)) {
1443  if (mi->Position() < Position)
1444  return mi;
1445  }
1446  return NULL;
1447 }
1448 
1449 cMark *cMarks::GetNext(int Position)
1450 {
1451  for (cMark *mi = First(); mi; mi = Next(mi)) {
1452  if (mi->Position() > Position)
1453  return mi;
1454  }
1455  return NULL;
1456 }
1457 
1458 // --- cRecordingUserCommand -------------------------------------------------
1459 
1460 const char *cRecordingUserCommand::command = NULL;
1461 
1462 void cRecordingUserCommand::InvokeCommand(const char *State, const char *RecordingFileName, const char *SourceFileName)
1463 {
1464  if (command) {
1465  cString cmd;
1466  if (SourceFileName)
1467  cmd = cString::sprintf("%s %s \"%s\" \"%s\"", command, State, *strescape(RecordingFileName, "\\\"$"), *strescape(SourceFileName, "\\\"$"));
1468  else
1469  cmd = cString::sprintf("%s %s \"%s\"", command, State, *strescape(RecordingFileName, "\\\"$"));
1470  isyslog("executing '%s'", *cmd);
1471  SystemExec(cmd);
1472  }
1473 }
1474 
1475 // --- cIndexFileGenerator ---------------------------------------------------
1476 
1477 #define IFG_BUFFER_SIZE KILOBYTE(100)
1478 
1480 private:
1482 protected:
1483  virtual void Action(void);
1484 public:
1485  cIndexFileGenerator(const char *RecordingName);
1487  };
1488 
1490 :cThread("index file generator")
1491 ,recordingName(RecordingName)
1492 {
1493  Start();
1494 }
1495 
1497 {
1498  Cancel(3);
1499 }
1500 
1502 {
1503  bool IndexFileComplete = false;
1504  bool IndexFileWritten = false;
1505  bool Rewind = false;
1506  cFileName FileName(recordingName, false);
1507  cUnbufferedFile *ReplayFile = FileName.Open();
1509  cPatPmtParser PatPmtParser;
1510  cFrameDetector FrameDetector;
1511  cIndexFile IndexFile(recordingName, true);
1512  int BufferChunks = KILOBYTE(1); // no need to read a lot at the beginning when parsing PAT/PMT
1513  off_t FileSize = 0;
1514  off_t FrameOffset = -1;
1515  Skins.QueueMessage(mtInfo, tr("Regenerating index file"));
1516  while (Running()) {
1517  // Rewind input file:
1518  if (Rewind) {
1519  ReplayFile = FileName.SetOffset(1);
1520  Buffer.Clear();
1521  Rewind = false;
1522  }
1523  // Process data:
1524  int Length;
1525  uchar *Data = Buffer.Get(Length);
1526  if (Data) {
1527  if (FrameDetector.Synced()) {
1528  // Step 3 - generate the index:
1529  if (TsPid(Data) == PATPID)
1530  FrameOffset = FileSize; // the PAT/PMT is at the beginning of an I-frame
1531  int Processed = FrameDetector.Analyze(Data, Length);
1532  if (Processed > 0) {
1533  if (FrameDetector.NewFrame()) {
1534  IndexFile.Write(FrameDetector.IndependentFrame(), FileName.Number(), FrameOffset >= 0 ? FrameOffset : FileSize);
1535  FrameOffset = -1;
1536  IndexFileWritten = true;
1537  }
1538  FileSize += Processed;
1539  Buffer.Del(Processed);
1540  }
1541  }
1542  else if (PatPmtParser.Vpid()) {
1543  // Step 2 - sync FrameDetector:
1544  int Processed = FrameDetector.Analyze(Data, Length);
1545  if (Processed > 0) {
1546  if (FrameDetector.Synced()) {
1547  // Synced FrameDetector, so rewind for actual processing:
1548  FrameDetector.Reset();
1549  Rewind = true;
1550  }
1551  Buffer.Del(Processed);
1552  }
1553  }
1554  else {
1555  // Step 1 - parse PAT/PMT:
1556  uchar *p = Data;
1557  while (Length >= TS_SIZE) {
1558  int Pid = TsPid(p);
1559  if (Pid == 0)
1560  PatPmtParser.ParsePat(p, TS_SIZE);
1561  else if (Pid == PatPmtParser.PmtPid())
1562  PatPmtParser.ParsePmt(p, TS_SIZE);
1563  Length -= TS_SIZE;
1564  p += TS_SIZE;
1565  if (PatPmtParser.Vpid()) {
1566  // Found Vpid, so rewind to sync FrameDetector:
1567  FrameDetector.SetPid(PatPmtParser.Vpid(), PatPmtParser.Vtype());
1568  BufferChunks = IFG_BUFFER_SIZE;
1569  Rewind = true;
1570  break;
1571  }
1572  }
1573  Buffer.Del(p - Data);
1574  }
1575  }
1576  // Read data:
1577  else if (ReplayFile) {
1578  int Result = Buffer.Read(ReplayFile, BufferChunks);
1579  if (Result == 0) { // EOF
1580  ReplayFile = FileName.NextFile();
1581  FileSize = 0;
1582  FrameOffset = -1;
1583  }
1584  }
1585  // Recording has been processed:
1586  else {
1587  IndexFileComplete = true;
1588  break;
1589  }
1590  }
1591  if (IndexFileComplete) {
1592  if (IndexFileWritten) {
1593  Skins.QueueMessage(mtInfo, tr("Index file regeneration complete"));
1594  return;
1595  }
1596  else
1597  Skins.QueueMessage(mtError, tr("Index file regeneration failed!"));
1598  }
1599  // Delete the index file if the recording has not been processed entirely:
1600  IndexFile.Delete();
1601 }
1602 
1603 // --- cIndexFile ------------------------------------------------------------
1604 
1605 #define INDEXFILESUFFIX "/index"
1606 
1607 // The maximum time to wait before giving up while catching up on an index file:
1608 #define MAXINDEXCATCHUP 8 // seconds
1609 
1610 struct tIndexPes {
1611  uint32_t offset;
1614  uint16_t reserved;
1615  };
1616 
1617 struct tIndexTs {
1618  uint64_t offset:40; // up to 1TB per file (not using off_t here - must definitely be exactly 64 bit!)
1619  int reserved:7; // reserved for future use
1620  int independent:1; // marks frames that can be displayed by themselves (for trick modes)
1621  uint16_t number:16; // up to 64K files per recording
1622  tIndexTs(off_t Offset, bool Independent, uint16_t Number)
1623  {
1624  offset = Offset;
1625  reserved = 0;
1626  independent = Independent;
1627  number = Number;
1628  }
1629  };
1630 
1631 #define MAXWAITFORINDEXFILE 10 // max. time to wait for the regenerated index file (seconds)
1632 #define INDEXFILECHECKINTERVAL 500 // ms between checks for existence of the regenerated index file
1633 #define INDEXFILETESTINTERVAL 10 // ms between tests for the size of the index file in case of pausing live video
1634 
1637 
1638 cIndexFile::cIndexFile(const char *FileName, bool Record, bool IsPesRecording, bool PauseLive)
1639 :resumeFile(FileName, IsPesRecording)
1640 {
1641  f = -1;
1642  size = 0;
1643  last = -1;
1644  index = NULL;
1645  isPesRecording = IsPesRecording;
1646  indexFileGenerator = NULL;
1647  if (FileName) {
1648  fileName = IndexFileName(FileName, isPesRecording);
1649  if (!Record && PauseLive) {
1650  // Wait until the index file contains at least two frames:
1651  time_t tmax = time(NULL) + MAXWAITFORINDEXFILE;
1652  while (time(NULL) < tmax && FileSize(fileName) < off_t(2 * sizeof(tIndexTs)))
1654  }
1655  int delta = 0;
1656  if (!Record && access(fileName, R_OK) != 0) {
1657  // Index file doesn't exist, so try to regenerate it:
1658  if (!isPesRecording) { // sorry, can only do this for TS recordings
1659  resumeFile.Delete(); // just in case
1660  indexFileGenerator = new cIndexFileGenerator(FileName);
1661  // Wait until the index file exists:
1662  time_t tmax = time(NULL) + MAXWAITFORINDEXFILE;
1663  do {
1664  cCondWait::SleepMs(INDEXFILECHECKINTERVAL); // start with a sleep, to give it a head start
1665  } while (access(fileName, R_OK) != 0 && time(NULL) < tmax);
1666  }
1667  }
1668  if (access(fileName, R_OK) == 0) {
1669  struct stat buf;
1670  if (stat(fileName, &buf) == 0) {
1671  delta = int(buf.st_size % sizeof(tIndexTs));
1672  if (delta) {
1673  delta = sizeof(tIndexTs) - delta;
1674  esyslog("ERROR: invalid file size (%"PRId64") in '%s'", buf.st_size, *fileName);
1675  }
1676  last = int((buf.st_size + delta) / sizeof(tIndexTs) - 1);
1677  if (!Record && last >= 0) {
1678  size = last + 1;
1679  index = MALLOC(tIndexTs, size);
1680  if (index) {
1681  f = open(fileName, O_RDONLY);
1682  if (f >= 0) {
1683  if (safe_read(f, index, size_t(buf.st_size)) != buf.st_size) {
1684  esyslog("ERROR: can't read from file '%s'", *fileName);
1685  free(index);
1686  index = NULL;
1687  close(f);
1688  f = -1;
1689  }
1690  // we don't close f here, see CatchUp()!
1691  else if (isPesRecording)
1693  }
1694  else
1696  }
1697  else
1698  esyslog("ERROR: can't allocate %zd bytes for index '%s'", size * sizeof(tIndexTs), *fileName);
1699  }
1700  }
1701  else
1702  LOG_ERROR;
1703  }
1704  else if (!Record)
1705  isyslog("missing index file %s", *fileName);
1706  if (Record) {
1707  if ((f = open(fileName, O_WRONLY | O_CREAT | O_APPEND, DEFFILEMODE)) >= 0) {
1708  if (delta) {
1709  esyslog("ERROR: padding index file with %d '0' bytes", delta);
1710  while (delta--)
1711  writechar(f, 0);
1712  }
1713  }
1714  else
1716  }
1717  }
1718  if (Record)
1719  AddToIndexList(this);
1720 }
1721 
1723 {
1724  RemoveFromIndexList(this);
1725  if (f >= 0)
1726  close(f);
1727  free(index);
1728  delete indexFileGenerator;
1729 }
1730 
1731 cString cIndexFile::IndexFileName(const char *FileName, bool IsPesRecording)
1732 {
1733  return cString::sprintf("%s%s", FileName, IsPesRecording ? INDEXFILESUFFIX ".vdr" : INDEXFILESUFFIX);
1734 }
1735 
1736 void cIndexFile::ConvertFromPes(tIndexTs *IndexTs, int Count)
1737 {
1738  tIndexPes IndexPes;
1739  while (Count-- > 0) {
1740  memcpy(&IndexPes, IndexTs, sizeof(IndexPes));
1741  IndexTs->offset = IndexPes.offset;
1742  IndexTs->independent = IndexPes.type == 1; // I_FRAME
1743  IndexTs->number = IndexPes.number;
1744  IndexTs++;
1745  }
1746 }
1747 
1748 void cIndexFile::ConvertToPes(tIndexTs *IndexTs, int Count)
1749 {
1750  tIndexPes IndexPes;
1751  while (Count-- > 0) {
1752  IndexPes.offset = uint32_t(IndexTs->offset);
1753  IndexPes.type = uchar(IndexTs->independent ? 1 : 2); // I_FRAME : "not I_FRAME" (exact frame type doesn't matter)
1754  IndexPes.number = uchar(IndexTs->number);
1755  IndexPes.reserved = 0;
1756  memcpy(IndexTs, &IndexPes, sizeof(*IndexTs));
1757  IndexTs++;
1758  }
1759 }
1760 
1761 bool cIndexFile::CatchUp(int Index)
1762 {
1763  // returns true unless something really goes wrong, so that 'index' becomes NULL
1764  if (index && f >= 0) {
1765  cMutexLock MutexLock(&mutex);
1766  for (int i = 0; i <= MAXINDEXCATCHUP && (Index < 0 || Index >= last); i++) {
1767  struct stat buf;
1768  if (fstat(f, &buf) == 0) {
1769  if (!IsInIndexList(this)) {
1770  close(f);
1771  f = -1;
1772  break;
1773  }
1774  int newLast = int(buf.st_size / sizeof(tIndexTs) - 1);
1775  if (newLast > last) {
1776  int NewSize = size;
1777  if (NewSize <= newLast) {
1778  NewSize *= 2;
1779  if (NewSize <= newLast)
1780  NewSize = newLast + 1;
1781  }
1782  if (tIndexTs *NewBuffer = (tIndexTs *)realloc(index, NewSize * sizeof(tIndexTs))) {
1783  size = NewSize;
1784  index = NewBuffer;
1785  int offset = (last + 1) * sizeof(tIndexTs);
1786  int delta = (newLast - last) * sizeof(tIndexTs);
1787  if (lseek(f, offset, SEEK_SET) == offset) {
1788  if (safe_read(f, &index[last + 1], delta) != delta) {
1789  esyslog("ERROR: can't read from index");
1790  free(index);
1791  index = NULL;
1792  close(f);
1793  f = -1;
1794  break;
1795  }
1796  if (isPesRecording)
1797  ConvertFromPes(&index[last + 1], newLast - last);
1798  last = newLast;
1799  }
1800  else
1802  }
1803  else {
1804  esyslog("ERROR: can't realloc() index");
1805  break;
1806  }
1807  }
1808  }
1809  else
1811  if (Index < last)
1812  break;
1813  cCondWait::SleepMs(1000);
1814  }
1815  }
1816  return index != NULL;
1817 }
1818 
1819 bool cIndexFile::Write(bool Independent, uint16_t FileNumber, off_t FileOffset)
1820 {
1821  if (f >= 0) {
1822  tIndexTs i(FileOffset, Independent, FileNumber);
1823  if (isPesRecording)
1824  ConvertToPes(&i, 1);
1825  if (safe_write(f, &i, sizeof(i)) < 0) {
1827  close(f);
1828  f = -1;
1829  return false;
1830  }
1831  last++;
1832  }
1833  return f >= 0;
1834 }
1835 
1836 bool cIndexFile::Get(int Index, uint16_t *FileNumber, off_t *FileOffset, bool *Independent, int *Length)
1837 {
1838  if (CatchUp(Index)) {
1839  if (Index >= 0 && Index < last) {
1840  *FileNumber = index[Index].number;
1841  *FileOffset = index[Index].offset;
1842  if (Independent)
1843  *Independent = index[Index].independent;
1844  if (Length) {
1845  uint16_t fn = index[Index + 1].number;
1846  off_t fo = index[Index + 1].offset;
1847  if (fn == *FileNumber)
1848  *Length = int(fo - *FileOffset);
1849  else
1850  *Length = -1; // this means "everything up to EOF" (the buffer's Read function will act accordingly)
1851  }
1852  return true;
1853  }
1854  }
1855  return false;
1856 }
1857 
1858 int cIndexFile::GetNextIFrame(int Index, bool Forward, uint16_t *FileNumber, off_t *FileOffset, int *Length)
1859 {
1860  if (CatchUp()) {
1861  int d = Forward ? 1 : -1;
1862  for (;;) {
1863  Index += d;
1864  if (Index >= 0 && Index < last) {
1865  if (index[Index].independent) {
1866  uint16_t fn;
1867  if (!FileNumber)
1868  FileNumber = &fn;
1869  off_t fo;
1870  if (!FileOffset)
1871  FileOffset = &fo;
1872  *FileNumber = index[Index].number;
1873  *FileOffset = index[Index].offset;
1874  if (Length) {
1875  // all recordings end with a non-independent frame, so the following should be safe:
1876  uint16_t fn = index[Index + 1].number;
1877  off_t fo = index[Index + 1].offset;
1878  if (fn == *FileNumber)
1879  *Length = int(fo - *FileOffset);
1880  else {
1881  esyslog("ERROR: 'I' frame at end of file #%d", *FileNumber);
1882  *Length = -1;
1883  }
1884  }
1885  return Index;
1886  }
1887  }
1888  else
1889  break;
1890  }
1891  }
1892  return -1;
1893 }
1894 
1895 int cIndexFile::Get(uint16_t FileNumber, off_t FileOffset)
1896 {
1897  if (CatchUp()) {
1898  //TODO implement binary search!
1899  int i;
1900  for (i = 0; i < last; i++) {
1901  if (index[i].number > FileNumber || (index[i].number == FileNumber) && off_t(index[i].offset) >= FileOffset)
1902  break;
1903  }
1904  return i;
1905  }
1906  return -1;
1907 }
1908 
1910 {
1911  return f >= 0;
1912 }
1913 
1915 {
1916  if (*fileName) {
1917  dsyslog("deleting index file '%s'", *fileName);
1918  if (f >= 0) {
1919  close(f);
1920  f = -1;
1921  }
1922  unlink(fileName);
1923  }
1924 }
1925 
1926 int cIndexFile::GetLength(const char *FileName, bool IsPesRecording)
1927 {
1928  struct stat buf;
1929  cString s = IndexFileName(FileName, IsPesRecording);
1930  if (*s && stat(s, &buf) == 0)
1931  return buf.st_size / (IsPesRecording ? sizeof(tIndexTs) : sizeof(tIndexPes));
1932  return -1;
1933 }
1934 
1936 {
1937  cMutexLock MutexLock(&indexListMutex);
1938  for (int i = 0; i < indexList.Size(); i++) {
1939  if (!indexList[i]) {
1940  indexList[i] = IndexFile;
1941  return;
1942  }
1943  }
1944  indexList.Append(IndexFile);
1945 }
1946 
1948 {
1949  cMutexLock MutexLock(&indexListMutex);
1950  for (int i = 0; i < indexList.Size(); i++) {
1951  if (indexList[i] == IndexFile) {
1952  indexList[i] = NULL;
1953  return;
1954  }
1955  }
1956 }
1957 
1959 {
1960  cMutexLock MutexLock(&indexListMutex);
1961  for (int i = 0; i < indexList.Size(); i++) {
1962  if (indexList[i] && !strcmp(indexList[i]->fileName, IndexFile->fileName))
1963  return true;
1964  }
1965  return false;
1966 }
1967 
1968 bool GenerateIndex(const char *FileName)
1969 {
1970  if (DirectoryOk(FileName)) {
1971  cRecording Recording(FileName);
1972  if (Recording.Name()) {
1973  if (!Recording.IsPesRecording()) {
1974  cString IndexFileName = AddDirectory(FileName, INDEXFILESUFFIX);
1975  unlink(IndexFileName);
1976  cIndexFileGenerator *IndexFileGenerator = new cIndexFileGenerator(FileName);
1977  while (IndexFileGenerator->Active())
1979  if (access(IndexFileName, R_OK) == 0)
1980  return true;
1981  else
1982  fprintf(stderr, "cannot create '%s'\n", *IndexFileName);
1983  }
1984  else
1985  fprintf(stderr, "'%s' is not a TS recording\n", FileName);
1986  }
1987  else
1988  fprintf(stderr, "'%s' is not a recording\n", FileName);
1989  }
1990  else
1991  fprintf(stderr, "'%s' is not a directory\n", FileName);
1992  return false;
1993 }
1994 
1995 // --- cFileName -------------------------------------------------------------
1996 
1997 #define MAXFILESPERRECORDINGPES 255
1998 #define RECORDFILESUFFIXPES "/%03d.vdr"
1999 #define MAXFILESPERRECORDINGTS 65535
2000 #define RECORDFILESUFFIXTS "/%05d.ts"
2001 #define RECORDFILESUFFIXLEN 20 // some additional bytes for safety...
2002 
2003 cFileName::cFileName(const char *FileName, bool Record, bool Blocking, bool IsPesRecording)
2004 {
2005  file = NULL;
2006  fileNumber = 0;
2007  record = Record;
2008  blocking = Blocking;
2009  isPesRecording = IsPesRecording;
2010  // Prepare the file name:
2011  fileName = MALLOC(char, strlen(FileName) + RECORDFILESUFFIXLEN);
2012  if (!fileName) {
2013  esyslog("ERROR: can't copy file name '%s'", fileName);
2014  return;
2015  }
2016  strcpy(fileName, FileName);
2017  pFileNumber = fileName + strlen(fileName);
2018  SetOffset(1);
2019 }
2020 
2022 {
2023  Close();
2024  free(fileName);
2025 }
2026 
2027 bool cFileName::GetLastPatPmtVersions(int &PatVersion, int &PmtVersion)
2028 {
2029  if (fileName && !isPesRecording) {
2030  // Find the last recording file:
2031  int Number = 1;
2032  for (; Number <= MAXFILESPERRECORDINGTS + 1; Number++) { // +1 to correctly set Number in case there actually are that many files
2033  sprintf(pFileNumber, RECORDFILESUFFIXTS, Number);
2034  if (access(fileName, F_OK) != 0) { // file doesn't exist
2035  Number--;
2036  break;
2037  }
2038  }
2039  for (; Number > 0; Number--) {
2040  // Search for a PAT packet from the end of the file:
2041  cPatPmtParser PatPmtParser;
2042  sprintf(pFileNumber, RECORDFILESUFFIXTS, Number);
2043  int fd = open(fileName, O_RDONLY | O_LARGEFILE, DEFFILEMODE);
2044  if (fd >= 0) {
2045  off_t pos = lseek(fd, -TS_SIZE, SEEK_END);
2046  while (pos >= 0) {
2047  // Read and parse the PAT/PMT:
2048  uchar buf[TS_SIZE];
2049  while (read(fd, buf, sizeof(buf)) == sizeof(buf)) {
2050  if (buf[0] == TS_SYNC_BYTE) {
2051  int Pid = TsPid(buf);
2052  if (Pid == 0)
2053  PatPmtParser.ParsePat(buf, sizeof(buf));
2054  else if (Pid == PatPmtParser.PmtPid()) {
2055  PatPmtParser.ParsePmt(buf, sizeof(buf));
2056  if (PatPmtParser.GetVersions(PatVersion, PmtVersion)) {
2057  close(fd);
2058  return true;
2059  }
2060  }
2061  else
2062  break; // PAT/PMT is always in one sequence
2063  }
2064  else
2065  return false;
2066  }
2067  pos = lseek(fd, pos - TS_SIZE, SEEK_SET);
2068  }
2069  close(fd);
2070  }
2071  else
2072  break;
2073  }
2074  }
2075  return false;
2076 }
2077 
2079 {
2080  if (!file) {
2081  int BlockingFlag = blocking ? 0 : O_NONBLOCK;
2082  if (record) {
2083  dsyslog("recording to '%s'", fileName);
2084  file = OpenVideoFile(fileName, O_RDWR | O_CREAT | O_LARGEFILE | BlockingFlag);
2085  if (!file)
2087  }
2088  else {
2089  if (access(fileName, R_OK) == 0) {
2090  dsyslog("playing '%s'", fileName);
2091  file = cUnbufferedFile::Create(fileName, O_RDONLY | O_LARGEFILE | BlockingFlag);
2092  if (!file)
2094  }
2095  else if (errno != ENOENT)
2097  }
2098  }
2099  return file;
2100 }
2101 
2103 {
2104  if (file) {
2105  if (CloseVideoFile(file) < 0)
2107  file = NULL;
2108  }
2109 }
2110 
2111 cUnbufferedFile *cFileName::SetOffset(int Number, off_t Offset)
2112 {
2113  if (fileNumber != Number)
2114  Close();
2115  int MaxFilesPerRecording = isPesRecording ? MAXFILESPERRECORDINGPES : MAXFILESPERRECORDINGTS;
2116  if (0 < Number && Number <= MaxFilesPerRecording) {
2117  fileNumber = uint16_t(Number);
2119  if (record) {
2120  if (access(fileName, F_OK) == 0) {
2121  // file exists, check if it has non-zero size
2122  struct stat buf;
2123  if (stat(fileName, &buf) == 0) {
2124  if (buf.st_size != 0)
2125  return SetOffset(Number + 1); // file exists and has non zero size, let's try next suffix
2126  else {
2127  // zero size file, remove it
2128  dsyslog("cFileName::SetOffset: removing zero-sized file %s", fileName);
2129  unlink(fileName);
2130  }
2131  }
2132  else
2133  return SetOffset(Number + 1); // error with fstat - should not happen, just to be on the safe side
2134  }
2135  else if (errno != ENOENT) { // something serious has happened
2137  return NULL;
2138  }
2139  // found a non existing file suffix
2140  }
2141  if (Open() >= 0) {
2142  if (!record && Offset >= 0 && file && file->Seek(Offset, SEEK_SET) != Offset) {
2144  return NULL;
2145  }
2146  }
2147  return file;
2148  }
2149  esyslog("ERROR: max number of files (%d) exceeded", MaxFilesPerRecording);
2150  return NULL;
2151 }
2152 
2154  const int maxVideoFileSize = isPesRecording ? MAXVIDEOFILESIZEPES : MAXVIDEOFILESIZETS;
2155  const int setupMaxVideoFileSize = min(maxVideoFileSize, Setup.MaxVideoFileSize);
2156  const int maxFileNumber = isPesRecording ? 255 : 65535;
2157 
2158  const off_t smallFiles = (maxFileNumber * off_t(maxVideoFileSize) - 1024 * Setup.MaxRecordingSize)
2159  / max(maxVideoFileSize - setupMaxVideoFileSize, 1);
2160 
2161  if (fileNumber <= smallFiles)
2162  return MEGABYTE(off_t(setupMaxVideoFileSize));
2163 
2164  return MEGABYTE(off_t(maxVideoFileSize));
2165 }
2166 
2168 {
2169  return SetOffset(fileNumber + 1);
2170 }
2171 
2172 // --- Index stuff -----------------------------------------------------------
2173 
2174 cString IndexToHMSF(int Index, bool WithFrame, double FramesPerSecond)
2175 {
2176  const char *Sign = "";
2177  if (Index < 0) {
2178  Index = -Index;
2179  Sign = "-";
2180  }
2181  double Seconds;
2182  int f = int(modf((Index + 0.5) / FramesPerSecond, &Seconds) * FramesPerSecond + 1);
2183  int s = int(Seconds);
2184  int m = s / 60 % 60;
2185  int h = s / 3600;
2186  s %= 60;
2187  return cString::sprintf(WithFrame ? "%s%d:%02d:%02d.%02d" : "%s%d:%02d:%02d", Sign, h, m, s, f);
2188 }
2189 
2190 int HMSFToIndex(const char *HMSF, double FramesPerSecond)
2191 {
2192  int h, m, s, f = 1;
2193  int n = sscanf(HMSF, "%d:%d:%d.%d", &h, &m, &s, &f);
2194  if (n == 1)
2195  return h - 1; // plain frame number
2196  if (n >= 3)
2197  return int(round((h * 3600 + m * 60 + s) * FramesPerSecond)) + f - 1;
2198  return 0;
2199 }
2200 
2201 int SecondsToFrames(int Seconds, double FramesPerSecond)
2202 {
2203  return int(round(Seconds * FramesPerSecond));
2204 }
2205 
2206 // --- ReadFrame -------------------------------------------------------------
2207 
2208 int ReadFrame(cUnbufferedFile *f, uchar *b, int Length, int Max)
2209 {
2210  if (Length == -1)
2211  Length = Max; // this means we read up to EOF (see cIndex)
2212  else if (Length > Max) {
2213  esyslog("ERROR: frame larger than buffer (%d > %d)", Length, Max);
2214  Length = Max;
2215  }
2216  int r = f->Read(b, Length);
2217  if (r < 0)
2218  LOG_ERROR;
2219  return r;
2220 }
2221 
2222 // --- Recordings Sort Mode --------------------------------------------------
2223 
2225 
2226 bool HasRecordingsSortMode(const char *Directory)
2227 {
2228  return access(AddDirectory(Directory, SORTMODEFILE), R_OK) == 0;
2229 }
2230 
2231 void GetRecordingsSortMode(const char *Directory)
2232 {
2233  if (FILE *f = fopen(AddDirectory(Directory, SORTMODEFILE), "r")) {
2234  char buf[8];
2235  if (fgets(buf, sizeof(buf), f))
2237  fclose(f);
2238  }
2239 }
2240 
2241 void SetRecordingsSortMode(const char *Directory, eRecordingsSortMode SortMode)
2242 {
2243  if (FILE *f = fopen(AddDirectory(Directory, SORTMODEFILE), "w")) {
2244  fputs(cString::sprintf("%d\n", SortMode), f);
2245  fclose(f);
2246  }
2247 }
2248 
2249 void IncRecordingsSortMode(const char *Directory)
2250 {
2251  GetRecordingsSortMode(Directory);
2256 }