]> git.ipfire.org Git - thirdparty/tvheadend.git/commitdiff
Merge remote-tracking branch 'origin/pr/301'
authorAdam Sutton <dev@adamsutton.me.uk>
Sat, 7 Jun 2014 21:53:48 +0000 (22:53 +0100)
committerAdam Sutton <dev@adamsutton.me.uk>
Sat, 7 Jun 2014 21:53:48 +0000 (22:53 +0100)
Conflicts:
src/dvr/dvr.h
src/dvr/dvr_db.c
src/webui/extjs.c
src/webui/static/app/dvr.js

1  2 
docs/html/config_dvr.html
src/dvr/dvr.h
src/dvr/dvr_db.c
src/webui/extjs.c
src/webui/static/app/dvr.js

index fd82ab2a2aaed041268fdafa79d0e215aeebe938,354b3297ae938ad29323301fbbcbc3aecad93cb0..1b5a652b5fb3911709fd76d957d5f9d7b7c36de9
    <dt>Media container
    <dd>Select the container format used to store recordings.
  
 -  <dt>DVR Log retention time (days)
 -  <dd>Time that Tvheadend will keep information about the recording in
 -      its internal database. Notice that the actual recorded file will not
 -      be deleted when the log entry is deleted.
 -
 -  <dt>Extra time before recordings (minutes)
 -  <dd>Specify the number of minutes to record before the events scheduled
 -      start time. Used to cope with small scheduling errors.
 -
 -  <dt>Extra time after recordings (minutes)
 -  <dd>Specify the number of minutes to record after the events scheduled
 -      stop time. Used to cope with small scheduling errors.
 +  <dt>Cache scheme
 +  <dd>Select the cache scheme used to store recordings. Leave as "system" unless you have a special case for one of the others.
 +  <br><br>  
 +  <dd>Whenever you read or write data to the filesystems, the information is kept (cached) in memory for a while. This means that regularly-access files are available quickly without going back to the disc; it also means that there's a disconnect when writing between the write request (from the application) and the actual write itself (to the disc/storage) as changes are buffered to be written in one go.</dd>
  
 -  <dt>Make sub-directories per day
 -  <dd>If checked, Tvheadend will 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)
 +    <dl>
  
 -  <dt>Make sub-directories per channel
 -  <dd>If checked, Tvheadend will create a directory per channel when storing
 -      events. If both this and the 'directory per day' checkbox is enabled,
 -      the date-directory will be parent to the per-channel directory.
 +      <dt>Unknown</dt>
 +      <dd>A placeholder status, meaning that the configuration isn't properly set.</dd>
  
 -  <dt>Make sub-directories per title
 -  <dd>If checked, Tvheadend will create a directory per title when storing
 -      events. If the day/channel directory checkboxes are also enabled, those
 -      directories will be parents of this directory.
 +      <dt>System</dt>
 +      <dd>Change nothing and rely on standard (default) system caching to behave as it normally would.</dd>
  
 -  <dt>Include channel name in title
 -  <dd>If checked, Tvheadend will include the name of the channel in the
 -      event title. This applies to both the titled stored in the file
 -      and to the file name itself.
 +      <dt>Do not keep</dt>
 +      <dd>Tell the system that you're not expecting to re-use the data soon, so don't keep it in cache. The data will still be buffered for writing. <i>Useful e.g. in a RAM-limited system like a Pi (given that you're unlikely to be watching while recording, so data can be discarded now and read back from disc later).</i></dd>
  
 -  <dt>Include date in title
 -  <dd>If checked, Tvheadend will include the date for the recording in the
 -      event title. This applies to both the titled stored in the file
 -      and to the file name itself.
 +      <dt>Sync</dt>
 +      <dd>Tell the system to write the data immediately. This doesn't affect whether or not it's cached. <i>Useful e.g. if you've a particular problem with data loss due to delayed write (such as if you get frequent transient power problems).</i></dd>
  
 -  <dt>Include time in title
 -  <dd>If checked, Tvheadend will include the time for the recording in the
 -      event title. This applies to both the titled stored in the file
 -      and to the file name itself.
 +      <dt>Sync + Do not keep</dt>
 +      <dd>A combination of last two variants above - data is written immediately and then discarded from cache.</dd>
  
 -  <dt>Include episode in title
 -  <dd>If checked, Tvheadend will include the season and episode in the
 -      title (if such info is available).
 +    </dl>
 +    
 +  <dt>DVR Log retention time (days)
 +  <dd>Time that Tvheadend will keep information about the recording in its internal database. Notice that the actual recorded file will not be deleted when the log entry is deleted.
  
 -  <dt>Remove all unsafe characters from filename
 -  <dd>If checked, all characters that could possibly cause problems for
 -      filenaming will be removed.
 -  
 -  <dt>Replace whitespace in title with '-'
 -  <dd>If checked, all whitespace characters will be replaced with '-'.
 -  
 -  <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>Extra time before recordings (minutes)
 +  <dd>Specify the number of minutes to record before the events scheduled start time. Used to cope with small scheduling errors.
  
 -  <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.
 +  <dt>Extra time after recordings (minutes)
 +  <dd>Specify the number of minutes to record after the events scheduled stop time. Used to cope with small scheduling errors.
  
+   <dt>Episode duplicate detection
+   <dd>If checked, broadcasts with matching title and matching non-zero episode number 
+       are considered duplicates.
    <dt>Post-processor command
 -  <dd>Command to run after finishing a recording. The command will be
 -      run in background and is executed even if a recording is aborted
 -      or an error occurred. Use the %e error formatting string to check
 -      for errors, the error string is empty if recording finished
 -      successfully.
 +  <dd>Command to run after finishing a recording. The command will be run in background and is executed even if a recording is aborted or an error occurred. Use the %e error formatting string to check for errors, the error string is empty if recording finished successfully.
        <br><br>
        Support format strings:<br>
        <table class="hts-doc-text" border="0">
diff --cc src/dvr/dvr.h
index fac68f1b44d6acb61a981a02615a4b955f958c18,a945e8f48e2ef6ba3f59b8091c06834fdb71a6c8..71bc16312a1e86001c01f1bcf1b7d3ec0bdc83fa
@@@ -69,8 -66,7 +69,9 @@@ extern struct dvr_entry_list dvrentries
  #define DVR_CLEAN_TITLE               0x100
  #define DVR_TAG_FILES           0x200
  #define DVR_SKIP_COMMERCIALS    0x400
 -#define DVR_EPISODE_DUPLICATE_DETECTION 0x800
 +#define DVR_SUBTITLE_IN_TITLE 0x800
 +#define DVR_EPISODE_BEFORE_DATE       0x1000
++#define DVR_EPISODE_DUPLICATE_DETECTION 0x2000
  
  typedef enum {
    DVR_PRIO_IMPORTANT,
index 81350813bb7a83b6d82748d3482f3b44ae4ebdb5,c6168d8769a7c618c2cb43ebaa554069dd21f46c..5c3f6d6ad477ff1a8a560321f1d8407b7858e909
@@@ -1187,14 -1150,9 +1216,17 @@@ dvr_init(void
        if(!htsmsg_get_u32(m, "skip-commercials", &u32) && !u32)
          cfg->dvr_flags &= ~DVR_SKIP_COMMERCIALS;
  
 +      if(!htsmsg_get_u32(m, "subtitle-in-title", &u32) && u32)
 +        cfg->dvr_flags |= DVR_SUBTITLE_IN_TITLE;
 +
 +      if(!htsmsg_get_u32(m, "episode-before-date", &u32) && u32)
 +        cfg->dvr_flags |= DVR_EPISODE_BEFORE_DATE;
 +
+       if(!htsmsg_get_u32(m, "episode-duplicate-detection", &u32) && u32)
+         cfg->dvr_flags |= DVR_EPISODE_DUPLICATE_DETECTION;
 +      dvr_charset_update(cfg, htsmsg_get_str(m, "charset"));
 +
        tvh_str_set(&cfg->dvr_postproc, htsmsg_get_str(m, "postproc"));
      }
  
@@@ -1418,11 -1319,8 +1450,12 @@@ dvr_save(dvr_config_t *cfg
    htsmsg_add_u32(m, "clean-title", !!(cfg->dvr_flags & DVR_CLEAN_TITLE));
    htsmsg_add_u32(m, "tag-files", !!(cfg->dvr_flags & DVR_TAG_FILES));
    htsmsg_add_u32(m, "skip-commercials", !!(cfg->dvr_flags & DVR_SKIP_COMMERCIALS));
 -  if(cfg->dvr_postproc != NULL)
 +  htsmsg_add_u32(m, "subtitle-in-title", !!(cfg->dvr_flags & DVR_SUBTITLE_IN_TITLE));
 +  htsmsg_add_u32(m, "episode-before-date", !!(cfg->dvr_flags & DVR_EPISODE_BEFORE_DATE));
+   htsmsg_add_u32(m, "episode-duplicate-detection", !!(cfg->dvr_flags & DVR_EPISODE_DUPLICATE_DETECTION));
 +  if (cfg->dvr_charset != NULL)
 +    htsmsg_add_str(m, "charset", cfg->dvr_charset);
 +  if (cfg->dvr_postproc != NULL)
      htsmsg_add_str(m, "postproc", cfg->dvr_postproc);
  
    hts_settings_save(m, "dvr/config%s", cfg->dvr_config_name);
index fa6d9d05579d51d22c7baecfe0545a8f2a258be4,86ffd2af383f79940745b21218935162be8f01d5..ea35b550930de5a58bf34b2c4e544377c6869264
mode 100755,100644..100755
@@@ -1158,8 -1306,7 +1158,9 @@@ extjs_dvr(http_connection_t *hc, const 
      htsmsg_add_u32(r, "cleanTitle", !!(cfg->dvr_flags & DVR_CLEAN_TITLE));
      htsmsg_add_u32(r, "tagFiles", !!(cfg->dvr_flags & DVR_TAG_FILES));
      htsmsg_add_u32(r, "commSkip", !!(cfg->dvr_flags & DVR_SKIP_COMMERCIALS));
 +    htsmsg_add_u32(r, "subtitleInTitle", !!(cfg->dvr_flags & DVR_SUBTITLE_IN_TITLE));
 +    htsmsg_add_u32(r, "episodeBeforeDate", !!(cfg->dvr_flags & DVR_EPISODE_BEFORE_DATE));
+     htsmsg_add_u32(r, "episodeDuplicateDetection", !!(cfg->dvr_flags & DVR_EPISODE_DUPLICATE_DETECTION));
  
      out = json_single_record(r, "dvrSettings");
  
        flags |= DVR_TAG_FILES;
      if(http_arg_get(&hc->hc_req_args, "commSkip") != NULL)
        flags |= DVR_SKIP_COMMERCIALS;
 +    if(http_arg_get(&hc->hc_req_args, "subtitleInTitle") != NULL)
 +      flags |= DVR_SUBTITLE_IN_TITLE;
 +    if(http_arg_get(&hc->hc_req_args, "episodeBeforeDate") != NULL)
 +      flags |= DVR_EPISODE_BEFORE_DATE;
+     if(http_arg_get(&hc->hc_req_args, "episodeDuplicateDetection") != NULL)
+       flags |= DVR_EPISODE_DUPLICATE_DETECTION;
  
  
      dvr_flags_set(cfg,flags);
index 10fbcf8b737618ff2f7916219d5b5be9fd78def2,a4bb8e4686ed6c64dfeab46e7d96999b18eedcb5..cdc8f43c0ab8e661e5c6d975a472f50edbaa460a
@@@ -793,371 -724,194 +793,376 @@@ tvheadend.dvr = function() 
   */
  tvheadend.dvrsettings = function() {
  
 -      var confreader = new Ext.data.JsonReader({
 -              root : 'dvrSettings'
 -      }, [ 'storage', 'postproc', 'retention', 'dayDirs', 'channelDirs',
 -              'channelInTitle', 'container', 'dateInTitle', 'timeInTitle',
 -              'preExtraTime', 'postExtraTime', 'whitespaceInTitle', 'titleDirs',
 -              'episodeInTitle', 'cleanTitle', 'tagFiles', 'commSkip', 'episodeDuplicateDetection' ]);
 -
 -      var confcombo = new Ext.form.ComboBox({
 -              store : tvheadend.configNames,
 -              triggerAction : 'all',
 -              mode : 'local',
 -              displayField : 'name',
 -              name : 'config_name',
 -              emptyText : '(default)',
 -              value : '',
 -              editable : true
 -      });
 -
 -      var delButton = new Ext.Toolbar.Button({
 -              tooltip : 'Delete named configuration',
 -              iconCls : 'remove',
 -              text : "Delete configuration",
 -              handler : deleteConfiguration,
 -              disabled : true
 -      });
 -
 -      var confpanel = new Ext.FormPanel({
 -              title : 'Digital Video Recorder',
 -              iconCls : 'drive',
 -              border : false,
 -              bodyStyle : 'padding:15px',
 -              anchor : '100% 50%',
 -              labelAlign : 'right',
 -              labelWidth : 250,
 -              waitMsgTarget : true,
 -              reader : confreader,
 -              defaultType : 'textfield',
 -              layout : 'form',
 -              items : [ {
 -                      width : 300,
 -                      fieldLabel : 'Recording system path',
 -                      name : 'storage'
 -              }, new Ext.form.ComboBox({
 -                      store : tvheadend.containers,
 -                      fieldLabel : 'Media container',
 -                      triggerAction : 'all',
 -                      displayField : 'description',
 -                      valueField : 'name',
 -                      editable : false,
 -                      width : 200,
 -                      hiddenName : 'container'
 -              }), new Ext.form.NumberField({
 -                      allowNegative : false,
 -                      allowDecimals : false,
 -                      minValue : 1,
 -                      fieldLabel : 'DVR Log retention time (days)',
 -                      name : 'retention'
 -              }), new Ext.form.NumberField({
 -                      allowDecimals : false,
 -                      fieldLabel : 'Extra time before recordings (minutes)',
 -                      name : 'preExtraTime'
 -              }), new Ext.form.NumberField({
 -                      allowDecimals : false,
 -                      fieldLabel : 'Extra time after recordings (minutes)',
 -                      name : 'postExtraTime'
 -              }), new Ext.form.Checkbox({
 -                      fieldLabel : 'Make subdirectories per day',
 -                      name : 'dayDirs'
 -              }), new Ext.form.Checkbox({
 -                      fieldLabel : 'Make subdirectories per channel',
 -                      name : 'channelDirs'
 -              }), new Ext.form.Checkbox({
 -                      fieldLabel : 'Make subdirectories per title',
 -                      name : 'titleDirs'
 -              }), new Ext.form.Checkbox({
 -                      fieldLabel : 'Include channel name in filename',
 -                      name : 'channelInTitle'
 -              }), new Ext.form.Checkbox({
 -                      fieldLabel : 'Include date in filename',
 -                      name : 'dateInTitle'
 -              }), new Ext.form.Checkbox({
 -                      fieldLabel : 'Include time in filename',
 -                      name : 'timeInTitle'
 -              }), new Ext.form.Checkbox({
 -                      fieldLabel : 'Include episode in filename',
 -                      name : 'episodeInTitle'
 -              }), new Ext.form.Checkbox({
 -                      fieldLabel : 'Remove all unsafe characters from filename',
 -                      name : 'cleanTitle'
 -              }), new Ext.form.Checkbox({
 -                      fieldLabel : 'Replace whitespace in title with \'-\'',
 -                      name : 'whitespaceInTitle'
 -              }), new Ext.form.Checkbox({
 -                      fieldLabel : 'Tag files with metadata',
 -                      name : 'tagFiles'
 -              }), new Ext.form.Checkbox({
 -                      fieldLabel : 'Skip commercials',
 -                      name : 'commSkip'
 -              }), new Ext.form.Checkbox({
 -                      fieldLabel : 'Episode duplicate detection',
 -                      name : 'episodeDuplicateDetection'
 -              }), {
 -                      width : 300,
 -                      fieldLabel : 'Post-processor command',
 -                      name : 'postproc'
 -              } ],
 -              tbar : [ confcombo, {
 -                      tooltip : 'Save changes made to dvr configuration below',
 -                      iconCls : 'save',
 -                      text : "Save configuration",
 -                      handler : saveChanges
 -              }, delButton, '->', {
 -                      text : 'Help',
 -                      handler : function() {
 -                              new tvheadend.help('DVR configuration', 'config_dvr.html');
 -                      }
 -              } ]
 -      });
 -
 -      function loadConfig() {
 -              confpanel.getForm().load({
 -                      url : 'dvr',
 -                      params : {
 -                              'op' : 'loadSettings',
 -                              'config_name' : confcombo.getValue()
 -                      },
 -                      success : function(form, action) {
 -                              confpanel.enable();
 -                      }
 -              });
 -      }
 -
 -      confcombo.on('select', function() {
 -              if (confcombo.getValue() == '') delButton.disable();
 -              else delButton.enable();
 -              loadConfig();
 -      });
 -
 -      confpanel.on('render', function() {
 -              loadConfig();
 -      });
 -
 -      function saveChanges() {
 -              var config_name = confcombo.getValue();
 -              confpanel.getForm().submit({
 -                      url : 'dvr',
 -                      params : {
 -                              'op' : 'saveSettings',
 -                              'config_name' : config_name
 -                      },
 -                      waitMsg : 'Saving Data...',
 -                      success : function(form, action) {
 -                              confcombo.setValue(config_name);
 -                              confcombo.fireEvent('select');
 -                      },
 -                      failure : function(form, action) {
 -                              Ext.Msg.alert('Save failed', action.result.errormsg);
 -                      }
 -              });
 -      }
 -
 -      function deleteConfiguration() {
 -              if (confcombo.getValue() != "") {
 -                      Ext.MessageBox.confirm('Message',
 -                              'Do you really want to delete DVR configuration \''
 -                                      + confcombo.getValue() + '\'?', deleteAction);
 -              }
 -      }
 -
 -      function deleteAction(btn) {
 -              if (btn == 'yes') {
 -                      confpanel.getForm().submit({
 -                              url : 'dvr',
 -                              params : {
 -                                      'op' : 'deleteSettings',
 -                                      'config_name' : confcombo.getValue()
 -                              },
 -                              waitMsg : 'Deleting Data...',
 -                              success : function(form, action) {
 -                                      confcombo.setValue('');
 -                                      confcombo.fireEvent('select');
 -                              },
 -                              failure : function(form, action) {
 -                                      Ext.Msg.alert('Delete failed', action.result.errormsg);
 -                              }
 -                      });
 -              }
 -      }
 -
 -      return confpanel;
 -}
 +    var confreader = new Ext.data.JsonReader({
 +        root: 'dvrSettings'
 +    }, ['storage', 'filePermissions', 'dirPermissions', 'postproc', 'retention', 'dayDirs', 'channelDirs',
 +        'channelInTitle', 'container', 'cache', 'charset', 'dateInTitle', 'timeInTitle',
 +        'preExtraTime', 'postExtraTime', 'whitespaceInTitle', 'titleDirs',
 +        'episodeInTitle', 'cleanTitle', 'tagFiles', 'commSkip', 'subtitleInTitle',
-         'episodeBeforeDate', 'rewritePAT', 'rewritePMT']);
++        'episodeBeforeDate', 'rewritePAT', 'rewritePMT', 'episodeDuplicateDetection']);
 +
 +    var confcombo = new Ext.form.ComboBox({
 +        store: tvheadend.configNames,
 +        triggerAction: 'all',
 +        mode: 'local',
 +        displayField: 'name',
 +        name: 'config_name',
 +        emptyText: '(default)',
 +        value: '',
 +        editable: true
 +    });
 +
 +    var delButton = new Ext.Toolbar.Button({
 +        tooltip: 'Delete named configuration',
 +        iconCls: 'remove',
 +        text: "Delete configuration",
 +        handler: deleteConfiguration,
 +        disabled: true
 +    });
 +
 +    /* Config panel variables */
 +
 +    /* DVR Behaviour */
 +
 +    var recordingContainer = new Ext.form.ComboBox({
 +        store: tvheadend.containers,
 +        fieldLabel: 'Media container',
 +        triggerAction: 'all',
 +        displayField: 'description',
 +        valueField: 'name',
 +        editable: false,
 +        width: 350,
 +        hiddenName: 'container'
 +    });
 +
 +    var cacheScheme = new Ext.form.ComboBox({
 +        store: tvheadend.caches,
 +        fieldLabel: 'Cache scheme',
 +        triggerAction: 'all',
 +        displayField: 'description',
 +        valueField: 'index',
 +        editable: false,
 +        width: 350,
 +        hiddenName: 'cache'
 +    });
 +
 +    var logRetention = new Ext.form.NumberField({
 +        allowNegative: false,
 +        allowDecimals: false,
 +        minValue: 1,
 +        fieldLabel: 'DVR Log retention time (days)',
 +        name: 'retention'
 +    });
 +
 +    var timeBefore = new Ext.form.NumberField({
 +        allowDecimals: false,
 +        fieldLabel: 'Extra time before recordings (minutes)',
 +        name: 'preExtraTime'
 +    });
 +
 +    var timeAfter = new Ext.form.NumberField({
 +        allowDecimals: false,
 +        fieldLabel: 'Extra time after recordings (minutes)',
 +        name: 'postExtraTime'
 +    });
 +
 +    var postProcessing = new Ext.form.TextField({
 +        width: 350,
 +        fieldLabel: 'Post-processor command',
 +        name: 'postproc'
 +    });
 +
 +    /* Recording File Options */
 +
 +    var recordingPath = new Ext.form.TextField({
 +        width: 350,
 +        fieldLabel: 'Recording system path',
 +        name: 'storage'
 +    });
 +
 +    /* NB: recordingPermissions is defined as a TextField for validation purposes (leading zeros), but is ultimately a number */
 +
 +    var recordingPermissions = new Ext.form.TextField({
 +        regex: /^[0][0-7]{3}$/,
 +        maskRe: /[0-7]/,
 +        width: 125,
 +        allowBlank: false,
 +        blankText: 'You must provide a value - use octal chmod notation, e.g. 0664',
 +        fieldLabel: 'File permissions (octal, e.g. 0664)',
 +        name: 'filePermissions'
 +    });
 +
 +    var charset = new Ext.form.ComboBox({
 +        store: tvheadend.charsets,
 +        fieldLabel: 'Filename charset',
 +        triggerAction: 'all',
 +        displayField: 'val',
 +        valueField: 'key',
 +        editable: false,
 +        width: 200,
 +        hiddenName: 'charset'
 +    });
 +
 +    /* TO DO - Add 'override user umask?' option, then trigger fchmod in mkmux.c, muxer_pass.c after file created */
 +
 +    var PATrewrite = new Ext.form.Checkbox({
 +        fieldLabel: 'Rewrite PAT in passthrough mode',
 +        name: 'rewritePAT'
 +    });
 +
 +    var PMTrewrite = new Ext.form.Checkbox({
 +        fieldLabel: 'Rewrite PMT in passthrough mode',
 +        name: 'rewritePMT'
 +    });
 +
 +    var tagMetadata = new Ext.form.Checkbox({
 +        fieldLabel: 'Tag files with metadata',
 +        name: 'tagFiles'
 +    });
 +
 +    var skipCommercials = new Ext.form.Checkbox({
 +        fieldLabel: 'Skip commercials',
 +        name: 'commSkip'
 +    });
 +
++    var episodeDuplicateDetection = new Ext.form.Checkbox({
++        fieldLabel: 'Episode Duplicate Detect',
++        name: 'episodeDuplicateDetection'
++    });
++
 +    /* Subdirectories and filename handling */
 +
 +    /* NB: directoryPermissions is defined as a TextField for validation purposes (leading zeros), but is ultimately a number */
 +
 +    var directoryPermissions = new Ext.form.TextField({
 +        regex: /^[0][0-7]{3}$/,
 +        maskRe: /[0-7]/,
 +        width: 125,
 +        allowBlank: false,
 +        blankText: 'You must provide a value - use octal chmod notation, e.g. 0775',
 +        fieldLabel: 'Directory permissions (octal, e.g. 0775)',
 +        name: 'dirPermissions'
 +    });
 +
 +    /* TO DO - Add 'override user umask?' option, then trigger fchmod in utils.c after directory created */
 +
 +    var dirsPerDay = new Ext.form.Checkbox({
 +        fieldLabel: 'Make subdirectories per day',
 +        name: 'dayDirs'
 +    });
 +
 +    var dirsPerChannel = new Ext.form.Checkbox({
 +        fieldLabel: 'Make subdirectories per channel',
 +        name: 'channelDirs'
 +    });
 +
 +    var dirsPerTitle = new Ext.form.Checkbox({
 +        fieldLabel: 'Make subdirectories per title',
 +        name: 'titleDirs'
 +    });
 +
 +    var incChannelInTitle = new Ext.form.Checkbox({
 +        fieldLabel: 'Include channel name in filename',
 +        name: 'channelInTitle'
 +    });
 +
 +    var incDateInTitle = new Ext.form.Checkbox({
 +        fieldLabel: 'Include date in filename',
 +        name: 'dateInTitle'
 +    });
 +
 +    var incTimeInTitle = new Ext.form.Checkbox({
 +        fieldLabel: 'Include time in filename',
 +        name: 'timeInTitle'
 +    });
 +
 +    var incEpisodeInTitle = new Ext.form.Checkbox({
 +        fieldLabel: 'Include episode in filename',
 +        name: 'episodeInTitle'
 +    });
 +
 +    var incSubtitleInTitle = new Ext.form.Checkbox({
 +        fieldLabel: 'Include subtitle in filename',
 +        name: 'subtitleInTitle'
 +    });
 +
 +    var episodeFirst = new Ext.form.Checkbox({
 +        fieldLabel: 'Put episode in filename before date and time',
 +        name: 'episodeBeforeDate'
 +    });
 +
 +    var stripUnsafeChars = new Ext.form.Checkbox({
 +        fieldLabel: 'Remove all unsafe characters from filename',
 +        name: 'cleanTitle'
 +    });
 +
 +    var stripWhitespace = new Ext.form.Checkbox({
 +        fieldLabel: 'Replace whitespace in title with \'-\'',
 +        name: 'whitespaceInTitle'
 +    });
 +
 +    /* Sub-Panel - DVR behaviour */
 +
 +    var DVRBehaviour = new Ext.form.FieldSet({
 +        title: 'DVR Behaviour',
 +        width: 700,
 +        autoHeight: true,
 +        collapsible: true,
 +        animCollapse: true,
 +        items: [recordingContainer, cacheScheme, logRetention, timeBefore, timeAfter, postProcessing]
 +    });
 +
 +    /* Sub-Panel - File Output */
 +
 +    var FileOutputPanel = new Ext.form.FieldSet({
 +        title: 'Recording File Options',
 +        width: 700,
 +        autoHeight: true,
 +        collapsible: true,
 +        animCollapse: true,
-         items: [recordingPath, recordingPermissions, charset, PATrewrite, PMTrewrite, tagMetadata, skipCommercials]
++        items: [recordingPath, recordingPermissions, charset, PATrewrite, PMTrewrite, tagMetadata, skipCommercials, episodeDuplicateDetection]
 +    });
 +
 +    /* Sub-Panel - Directory operations */
 +
 +    var DirHandlingPanel = new Ext.form.FieldSet({
 +        title: 'Subdirectory Options',
 +        width: 700,
 +        autoHeight: true,
 +        collapsible: true,
 +        animCollapse: true,
 +        items: [directoryPermissions, dirsPerDay, dirsPerChannel, dirsPerTitle]
 +    });
 +
 +    /* Sub-Panel - File operations - Break into two 4-item panels */
 +
 +    var FileHandlingPanelA = new Ext.form.FieldSet({
 +        width: 350,
 +        border: false,
 +        autoHeight: true,
 +        items : [incChannelInTitle, incDateInTitle, incTimeInTitle, incEpisodeInTitle]
 +    });
 +
 +    var FileHandlingPanelB = new Ext.form.FieldSet({
 +        width: 350,
 +        border: false,
 +        autoHeight: true,
 +        items : [incSubtitleInTitle, episodeFirst, stripUnsafeChars, stripWhitespace]
 +    });
 +
 +    var FileHandlingPanel = new Ext.form.FieldSet({
 +        title: 'Filename Options',
 +        width: 700,
 +        autoHeight: true,
 +        collapsible: true,
 +        animCollapse : true,
 +        items : [{
 +            layout: 'column',
 +            border: false,
 +            items : [FileHandlingPanelA, FileHandlingPanelB] 
 +        }]
 +    });
 +
 +    /* Main (form) panel */
 +
 +    var confpanel = new Ext.FormPanel({
 +        title: 'Digital Video Recorder',
 +        iconCls: 'drive',
 +        border: false,
 +        bodyStyle: 'padding:15px',
 +        anchor: '100% 50%',
 +        labelAlign: 'right',
 +        labelWidth: 300,
 +        autoScroll: true,
 +        waitMsgTarget: true,
 +        reader: confreader,
 +        defaultType: 'textfield',
 +        layout: 'form',
 +        items: [DVRBehaviour, FileOutputPanel, DirHandlingPanel, FileHandlingPanel],
 +        tbar: [confcombo, {
 +                tooltip: 'Save changes made to dvr configuration below',
 +                iconCls: 'save',
 +                text: "Save configuration",
 +                handler: saveChanges
 +            }, delButton, '->', {
 +                text: 'Help',
 +                handler: function() {
 +                    new tvheadend.help('DVR configuration', 'config_dvr.html');
 +                }
 +            }]
 +    });
 +
 +    function loadConfig() {
 +        confpanel.getForm().load({
 +            url: 'dvr',
 +            params: {
 +                'op': 'loadSettings',
 +                'config_name': confcombo.getValue()
 +            },
 +            success: function(form, action) {
 +                confpanel.enable();
 +            }
 +        });
 +    }
 +
 +    confcombo.on('select', function() {
 +        if (confcombo.getValue() === '')
 +            delButton.disable();
 +        else
 +            delButton.enable();
 +        loadConfig();
 +    });
 +
 +    confpanel.on('render', function() {
 +        loadConfig();
 +    });
 +
 +    function saveChanges() {
 +        var config_name = confcombo.getValue();
 +        confpanel.getForm().submit({
 +            url: 'dvr',
 +            params: {
 +                'op': 'saveSettings',
 +                'config_name': config_name
 +            },
 +            waitMsg: 'Saving Data...',
 +            success: function(form, action) {
 +                confcombo.setValue(config_name);
 +                confcombo.fireEvent('select');
 +            },
 +            failure: function(form, action) {
 +                Ext.Msg.alert('Save failed', action.result.errormsg);
 +            }
 +        });
 +    }
 +
 +    function deleteConfiguration() {
 +        if (confcombo.getValue() !== "") {
 +            Ext.MessageBox.confirm('Message',
 +                    'Do you really want to delete DVR configuration \''
 +                    + confcombo.getValue() + '\'?', deleteAction);
 +        }
 +    }
 +
 +    function deleteAction(btn) {
 +        if (btn === 'yes') {
 +            confpanel.getForm().submit({
 +                url: 'dvr',
 +                params: {
 +                    'op': 'deleteSettings',
 +                    'config_name': confcombo.getValue()
 +                },
 +                waitMsg: 'Deleting Data...',
 +                success: function(form, action) {
 +                    confcombo.setValue('');
 +                    confcombo.fireEvent('select');
 +                },
 +                failure: function(form, action) {
 +                    Ext.Msg.alert('Delete failed', action.result.errormsg);
 +                }
 +            });
 +        }
 +    }
 +
 +    return confpanel;
 +};