fsItems module

require all the things

fancy aligned require block..

var _                   = require('underscore');
var fs                  = require('./fs');
var path                = require('path');
var async               = require('async');
var format              = require('string-format');
var config              = require('./config');
var getConductor        = require('./getConductor');
var Walker              = require('./walkDepth');
var log                 = require('./log');
var s                   = require('string');
var Contravention       = require('./Contravention');


declaration block

var Item;
var Directory;
var File;


config

deal with configuration options specific to this module

  • output directory
  • otherExtensions
config.push({
  options: {
    output: [
      'o',
      'output directory',
      'string',
      'inPlace'
    ],
    otherExtensions: [
      false,
      'other extensions for files you\'d like to keep. eg: \'jpg, sfv\'',
      'string'
    ]
  },
  validation: function(config) {

fix lower case p cludges

    if (config.output == 'inplace') {
      config.output = 'inPlace';
    }

check output path

    if (
      (config.output != 'inPlace') &&
      (!fs.existsSync(config.output))
    ) {
      log.throwError('output directory does not exist');
    }

convert otherExtensions to array

    if (_.isString(config.otherExtensions)) {
      config.otherExtensions = config.otherExtensions.replace(' ', '');
      config.otherExtensions = config.otherExtensions.split(',');
    } else {
      config.otherExtensions = [];
    }
    config.otherExtensions = _.union(
      config.otherExtensions,
      [ 'nfo' ]
    );
  }
});



Item Class

Item is the parent class for Directory and File Class, it takes care of some commonly used properties:

consider a path like /srv/media/Lion.King.(2015)/Lion.King.avi you'd end up with these properties:

  • path absolute path /srv/media/Lion.King/Lion.King.avi
  • basename filename no ext Lion.King
  • extname extension avi
  • dirname parent dir /srv/media/Lion.King.(2015)
  • logname short & pretty LionKing.avi
  • isDirectory boolean true
  • isFile boolean false
class
classdesc
common properties and methods for Directory and File classes
Params
properties object Properties to attach to instance
properties.path string relative or absolute path to item
properties.stat Object A stat object as generated by fs.stat
Item = function(properties) {
  _.extend(this, { meta: {} }, properties);
  if (!/^\//.exec(this.path)) {
    this.path = path.join(process.cwd(), this.path);
  }
  if (!fs.existsSync(this.path)) {
    log.throwError('path does not exist');
  }
  if (!this.stat) {
    this.stat = fs.statSync(this.path);
  }
  this.setPath(this.path);
  this.determineLogName();
  this.determineType();
  log.silly(JSON.stringify({
    path: this.path,
    type: this.type,
    extname: this.extname,
    basename: this.basename,
    dirname: this.dirname,
    logName: this.logName
  }, null, '  '));
};


Item.determineLogName

create a nice string suitable for log files.. because usenet release file names are so unwieldy...

  • try to remove years
  • remove media types like 720p etc
  • camel case
  • truncate to 20 characters with ... where appropriate
  • right align the file extension
Item.prototype.determineLogName = function() {
  var logName;
  var match;
  var length = 20;


Discard everything from 3 consecutive digits (year, 720p 1080p etc)

  match = /^(.*?)[0-9]{3}/.exec(this.basename);
  logName = match ? match[1] : this.basename;

Discard non word

  logName = logName.replace(/\W/g, '');

Attach extension

  logName += this.extname ? '.' + this.extname : '';

  if (logName.length < length) {
    logName = s(logName).padRight(length).s;
  } else {

this was the best way I could think of to right align the extensions.. sorry. Join the first bit to the last bit with '...' in between

    logName = [
      logName.slice(0, length - (this.extname.length + 3)),
      this.extname ? logName.slice(this.extname.length * -1) : ''
    ].join('...');
  }
  this.logName = logName;
};


Item.prototype.determineType

sets Item.type property to a string which describes the type of this instance

Item.prototype.determineType = function() {
  var media;
  var subtitle;
  var other;
  var tests;
  media = ['mkv', 'mp4', 'avi'];
  subtitle = ['sub', 'srt'];
  other = config.otherExtensions;


define tests, each property will return a boolean not super efficient because all tests are run on each instance, even if the first one is true. The only alternative I could think of was a long winded if else if structure

  tests = {
    subdirectory: this.stat.isDirectory() && this.parent,
    directory: this.stat.isDirectory() && !this.parent,
    media: _.contains(media, this.extname),
    subtitle: _.contains(subtitle, this.extname),
    other: _.contains(other, this.extname)
  };

  this.type = _.findKey(tests, function(value, key) {
    return value;
  }) || 'junk';

};


Directory Class

the directory class contains:

  • properties describing the directory (path et cetera)
    • a children property
    • a meta property
    • methods relating to this directory as it exists on the hard drive

I'd like to limit the directory class to these concepts to avoid it becoming some sort of god class.

instantiate with the same params as Item Class

class
classdesc
object describing a directory

directory.meta

every directory instance should contain a meta property which contains:
  • meta generated by avprobe, like av codecs, bitrates, length, et cetera
  • meta retrieved from tmdb, like title, release date, et cetera
  • other information about the directory which may need to be stored between concierge runs.
Directory = function() {
  Item.apply(this, arguments);

  _.defaults(
    this,
    {
      ignoreMetaCache: false,
      ignoreTmdbCache: false,
      children: []
    }
  );
};


these two lines are the only way I could reliably extend a class from another's prototype. Util.inherits is supposed to work, but I couldn't get it to.

Directory.prototype = Object.create(Item.prototype);
Directory.prototype.constructor = Directory;


Directory.prototype.setPath

this method simply sets the path property and breaks it into the other relevant properties like basename and dirname

Params
folderPath string absolute or relative path
Directory.prototype.setPath = function(folderPath) {
  this.path = folderPath;
  this.extname = '';
  this.basename = path.basename(this.path);
  this.dirname = path.dirname(this.path);
}


Directory.prototype.getChildren

use a walker to get children of this directory

directory.children

every instance of Directory should have a children property which is an array of either Directory or File instances. the children property contains all direct and indirect children in the same array, so it ignores the fact that children may be contained in subfolders, but each instance keeps a path property. The upshot here is that the destination structure will always be flat. This approach deals with a lot of weirdness in scene release structures.

Params
callback function
Directory.prototype.getChildren = function(callback) {
  var folder = this;
  new Walker(
    this.path,
    {
      on: {
        file: function(parent, child, stat, next) {
          var file;
          file = new File({
            path: path.join(parent, child),
            stat: stat,
            parent: folder
          });
          folder.children.push(file);
          next();
        },
        directory: function(parent, child, stat, next) {
          var directory;
          directory = new Directory({
            path: path.join(parent, child),
            stat: stat,
            parent: folder
          });

No need to call getChildren here because this walker will recurse.

          folder.children.push(directory);
          next();
        }
      },
      callback: function(err) {
        if (err) {
          callback(err);
        }
        if (!_.findWhere(folder.children, {type: 'media'})) {
          new Contravention(folder, 'noMedia');
        }
        callback();
      }
    }
  );
};


Directory.prototype.mvToTemp

basically just append .temp to the existing path. When a directory is being processed with the inPlace option there's obviously a chance that the target directory path will be the same as the source, but concierge doesn't actually delete files which aren't required, it just doesn't copy them to the destination. Hence the need to move source directory's to a .temp directory prior to processing.

Params
callback function
Directory.prototype.mvToTemp = function(callback) {
  var directory = this;
  var target;
  target = this.path.slice(0, -1) + '.temp' + path.sep;
  fs.mv(this.path, target, function(err) {
    if (err) {
      throw err;
    }
    directory.setPath(target);
    if (!config.dry) {
      _.each(directory.children, function(child) {
        child.setParent(target);
      });
    }
    callback();
  });
};


Directory.prototype.mvToOutput

this doesn't actually move child files, nor does it purge the this.children, it simply reassigns this.path to the output directory, and deletes the old directory

Params
callback function
Directory.prototype.mvToOutput = function(callback) {
  var directory = this;
  var target;
  target = this.getOutputPath();

  fs.rmdir(this.path, function(err) {
    if (err) {
      throw err;
    }
    directory.setPath(target);
    callback();
  });
};


Directory.prototype.createOutputPath

basically mkdirp

Directory.prototype.createOutputPath = function() {
  fs.mkdirpSync(this.getOutputPath());
};


Directory.prototype.getCanonicalBasename

canonical as in 'according to convention', so wer'e going to apply the user defined format to the movie name to generate a canonical name

Directory.prototype.getCanonicalBasename = function() {
  var canonicalBasename;
  if (this.canonicalBasename) {
    return this.canonicalBasename;
  }
  canonicalBasename = format(config['directoryFormat'], this.meta);
  canonicalBasename = s(canonicalBasename).stripPunctuation().s;
  this.canonicalBasename = canonicalBasename;
  return canonicalBasename;
};


Directory.prototype.getOutputPath

attaches the appropriate target parent folder to the canonical basename

Directory.prototype.getOutputPath = function() {
  var outputPath;
  if (this.outputPath) {
    return this.outputPath;
  }

  if (config.output == 'inPlace') {
    outputPath = path.join(this.dirname, this.getCanonicalBasename());
  } else {
    outputPath = path.join(config.output, this.getCanonicalBasename());
  }
  this.outputPath = outputPath;
  return outputPath;

};


Directory.prototype.hasError

A convenience method to save rewriting these conditionals.. used by plugins quite a lot.

Params
name string The error name, see Contravention object
Directory.prototype.hasError = function(name) {
  if (!name) {
    return (this.meta.contravention);
  }
  return (
    (this.meta.contravention) &&
    (this.meta.contravention.name == name)
  );
};


Directory.prototype.setParent

convenience method which basically wraps setPath

Params
parentPath string Path to parent directory
Directory.prototype.setParent = function(parentPath) {
  log.silly(this.logName, '| setParent');
  this.setPath(path.join(parentPath, this.basename));
};


File Class

the file class contains:

  • properties describing the file (path et cetera)
    • methods relating to this file as it exists on the hard drive

instantiate with the same params as Item Class

As with the directory class, methods & properties stored on this class should be limited to the above to avoid creating a god class.

File = function() {
  Item.apply(this, arguments);
}
File.prototype = Object.create(Item.prototype);
File.prototype.constructor = File;


File.prototype.setPath

set path property and break it into other properties like:

  • extname
    • filename
    • basename
    • dirname
Params
filePath string the file path (absolute or relative)
File.prototype.setPath = function(filePath) {
  this.path = filePath;
  this.extname =  path.extname(filePath);
  this.filename = path.basename(filePath);
  this.basename = path.basename(filePath, this.extname);
  this.extname = this.extname.slice(1);
  this.dirname = path.dirname(filePath);
};


File.prototype.setParent

convenience method which basically wraps setPath

Params
parentPath string Path to parent directory
File.prototype.setParent = function(parentPath) {
  log.silly(this.logName, '| setParent');
  this.setPath(path.join(parentPath, this.filename));
};


File.prototype.getCanonicalFilename

generates file name according to format defined in options.

File.prototype.getCanonicalFilename = function() {
  var canonicalName;
  var basename;
  basename = format(config['fileFormat'], this.parent.meta);
  basename = s(basename).stripPunctuation().s;
  canonicalName = [
    basename,
    this.suffix,
    '.',
    this.extname
  ].join('');
  return canonicalName;
};


File.prototype.getOutputPath

simple wrapper to attach canonical to destination path

File.prototype.getOutputPath = function() {
  return path.join(
    this.parent.getOutputPath(),
    this.getCanonicalFilename()
  );
};


File.prototype.mvToOutput

moves file on hard drive

Params
callback function
File.prototype.mvToOutput = function(callback) {
  var file = this;
  var output = this.getOutputPath();
  fs.mv(this.path, output, {mkdirp: true}, function(err) {
    if (err) {
      throw err;
    }
    file.setPath(output);
    callback();
  });
};


module.exports

just the Directory and File classes

module.exports = {
  Directory: Directory,
  File: File
}