]> git.ipfire.org Git - thirdparty/tvheadend.git/commitdiff
DVR: Allow custom path/filename specification, fixes #2534
authorJaroslav Kysela <perex@perex.cz>
Thu, 21 May 2015 14:59:32 +0000 (16:59 +0200)
committerJaroslav Kysela <perex@perex.cz>
Thu, 21 May 2015 15:12:55 +0000 (17:12 +0200)
docs/html/config_dvr.html
src/dvr/dvr.h
src/dvr/dvr_config.c
src/dvr/dvr_db.c
src/dvr/dvr_rec.c
src/tvheadend.h
src/utils.c

index a0efaa2f2c7b42750c064633cacb8302cb12a3fd..ff130d40fb3f2050be86cfde44eb7e3ecc08866a 100644 (file)
   <dt>Filename charset
   <dd>Character set for the created filename. Tvheadend will try to approximate characters to similarly looking ones.
 
-  <dt>Rewrite PAT in passthrough mode
-  <dd>Rewrite the original Program Association Table to only include the active service.  When this option is disabled, Tvheadend will write the original PAT as broadcast, which lists all services from the original multiplex.
-
-  <dt>Rewrite PMT in passthrough mode
-  <dd>Generate a Program Map Table only listing the streams actually in the output transport stream.  When this option is disabled, Tvheadend will write the original Program Map Table as broadcast, which may include references to excluded streams such as data and ECMs.
-
   <dt>Tag files with metadata
   <dd>If checked, media containers that support metadata will be tagged with the metadata associated with the event being recorded.
 
   <dt>Skip commercials
   <dd>If checked, commercials will be dropped from the recordings. At the moment, commercial detection only works for the swedish channel TV4.
 
+  <br><br>
+  <hr>
+  <b>Full Pathname Specification</b>
+  <hr>
+
+  <dt>Format String
+  <dd>The string allow to manually specify the full path generation using
+  the predefined modifiers for strftime (see '<i>man strftime</i>', except
+  <i>%n</i> and <i>%t</i>) and Tvheadend specific. Note that you may modify some of
+  this format string setting using the GUI fields bellow.
+
+      <table class="hts-doc-text" border="0">
+      <tr><th>Format</th><th>Description</th><th>Example</th></tr>
+      <br>
+      <tr><td>$t$n.$x</td><td>Default format (title, unique number, extension)</td><td>Tenis - Wimbledon-1.mkv</td></tr>
+      <br>
+      <tr><td>$t</td><td>Event title name</td><td>Tenis - Wimbledon</td></tr>
+      <tr><td>$e</td><td>Event episode name</td><td>S02-E06</td></tr>
+      <tr><td>$n</td><td>Unique number added when the file already exists&nbsp;&nbsp;&nbsp;&nbsp;</td><td>-1</td></tr>
+      <tr><td>$x</td><td>Filename extension (from the active stream muxer</td><td>mkv</td></tr>
+      <br>
+      <tr><td>%F</td><td>ISO 8601 date format</td><td>2011-03-19</td></tr>
+      <tr><td>%R</td><td>The time in 24-hour notation</td><td>14:12</td></tr>
+      </table>
+
   <br><br>
   <hr>
   <b>Subdirectory Options</b>
       <br>
       <tr><td>0755 == rwxr-xr-x</td></tr>
       <tr><td>0775 == rwxrwxr-x (default)</td></tr>
-         <tr><td>0777 == rwxrwxrwx</td></tr>
-         </table>
+      <tr><td>0777 == rwxrwxrwx</td></tr>
+      </table>
          
-         Note that the applicable umask applies, so 0777 with umask 0022 will produce 0755 (rwxr-xr-x).
+      Note that the applicable umask applies, so 0777 with umask 0022 will produce 0755 (rwxr-xr-x).
          
-         See also <i>File permissions</i> in <i>Recording File Options</i>. 
+      See also <i>File permissions</i> in <i>Recording File Options</i>.
          
   <dt>Make sub-directories per day
   <dd>If checked, create a new directory per day in the recording system path. Only days when anything is recorded will be created. The format of the directory will be 'YYYY-MM-DD' (ISO standard)
index 1959dfc41a0d0d9693d4d57451a34155c23f0305..8a4c000edbbb3609bddc3287171764c2bd3bb66e 100644 (file)
@@ -49,6 +49,9 @@ typedef struct dvr_config {
 
   muxer_config_t dvr_muxcnf;
 
+  char *dvr_pathname;
+  int dvr_pathname_changed;
+
   int dvr_dir_per_day;
   int dvr_channel_dir;
   int dvr_channel_in_title;
@@ -62,7 +65,6 @@ typedef struct dvr_config {
   int dvr_tag_files;
   int dvr_skip_commercials;
   int dvr_subtitle_in_title;
-  int dvr_episode_before_date;
   int dvr_windows_compatible_filenames;
 
   /* Series link support */
@@ -391,8 +393,6 @@ void dvr_config_destroy_by_profile(profile_t *pro, int delconf);
  *
  */
 
-void dvr_make_title(char *output, size_t outlen, dvr_entry_t *de);
-
 uint32_t dvr_usage_count(access_t *aa);
 
 static inline int dvr_entry_is_editable(dvr_entry_t *de)
index 62bb984b20bf5166bb9032ac3fab865036ea36bf..36fff4227d3f0a997b142fda54fa9a3ea7d355a7 100644 (file)
@@ -178,6 +178,7 @@ dvr_config_create(const char *name, const char *uuid, htsmsg_t *conf)
   cfg->dvr_skip_commercials = 1;
   dvr_charset_update(cfg, intlconv_filesystem_charset());
   cfg->dvr_update_window = 24 * 3600;
+  cfg->dvr_pathname = strdup("$t$n.$x");
 
   /* series link support */
   cfg->dvr_sl_brand_lock   = 1; // use brand linking
@@ -250,6 +251,7 @@ dvr_config_destroy(dvr_config_t *cfg, int delconf)
   autorec_destroy_by_config(cfg, delconf);
   timerec_destroy_by_config(cfg, delconf);
 
+  free(cfg->dvr_pathname);
   free(cfg->dvr_charset_id);
   free(cfg->dvr_charset);
   free(cfg->dvr_storage);
@@ -260,7 +262,6 @@ dvr_config_destroy(dvr_config_t *cfg, int delconf)
 /**
  *
  */
-
 static void
 dvr_config_storage_check(dvr_config_t *cfg)
 {
@@ -294,6 +295,193 @@ dvr_config_storage_check(dvr_config_t *cfg)
          cfg->dvr_config_name, cfg->dvr_storage);
 }
 
+/**
+ *
+ */
+static int
+dvr_match_fmtstr(dvr_config_t *cfg, const char *needle)
+{
+  char *str = cfg->dvr_pathname, *p;
+  const char *x;
+
+  if (needle == NULL)
+    return -1;
+
+  while (*str) {
+    if (*str == '\\') {
+      str++;
+      if (*str)
+        str++;
+      continue;
+    }
+    for (p = str, x = needle; *p && *x; p++, x++)
+      if (*p != *x)
+        break;
+    if (*x == '\0')
+      return str - cfg->dvr_pathname;
+    else
+      str++;
+  }
+  return -1;
+}
+
+/**
+ *
+ */
+static int
+dvr_match_fmtstr_nodir(dvr_config_t *cfg, const char *needle)
+{
+  char *str = cfg->dvr_pathname, *p;
+  const char *x;
+
+  for (str = p = cfg->dvr_pathname; *p; p++) {
+    if (*p == '\\')
+      p++;
+    else if (*p == '/')
+      str = p;
+  }
+
+  while (*str) {
+    if (*str == '\\') {
+      str++;
+      if (*str)
+        str++;
+      continue;
+    }
+    for (p = str, x = needle; *p && *x; p++, x++)
+      if (*p != *x)
+        break;
+    if (*x == '\0')
+      return str - cfg->dvr_pathname;
+    else
+      str++;
+  }
+
+  return -1;
+}
+
+/**
+ *
+ */
+static void
+dvr_insert_fmtstr(dvr_config_t *cfg, int idx, const char *str)
+{
+  size_t t = strlen(cfg->dvr_pathname);
+  size_t l = strlen(str);
+  char *n = malloc(t + l);
+  memcpy(n, cfg->dvr_pathname, idx);
+  memcpy(n + idx, str, l);
+  memcpy(n + idx + l, cfg->dvr_pathname + idx, t - idx);
+  n[t + l] = '\0';
+  free(cfg->dvr_pathname);
+  cfg->dvr_pathname = n;
+}
+
+/**
+ *
+ */
+static void
+dvr_insert_fmtstr_before_extension(dvr_config_t *cfg, const char *str)
+{
+  int idx = dvr_match_fmtstr_nodir(cfg, "%x");
+  if (idx < 0) {
+    idx = strlen(str);
+  } else {
+    while (idx > 0) {
+      if (cfg->dvr_pathname[idx - 1] != '.')
+        break;
+      idx--;
+    }
+  }
+  dvr_insert_fmtstr(cfg, idx, str);
+}
+
+/**
+ *
+ */
+static void
+dvr_remove_fmtstr(dvr_config_t *cfg, int idx, int len)
+{
+  char *pathname = cfg->dvr_pathname;
+  size_t l = strlen(pathname);
+  memmove(pathname + idx, pathname + idx + len, l - idx - len);
+  pathname[l - len] = '\0';
+}
+
+/**
+ *
+ */
+static void
+dvr_match_and_insert_or_remove(dvr_config_t *cfg, const char *str, int val, int idx)
+{
+  int i = dvr_match_fmtstr(cfg, str);
+  if (val) {
+    if (i < 0) {
+      if (idx < 0)
+        dvr_insert_fmtstr_before_extension(cfg, str);
+      else
+        dvr_insert_fmtstr(cfg, idx, str);
+    }
+  } else {
+    if (i >= 0)
+      dvr_remove_fmtstr(cfg, i, strlen(str));
+  }
+}
+
+
+/**
+ *
+ */
+static void
+dvr_update_pathname_from_fmtstr(dvr_config_t *cfg)
+{
+  if (cfg->dvr_pathname == NULL)
+    return;
+
+  cfg->dvr_dir_per_day = dvr_match_fmtstr(cfg, "%F/") >= 0;
+  cfg->dvr_channel_dir = dvr_match_fmtstr(cfg, "$c/") >= 0;
+  cfg->dvr_title_dir   = dvr_match_fmtstr(cfg, "$t/") >= 0;
+
+  cfg->dvr_channel_in_title  = dvr_match_fmtstr_nodir(cfg, "$c-") >= 0;
+  cfg->dvr_date_in_title     = dvr_match_fmtstr_nodir(cfg, "%F") >= 0;
+  cfg->dvr_time_in_title     = dvr_match_fmtstr_nodir(cfg, "%R") >= 0;
+  cfg->dvr_episode_in_title  = dvr_match_fmtstr_nodir(cfg, "$e") >= 0;
+  cfg->dvr_subtitle_in_title = dvr_match_fmtstr_nodir(cfg, ".$s") >= 0;
+
+  cfg->dvr_omit_title = dvr_match_fmtstr_nodir(cfg, "$t") < 0;
+}
+
+/**
+ *
+ */
+static void
+dvr_update_pathname_from_booleans(dvr_config_t *cfg)
+{
+  int i;
+
+  i = dvr_match_fmtstr_nodir(cfg, "$t");
+  if (cfg->dvr_omit_title) {
+    if (i >= 0)
+      dvr_remove_fmtstr(cfg, i, 2);
+  } else if (i < 0) {
+    i = dvr_match_fmtstr_nodir(cfg, "$n");
+    if (i >= 0)
+      dvr_insert_fmtstr(cfg, i, "$t");
+    else
+      dvr_insert_fmtstr_before_extension(cfg, "$t");
+  }
+
+  dvr_match_and_insert_or_remove(cfg, "$c-", cfg->dvr_channel_in_title, -1);
+  dvr_match_and_insert_or_remove(cfg, ".$s", cfg->dvr_subtitle_in_title, -1);
+  dvr_match_and_insert_or_remove(cfg, "%F",  cfg->dvr_date_in_title, -1);
+  dvr_match_and_insert_or_remove(cfg, "%R",  cfg->dvr_time_in_title, -1);
+  dvr_match_and_insert_or_remove(cfg, "$e",  cfg->dvr_episode_in_title, -1);
+
+  dvr_match_and_insert_or_remove(cfg, "$t/", cfg->dvr_title_dir, 0);
+  dvr_match_and_insert_or_remove(cfg, "$c/", cfg->dvr_channel_dir, 0);
+  dvr_match_and_insert_or_remove(cfg, "%F/", cfg->dvr_dir_per_day, 0);
+}
+
 /**
  *
  */
@@ -336,6 +524,12 @@ dvr_config_class_save(idnode_t *self)
   if (dvr_config_is_default(cfg))
     cfg->dvr_enabled = 1;
   cfg->dvr_valid = 1;
+  if (cfg->dvr_pathname_changed) {
+    cfg->dvr_pathname_changed = 0;
+    dvr_update_pathname_from_fmtstr(cfg);
+  } else {
+    dvr_update_pathname_from_booleans(cfg);
+  }
   dvr_config_save(cfg);
 }
 
@@ -504,6 +698,20 @@ dvr_config_entry_class_update_window_list(void *o)
   return dvr_entry_class_duration_list(o, "Update Disabled", 24*3600, 60);
 }
 
+static int
+dvr_config_class_pathname_set(void *o, const void *v)
+{
+  dvr_config_t *cfg = (dvr_config_t *)o;
+  const char *s = v;
+  if (strcmp(cfg->dvr_pathname ?: "", s ?: "")) {
+    free(cfg->dvr_pathname);
+    cfg->dvr_pathname = s ? strdup(s) : NULL;
+    cfg->dvr_pathname_changed = 1;
+    return 1;
+  }
+  return 0;
+}
+
 const idclass_t dvr_config_class = {
   .ic_class      = "dvrconfig",
   .ic_caption    = "DVR Configuration Profile",
@@ -522,17 +730,21 @@ const idclass_t dvr_config_class = {
          .number = 2,
       },
       {
-         .name   = "Subdirectory Options",
+         .name   = "Full Pathname Specification",
          .number = 3,
       },
       {
-         .name   = "Filename Options",
+         .name   = "Subdirectory Options",
          .number = 4,
+      },
+      {
+         .name   = "Filename Options",
+         .number = 5,
          .column = 1,
       },
       {
          .name   = "",
-         .number = 5,
+         .number = 6,
          .parent = 4,
          .column = 2,
       },
@@ -666,104 +878,105 @@ const idclass_t dvr_config_class = {
       .def.i    = 1,
       .group    = 2,
     },
+    {
+      .type     = PT_STR,
+      .id       = "pathname",
+      .name     = "Format String",
+      .set      = dvr_config_class_pathname_set,
+      .off      = offsetof(dvr_config_t, dvr_pathname),
+      .group    = 3,
+    },
     {
       .type     = PT_PERM,
       .id       = "directory-permissions",
       .name     = "Directory Permissions (octal, e.g. 0775)",
       .off      = offsetof(dvr_config_t, dvr_muxcnf.m_directory_permissions),
       .def.u32  = 0775,
-      .group    = 3,
+      .group    = 4,
     },
     {
       .type     = PT_BOOL,
       .id       = "day-dir",
       .name     = "Make Subdirectories Per Day",
       .off      = offsetof(dvr_config_t, dvr_dir_per_day),
-      .group    = 3,
+      .group    = 4,
     },
     {
       .type     = PT_BOOL,
       .id       = "channel-dir",
       .name     = "Make Subdirectories Per Channel",
       .off      = offsetof(dvr_config_t, dvr_channel_dir),
-      .group    = 3,
+      .group    = 4,
     },
     {
       .type     = PT_BOOL,
       .id       = "title-dir",
       .name     = "Make Subdirectories Per Title",
       .off      = offsetof(dvr_config_t, dvr_title_dir),
-      .group    = 3,
+      .group    = 4,
     },
     {
       .type     = PT_BOOL,
       .id       = "channel-in-title",
       .name     = "Include Channel Name In Filename",
       .off      = offsetof(dvr_config_t, dvr_channel_in_title),
-      .group    = 4,
+      .group    = 5,
     },
     {
       .type     = PT_BOOL,
       .id       = "date-in-title",
       .name     = "Include Date In Filename",
       .off      = offsetof(dvr_config_t, dvr_date_in_title),
-      .group    = 4,
+      .group    = 5,
     },
     {
       .type     = PT_BOOL,
       .id       = "time-in-title",
       .name     = "Include Time In Filename",
       .off      = offsetof(dvr_config_t, dvr_time_in_title),
-      .group    = 4,
+      .group    = 5,
     },
     {
       .type     = PT_BOOL,
       .id       = "episode-in-title",
       .name     = "Include Episode In Filename",
       .off      = offsetof(dvr_config_t, dvr_episode_in_title),
-      .group    = 4,
-    },
-    {
-      .type     = PT_BOOL,
-      .id       = "episode-before-date",
-      .name     = "Put Episode In Filename Before Date And Time",
-      .off      = offsetof(dvr_config_t, dvr_episode_before_date),
-      .group    = 4,
+      .group    = 5,
     },
     {
       .type     = PT_BOOL,
       .id       = "subtitle-in-title",
       .name     = "Include Subtitle In Filename",
       .off      = offsetof(dvr_config_t, dvr_subtitle_in_title),
-      .group    = 5,
+      .group    = 6,
     },
     {
       .type     = PT_BOOL,
       .id       = "omit-title",
       .name     = "Do Not Include Title To Filename",
       .off      = offsetof(dvr_config_t, dvr_omit_title),
-      .group    = 5,
+      .group    = 6,
     },
     {
       .type     = PT_BOOL,
       .id       = "clean-title",
       .name     = "Remove All Unsafe Characters From Filename",
       .off      = offsetof(dvr_config_t, dvr_clean_title),
-      .group    = 5,
+      .group    = 6,
     },
     {
       .type     = PT_BOOL,
       .id       = "whitespace-in-title",
       .name     = "Replace Whitespace In Title with '-'",
       .off      = offsetof(dvr_config_t, dvr_whitespace_in_title),
-      .group    = 5,
+      .group    = 6,
     },
     {
       .type     = PT_BOOL,
       .id       = "windows-compatible-filenames",
       .name     = "Use Windows-compatible filenames",
       .off      = offsetof(dvr_config_t, dvr_windows_compatible_filenames),
-      .group    = 5,
+      .group    = 6,
     },
     {}
   },
index ccca1a9322a480ac0a012beab0d77c71869d9c12..823bfd3460bbdaa470347422c18e59d38a7afd30 100644 (file)
@@ -308,61 +308,6 @@ dvr_entry_schedstatus(dvr_entry_t *de)
   }
 }
 
-/**
- *
- */
-void
-dvr_make_title(char *output, size_t outlen, dvr_entry_t *de)
-{
-  struct tm tm;
-  char buf[40];
-  dvr_config_t *cfg = de->de_config;
-
-  if(cfg->dvr_channel_in_title)
-    snprintf(output, outlen, "%s-", DVR_CH_NAME(de));
-  else
-    output[0] = 0;
-  
-  if (cfg->dvr_omit_title == 0)
-    snprintf(output + strlen(output), outlen - strlen(output),
-            "%s", lang_str_get(de->de_title, NULL));
-
-  if (cfg->dvr_episode_before_date) {
-    if (cfg->dvr_episode_in_title && de->de_bcast && de->de_bcast->episode)
-      epg_episode_number_format(de->de_bcast->episode,
-                                output + strlen(output),
-                                outlen - strlen(output),
-                                ".", "S%02d", NULL, "E%02d", NULL);
-  }
-
-  if (cfg->dvr_subtitle_in_title && de->de_subtitle) {
-      snprintf(output + strlen(output), outlen - strlen(output),
-           ".%s", lang_str_get(de->de_subtitle, NULL));
-  }
-
-  localtime_r(&de->de_start, &tm);
-  
-  if (cfg->dvr_date_in_title) {
-    strftime(buf, sizeof(buf), "%F", &tm);
-    snprintf(output + strlen(output), outlen - strlen(output), ".%s", buf);
-  }
-
-  if (cfg->dvr_time_in_title) {
-    strftime(buf, sizeof(buf), "%H-%M", &tm);
-    snprintf(output + strlen(output), outlen - strlen(output), ".%s", buf);
-  }
-
-  if (!cfg->dvr_episode_before_date) {
-    if(cfg->dvr_episode_in_title) {
-      if(de->de_bcast && de->de_bcast->episode)
-        epg_episode_number_format(de->de_bcast->episode,
-                                  output + strlen(output),
-                                  outlen - strlen(output),
-                                  ".", "S%02d", NULL, "E%02d", NULL);
-    }
-  }
-}
-
 /**
  *
  */
index af5f069400058c4455ea95ceb4a4dcc8e44993ca..dc0713e5403db87dc17b7158753bef35fe194f11 100644 (file)
@@ -157,10 +157,10 @@ dvr_rec_unsubscribe(dvr_entry_t *de)
  * Replace various chars with a dash
  */
 static char *
-cleanup_filename(char *s, dvr_config_t *cfg)
+cleanup_filename(dvr_config_t *cfg, char *s)
 {
-  int i, len = strlen(s), len2;
-  char *s1;
+  int len = strlen(s);
+  char *s1, *p;
 
   s1 = intlconv_utf8safestr(cfg->dvr_charset_id, s, len * 2);
   if (s1 == NULL) {
@@ -170,41 +170,208 @@ cleanup_filename(char *s, dvr_config_t *cfg)
     if (s1 == NULL)
       return NULL;
   }
-  s = s1;
 
   /* Do not create hidden files */
-  if (s[0] == '.')
-    s[0] = '_';
-
-  len2 = strlen(s);
-  for (i = 0; i < len2; i++) {
-
-    if(s[i] == '/')
-      s[i] = '-';
-
-    else if(cfg->dvr_whitespace_in_title &&
-            (s[i] == ' ' || s[i] == '\t'))
-      s[i] = '-';      
-
-    else if(cfg->dvr_clean_title &&
-            ((s[i] < 32) || (s[i] > 122) ||
-             (strchr("/:\\<>|*?'\"", s[i]) != NULL)))
-      s[i] = '_';
-    else if(cfg->dvr_windows_compatible_filenames &&
-             (strchr("/:\\<>|*?'\"", s[i]) != NULL))
-      s[i] = '_';
-  }
+  if (s1[0] == '.')
+    s1[0] = '_';
+  if (s1[0] == '\\' && s1[1] == '.')
+    s1[1] = '_';
+
+  for (s = s1 ; *s; s++) {
 
-  if(cfg->dvr_windows_compatible_filenames) {
-    // trim trailing spaces and dots
-    for (i = len2 - 1; i >= 0; i--) {
-      if((s[i] != ' ') && (s[i] != '.'))
+    if (*s == '\\') {
+      s++;
+      if (*s == '\0')
         break;
-      s[i] = '\0';
+    }
+
+    if (*s == '/')
+      *s = '-';
+
+    else if (cfg->dvr_whitespace_in_title &&
+             (*s == ' ' || *s == '\t'))
+      *s = '-';        
+
+    else if (cfg->dvr_clean_title &&
+             ((*s < 32) || (*s > 122) ||
+             (strchr("/:\\<>|*?'\"", *s) != NULL)))
+      *s = '_';
+
+    else if (cfg->dvr_windows_compatible_filenames &&
+             (strchr("/:\\<>|*?'\"", *s) != NULL))
+      *s = '_';
+  }
+
+  if (cfg->dvr_windows_compatible_filenames) {
+    /* trim trailing spaces and dots */
+    for (s = p = s1; *s; s++) {
+      if (*s == '\\')
+        s++;
+      if (*s != ' ' && *s != '.')
+        p = s + 1;
+    }
+    *p = '\0';
+  }
+
+  return s1;
+}
+
+/**
+ *
+ */
+
+static const char *dvr_sub_title(const char *id, const void *aux)
+{
+  return lang_str_get(((dvr_entry_t *)aux)->de_title, NULL);
+}
+
+static const char *dvr_sub_subtitle(const char *id, const void *aux)
+{
+  return lang_str_get(((dvr_entry_t *)aux)->de_subtitle, NULL);
+}
+
+static const char *dvr_sub_episode(const char *id, const void *aux)
+{
+  const dvr_entry_t *de = aux;
+  static char buf[64];
+
+  if (de->de_bcast == NULL || de->de_bcast->episode == NULL)
+    return "";
+  epg_episode_number_format(de->de_bcast->episode,
+                            buf, sizeof(buf),
+                            ".", "S%02d", NULL, "E%02d", NULL);
+  return buf;
+}
+
+static const char *dvr_sub_channel(const char *id, const void *aux)
+{
+  return DVR_CH_NAME((dvr_entry_t *)aux);
+}
+
+
+static str_substitute_t dvr_subs_entry[] = {
+  { .id = "t", .getval = dvr_sub_title },
+  { .id = "s", .getval = dvr_sub_subtitle },
+  { .id = "e", .getval = dvr_sub_episode },
+  { .id = "c", .getval = dvr_sub_channel },
+  { .id = NULL, .getval = NULL }
+};
+
+static const char *dvr_sub_strftime(const char *id, const void *aux)
+{
+  char fid[8];
+  static char buf[40];
+  snprintf(fid, sizeof(fid), "%%%s", id);
+  strftime(buf, sizeof(buf), fid, (struct tm *)aux);
+  return buf;
+}
+
+static str_substitute_t dvr_subs_time[] = {
+  { .id = "a", .getval = dvr_sub_strftime }, /* The abbreviated name of the day of the week */
+  { .id = "A", .getval = dvr_sub_strftime }, /* The full name of the day of the week */
+  { .id = "b", .getval = dvr_sub_strftime }, /* The abbreviated month name */
+  { .id = "B", .getval = dvr_sub_strftime }, /* The full month name */
+  { .id = "c", .getval = dvr_sub_strftime }, /* The preferred date and time representation */
+  { .id = "C", .getval = dvr_sub_strftime }, /* The century number (year/100) as a 2-digit integer */
+  { .id = "d", .getval = dvr_sub_strftime }, /* The day of the month as a decimal number (range 01 to 31) */
+  { .id = "D", .getval = dvr_sub_strftime }, /* Equivalent to %m/%d/%y */
+  { .id = "e", .getval = dvr_sub_strftime }, /* The day of the month as a decimal number (range 01 to 31) */
+
+  { .id = "Ec", .getval = dvr_sub_strftime }, /* alternatives */
+  { .id = "EC", .getval = dvr_sub_strftime },
+  { .id = "Ex", .getval = dvr_sub_strftime },
+  { .id = "EX", .getval = dvr_sub_strftime },
+  { .id = "Ey", .getval = dvr_sub_strftime },
+  { .id = "EY", .getval = dvr_sub_strftime },
+
+  { .id = "F", .getval = dvr_sub_strftime }, /* Equivalent to %m/%d/%y */
+  { .id = "G", .getval = dvr_sub_strftime }, /* The ISO 8601 week-based year with century */
+  { .id = "g", .getval = dvr_sub_strftime }, /* Like %G, but without century */
+  { .id = "h", .getval = dvr_sub_strftime }, /* Equivalent to %b */
+  { .id = "H", .getval = dvr_sub_strftime }, /* The hour (range 00 to 23) */
+  { .id = "j", .getval = dvr_sub_strftime }, /* The day of the year (range 000 to 366) */
+  { .id = "k", .getval = dvr_sub_strftime }, /* The hour (range 0 to 23) - with space */
+  { .id = "l", .getval = dvr_sub_strftime }, /* The hour (range 1 to 12) - with space */
+  { .id = "m", .getval = dvr_sub_strftime }, /* The month (range 01 to 12) */
+  { .id = "M", .getval = dvr_sub_strftime }, /* The minute (range 00 to 59) */
+
+  { .id = "Od", .getval = dvr_sub_strftime }, /* alternatives */
+  { .id = "Oe", .getval = dvr_sub_strftime },
+  { .id = "OH", .getval = dvr_sub_strftime },
+  { .id = "OI", .getval = dvr_sub_strftime },
+  { .id = "Om", .getval = dvr_sub_strftime },
+  { .id = "OM", .getval = dvr_sub_strftime },
+  { .id = "OS", .getval = dvr_sub_strftime },
+  { .id = "Ou", .getval = dvr_sub_strftime },
+  { .id = "OU", .getval = dvr_sub_strftime },
+  { .id = "OV", .getval = dvr_sub_strftime },
+  { .id = "Ow", .getval = dvr_sub_strftime },
+  { .id = "OW", .getval = dvr_sub_strftime },
+  { .id = "Oy", .getval = dvr_sub_strftime },
+
+  { .id = "p", .getval = dvr_sub_strftime }, /* AM/PM */
+  { .id = "P", .getval = dvr_sub_strftime }, /* am/pm */
+  { .id = "r", .getval = dvr_sub_strftime }, /* a.m./p.m. */
+  { .id = "R", .getval = dvr_sub_strftime }, /* %H:%M */
+  { .id = "s", .getval = dvr_sub_strftime }, /* The number of seconds since the Epoch */
+  { .id = "S", .getval = dvr_sub_strftime }, /* The seconds (range 00 to 60) */
+  { .id = "T", .getval = dvr_sub_strftime }, /* %H:%M:%S */
+  { .id = "u", .getval = dvr_sub_strftime }, /* The day of the week as a decimal, range 1 to 7, Monday being 1 */
+  { .id = "U", .getval = dvr_sub_strftime }, /* The week number of the current year as a decimal number, range 00 to 53 (Sunday) */
+  { .id = "V", .getval = dvr_sub_strftime }, /* The  ISO 8601  week  number (range 01 to 53) */
+  { .id = "w", .getval = dvr_sub_strftime }, /* The day of the week as a decimal, range 0 to 6, Sunday being 0 */
+  { .id = "W", .getval = dvr_sub_strftime }, /* The week number of the current year as a decimal number, range 00 to 53 (Monday) */
+  { .id = "x", .getval = dvr_sub_strftime }, /* The preferred date representation */
+  { .id = "X", .getval = dvr_sub_strftime }, /* The preferred time representation */
+  { .id = "y", .getval = dvr_sub_strftime }, /* The year as a decimal number without a century (range 00 to 99) */
+  { .id = "Y", .getval = dvr_sub_strftime }, /* The year as a decimal number including the century */
+  { .id = "z", .getval = dvr_sub_strftime }, /* The +hhmm or -hhmm numeric timezone */
+  { .id = "Z", .getval = dvr_sub_strftime }, /* The timezone name or abbreviation */
+
+  { .id = NULL, .getval = NULL }
+};
+
+static const char *dvr_sub_str(const char *id, const void *aux)
+{
+  return (const char *)aux;
+}
+
+static str_substitute_t dvr_subs_extension[] = {
+  { .id = "x", .getval = dvr_sub_str },
+  { .id = NULL, .getval = NULL }
+};
+
+static str_substitute_t dvr_subs_tally[] = {
+  { .id = "n", .getval = dvr_sub_str },
+  { .id = NULL, .getval = NULL }
+};
+
+static char *dvr_find_last_path_component(char *path)
+{
+  char *res, *p;
+  for (p = res = path; *p; p++) {
+    if (*p == '\\') {
+      p++;
+    } else {
+      if (*p == '/')
+        res = p;
     }
   }
+  return res;
+}
 
-  return s;
+static char *dvr_find_next_path_component(char *path)
+{
+  char *res, *p;
+  for (p = res = path; *p; p++) {
+    if (*p == '\\') {
+      p++;
+    } else {
+      if (*p == '/')
+        return p + 1;
+    }
+  }
+  return NULL;
 }
 
 /**
@@ -217,14 +384,17 @@ cleanup_filename(char *s, dvr_config_t *cfg)
 static int
 pvr_generate_filename(dvr_entry_t *de, const streaming_start_t *ss)
 {
-  char fullname[PATH_MAX];
+  char filename[PATH_MAX];
   char path[PATH_MAX];
+  char ptmp[PATH_MAX];
+  char number[16];
   int tally = 0;
   struct stat st;
-  char *filename, *s;
+  char *s, *x, *fmtstr, *dirsep;
   struct tm tm;
   dvr_config_t *cfg;
   htsmsg_t *m;
+  size_t l, j;
 
   if (de == NULL)
     return -1;
@@ -232,91 +402,124 @@ pvr_generate_filename(dvr_entry_t *de, const streaming_start_t *ss)
   cfg = de->de_config;
   if (cfg->dvr_storage == NULL || cfg->dvr_storage == '\0')
     return -1;
+
+  localtime_r(&de->de_start, &tm);
+
   strncpy(path, cfg->dvr_storage, sizeof(path));
   path[sizeof(path)-1] = '\0';
+  l = strlen(path);
+  if (l + 1 >= sizeof(path)) {
+    tvherror("dvr", "wrong storage path");
+    return -1;
+  }
 
   /* Remove trailing slash */
-  if (path[strlen(path)-1] == '/')
-    path[strlen(path)-1] = '\0';
+  while (l > 0 && path[l-1] == '/')
+    path[--l] = '\0';
+  if (l + 1 >= sizeof(path))
+    l--;
+  path[l++] = '/';
+  path[l] = '\0';
 
-  /* Use the specified directory if set, otherwise construct it from the DVR 
-     configuration */
+  fmtstr = cfg->dvr_pathname;
+  while (*fmtstr == '/')
+    fmtstr++;
+
+  /* Substitute DVR entry fields */
+  str_substitute(fmtstr, path + l, sizeof(path) - l, '$', dvr_subs_entry, de);
+
+  /* Own directory? */
   if (de->de_directory) {
-    char *directory = strdup(de->de_directory);
-    s = cleanup_filename(directory, cfg);
-    if (s == NULL)
-      return -1;
-    snprintf(path + strlen(path), sizeof(path) - strlen(path), "/%s", s);
-    free(s);
-  } else {
-    /* Append per-day directory */
-    if (cfg->dvr_dir_per_day) {
-      localtime_r(&de->de_start, &tm);
-      strftime(fullname, sizeof(fullname), "%F", &tm);
-      s = cleanup_filename(fullname, cfg);
-      if (s == NULL)
-      return -1;
-      snprintf(path + strlen(path), sizeof(path) - strlen(path), "/%s", s);
-      free(s);
-    }
+    dirsep = dvr_find_last_path_component(path + l);
+    if (dirsep)
+      strcpy(filename, dirsep + 1);
+    else
+      filename[0] = '\0';
+    str_substitute(de->de_directory, ptmp, sizeof(ptmp), '$', dvr_subs_entry, de);
+    s = ptmp;
+    while (*s == '/')
+      s++;
+    j = strlen(s);
+    while (j >= 0 && s[j-1] == '/')
+      j--;
+    s[j] = '\0';
+    snprintf(path + l, sizeof(path) - l, "%s", s);
+    snprintf(path + l + j, sizeof(path) - l + j, "/%s", filename);
+  }
 
-    /* Append per-channel directory */
-    if (cfg->dvr_channel_dir) {
-      char *chname = strdup(DVR_CH_NAME(de));
-      s = cleanup_filename(chname, cfg);
-      free(chname);
-      if (s == NULL)
-      return -1;
-      snprintf(path + strlen(path), sizeof(path) - strlen(path), "/%s", s);
-      free(s);
-    }
+  /* Substitute time fields */
+  str_substitute(path + l, filename, sizeof(filename), '%', dvr_subs_time, &tm);
 
-    // TODO: per-brand, per-season
+  /* Substitute extension */
+  str_substitute(filename, path + l, sizeof(path) - l, '$', dvr_subs_extension,
+                 muxer_suffix(de->de_chain->prch_muxer, ss) ?: "");
 
-    /* Append per-title directory */
-    if (cfg->dvr_title_dir) {
-      char *title = strdup(lang_str_get(de->de_title, NULL));
-      s = cleanup_filename(title, cfg);
-      free(title);
-      if (s == NULL)
-      return -1;
-      snprintf(path + strlen(path), sizeof(path) - strlen(path), "/%s", s);
+  /* Cleanup all directory names */
+  x = path + l;
+  filename[j = 0] = '\0';
+  while (1) {
+    dirsep = dvr_find_next_path_component(x);
+    if (dirsep == NULL || *dirsep == '\0')
+      break;
+    *(dirsep - 1) = '\0';
+    if (*x) {
+      s = cleanup_filename(cfg, x);
+      tvh_strlcatf(filename, sizeof(filename), j, "%s/", s);
       free(s);
     }
+    x = dirsep;
   }
-
-  if (makedirs(path, cfg->dvr_muxcnf.m_directory_permissions, -1, -1) != 0)
+  tvh_strlcatf(filename, sizeof(filename), j, "%s", x);
+  snprintf(path + l, sizeof(path) - l, "%s", filename);
+
+  /* Deescape directory path and create directory tree */
+  dirsep = dvr_find_last_path_component(path + l);
+  *dirsep = '\0';
+  dirsep++;
+  str_unescape(path, filename, sizeof(filename));
+  if (makedirs(filename, cfg->dvr_muxcnf.m_directory_permissions, -1, -1) != 0)
     return -1;
+  j = strlen(filename);
+  snprintf(filename + j, sizeof(filename) - j, "/%s", dirsep);
+  if (filename[j] == '/')
+    j++;
   
-  /* Construct final name */
-  dvr_make_title(fullname, sizeof(fullname), de);
-  filename = cleanup_filename(fullname, cfg);
-  if (filename == NULL)
-    return -1;
-  snprintf(fullname, sizeof(fullname), "%s/%s.%s",
-          path, filename, muxer_suffix(de->de_chain->prch_muxer, ss));
+  /* Unique filename loop */
+  while (1) {
+
+    /* Prepare the name portion */
+    if (tally > 0) {
+      snprintf(number, sizeof(number), "-%d", tally);
+    } else {
+      number[0] = '\0';
+    }
+    str_substitute(filename + j, ptmp, sizeof(ptmp), '$', dvr_subs_tally, number);
+    s = cleanup_filename(cfg, ptmp);
+    if (s == NULL)
+      return -1;
 
-  while(1) {
-    if(stat(fullname, &st) == -1) {
+    /* Construct the final filename */
+    memcpy(path, filename, j);
+    path[j] = '\0';
+    str_unescape(s, path + j, sizeof(path) - j);
+    free(s);
+
+    if(stat(path, &st) == -1) {
       tvhlog(LOG_DEBUG, "dvr", "File \"%s\" -- %s -- Using for recording",
-            fullname, strerror(errno));
+            path, strerror(errno));
       break;
     }
 
-    tvhlog(LOG_DEBUG, "dvr", "Overwrite protection, file \"%s\" exists", 
-          fullname);
+    tvhlog(LOG_DEBUG, "dvr", "Overwrite protection, file \"%s\" exists",
+          path);
 
     tally++;
-
-    snprintf(fullname, sizeof(fullname), "%s/%s-%d.%s",
-            path, filename, tally, muxer_suffix(de->de_chain->prch_muxer, ss));
   }
-  free(filename);
 
   if (de->de_files == NULL)
     de->de_files = htsmsg_create_list();
   m = htsmsg_create_map();
-  htsmsg_add_str(m, "filename", fullname);
+  htsmsg_add_str(m, "filename", path);
   htsmsg_add_msg(de->de_files, NULL, m);
 
   return 0;
index be0948d5c23b7fc9d8043522f69ed08f28efc860..eb547d029cf4f49216ce014d5f2626fa5957f31f 100644 (file)
@@ -759,6 +759,16 @@ char *url_encode(char *str);
 
 int mpegts_word_count(const uint8_t *tsb, int len, uint32_t mask);
 
+typedef struct {
+  const char *id;
+  const char *(*getval)(const char *id, const void *aux);
+} str_substitute_t;
+
+char *str_substitute(const char *src, char *dst, size_t dstlen,
+                     int first, str_substitute_t *sub, const void *aux);
+
+char *str_unescape(const char *src, char *dst, size_t dstlen);
+
 int deferred_unlink(const char *filename, const char *rootdir);
 
 static inline int32_t deltaI32(int32_t a, int32_t b) { return (a > b) ? (a - b) : (b - a); }
index 0e621c71f631d00de8ee962625dc11887824ded3..40f195b795a284105ee22d559f62e68505a26abc 100644 (file)
@@ -645,6 +645,88 @@ mpegts_word_count ( const uint8_t *tsb, int len, uint32_t mask )
   return r;
 }
 
+char *str_substitute(const char *src, char *dst, size_t dstlen,
+                     int first, str_substitute_t *sub, const void *aux)
+{
+  str_substitute_t *s;
+  const char *p, *x, *v;
+  char *res = dst;
+  size_t l;
+
+  if (!dstlen)
+    return NULL;
+  while (*src && dstlen > 0) {
+    if (*src == '\\') {
+      if (dstlen < 2)
+        break;
+      *dst = '\\'; src++; dst++; dstlen--;
+      if (*src)
+        *dst = *src; src++; dst++; dstlen--;
+      continue;
+    }
+    if (first >= 0) {
+      if (*src != first) {
+        *dst = *src; src++; dst++; dstlen--;
+        continue;
+      }
+      src++;
+    }
+    for (s = sub; s->id; s++) {
+      for (p = s->id, x = src; *p; p++, x++)
+        if (*p != *x)
+          break;
+      if (*p == '\0') {
+        src = x;
+        if ((l = dstlen) > 0) {
+          v = s->getval(s->id, aux);
+          strncpy(dst, v, l);
+          l = MIN(strlen(v), l);
+          dst += l;
+          dstlen -= l;
+        }
+        break;
+      }
+    }
+    if (!s->id) {
+      if (first >= 0) {
+        *dst = first;
+      } else {
+        *dst = *src;
+        src++;
+      }
+      dst++; dstlen--;
+    }
+  }
+  if (dstlen == 0)
+    *(dst - 1) = '\0';
+  else if (dstlen > 0)
+    *dst = '\0';
+  return res;
+}
+
+char *str_unescape(const char *src, char *dst, size_t dstlen)
+{
+  char *res = dst;
+
+  while (*src && dstlen > 0) {
+    if (*src == '\\') {
+      if (dstlen < 2)
+        break;
+      *dst = '\\'; src++; dst++; dstlen--;
+      if (*src)
+        *dst = *src; src++; dst++; dstlen--;
+      continue;
+    } else {
+      *dst = *src; src++; dst++; dstlen--;
+    }
+  }
+  if (dstlen == 0)
+    *(dst - 1) = '\0';
+  else if (dstlen > 0)
+    *dst = '\0';
+  return res;
+}
+
 static void
 deferred_unlink_cb(void *s, int dearmed)
 {