text: var cheerio = require('cheerio');
  var https = require('https');
  var fs = require('fs');
  var path = require('path');
  var querystring = require('querystring');
  var crypto = require('crypto');
  var tryFromEnv = require("./environment_variables").tryFromEnv;
  
  const LOGGER = require('@calzoneman/jsli')('google2vtt');
  
  function md5(input) {
      var hash = crypto.createHash('md5');
      hash.update(input);
      return hash.digest('base64').replace(/\//g, ' ')
          .replace(/\+/g, '#')
          .replace(/=/g, '-');
  }
  
  var slice = Array.prototype.slice;
  var subtitleDir = tryFromEnv("google2vtt.js", "google-drive-subtitles", path.resolve(__dirname, '..', 'google-drive-subtitles'));
  console.log("google2vtt.js subtitleDir is:", subtitleDir);
  var subtitleLock = {};
  var ONE_HOUR = 60 * 60 * 1000;
  var ONE_DAY = 24 * ONE_HOUR;
  
  function padZeros(n) {
      n = n.toString();
      if (n.length < 2) n = '0' + n;
      return n;
  }
  
  function formatTime(time) {
      var hours = Math.floor(time / 3600);
      time = time % 3600;
      var minutes = Math.floor(time / 60);
      time = time % 60;
      var seconds = Math.floor(time);
      var ms = time - seconds;
  
      var list = [minutes, seconds];
      if (hours) {
          list.unshift(hours);
      }
  
      return list.map(padZeros).join(':') + ms.toFixed(3).substring(1);
  }
  
  function fixText(text) {
      return text.replace(/&amp;/g, '&')
          .replace(/&lt;/g, '<')
          .replace(/&gt;/g, '>')
          .replace(/&quot;/g, '"')
          .replace(/&#39;/g, "'")
          .replace(/-->/g, '--&gt;');
  }
  
  exports.convert = function convertSubtitles(subtitles) {
      var $ = cheerio.load(subtitles, { xmlMode: true });
      var lines = slice.call($('transcript text').map(function (index, elem) {
          var start = parseFloat(elem.attribs.start);
          var end = start + parseFloat(elem.attribs.dur);
          var text;
          if (elem.children.length) {
              text = elem.children[0].data;
          } else {
              text = '';
          }
  
          var line = formatTime(start) + ' --> ' + formatTime(end);
          line += '\n' + fixText(text) + '\n';
          return line;
      }));
  
      return 'WEBVTT\n\n' + lines.join('\n');
  };
  
  exports.attach = function setupRoutes(app) {
      app.get('/gdvtt/:id/:lang/(:name)?.vtt', handleGetSubtitles);
  };
  
  function handleGetSubtitles(req, res) {
      var id = req.params.id;
      var lang = req.params.lang;
      var name = req.params.name || '';
      var vid = req.query.vid;
      if (typeof vid !== 'string' || typeof id !== 'string' || typeof lang !== 'string') {
          return res.sendStatus(400);
      }
      var file = [id, lang, md5(name)].join('_') + '.vtt';
      var fileAbsolute = path.join(subtitleDir, file);
  
      takeSubtitleLock(fileAbsolute, function () {
          fs.exists(fileAbsolute, function (exists) {
              if (exists) {
                  res.sendFile(file, { root: subtitleDir });
                  delete subtitleLock[fileAbsolute];
              } else {
                  fetchSubtitles(id, lang, name, vid, fileAbsolute, function (err) {
                      delete subtitleLock[fileAbsolute];
                      if (err) {
                          LOGGER.error(err.stack);
                          return res.sendStatus(500);
                      }
  
                      res.sendFile(file, { root: subtitleDir });
                  });
              }
          });
      });
  }
  
  function fetchSubtitles(id, lang, name, vid, file, cb) {
      var query = {
          id: id,
          v: id,
          vid: vid,
          lang: lang,
          name: name,
          type: 'track',
          kind: undefined
      };
  
      var url = 'https://drive.google.com/timedtext?' + querystring.stringify(query);
      https.get(url, function (res) {
          if (res.statusCode !== 200) {
              return cb(new Error(res.statusMessage));
          }
  
          var buf = '';
          res.setEncoding('utf-8');
          res.on('data', function (data) {
              buf += data;
          });
  
          res.on('end', function () {
              try {
                  buf = exports.convert(buf);
              } catch (e) {
                  return cb(e);
              }
  
              fs.writeFile(file, buf, function (err) {
                  if (err) {
                      cb(err);
                  } else {
                      LOGGER.info('Saved subtitle file ' + file);
                      cb();
                  }
              });
          });
      }).on('error', function (err) {
          cb(err);
      });
  }
  
  function clearOldSubtitles() {
      fs.readdir(subtitleDir, function (err, files) {
          if (err) {
              LOGGER.error(err.stack);
              return;
          }
  
          files.forEach(function (file) {
              fs.stat(path.join(subtitleDir, file), function (err, stats) {
                  if (err) {
                      LOGGER.error(err.stack);
                      return;
                  }
  
                  if (stats.mtime.getTime() < Date.now() - ONE_DAY) {
                      LOGGER.info('Deleting old subtitle file: ' + file);
                      fs.unlink(path.join(subtitleDir, file), error => {
                          if (error) {
                              LOGGER.error(
                                  'Failed to remove file %s: %s',
                                  file,
                                  error.stack
                              );
                          }
                      });
                  }
              });
          });
      });
  }
  
  function takeSubtitleLock(filename, cb) {
      if (!subtitleLock.hasOwnProperty(filename)) {
          subtitleLock[filename] = true;
          return setImmediate(cb);
      }
  
      var tries = 1;
      var interval = setInterval(function () {
          tries++;
          if (!subtitleLock.hasOwnProperty(filename) || tries >= 5) {
              subtitleLock[filename] = true;
              clearInterval(interval);
              return setImmediate(cb);
          }
      }, 200);
  }
  
  setInterval(clearOldSubtitles, ONE_HOUR);
  clearOldSubtitles();
  