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