/*
 * recording.c: Recording file handling
 *
 * See the main source file 'vdr.c' for copyright information and
 * how to reach the author.
 *
 * $Id: recording.c 2.0 2008/02/24 10:28:53 kls Exp $
 */

#include "recording.h"
#ifdef USE_WAREAGLEICON
#include "iconpatch.h"
#endif /* WAREAGLEICON */
#include <ctype.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include "channels.h"
#include "i18n.h"
#include "interface.h"
#include "remux.h" //XXX+ I_FRAME
#include "skins.h"
#include "tools.h"
#include "videodir.h"

#if defined (USE_DVDCHAPJUMP) && defined (USE_DVDARCHIVE)
#include <assert.h>
/* libdvdread stuff */
#include <dvdread/dvd_reader.h>
#include <dvdread/ifo_types.h>
#include <dvdread/ifo_read.h>
#endif /* DVDCHAPJUMP & DVDARCHIVE */

#define SUMMARYFALLBACK

#define RECEXT       ".rec"
#define DELEXT       ".del"
/* This was the original code, which works fine in a Linux only environment.
   Unfortunately, because of Windows and its brain dead file system, we have
   to use a more complicated approach, in order to allow users who have enabled
   the VFAT compile time option to see their recordings even if they forget to
   enable VFAT when compiling a new version of VDR... Gee, do I hate Windows.
   (kls 2002-07-27)
#define DATAFORMAT   "%4d-%02d-%02d.%02d:%02d.%02d.%02d" RECEXT
#define NAMEFORMAT   "%s/%s/" DATAFORMAT
*/
#define DATAFORMAT   "%4d-%02d-%02d.%02d%*c%02d.%02d.%02d" RECEXT
#define NAMEFORMAT   "%s/%s/" "%4d-%02d-%02d.%02d.%02d.%02d.%02d" RECEXT

#define RESUMEFILESUFFIX  "/resume%s%s.vdr"
#ifdef SUMMARYFALLBACK
#define SUMMARYFILESUFFIX "/summary.vdr"
#endif
#define INFOFILESUFFIX    "/info.vdr"
#define MARKSFILESUFFIX   "/marks.vdr"
#ifdef USE_LIEMIEXT
#define INDEXFILESUFFIX   "/index.vdr"
#endif /* LIEMIEXT */
#ifdef USE_DVDARCHIVE
#define DVDARCHIVEFILENAME "/dvd.vdr"
#endif /* DVDARCHIVE */

#define MINDISKSPACE 1024 // MB

#define REMOVECHECKDELTA   60 // seconds between checks for removing deleted files
#define DELETEDLIFETIME   300 // seconds after which a deleted recording will be actually removed
#define DISKCHECKDELTA    100 // seconds between checks for free disk space
#define REMOVELATENCY      10 // seconds to wait until next check after removing a file

#define TIMERMACRO_TITLE    "TITLE"
#define TIMERMACRO_EPISODE  "EPISODE"

#define MAX_SUBTITLE_LENGTH  40

#define MAX_LINK_LEVEL  6

bool VfatFileSystem = false;
#ifdef USE_LIEMIEXT
bool DirOrderState = false;
#endif /* LIEMIEXT */

#ifdef USE_DVLFRIENDLYFNAMES
char *MakeFriendlyFilename(char **buf);
#endif /* DVLFRIENDLYFNAMES */

cRecordings DeletedRecordings(true);

// --- cRemoveDeletedRecordingsThread ----------------------------------------

class cRemoveDeletedRecordingsThread : public cThread {
protected:
  virtual void Action(void);
public:
  cRemoveDeletedRecordingsThread(void);
  };

cRemoveDeletedRecordingsThread::cRemoveDeletedRecordingsThread(void)
:cThread("remove deleted recordings")
{
}

void cRemoveDeletedRecordingsThread::Action(void)
{
  SetPriority(19);
  // Make sure only one instance of VDR does this:
  cLockFile LockFile(VideoDirectory);
  if (LockFile.Lock()) {
     bool deleted = false;
     cThreadLock DeletedRecordingsLock(&DeletedRecordings);
     for (cRecording *r = DeletedRecordings.First(); r; ) {
         if (r->deleted && time(NULL) - r->deleted > DELETEDLIFETIME) {
            cRecording *next = DeletedRecordings.Next(r);
            r->Remove();
            DeletedRecordings.Del(r);
            r = next;
            deleted = true;
            continue;
            }
         r = DeletedRecordings.Next(r);
         }
     if (deleted)
        RemoveEmptyVideoDirectories();
     }
}

static cRemoveDeletedRecordingsThread RemoveDeletedRecordingsThread;

// ---

void RemoveDeletedRecordings(void)
{
  static time_t LastRemoveCheck = 0;
  if (time(NULL) - LastRemoveCheck > REMOVECHECKDELTA) {
     if (!RemoveDeletedRecordingsThread.Active()) {
        cThreadLock DeletedRecordingsLock(&DeletedRecordings);
        for (cRecording *r = DeletedRecordings.First(); r; r = DeletedRecordings.Next(r)) {
            if (r->deleted && time(NULL) - r->deleted > DELETEDLIFETIME) {
               RemoveDeletedRecordingsThread.Start();
               break;
               }
            }
        }
     LastRemoveCheck = time(NULL);
     }
}

void AssertFreeDiskSpace(int Priority, bool Force)
{
  static cMutex Mutex;
  cMutexLock MutexLock(&Mutex);
  // With every call to this function we try to actually remove
  // a file, or mark a file for removal ("delete" it), so that
  // it will get removed during the next call.
  static time_t LastFreeDiskCheck = 0;
  int Factor = (Priority == -1) ? 10 : 1;
  if (Force || time(NULL) - LastFreeDiskCheck > DISKCHECKDELTA / Factor) {
     if (!VideoFileSpaceAvailable(MINDISKSPACE)) {
        // Make sure only one instance of VDR does this:
        cLockFile LockFile(VideoDirectory);
        if (!LockFile.Lock())
           return;
        // Remove the oldest file that has been "deleted":
        isyslog("low disk space while recording, trying to remove a deleted recording...");
        cThreadLock DeletedRecordingsLock(&DeletedRecordings);
        if (DeletedRecordings.Count()) {
           cRecording *r = DeletedRecordings.First();
           cRecording *r0 = NULL;
           while (r) {
                 if (IsOnVideoDirectoryFileSystem(r->FileName())) { // only remove recordings that will actually increase the free video disk space
                    if (!r0 || r->start < r0->start)
                       r0 = r;
                    }
                 r = DeletedRecordings.Next(r);
                 }
           if (r0 && r0->Remove()) {
              DeletedRecordings.Del(r0);
              LastFreeDiskCheck += REMOVELATENCY / Factor;
              return;
              }
           }
        else {
           // DeletedRecordings was empty, so to be absolutely sure there are no
           // deleted recordings we need to double check:
           DeletedRecordings.Update(true);
           if (DeletedRecordings.Count())
              return; // the next call will actually remove it
           }
        // No "deleted" files to remove, so let's see if we can delete a recording:
        isyslog("...no deleted recording found, trying to delete an old recording...");
        cThreadLock RecordingsLock(&Recordings);
        if (Recordings.Count()) {
           cRecording *r = Recordings.First();
           cRecording *r0 = NULL;
           while (r) {
                 if (IsOnVideoDirectoryFileSystem(r->FileName())) { // only delete recordings that will actually increase the free video disk space
                    if (!r->IsEdited() && r->lifetime < MAXLIFETIME) { // edited recordings and recordings with MAXLIFETIME live forever
                       if ((r->lifetime == 0 && Priority > r->priority) || // the recording has no guaranteed lifetime and the new recording has higher priority
                           (r->lifetime > 0 && (time(NULL) - r->start) / SECSINDAY >= r->lifetime)) { // the recording's guaranteed lifetime has expired
                          if (r0) {
                             if (r->priority < r0->priority || (r->priority == r0->priority && r->start < r0->start))
                                r0 = r; // in any case we delete the one with the lowest priority (or the older one in case of equal priorities)
                             }
                          else
                             r0 = r;
                          }
                       }
                    }
                 r = Recordings.Next(r);
                 }
           if (r0 && r0->Delete()) {
              Recordings.Del(r0);
              return;
              }
           }
        // Unable to free disk space, but there's nothing we can do about that...
        isyslog("...no old recording found, giving up");
        Skins.QueueMessage(mtWarning, tr("Low disk space!"), 5, -1);
        }
     LastFreeDiskCheck = time(NULL);
     }
}

// --- cResumeFile -----------------------------------------------------------

cResumeFile::cResumeFile(const char *FileName)
{
  fileName = MALLOC(char, strlen(FileName) + strlen(RESUMEFILESUFFIX) + 1);
  if (fileName) {
     strcpy(fileName, FileName);
     sprintf(fileName + strlen(fileName), RESUMEFILESUFFIX, Setup.ResumeID ? "." : "", Setup.ResumeID ? *itoa(Setup.ResumeID) : "");
     }
  else
     esyslog("ERROR: can't allocate memory for resume file name");
}

cResumeFile::~cResumeFile()
{
  free(fileName);
}

int cResumeFile::Read(void)
{
  int resume = -1;
  if (fileName) {
     struct stat st;
     if (stat(fileName, &st) == 0) {
        if ((st.st_mode & S_IWUSR) == 0) // no write access, assume no resume
           return -1;
        }
     int f = open(fileName, O_RDONLY);
     if (f >= 0) {
        if (safe_read(f, &resume, sizeof(resume)) != sizeof(resume)) {
           resume = -1;
           LOG_ERROR_STR(fileName);
           }
        close(f);
        }
     else if (errno != ENOENT)
        LOG_ERROR_STR(fileName);
     }
  return resume;
}

bool cResumeFile::Save(int Index)
{
  if (fileName) {
     int f = open(fileName, O_WRONLY | O_CREAT | O_TRUNC, DEFFILEMODE);
     if (f >= 0) {
        if (safe_write(f, &Index, sizeof(Index)) < 0)
           LOG_ERROR_STR(fileName);
        close(f);
        Recordings.ResetResume(fileName);
        return true;
        }
     }
  return false;
}

void cResumeFile::Delete(void)
{
  if (fileName) {
     if (remove(fileName) < 0 && errno != ENOENT)
        LOG_ERROR_STR(fileName);
     Recordings.ResetResume(fileName);
     }
}

// --- cRecordingInfo --------------------------------------------------------

cRecordingInfo::cRecordingInfo(const cChannel *Channel, const cEvent *Event)
{
  channelID = Channel ? Channel->GetChannelID() : tChannelID::InvalidID;
  channelName = Channel ? strdup(Channel->Name()) : NULL;
  ownEvent = Event ? NULL : new cEvent(0);
  event = ownEvent ? ownEvent : Event;
  aux = NULL;
  if (Channel) {
     // Since the EPG data's component records can carry only a single
     // language code, let's see whether the channel's PID data has
     // more information:
     cComponents *Components = (cComponents *)event->Components();
     if (!Components)
        Components = new cComponents;
     for (int i = 0; i < MAXAPIDS; i++) {
         const char *s = Channel->Alang(i);
         if (*s) {
            tComponent *Component = Components->GetComponent(i, 2, 3);
            if (!Component)
               Components->SetComponent(Components->NumComponents(), 2, 3, s, NULL);
            else if (strlen(s) > strlen(Component->language))
               strn0cpy(Component->language, s, sizeof(Component->language));
            }
         }
     // There's no "multiple languages" for Dolby Digital tracks, but
     // we do the same procedure here, too, in case there is no component
     // information at all:
     for (int i = 0; i < MAXDPIDS; i++) {
         const char *s = Channel->Dlang(i);
         if (*s) {
            tComponent *Component = Components->GetComponent(i, 2, 5);
            if (!Component)
               Components->SetComponent(Components->NumComponents(), 2, 5, s, NULL);
            else if (strlen(s) > strlen(Component->language))
               strn0cpy(Component->language, s, sizeof(Component->language));
            }
         }
     // The same applies to subtitles:
     for (int i = 0; i < MAXSPIDS; i++) {
         const char *s = Channel->Slang(i);
         if (*s) {
            tComponent *Component = Components->GetComponent(i, 3, 3);
            if (!Component)
               Components->SetComponent(Components->NumComponents(), 3, 3, s, NULL);
            else if (strlen(s) > strlen(Component->language))
               strn0cpy(Component->language, s, sizeof(Component->language));
            }
         }
     if (Components != event->Components())
        ((cEvent *)event)->SetComponents(Components);
     }
}

cRecordingInfo::~cRecordingInfo()
{
  delete ownEvent;
  free(aux);
  free(channelName);
}

void cRecordingInfo::SetData(const char *Title, const char *ShortText, const char *Description)
{
  if (!isempty(Title))
     ((cEvent *)event)->SetTitle(Title);
  if (!isempty(ShortText))
     ((cEvent *)event)->SetShortText(ShortText);
  if (!isempty(Description))
     ((cEvent *)event)->SetDescription(Description);
}

void cRecordingInfo::SetAux(const char *Aux)
{
  free(aux);
  aux = Aux ? strdup(Aux) : NULL;
}

bool cRecordingInfo::Read(FILE *f)
{
  if (ownEvent) {
     cReadLine ReadLine;
     char *s;
     int line = 0;
     while ((s = ReadLine.Read(f)) != NULL) {
           ++line;
           char *t = skipspace(s + 1);
           switch (*s) {
             case 'C': {
                         char *p = strchr(t, ' ');
                         if (p) {
                            free(channelName);
                            channelName = strdup(compactspace(p));
                            *p = 0; // strips optional channel name
                            }
                         if (*t)
                            channelID = tChannelID::FromString(t);
                       }
                       break;
             case 'E': {
                         unsigned int EventID;
                         time_t StartTime;
                         int Duration;
                         unsigned int TableID = 0;
                         unsigned int Version = 0xFF;
                         int n = sscanf(t, "%u %ld %d %X %X", &EventID, &StartTime, &Duration, &TableID, &Version);
                         if (n >= 3 && n <= 5) {
                            ownEvent->SetEventID(EventID);
                            ownEvent->SetStartTime(StartTime);
                            ownEvent->SetDuration(Duration);
                            ownEvent->SetTableID(TableID);
                            ownEvent->SetVersion(Version);
                            }
                       }
                       break;
             case '@': free(aux);
                       aux = strdup(t);
                       break;
             case '#': break; // comments are ignored
             default: if (!ownEvent->Parse(s)) {
                         esyslog("ERROR: EPG data problem in line %d", line);
                         return false;
                         }
                      break;
             }
           }
     return true;
     }
  return false;
}

bool cRecordingInfo::Write(FILE *f, const char *Prefix) const
{
  if (channelID.Valid())
     fprintf(f, "%sC %s%s%s\n", Prefix, *channelID.ToString(), channelName ? " " : "", channelName ? channelName : "");
  event->Dump(f, Prefix, true);
  if (aux)
     fprintf(f, "%s@ %s\n", Prefix, aux);
  return true;
}

// --- cRecording ------------------------------------------------------------

#define RESUME_NOT_INITIALIZED (-2)

struct tCharExchange { char a; char b; };
tCharExchange CharExchange[] = {
  { '~',  '/'    },
  { '/',  '~'    },
  { ' ',  '_'    },
  // backwards compatibility:
  { '\'', '\''   },
  { '\'', '\x01' },
  { '/',  '\x02' },
  { 0, 0 }
  };

char *ExchangeChars(char *s, bool ToFileSystem)
{
  char *p = s;
  while (*p) {
        if (VfatFileSystem) {
           // The VFAT file system can't handle all characters, so we
           // have to take extra efforts to encode/decode them:
           if (ToFileSystem) {
              const char *InvalidChars = "\"\\/:*?|<>#";
              switch (*p) {
                     // characters that can be mapped to other characters:
                     case ' ': *p = '_'; break;
                     case '~': *p = '/'; break;
                     // characters that have to be encoded:
                     default:
                       if (strchr(InvalidChars, *p) || *p == '.' && (!*(p + 1) || *(p + 1) == '~')) { // Windows can't handle '.' at the end of file/directory names
                          int l = p - s;
                          s = (char *)realloc(s, strlen(s) + 10);
                          p = s + l;
                          char buf[4];
                          sprintf(buf, "#%02X", (unsigned char)*p);
                          memmove(p + 2, p, strlen(p) + 1);
                          strncpy(p, buf, 3);
                          p += 2;
                          }
                     }
              }
           else {
              switch (*p) {
                // mapped characters:
                case '_': *p = ' '; break;
                case '/': *p = '~'; break;
                // encoded characters:
                case '#': {
                     if (strlen(p) > 2 && isxdigit(*(p + 1)) && isxdigit(*(p + 2))) {
                        char buf[3];
                        sprintf(buf, "%c%c", *(p + 1), *(p + 2));
                        unsigned char c = strtol(buf, NULL, 16);
                        if (c) {
                           *p = c;
                           memmove(p + 1, p + 3, strlen(p) - 2);
                           }
                        }
                     }
                     break;
                // backwards compatibility:
                case '\x01': *p = '\''; break;
                case '\x02': *p = '/';  break;
                case '\x03': *p = ':';  break;
                }
              }
           }
        else {
           for (struct tCharExchange *ce = CharExchange; ce->a && ce->b; ce++) {
               if (*p == (ToFileSystem ? ce->a : ce->b)) {
                  *p = ToFileSystem ? ce->b : ce->a;
                  break;
                  }
               }
           }
        p++;
        }
  return s;
}

cRecording::cRecording(cTimer *Timer, const cEvent *Event)
{
  resume = RESUME_NOT_INITIALIZED;
  titleBuffer = NULL;
#ifdef USE_SORTRECORDS
  for (int i = 0; i < MAXSORTMODES; i++) {
      sortBuffer[i] = NULL;
      lastDirsFirst[i] = -1;
      }
#else
  sortBuffer = NULL;
#endif /* SORTRECORDS */
  fileName = NULL;
  name = NULL;
#ifdef USE_DVDARCHIVE
  dvdname = NULL;
  dvdtrack = NULL;
  dvdchapters = NULL;
  isArchived = false;
  isOnlyOnDvd = false;
  dvdtype = DVD_TYPE_UNKNOWN;
#endif /* DVDARCHIVE */
  fileSizeMB = -1; // unknown
  deleted = 0;
  // set up the actual name:
  const char *Title = Event ? Event->Title() : NULL;
  const char *Subtitle = Event ? Event->ShortText() : NULL;
  char SubtitleBuffer[MAX_SUBTITLE_LENGTH];
  if (isempty(Title))
     Title = Timer->Channel()->Name();
  if (isempty(Subtitle))
     Subtitle = " ";
  else if (strlen(Subtitle) > MAX_SUBTITLE_LENGTH) {
     // let's make sure the Subtitle doesn't produce too long a file name:
     Utf8Strn0Cpy(SubtitleBuffer, Subtitle, MAX_SUBTITLE_LENGTH);
     Subtitle = SubtitleBuffer;
     }
  char *macroTITLE   = strstr(Timer->File(), TIMERMACRO_TITLE);
  char *macroEPISODE = strstr(Timer->File(), TIMERMACRO_EPISODE);
  if (macroTITLE || macroEPISODE) {
     name = strdup(Timer->File());
     name = strreplace(name, TIMERMACRO_TITLE, Title);
     name = strreplace(name, TIMERMACRO_EPISODE, Subtitle);
     // avoid blanks at the end:
     int l = strlen(name);
     while (l-- > 2) {
           if (name[l] == ' ' && name[l - 1] != '~')
              name[l] = 0;
           else
              break;
           }
     if (Timer->IsSingleEvent()) {
#ifdef USE_DVLFRIENDLYFNAMES
    if (Setup.UseFriendlyFNames == 1)
       Timer -> SetFile(MakeFriendlyFilename(&name));
    else
#endif /* DVLFRIENDLYFNAMES */
        Timer->SetFile(name); // this was an instant recording, so let's set the actual data
        Timers.SetModified();
        }
     }
  else if (Timer->IsSingleEvent() || !Setup.UseSubtitle)
     name = strdup(Timer->File());
  else
     name = strdup(cString::sprintf("%s~%s", Timer->File(), Subtitle));
  // substitute characters that would cause problems in file names:
  strreplace(name, '\n', ' ');
#ifdef USE_DVLFRIENDLYFNAMES
  if (Setup.UseFriendlyFNames == 1)
     MakeFriendlyFilename(&name);
#endif /* DVLFRIENDLYFNAMES */
  start = Timer->StartTime();
  priority = Timer->Priority();
  lifetime = Timer->Lifetime();
  // handle info:
  info = new cRecordingInfo(Timer->Channel(), Event);
  info->SetAux(Timer->Aux());
}

cRecording::cRecording(const char *FileName)
{
  resume = RESUME_NOT_INITIALIZED;
  fileSizeMB = -1; // unknown
  deleted = 0;
  titleBuffer = NULL;
#ifdef USE_SORTRECORDS
  for (int i = 0; i < MAXSORTMODES; i++) {
      sortBuffer[i] = NULL;
      lastDirsFirst[i] = -1;
      }
#else
  sortBuffer = NULL;
#endif /* SORTRECORDS */
  fileName = strdup(FileName);
  FileName += strlen(VideoDirectory) + 1;
  char *p = strrchr(FileName, '/');

  name = NULL;
#ifdef USE_DVDARCHIVE
  dvdname = NULL;
  dvdtrack = NULL;
  dvdchapters = NULL;
  isArchived = false;
  isOnlyOnDvd = false;
  dvdtype = DVD_TYPE_NOT_READ;
#endif /* DVDARCHIVE */
  info = new cRecordingInfo;
  if (p) {
     time_t now = time(NULL);
     struct tm tm_r;
     struct tm t = *localtime_r(&now, &tm_r); // this initializes the time zone in 't'
     t.tm_isdst = -1; // makes sure mktime() will determine the correct DST setting
     if (7 == sscanf(p + 1, DATAFORMAT, &t.tm_year, &t.tm_mon, &t.tm_mday, &t.tm_hour, &t.tm_min, &priority, &lifetime)) {
        t.tm_year -= 1900;
        t.tm_mon--;
        t.tm_sec = 0;
        start = mktime(&t);
        name = MALLOC(char, p - FileName + 1);
        strncpy(name, FileName, p - FileName);
        name[p - FileName] = 0;
        name = ExchangeChars(name, false);
        }
     GetResume();
     // read an optional info file:
     cString InfoFileName = cString::sprintf("%s%s", fileName, INFOFILESUFFIX);
     FILE *f = fopen(InfoFileName, "r");
     if (f) {
        if (!info->Read(f))
           esyslog("ERROR: EPG data problem in file %s", *InfoFileName);
        fclose(f);
        }
     else if (errno != ENOENT)
        LOG_ERROR_STR(*InfoFileName);
#ifdef SUMMARYFALLBACK
     // fall back to the old 'summary.vdr' if there was no 'info.vdr':
     if (isempty(info->Title())) {
        cString SummaryFileName = cString::sprintf("%s%s", fileName, SUMMARYFILESUFFIX);
        FILE *f = fopen(SummaryFileName, "r");
        if (f) {
           int line = 0;
           char *data[3] = { NULL };
           cReadLine ReadLine;
           char *s;
           while ((s = ReadLine.Read(f)) != NULL) {
                 if (*s || line > 1) {
                    if (data[line]) {
                       int len = strlen(s);
                       len += strlen(data[line]) + 1;
                       data[line] = (char *)realloc(data[line], len + 1);
                       strcat(data[line], "\n");
                       strcat(data[line], s);
                       }
                    else
                       data[line] = strdup(s);
                    }
                 else
                    line++;
                 }
           fclose(f);
           if (!data[2]) {
              data[2] = data[1];
              data[1] = NULL;
              }
           else if (data[1] && data[2]) {
              // if line 1 is too long, it can't be the short text,
              // so assume the short text is missing and concatenate
              // line 1 and line 2 to be the long text:
              int len = strlen(data[1]);
              if (len > 80) {
                 data[1] = (char *)realloc(data[1], len + 1 + strlen(data[2]) + 1);
                 strcat(data[1], "\n");
                 strcat(data[1], data[2]);
                 free(data[2]);
                 data[2] = data[1];
                 data[1] = NULL;
                 }
              }
           info->SetData(data[0], data[1], data[2]);
           for (int i = 0; i < 3; i ++)
               free(data[i]);
           }
        else if (errno != ENOENT)
           LOG_ERROR_STR(*SummaryFileName);
        }
#endif
#ifdef USE_DVDARCHIVE
     if (CheckFileExistence("dvd.vdr")) {
        GetDvdName(fileName);
        isArchived = true;
        if (!CheckFileExistence("001.vdr"))
           isOnlyOnDvd = true;
        }
#endif /* DVDARCHIVE */
     }
}

cRecording::~cRecording()
{
  free(titleBuffer);
#ifdef USE_SORTRECORDS
  for (int i = 0; i < MAXSORTMODES; i++) {
      free(sortBuffer[i]);
      }
#else
  free(sortBuffer);
#endif /* SORTRECORDS */
  free(fileName);
  free(name);
#ifdef USE_DVDARCHIVE
  free(dvdname);
  free(dvdtrack);
  free(dvdchapters);
#endif /* DVDARCHIVE */
  delete info;
}

char *cRecording::StripEpisodeName(char *s)
{
  char *t = s, *s1 = NULL, *s2 = NULL;
  while (*t) {
        if (*t == '/') {
           if (s1) {
              if (s2)
                 s1 = s2;
              s2 = t;
              }
           else
              s1 = t;
           }
        t++;
        }
  if (s1 && s2)
#ifdef USE_SORTRECORDS
    if (Setup.RecordingsSortDirsFirst)
       *s1 = 'b';

    if ((Setup.RecordingsSortMode <= 1 && s1 != s && !strchr(".-$ª·", *(s1 - 1))) ||
        (Setup.RecordingsSortMode == 1 && s1 == s) ||
        (Setup.RecordingsSortMode == 3))
#endif /* SORTRECORDS */
     memmove(s1 + 1, s2, t - s2 + 1);
  return s;
}

char *cRecording::SortName(void) const
{
#ifdef USE_SORTRECORDS
  if (!sortBuffer[Setup.RecordingsSortMode] ||
        lastDirsFirst[Setup.RecordingsSortMode] != Setup.RecordingsSortDirsFirst) {
     free(sortBuffer[Setup.RecordingsSortMode]);
     lastDirsFirst[Setup.RecordingsSortMode] = Setup.RecordingsSortDirsFirst;
     char *s = StripEpisodeName(strdup(FileName() + strlen(VideoDirectory)));
#else
  if (!sortBuffer) {
     char *s = StripEpisodeName(strdup(FileName() + strlen(VideoDirectory) + 1));
#endif /* SORTRECORDS */
     strreplace(s, '/', 'a'); // some locales ignore '/' when sorting
     int l = strxfrm(NULL, s, 0) + 1;
#ifdef USE_SORTRECORDS
     sortBuffer[Setup.RecordingsSortMode] = MALLOC(char, l);
     strxfrm(sortBuffer[Setup.RecordingsSortMode], s, l);
#else
     sortBuffer = MALLOC(char, l);
     strxfrm(sortBuffer, s, l);
#endif /* SORTRECORDS */
     free(s);
     }
#ifdef USE_SORTRECORDS
  return sortBuffer[Setup.RecordingsSortMode];
#else
  return sortBuffer;
#endif /* SORTRECORDS */
}

int cRecording::GetResume(void) const
{
  if (resume == RESUME_NOT_INITIALIZED) {
     cResumeFile ResumeFile(FileName());
     resume = ResumeFile.Read();
     }
  return resume;
}

int cRecording::Compare(const cListObject &ListObject) const
{
  cRecording *r = (cRecording *)&ListObject;
#ifdef USE_SORTRECORDS
  return Recordings.GetSortOrder() * strcasecmp(SortName(), r->SortName());
#else
#ifdef USE_LIEMIEXT
  if (DirOrderState)
     return strcasecmp(FileName(), r->FileName());
#endif /* LIEMIEXT */
  return strcasecmp(SortName(), r->SortName());
#endif  /* USE_SORTRECORDS */
}

const char *cRecording::FileName(void) const
{
  if (!fileName) {
     struct tm tm_r;
     struct tm *t = localtime_r(&start, &tm_r);
     name = ExchangeChars(name, true);
     fileName = strdup(cString::sprintf(NAMEFORMAT, VideoDirectory, name, t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, priority, lifetime));
     name = ExchangeChars(name, false);
     }
  return fileName;
}

#ifdef USE_DVDARCHIVE
bool cRecording::CheckFileExistence(const char* FileNameToTest, const bool useVideoDir) const
{
  if (!useVideoDir || (useVideoDir && FileName())) {
     cString filename = cString::sprintf("%s%s%s", useVideoDir ? FileName() : "",
                                                   useVideoDir ? "/" : "",
                                                   FileNameToTest);
     struct stat statBuf;
     if (lstat(filename, &statBuf) == -1) return false;
     return S_ISREG(statBuf.st_mode) || S_ISLNK(statBuf.st_mode);
     }
  return false;
}

bool cRecording::GetDvdName(const char* Directory) const
{
  char* filename = (char*)alloca(strlen(Directory) + strlen(DVDARCHIVEFILENAME) + 1);
  if (filename) {
     strcpy(filename, Directory);
     char *end = filename + strlen(filename);
     strcpy(end, DVDARCHIVEFILENAME);
     FILE* file;
     if ((file = fopen(filename, "r"))) {
        cReadLine ReadLine;
        char* buffer = (char*)alloca(BUFSIZ);
        if (buffer) {
           buffer = ReadLine.Read(file);
           if (buffer)
              ((cRecording*)this)->dvdname = strdup(buffer);
           else
              ((cRecording*)this)->dvdname = NULL;

           buffer = ReadLine.Read(file);
           if (buffer) {
              ((cRecording*)this)->dvdtrack = strdup(buffer);
              if (atoi(buffer) == 0)
                 ((cRecording*)this)->dvdtype = DVD_VIDEO_TYPE;
              else
                 ((cRecording*)this)->dvdtype = DVD_VIDEO_ARCHIVE_TYPE;
              }
           else {
              ((cRecording*)this)->dvdtrack = NULL;
              ((cRecording*)this)->dvdtype = DVD_ARCHIVE_TYPE;
              }

           fclose(file);
           return true;
           }
        }
     }
     return false;
}

bool cRecording::GetDvdChaptersFromDvd(int title) const
{
#ifdef USE_DVDCHAPJUMP
  cString buf;

  dvd_reader_t *dvd;
  ifo_handle_t *ifo_file;
  tt_srpt_t *tt_srpt;
  ifo_handle_t *vts_file;
  pgc_t *cur_pgc;

  dvd = DVDOpen(DVD_DEVICE);
  if (!dvd) {
     esyslog("DVD-ARCHIVE: Couldn't open DVD device %s!", DVD_DEVICE);
     return false;
     }

  /* open title manager */
  ifo_file = ifoOpen(dvd,0);
  if (!ifo_file) {
     esyslog("DVD-ARCHIVE: Can't open VMG info.");
     DVDClose(dvd);
     return false;
     }

  /* read total_title */
  tt_srpt = ifo_file->tt_srpt;

  /* get total chapters */
  int title_set_nr   = tt_srpt->title[title-1].title_set_nr;
  int total_chap     = tt_srpt->title[title-1].nr_of_ptts;
  int local_title_id = tt_srpt->title[title-1].vts_ttn - 1;

  /* access title set file */
  vts_file = ifoOpen(dvd, title_set_nr);
  if (!vts_file) {
     esyslog("DVD-ARCHIVE: Can't open info file for title set %d!",title_set_nr);
     DVDClose(dvd);
     return false;
     }

  /* find program chain and check programs 
     all chapters should be in the same prog chain and
     should be numbered from 1 to <total_chap>
  */
  {
    vts_ptt_srpt_t *vts_ptt_srpt = vts_file->vts_ptt_srpt;
    int pgc_nr = vts_ptt_srpt->title[local_title_id].ptt[0].pgcn;
    int pg     = vts_ptt_srpt->title[local_title_id].ptt[0].pgn;
    int p;

    assert(pg==1);
    for (p=1; p<total_chap; p++) {
        int next_pg;
        int this_pgc;
        this_pgc = vts_ptt_srpt->title[local_title_id].ptt[p].pgcn;
        assert(pgc_nr == this_pgc);
        next_pg = vts_ptt_srpt->title[local_title_id].ptt[p].pgn;
        assert(pg+1 == next_pg);
        pg = next_pg;
        }

    /* fetch program chain */
    cur_pgc = vts_file->vts_pgcit->pgci_srp[pgc_nr-1].pgc;
    assert(cur_pgc->nr_of_programs == total_chap);
  }

  /* --- main cell loop --- */
  {
    pgc_program_map_t *chap_cell;
    cell_playback_t *cell_pb;
    int c;
    int chap;

    /* total cells in chain */
    int total_cell = cur_pgc->nr_of_cells;

    /* get info */
    chap_cell = cur_pgc->program_map;
    cell_pb   = cur_pgc->cell_playback;

    /* loop through all cells */
    chap = -1;
    int position = 0;
    for (c=0; c<total_cell; c++) {
        int cell_mode;
        const char *mode;

        dvd_time_t *time = (dvd_time_t *)&cell_pb->playback_time;

        int framerate = time->frame_u>>6;
        assert(framerate == 1 || framerate == 3);
        int frames_per_sec = (framerate == 1) ? 25 : 30;

        /* upper 4 bits are first digit, down 4 bits are second digit */
        int hour = (time->hour>>4) * 10 + (time->hour&15);
        int minute = (time->minute>>4) * 10 + (time->minute&15);
        int second = (time->second>>4) * 10 + (time->second&15);
        /* upper 4 bits are first digit, down 4 bits are second digit */
        int frame = (time->frame_u>>4&3) * 10 + (time->frame_u&15);

        int frames = ((hour * 3600) + (minute * 60) + second) * frames_per_sec + frame;

        /* this cell is the begin of a new chapter! */
        if (chap_cell[chap+1] == c+1) {
           cString oldbuf = cString::sprintf("%s", *buf);
           buf = cString::sprintf("%s%d%s", *oldbuf, position, ((chap+2) < total_chap) ? "," : "");
           chap++;
           }

      /* cell_mode: 0=normal, 1=first of angle, 2=in angle, 3=last of angle */
      cell_mode = cell_pb->block_mode;
      if ((cell_mode==0) || (cell_mode==1)) {
         /* only account for normal or begin of angle cells */
         position += frames;
         mode = "counted";
         }
      else
         mode = "skipped";

      cell_pb++;
    }
  }

  ifoClose(ifo_file);
  ifoClose(vts_file);
  DVDClose(dvd);

  ((cRecording*)this)->dvdchapters = strdup(buf);

  return true;
#else
  return false;
#endif /* DVDCHAPJUMP */
}

const char *cRecording::GetDvdChapters(void) const
{
  // Read chapters from dvd
  if (dvdtype == DVD_VIDEO_ARCHIVE_TYPE) {
     if (dvdtrack) {
        if (!GetDvdChaptersFromDvd(atoi(dvdtrack)))
           ((cRecording*)this)->dvdchapters = NULL;
        else
           isyslog("DVD-ARCHIVE: Using following positions for chapter jumping: %s", dvdchapters);
        return dvdchapters;
        }
     }
  return NULL;
}

int cRecording::MountDvd(void) const
{
  cString cmd;
  if (Setup.DvdSpeedLimit > 0) {
     cmd = cString::sprintf("speedcontrol -x %d %s", Setup.DvdSpeedLimit, DVD_DEVICE);
     SystemExec(cmd);
     }

  cString msg;
  if (atoi(dvdname) == 0)
     msg = cString::sprintf(tr("Please mount %s"), dvdname);
  else {
     if (Setup.DvdDisplayZeros)
        msg = cString::sprintf(tr("Please mount DVD %04d"), atoi(dvdname));
     else
        msg = cString::sprintf(tr("Please mount DVD %d"), atoi(dvdname));
     }

  bool rep = true;
  while (rep) {
        if (Setup.DvdTrayMode==1 || Setup.DvdTrayMode==3)
           cmd = cString::sprintf("umount %s; eject %s", DVD_DEVICE, DVD_DEVICE);
        else
           cmd = cString::sprintf("umount %s", DVD_DEVICE);
        SystemExec(cmd);

        if (Interface->Confirm(msg, 300)) {
           Skins.Message(mtStatus, tr("Please wait. Checking DVD..."));
           Skins.Flush();
           cmd = cString::sprintf("eject -t %s; mkdir -p %s; mount -o ro -t %s %s %s",
                                                             DVD_DEVICE, DVD_MOUNT_PATH,
                                                             (dvdtrack ? "udf" : "iso9660"),
                                                             DVD_DEVICE, DVD_MOUNT_PATH);
           SystemExec(cmd);

           bool correctDvd = true;

           char *olddvdname, *olddvdtrack;
           int olddvdtype;
           olddvdname = dvdname;
           olddvdtrack = dvdtrack;
           olddvdtype = dvdtype;
           if (GetDvdName(DVD_MOUNT_PATH)) {
              if (atoi(dvdname) != atoi(olddvdname)) correctDvd = false;
              }
           ((cRecording*)this)->dvdname = olddvdname;
           ((cRecording*)this)->dvdtrack = olddvdtrack;
           ((cRecording*)this)->dvdtype = olddvdtype;

           if (correctDvd) {
              if (dvdtrack == NULL) {
                 // Archived DVD in VDR format
                 char fn[BUFSIZ];
                 strcpy(fn, FileName());
                 char *p = strrchr(fn, '/');
                 cmd = cString::sprintf("find '%s' -name '%s'", DVD_MOUNT_PATH, p+1);
                 }
              else {
                 // Either archived DVD in DVD-Video format or DVD-Video which
                 // should be played with the DVD plugin
                 cmd = cString::sprintf("find '%s' -iname 'VIDEO_TS'", DVD_MOUNT_PATH);
                 }

              cReadLine pipe;
              FILE* file;
              char *dirname = NULL;
              if ((file = popen(cmd, "r")) != (FILE *)NULL) {
                 if ((dirname = pipe.Read(file)) != NULL) {
                    pclose(file);
                    if (dvdtrack != NULL && atoi(dvdtrack) == 0) {
                       // It is a valid Video-DVD and DVD plugin can be started
                       return MOUNT_DVD_LAUNCH_DVD_PLUGIN;
                       }
                    else {
                       // It is a valid Archive-DVD or an archived Video-DVD
                       // and the links can now be established
                       cString srcFn;
                       int n = 1;

                       do {
                            if (dvdtrack == NULL)
                               srcFn = cString::sprintf("%s/%03d.vdr", dirname, n);
                            else
                               srcFn = cString::sprintf("%s/VTS_%02d_%d.VOB", dirname, atoi(dvdtrack), n);

                            if (!access(srcFn, R_OK)) {
                               cmd = cString::sprintf("ln -sf '%s' '%s/%03d.vdr'", *srcFn, FileName(), n);
                               SystemExec(cmd);
                               isyslog("DVD-ARCHIVE: Linking %s/%03d.vdr -> %s", FileName(), n, *srcFn);
                               }
                            else
                               break;
                          } while ( ++n < 999);

                       if (!CheckFileExistence("index.vdr")) {
                          if (dvdtrack == NULL)
                             srcFn = cString::sprintf("%s/index.vdr", dirname);
                          else
                             srcFn = cString::sprintf("%s/index_%02d.vdr", dirname, atoi(dvdtrack));

                          if (!CheckFileExistence(srcFn, false)) {
                             msg = cString::sprintf(tr("No index-file found. Creating may take minutes. Create one?"));
                             if (Interface->Confirm(msg, 300)) {
                                Skins.Message(mtStatus, tr("Please wait. Creating index-file..."));
                                cmd = cString::sprintf("speedcontrol -x 999 %s; cd %s && genindex &", DVD_DEVICE, FileName());
                                SystemExec(cmd);
                                return MOUNT_DVD_ABORT;
                                }
                             }
                          else {
                             cmd = cString::sprintf("ln -sf '%s' '%s/index.vdr'", *srcFn, FileName());
                             SystemExec(cmd);
                             isyslog("DVD-ARCHIVE: Linking %s/index.vdr -> %s", FileName(), *srcFn);
                             }
                          }
                       return MOUNT_DVD_REPLAY;
                       }
                    }
                 else {
                    Skins.Message(mtError, tr("Wrong DVD!"), 3);
                    Skins.Flush();
                    }
                 }
              pclose(file);
              }
           else {
              Skins.Message(mtError, tr("Wrong DVD!"), 3);
              Skins.Flush();
              }
           }
        else {
           rep = false;
           }
        }
  return MOUNT_DVD_ABORT;
}

#endif /* DVDARCHIVE */
#ifdef USE_LIEMIEXT
const char *cRecording::Title(char Delimiter, bool NewIndicator, int Level, bool Original) const
#else
const char *cRecording::Title(char Delimiter, bool NewIndicator, int Level) const
#endif /* LIEMIEXT */
{
#ifdef USE_WAREAGLEICON
  const char *New = NewIndicator && IsNew() ? Setup.WarEagleIcons ? IsLangUtf8() ? ICON_NEW_UTF8 : ICON_NEW : "*" : " ";
#else
  char New = NewIndicator && IsNew() ? '*' : ' ';
#endif /* WAREAGLEICON */
  free(titleBuffer);
  titleBuffer = NULL;
  if (Level < 0 || Level == HierarchyLevels()) {
     struct tm tm_r;
     struct tm *t = localtime_r(&start, &tm_r);
     char *s;
     if (Level > 0 && (s = strrchr(name, '~')) != NULL)
        s++;
     else
        s = name;
#ifdef USE_LIEMIEXT
     if (Original) {
#endif /* LIEMIEXT */
#ifdef USE_WAREAGLEICON
     titleBuffer = strdup(cString::sprintf("%02d.%02d.%02d%c%02d:%02d%s%c%s",
#else
     titleBuffer = strdup(cString::sprintf("%02d.%02d.%02d%c%02d:%02d%c%c%s",
#endif /* WAREAGLEICON */
                            t->tm_mday,
                            t->tm_mon + 1,
                            t->tm_year % 100,
                            Delimiter,
                            t->tm_hour,
                            t->tm_min,
                            New,
                            Delimiter,
                            s));
#ifdef USE_LIEMIEXT
        }
     else {
        cString RecLength("---");
        if (Setup.ShowRecLength && FileName()) {
           cString filename = cString::sprintf("%s%s", FileName(), INDEXFILESUFFIX);
           if (*filename) {
              if (access(filename, R_OK) == 0) {
                 struct stat buf;
                 if (stat(filename, &buf) == 0) {
                    struct tIndex { int offset; uchar type; uchar number; short reserved; };
                    int delta = buf.st_size % sizeof(tIndex);
                    if (delta) {
                       delta = sizeof(tIndex) - delta;
                       esyslog("ERROR: invalid file size (%ld) in '%s'", buf.st_size, *filename);
                       }
                    RecLength = cString::sprintf("%ld'", (buf.st_size + delta) / sizeof(tIndex) / SecondsToFrames(60));
                    }
                 }
              }
           }
#endif /* LIEMIEXT */
#ifdef USE_DVDARCHIVE
#ifdef USE_WAREAGLEICON
        if (isArchived && !isOnlyOnDvd) New = Setup.WarEagleIcons ? IsLangUtf8() ? ICON_DVD_UTF8 : ICON_DVD : "~";
#else
        if (isArchived && !isOnlyOnDvd) New = '~';
#endif /* WAREAGLEICON */

        if (isOnlyOnDvd && Setup.DvdDisplayMode >= 1) {
           char oldLength[21];

           if (strrchr(RecLength, '\''))
              sprintf(oldLength,"%s", *RecLength);
           else
              oldLength[0] = 0;

           if (dvdname) {
              if (atoi(dvdname) != 0) {
                 cString tmp;
                 if (Setup.DvdDisplayZeros)
                    tmp = cString::sprintf("%04d", atoi(dvdname));
                 else {
                    int num = atoi(dvdname);
                    bool displaySpace = !(Setup.DvdDisplayMode == 1 && oldLength[0] != 0);
                    // ugly hack to have 2 spaces instead of one 0 for each place
                    tmp = cString::sprintf("%s%s%s%d", displaySpace && (num < 1000) ? "  " : "",
                                                       displaySpace && (num < 100)  ? "  " : "",
                                                       displaySpace && (num < 10)   ? "  " : "",
                                                       num);
                    }
                 ((cRecording*)this)->dvdname = strdup(tmp);
                 }
              }

           RecLength = strdup(cString::sprintf("%s%s%s%s", (Setup.ShowRecLength && (Setup.DvdDisplayMode == 1) && (oldLength[0] != 0)) ? oldLength : "",
                                                           (dvdname && isArchived && isOnlyOnDvd && Setup.ShowRecLength && (Setup.DvdDisplayMode == 1) && (oldLength[0] != 0)) ? " / " : "",
                                                           (dvdname && isArchived && isOnlyOnDvd) ? dvdname : "",
                                                           (dvdname && isArchived && isOnlyOnDvd) ? " " : ""
                                                           ));
           }
#endif /* DVDARCHIVE */
#ifdef USE_LIEMIEXT
        cString RecDate = cString::sprintf("%02d.%02d.%02d", t->tm_mday, t->tm_mon + 1, t->tm_year % 100);
        cString RecTime = cString::sprintf("%02d:%02d", t->tm_hour, t->tm_min);
        cString RecDelimiter = cString::sprintf("%c", Delimiter);
#ifdef USE_WAREAGLEICON
        titleBuffer = strdup(cString::sprintf("%s%s%s%s%s%s%s%s",
#else
        titleBuffer = strdup(cString::sprintf("%s%s%s%c%s%s%s%s",
#endif /* WAREAGLEICON */
                               (Setup.ShowRecDate ? *RecDate        : ""),
                               (Setup.ShowRecDate && Setup.ShowRecTime ? *RecDelimiter : ""),
                               (Setup.ShowRecTime ? *RecTime        : ""),
                               New,
                               (Setup.ShowRecTime || Setup.ShowRecDate ? *RecDelimiter : ""),
#ifdef USE_DVDARCHIVE
                               (((Setup.ShowRecLength + Setup.DvdDisplayMode) > 0) ? *RecLength    : ""),
                               (((Setup.ShowRecLength + Setup.DvdDisplayMode) > 0) ? *RecDelimiter : ""),
#else
                               (Setup.ShowRecLength ? *RecLength    : ""),
                               (Setup.ShowRecLength ? *RecDelimiter : ""),
#endif /* DVDARCHIVE */
                               s));
        }
#endif /* LIEMIEXT */
     // let's not display a trailing '~':
     if (!NewIndicator)
        stripspace(titleBuffer);
     s = &titleBuffer[strlen(titleBuffer) - 1];
     if (*s == '~')
        *s = 0;
     }
  else if (Level < HierarchyLevels()) {
     const char *s = name;
     const char *p = s;
     while (*++s) {
           if (*s == '~') {
              if (Level--)
                 p = s + 1;
              else
                 break;
              }
           }
     titleBuffer = MALLOC(char, s - p + 3);
     *titleBuffer = Delimiter;
     *(titleBuffer + 1) = Delimiter;
     strn0cpy(titleBuffer + 2, p, s - p + 1);
     }
  else
     return "";
  return titleBuffer;
}

#ifdef USE_CUTTIME
void cRecording::SetStartTime(time_t Start) 
{
  start=Start;
  if (fileName) {
     free(fileName);
     fileName = NULL;
     }
}
#endif /* CUTTIME */

const char *cRecording::PrefixFileName(char Prefix)
{
  cString p = PrefixVideoFileName(FileName(), Prefix);
  if (*p) {
     free(fileName);
     fileName = strdup(p);
     return fileName;
     }
  return NULL;
}

int cRecording::HierarchyLevels(void) const
{
  const char *s = name;
  int level = 0;
  while (*++s) {
        if (*s == '~')
           level++;
        }
  return level;
}

bool cRecording::IsEdited(void) const
{
  const char *s = strrchr(name, '~');
  s = !s ? name : s + 1;
  return *s == '%';
}

bool cRecording::WriteInfo(void)
{
  cString InfoFileName = cString::sprintf("%s%s", fileName, INFOFILESUFFIX);
  FILE *f = fopen(InfoFileName, "w");
  if (f) {
     info->Write(f);
     fclose(f);
     }
  else
     LOG_ERROR_STR(*InfoFileName);
  return true;
}

bool cRecording::Delete(void)
{
  bool result = true;
  char *NewName = strdup(FileName());
  char *ext = strrchr(NewName, '.');
  if (ext && strcmp(ext, RECEXT) == 0) {
     strncpy(ext, DELEXT, strlen(ext));
     if (access(NewName, F_OK) == 0) {
        // the new name already exists, so let's remove that one first:
        isyslog("removing recording '%s'", NewName);
        RemoveVideoFile(NewName);
        }
     isyslog("deleting recording '%s'", FileName());
     if (access(FileName(), F_OK) == 0)
        result = RenameVideoFile(FileName(), NewName);
     else {
        isyslog("recording '%s' vanished", FileName());
        result = true; // well, we were going to delete it, anyway
        }
     }
  free(NewName);
  return result;
}

bool cRecording::Remove(void)
{
  // let's do a final safety check here:
  if (!endswith(FileName(), DELEXT)) {
     esyslog("attempt to remove recording %s", FileName());
     return false;
     }
  isyslog("removing recording %s", FileName());
  return RemoveVideoFile(FileName());
}

bool cRecording::Undelete(void)
{
  bool result = true;
  char *NewName = strdup(FileName());
  char *ext = strrchr(NewName, '.');
  if (ext && strcmp(ext, DELEXT) == 0) {
     strncpy(ext, RECEXT, strlen(ext));
     if (access(NewName, F_OK) == 0) {
        // the new name already exists, so let's not remove that one:
        esyslog("ERROR: attempt to undelete '%s', while recording '%s' exists", FileName(), NewName);
        result = false;
        }
     else {
        isyslog("undeleting recording '%s'", FileName());
        if (access(FileName(), F_OK) == 0)
           result = RenameVideoFile(FileName(), NewName);
        else {
           isyslog("deleted recording '%s' vanished", FileName());
           result = false;
           }
        }
     }
  free(NewName);
  return result;
}

void cRecording::ResetResume(void) const
{
  resume = RESUME_NOT_INITIALIZED;
}

#ifdef USE_LIEMIEXT
bool cRecording::Rename(const char *newName, int *newPriority, int *newLifetime)
{
  bool result = false;
  struct tm tm_r;
  struct tm *t = localtime_r(&start, &tm_r);
  char *localNewName = ExchangeChars(strdup(newName), true);
  char *newFileName = strdup(cString::sprintf(NAMEFORMAT, VideoDirectory, localNewName, t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, *newPriority, *newLifetime));
  free(localNewName);
  if (strcmp(FileName(), newFileName)) {
     if (access(newFileName, F_OK) == 0) {
        isyslog("recording %s already exists", newFileName);
        }
     else {
        isyslog("renaming recording %s to %s", FileName(), newFileName);
        result = MakeDirs(newFileName, true);
        if (result)
           result = RenameVideoFile(FileName(), newFileName);
        if (result) {
           priority = *newPriority;
           lifetime = *newLifetime;
           free(fileName);
           fileName = strdup(newFileName);
           free(name);
           name = strdup(newName);
#ifdef USE_SORTRECORDS
           for (int i = 0; i < MAXSORTMODES; i++) {
              free(sortBuffer[i]);
              sortBuffer[i] = NULL;
           }
#else
           free(sortBuffer);
           sortBuffer = NULL;
#endif /* SORTRECORDS */
           free(titleBuffer);
           titleBuffer = NULL;
           }
        }
     }
  free(newFileName);
  return result;
}
#endif /* LIEMIEXT */

// --- cRecordings -----------------------------------------------------------

cRecordings Recordings;

char *cRecordings::updateFileName = NULL;

cRecordings::cRecordings(bool Deleted)
:cThread("video directory scanner")
{
  deleted = Deleted;
  lastUpdate = 0;
  state = 0;
#ifdef USE_SORTRECORDS
  SortOrder = 1;
#endif /* SORTRECORDS */
}

cRecordings::~cRecordings()
{
  Cancel(3);
}

void cRecordings::Action(void)
{
  Refresh();
}

const char *cRecordings::UpdateFileName(void)
{
  if (!updateFileName)
     updateFileName = strdup(AddDirectory(VideoDirectory, ".update"));
  return updateFileName;
}

void cRecordings::Refresh(bool Foreground)
{
  lastUpdate = time(NULL); // doing this first to make sure we don't miss anything
  Lock();
  Clear();
  ChangeState();
  Unlock();
  ScanVideoDir(VideoDirectory, Foreground);
}

void cRecordings::ScanVideoDir(const char *DirName, bool Foreground, int LinkLevel)
{
  cReadDir d(DirName);
  struct dirent *e;
  while ((Foreground || Running()) && (e = d.Next()) != NULL) {
        if (strcmp(e->d_name, ".") && strcmp(e->d_name, "..")) {
           char *buffer = strdup(AddDirectory(DirName, e->d_name));
           struct stat st;
           if (stat(buffer, &st) == 0) {
              int Link = 0;
              if (S_ISLNK(st.st_mode)) {
                 if (LinkLevel > MAX_LINK_LEVEL) {
                    isyslog("max link level exceeded - not scanning %s", buffer);
                    continue;
                    }
                 Link = 1;
                 char *old = buffer;
                 buffer = ReadLink(old);
                 free(old);
                 if (!buffer)
                    continue;
                 if (stat(buffer, &st) != 0) {
                    free(buffer);
                    continue;
                    }
                 }
              if (S_ISDIR(st.st_mode)) {
                 if (endswith(buffer, deleted ? DELEXT : RECEXT)) {
                    cRecording *r = new cRecording(buffer);
                    if (r->Name()) {
                       Lock();
                       Add(r);
                       ChangeState();
                       Unlock();
                       if (deleted) {
                          r->fileSizeMB = DirSizeMB(buffer);
                          r->deleted = time(NULL);
                          }
                       }
                    else
                       delete r;
                    }
                 else
                    ScanVideoDir(buffer, Foreground, LinkLevel + Link);
                 }
              }
           free(buffer);
           }
        }
}

bool cRecordings::StateChanged(int &State)
{
  int NewState = state;
  bool Result = State != NewState;
  State = state;
  return Result;
}

void cRecordings::TouchUpdate(void)
{
  bool needsUpdate = NeedsUpdate();
  TouchFile(UpdateFileName());
  if (!needsUpdate)
     lastUpdate = time(NULL); // make sure we don't tigger ourselves
}

bool cRecordings::NeedsUpdate(void)
{
  time_t lastModified = LastModifiedTime(UpdateFileName());
  if (lastModified > time(NULL))
     return false; // somebody's clock isn't running correctly
  return lastUpdate < lastModified;
}

bool cRecordings::Update(bool Wait)
{
  if (Wait) {
     Refresh(true);
     return Count() > 0;
     }
  else
     Start();
  return false;
}

cRecording *cRecordings::GetByName(const char *FileName)
{
  if (FileName) {
     for (cRecording *recording = First(); recording; recording = Next(recording)) {
         if (strcmp(recording->FileName(), FileName) == 0)
            return recording;
         }
     }
  return NULL;
}

void cRecordings::AddByName(const char *FileName, bool TriggerUpdate)
{
  LOCK_THREAD;
  cRecording *recording = GetByName(FileName);
  if (!recording) {
     recording = new cRecording(FileName);
     Add(recording);
     ChangeState();
     if (TriggerUpdate)
        TouchUpdate();
     }
}

void cRecordings::DelByName(const char *FileName)
{
  LOCK_THREAD;
  cRecording *recording = GetByName(FileName);
  if (recording) {
     cThreadLock DeletedRecordingsLock(&DeletedRecordings);
     Del(recording, false);
     char *ext = strrchr(recording->FileName(), '.');
     if (ext) {
        strncpy(ext, DELEXT, strlen(ext));
        recording->fileSizeMB = DirSizeMB(recording->FileName());
        recording->deleted = time(NULL);
        DeletedRecordings.Add(recording);
        }
     else
        delete recording;
     ChangeState();
     TouchUpdate();
     }
}

int cRecordings::TotalFileSizeMB(void)
{
  int size = 0;
  LOCK_THREAD;
  for (cRecording *recording = First(); recording; recording = Next(recording)) {
      if (recording->fileSizeMB > 0 && IsOnVideoDirectoryFileSystem(recording->FileName()))
         size += recording->fileSizeMB;
      }
  return size;
}

void cRecordings::ResetResume(const char *ResumeFileName)
{
  LOCK_THREAD;
  for (cRecording *recording = First(); recording; recording = Next(recording)) {
      if (!ResumeFileName || strncmp(ResumeFileName, recording->FileName(), strlen(recording->FileName())) == 0)
         recording->ResetResume();
      }
  ChangeState();
}

// --- cMark -----------------------------------------------------------------

cMark::cMark(int Position, const char *Comment)
{
  position = Position;
  comment = Comment ? strdup(Comment) : NULL;
}

cMark::~cMark()
{
  free(comment);
}

cString cMark::ToText(void)
{
  return cString::sprintf("%s%s%s\n", *IndexToHMSF(position, true), comment ? " " : "", comment ? comment : "");
}

bool cMark::Parse(const char *s)
{
  free(comment);
  comment = NULL;
  position = HMSFToIndex(s);
  const char *p = strchr(s, ' ');
  if (p) {
     p = skipspace(p);
     if (*p)
        comment = strdup(p);
     }
  return true;
}

bool cMark::Save(FILE *f)
{
  return fprintf(f, "%s", *ToText()) > 0;
}

// --- cMarks ----------------------------------------------------------------

bool cMarks::Load(const char *RecordingFileName)
{
  if (cConfig<cMark>::Load(AddDirectory(RecordingFileName, MARKSFILESUFFIX))) {
     Sort();
     return true;
     }
  return false;
}

void cMarks::Sort(void)
{
  for (cMark *m1 = First(); m1; m1 = Next(m1)) {
      for (cMark *m2 = Next(m1); m2; m2 = Next(m2)) {
          if (m2->position < m1->position) {
             swap(m1->position, m2->position);
             swap(m1->comment, m2->comment);
             }
          }
      }
}

cMark *cMarks::Add(int Position)
{
  cMark *m = Get(Position);
  if (!m) {
     cConfig<cMark>::Add(m = new cMark(Position));
     Sort();
     }
  return m;
}

cMark *cMarks::Get(int Position)
{
  for (cMark *mi = First(); mi; mi = Next(mi)) {
      if (mi->position == Position)
         return mi;
      }
  return NULL;
}

cMark *cMarks::GetPrev(int Position)
{
  for (cMark *mi = Last(); mi; mi = Prev(mi)) {
      if (mi->position < Position)
         return mi;
      }
  return NULL;
}

cMark *cMarks::GetNext(int Position)
{
  for (cMark *mi = First(); mi; mi = Next(mi)) {
      if (mi->position > Position)
         return mi;
      }
  return NULL;
}

#ifdef USE_JUMPPLAY
// --- cMarksReload ----------------------------------------------------------

#define MARKS_RELOAD_MS 10000

time_t cMarksReload::lastsavetime = 0;

cMarksReload::cMarksReload(const char *RecordingFileName)
:recDir(RecordingFileName)
{
  struct stat sbuf;
  if (Load(recDir) && stat(FileName(), &sbuf) == 0)
     lastmodtime = sbuf.st_mtime;
  else
     lastmodtime = 0;
  nextreload.Set(MARKS_RELOAD_MS - cTimeMs::Now() % MARKS_RELOAD_MS);
}

bool cMarksReload::Reload(void)
{
  // Check the timestamp of marks.vdr in 10 seconds intervals
  // Independent but synchronized reloading of marks in two threads
  if ((Setup.ReloadMarks && nextreload.TimedOut()) || lastsavetime > lastmodtime) {
     nextreload.Set(MARKS_RELOAD_MS - cTimeMs::Now() % MARKS_RELOAD_MS);
     struct stat sbuf;
     if (stat(FileName(), &sbuf) == 0 && sbuf.st_mtime != lastmodtime) {
        lastmodtime = sbuf.st_mtime;
        if (Load(recDir))
           return true;
        }
     }
  return false;
}

bool cMarksReload::Save(void)
{
  bool ok = cMarks::Save();
  struct stat sbuf;
  if (ok && stat(FileName(), &sbuf) == 0)
     lastsavetime = lastmodtime = sbuf.st_mtime;
  return ok;
}
#endif /* JUMPPLAY */

// --- cRecordingUserCommand -------------------------------------------------

const char *cRecordingUserCommand::command = NULL;

#ifdef USE_DVLRECSCRIPTADDON
void cRecordingUserCommand::InvokeCommand(const char *State, const char *RecordingFileName, char *chanName)
#else
void cRecordingUserCommand::InvokeCommand(const char *State, const char *RecordingFileName)
#endif /* DVLRECSCRIPTADDON */
{
  if (command) {
#ifdef USE_DVLRECSCRIPTADDON
     cString cmd;
     if (chanName != NULL)
        cmd = cString::sprintf("%s %s \"%s\" \"%s\"", command, State, *strescape(RecordingFileName, "\\\"$"), chanName);
     else
        cmd = cString::sprintf("%s %s \"%s\"", command, State, *strescape(RecordingFileName, "\\\"$"));
#else
     cString cmd = cString::sprintf("%s %s \"%s\"", command, State, *strescape(RecordingFileName, "\\\"$"));
#endif /* DVLRECSCRIPTADDON */
     isyslog("executing '%s'", *cmd);
     SystemExec(cmd);
     }
}

// --- XXX+

//XXX+ somewhere else???
// --- cIndexFile ------------------------------------------------------------

#ifndef USE_LIEMIEXT
#define INDEXFILESUFFIX     "/index.vdr"
#endif /* LIEMIEXT */

// The number of frames to stay off the end in case of time shift:
#define INDEXSAFETYLIMIT 150 // frames

// The maximum time to wait before giving up while catching up on an index file:
#define MAXINDEXCATCHUP   8 // seconds

// The minimum age of an index file for considering it no longer to be written:
#define MININDEXAGE    3600 // seconds

cIndexFile::cIndexFile(const char *FileName, bool Record)
:resumeFile(FileName)
{
  f = -1;
  fileName = NULL;
  size = 0;
  last = -1;
  index = NULL;
  if (FileName) {
     fileName = MALLOC(char, strlen(FileName) + strlen(INDEXFILESUFFIX) + 1);
     if (fileName) {
        strcpy(fileName, FileName);
        char *pFileExt = fileName + strlen(fileName);
        strcpy(pFileExt, INDEXFILESUFFIX);
        int delta = 0;
        if (access(fileName, R_OK) == 0) {
           struct stat buf;
           if (stat(fileName, &buf) == 0) {
              delta = buf.st_size % sizeof(tIndex);
              if (delta) {
                 delta = sizeof(tIndex) - delta;
                 esyslog("ERROR: invalid file size (%ld) in '%s'", buf.st_size, fileName);
                 }
              last = (buf.st_size + delta) / sizeof(tIndex) - 1;
              if (!Record && last >= 0) {
                 size = last + 1;
                 index = MALLOC(tIndex, size);
                 if (index) {
                    f = open(fileName, O_RDONLY);
                    if (f >= 0) {
                       if ((int)safe_read(f, index, buf.st_size) != buf.st_size) {
                          esyslog("ERROR: can't read from file '%s'", fileName);
                          free(index);
                          index = NULL;
                          close(f);
                          f = -1;
                          }
                       // we don't close f here, see CatchUp()!
                       }
                    else
                       LOG_ERROR_STR(fileName);
                    }
                 else
                    esyslog("ERROR: can't allocate %zd bytes for index '%s'", size * sizeof(tIndex), fileName);
                 }
              }
           else
              LOG_ERROR;
           }
        else if (!Record)
           isyslog("missing index file %s", fileName);
        if (Record) {
           if ((f = open(fileName, O_WRONLY | O_CREAT | O_APPEND, DEFFILEMODE)) >= 0) {
              if (delta) {
                 esyslog("ERROR: padding index file with %d '0' bytes", delta);
                 while (delta--)
                       writechar(f, 0);
                 }
              }
           else
              LOG_ERROR_STR(fileName);
           }
        }
     else
        esyslog("ERROR: can't copy file name '%s'", FileName);
     }
}

cIndexFile::~cIndexFile()
{
  if (f >= 0)
     close(f);
  free(fileName);
  free(index);
}

bool cIndexFile::CatchUp(int Index)
{
  // returns true unless something really goes wrong, so that 'index' becomes NULL
  if (index && f >= 0) {
     cMutexLock MutexLock(&mutex);
     for (int i = 0; i <= MAXINDEXCATCHUP && (Index < 0 || Index >= last); i++) {
         struct stat buf;
         if (fstat(f, &buf) == 0) {
            if (time(NULL) - buf.st_mtime > MININDEXAGE) {
               // apparently the index file is not being written any more
               close(f);
               f = -1;
               break;
               }
            int newLast = buf.st_size / sizeof(tIndex) - 1;
            if (newLast > last) {
               if (size <= newLast) {
                  size *= 2;
                  if (size <= newLast)
                     size = newLast + 1;
                  }
               index = (tIndex *)realloc(index, size * sizeof(tIndex));
               if (index) {
                  int offset = (last + 1) * sizeof(tIndex);
                  int delta = (newLast - last) * sizeof(tIndex);
                  if (lseek(f, offset, SEEK_SET) == offset) {
                     if (safe_read(f, &index[last + 1], delta) != delta) {
                        esyslog("ERROR: can't read from index");
                        free(index);
                        index = NULL;
                        close(f);
                        f = -1;
                        break;
                        }
                     last = newLast;
                     }
                  else
                     LOG_ERROR_STR(fileName);
                  }
               else
                  esyslog("ERROR: can't realloc() index");
               }
            }
         else
            LOG_ERROR_STR(fileName);
         if (Index < last - (i ? 2 * INDEXSAFETYLIMIT : 0) || Index > 10 * INDEXSAFETYLIMIT) // keep off the end in case of "Pause live video"
            break;
         cCondWait::SleepMs(1000);
         }
     }
  return index != NULL;
}

bool cIndexFile::Write(uchar PictureType, uchar FileNumber, int FileOffset)
{
  if (f >= 0) {
     tIndex i = { FileOffset, PictureType, FileNumber, 0 };
     if (safe_write(f, &i, sizeof(i)) < 0) {
        LOG_ERROR_STR(fileName);
        close(f);
        f = -1;
        return false;
        }
     last++;
     }
  return f >= 0;
}

bool cIndexFile::Get(int Index, uchar *FileNumber, int *FileOffset, uchar *PictureType, int *Length)
{
  if (CatchUp(Index)) {
     if (Index >= 0 && Index < last) {
        *FileNumber = index[Index].number;
        *FileOffset = index[Index].offset;
        if (PictureType)
           *PictureType = index[Index].type;
        if (Length) {
           int fn = index[Index + 1].number;
           int fo = index[Index + 1].offset;
           if (fn == *FileNumber)
              *Length = fo - *FileOffset;
           else
              *Length = -1; // this means "everything up to EOF" (the buffer's Read function will act accordingly)
           }
        return true;
        }
     }
  return false;
}

int cIndexFile::GetNextIFrame(int Index, bool Forward, uchar *FileNumber, int *FileOffset, int *Length, bool StayOffEnd)
{
  if (CatchUp()) {
     int d = Forward ? 1 : -1;
     for (;;) {
         Index += d;
         if (Index >= 0 && Index < last - ((Forward && StayOffEnd) ? INDEXSAFETYLIMIT : 0)) {
            if (index[Index].type == I_FRAME) {
               if (FileNumber)
                  *FileNumber = index[Index].number;
               else
                  FileNumber = &index[Index].number;
               if (FileOffset)
                  *FileOffset = index[Index].offset;
               else
                  FileOffset = &index[Index].offset;
               if (Length) {
                  // all recordings end with a non-I_FRAME, so the following should be safe:
                  int fn = index[Index + 1].number;
                  int fo = index[Index + 1].offset;
                  if (fn == *FileNumber)
                     *Length = fo - *FileOffset;
                  else {
                     esyslog("ERROR: 'I' frame at end of file #%d", *FileNumber);
                     *Length = -1;
                     }
                  }
               return Index;
               }
            }
         else
            break;
         }
     }
  return -1;
}

int cIndexFile::Get(uchar FileNumber, int FileOffset)
{
  if (CatchUp()) {
     //TODO implement binary search!
     int i;
     for (i = 0; i < last; i++) {
         if (index[i].number > FileNumber || (index[i].number == FileNumber) && index[i].offset >= FileOffset)
            break;
         }
     return i;
     }
  return -1;
}

bool cIndexFile::IsStillRecording()
{
  return f >= 0;
}

// --- cFileName -------------------------------------------------------------

#define MAXFILESPERRECORDING 255
#define RECORDFILESUFFIX    "/%03d.vdr"
#define RECORDFILESUFFIXLEN 20 // some additional bytes for safety...

cFileName::cFileName(const char *FileName, bool Record, bool Blocking)
{
  file = NULL;
  fileNumber = 0;
  record = Record;
  blocking = Blocking;
  // Prepare the file name:
  fileName = MALLOC(char, strlen(FileName) + RECORDFILESUFFIXLEN);
  if (!fileName) {
     esyslog("ERROR: can't copy file name '%s'", fileName);
     return;
     }
  strcpy(fileName, FileName);
  pFileNumber = fileName + strlen(fileName);
  SetOffset(1);
}

cFileName::~cFileName()
{
  Close();
#ifdef USE_DVDARCHIVE

  char fn[BUFSIZ];
  strcpy(fn, fileName);

  char *p;
  if((p = strrchr(fn, '/'))) {
    p[0] = 0;
  }

  cString cmd = cString::sprintf("find \"%s\" -type l -lname \"%s/*\"", fn, DVD_MOUNT_PATH);

  bool isOnDvd = false;

  cReadLine pipe;
  FILE* file;
  char* filename;
  if ((file = popen(cmd, "r")) != (FILE *)NULL) {
     while ((filename = pipe.Read(file)) != NULL) {
           isOnDvd = true;
           unlink(filename);
           isyslog("DVD-ARCHIVE: Deleting %s", filename);
           }
     pclose(file);
     }

  if (isOnDvd) {
     if (Setup.DvdTrayMode==2 || Setup.DvdTrayMode==3)
        cmd = cString::sprintf("umount %s; eject %s", DVD_DEVICE, DVD_DEVICE);
     else
        cmd = cString::sprintf("umount %s", DVD_DEVICE);

     SystemExec(cmd);

     if (Setup.DvdSpeedLimit > 0) {
        cmd = cString::sprintf("speedcontrol -x 999 %s", DVD_DEVICE);
        SystemExec(cmd);
        }
     }

#endif /* DVDARCHIVE */

  free(fileName);
}

cUnbufferedFile *cFileName::Open(void)
{
  if (!file) {
     int BlockingFlag = blocking ? 0 : O_NONBLOCK;
     if (record) {
        dsyslog("recording to '%s'", fileName);
        file = OpenVideoFile(fileName, O_RDWR | O_CREAT | BlockingFlag);
        if (!file)
           LOG_ERROR_STR(fileName);
        }
     else {
        if (access(fileName, R_OK) == 0) {
           dsyslog("playing '%s'", fileName);
           file = cUnbufferedFile::Create(fileName, O_RDONLY | BlockingFlag);
           if (!file)
              LOG_ERROR_STR(fileName);
           }
        else if (errno != ENOENT)
           LOG_ERROR_STR(fileName);
        }
     }
  return file;
}

void cFileName::Close(void)
{
  if (file) {
     if (CloseVideoFile(file) < 0)
        LOG_ERROR_STR(fileName);
     file = NULL;
     }
}

cUnbufferedFile *cFileName::SetOffset(int Number, int Offset)
{
  if (fileNumber != Number)
     Close();
  if (0 < Number && Number <= MAXFILESPERRECORDING) {
     fileNumber = Number;
     sprintf(pFileNumber, RECORDFILESUFFIX, fileNumber);
     if (record) {
        if (access(fileName, F_OK) == 0) {
           // files exists, check if it has non-zero size
           struct stat buf;
           if (stat(fileName, &buf) == 0) {
              if (buf.st_size != 0)
                 return SetOffset(Number + 1); // file exists and has non zero size, let's try next suffix
              else {
                 // zero size file, remove it
                 dsyslog ("cFileName::SetOffset: removing zero-sized file %s", fileName);
                 unlink (fileName);
                 }
              }
           else
              return SetOffset(Number + 1); // error with fstat - should not happen, just to be on the safe side
           }
        else if (errno != ENOENT) { // something serious has happened
           LOG_ERROR_STR(fileName);
           return NULL;
           }
        // found a non existing file suffix
        }
     if (Open() >= 0) {
        if (!record && Offset >= 0 && file && file->Seek(Offset, SEEK_SET) != Offset) {
           LOG_ERROR_STR(fileName);
           return NULL;
           }
        }
     return file;
     }
  esyslog("ERROR: max number of files (%d) exceeded", MAXFILESPERRECORDING);
  return NULL;
}

#ifdef USE_HARDLINKCUTTER
int cFileName::MaxFileSize() {
  const int smallFiles = (255 * MAXVIDEOFILESIZE - 1024 * Setup.MaxRecordingSize)
                          / max(MAXVIDEOFILESIZE - Setup.MaxVideoFileSize, 1);

  if (fileNumber <= smallFiles)
     return MEGABYTE(Setup.MaxVideoFileSize);

  return MEGABYTE(MAXVIDEOFILESIZE);
}
#endif /* HARDLINKCUTTER */

cUnbufferedFile *cFileName::NextFile(void)
{
  return SetOffset(fileNumber + 1);
}

// --- Index stuff -----------------------------------------------------------

cString IndexToHMSF(int Index, bool WithFrame, int FramesPerSec)
{
  char buffer[16];
  int f = (Index % FramesPerSec) + 1;
  int s = (Index / FramesPerSec);
  int m = s / 60 % 60;
  int h = s / 3600;
  s %= 60;
  snprintf(buffer, sizeof(buffer), WithFrame ? "%d:%02d:%02d.%02d" : "%d:%02d:%02d", h, m, s, f);
  return buffer;
}

int HMSFToIndex(const char *HMSF, int FramesPerSec)
{
  int h, m, s, f = 0;
  if (3 <= sscanf(HMSF, "%d:%d:%d.%d", &h, &m, &s, &f))
     return (h * 3600 + m * 60 + s) * FramesPerSec + f - 1;
  return 0;
}

int SecondsToFrames(int Seconds, int FramesPerSec)
{
  return Seconds * FramesPerSec;
}

// --- ReadFrame -------------------------------------------------------------

int ReadFrame(cUnbufferedFile *f, uchar *b, int Length, int Max)
{
  if (Length == -1)
     Length = Max; // this means we read up to EOF (see cIndex)
  else if (Length > Max) {
     esyslog("ERROR: frame larger than buffer (%d > %d)", Length, Max);
     Length = Max;
     }
  int r = f->Read(b, Length);
  if (r < 0)
     LOG_ERROR;
  return r;
}

#ifdef USE_DVLFRIENDLYFNAMES
char *MakeFriendlyFilename(char **buf)
{
  char *b, *x, *y;

  if (buf == NULL || *buf == NULL)
     return(NULL);

  b = (char *)malloc(strlen(*buf) * 2);
  x = *buf;
  y = b;

  while (*x != 0) {
        switch (*x) {
          case 'Ä':
               *y = 'A';
               y++;
               *y = 'e';
               y++; x++;
               break;

          case 'ä':
               *y = 'a';
               y++;
               *y = 'e';
               y++; x++;
               break;

          case 'Ö':
               *y = 'O';
               y++;
               *y = 'e';
               y++; x++;
               break;

          case 'ö':
               *y = 'o';
               y++;
               *y = 'e';
               y++; x++;
               break;

          case 'Ü':
               *y = 'U';
               y++;
               *y = 'e';
               y++; x++;
               break;

          case 'ü':
               *y = 'u';
               y++;
               *y = 'e';
               y++; x++;
               break;

          case 'ß':
               *y = 's';
               y++;
               *y = 's';
               y++; x++;
               break;

          // chars to replace
          case ':':
          case ';':
          case '?':
          case ' ':
          case '\t':
               *y = '_';
               y++; x++;
               break;

          // chars to simply strip
          case '\"':
          case '*':
          case '{':
          case '}':
          case '[':
          case ']':
          case '=':
          case '<':
          case '>':
          case '#':
          case '`':
          case '|':
          case '\\':
          case '\n':
          case '\r':
               x++;
               break;

          default:
               *y = *x;
               y++; x++;
               break;
          }
        }
  *y = 0;

  x = strdup(b);
  free(b);

  free(*buf);
  *buf = x;

  return(*buf);
}
#endif /* DVLFRIENDLYFNAMES */
