Greasy Fork is available in English.

monkey-videos

播放网页里的视频, 不再需要Adobe Flash Player

// ==UserScript==
// @name         monkey-videos
// @description  播放网页里的视频, 不再需要Adobe Flash Player
// @version      1.0.7
// @license      GPLv3
// @author       LiuLang
// @email        gsushzhsosgsu@gmail.com
// @include      http://www.56.com/u*
// @include      http://www.56.com/w*
// @include      http://www.acfun.tv/v/ac*
// @include      http://www.bilibili.com/video/av*
// @include      http://tv.cntv.cn/video/*
// @include      http://search.cctv.com/playVideo.php*
// @include      http://www.fun.tv/vplay/*
// @include      http://fun.tv/vplay/*
// @include      http://phtv.ifeng.com/program/*
// @include      http://v.ifeng.com/*
// @include      http://www.iqiyi.com/v_*
// @include      http://www.iqiyi.com/jilupian/*
// @include      http://www.letv.com/ptv/vplay/*
// @include      http://www.justing.com.cn/page/*
// @include      http://v.ku6.com/*
// @include      http://v.163.com/*
// @include      http://open.163.com/*
// @include      http://v.pps.tv/play_*
// @include      http://ipd.pps.tv/play_*
// @include      http://video.sina.com.cn/*
// @include      http://open.sina.com.cn/course/*
// @include      http://tv.sohu.com/*
// @include      http://www.tucao.cc/play/*
// @include      http://www.tudou.com/albumplay/*
// @include      http://www.tudou.com/listplay/*
// @include      http://www.tudou.com/programs/view/*
// @include      http://www.wasu.cn/Play/show/id/*
// @include      http://www.wasu.cn/play/show/id/*
// @include      http://www.wasu.cn/wap/Play/show/id/*
// @include      http://www.wasu.cn/wap/play/show/id/*
// @include      http://www.weiqitv.com/index/live_back?videoId=*
// @include      http://www.weiqitv.com/index/video_play?videoId=*
// @include      http://v.youku.com/v_show/id_*
// @include      http://v.youku.com/v_playlist/*
// @include      http://www.youtube.com/watch?v=*
// @include      https://www.youtube.com/watch?v=*
// @include      http://www.youtube.com/embed/*
// @include      https://www.youtube.com/embed/*
// @run-at       document-end
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// @namespace https://greasyfork.org/users/7422
// ==/UserScript==

(function() {

////////////////////////////////////////////////////////////////////////
////  Router
////////////////////////////////////////////////////////////////////////
  var monkey = {
    pathnames: {},
    handlers: {},

    run: function() {
      console.log('monkey.run: ', this);
      var handler = this.matchHandler();
      console.log('handler:', handler);
      if (handler !== null) {
        handler.run();
      }
    },

    extend: function(hostname, pathnameList, handler) {
      this.pathnames[hostname] = pathnameList;
      this.handlers[hostname] = handler;
    },

    matchHandler: function() {
      console.log('matchHandler() --');
      var host = location.host,
          url = location.href,
          pathnames;
      if (host in this.pathnames) {
        pathnames = this.pathnames[host];
        if (pathnames.some(function(pathname) {
              return url.startsWith(pathname);
            })) {
          return this.handlers[host];
        }
      }
      return null;
    },
  };

////////////////////////////////////////////////////////////////////////
////  Utils
////////////////////////////////////////////////////////////////////////

  /*
  * JavaScript MD5 1.0.1
  * https://github.com/blueimp/JavaScript-MD5
  *
  * Copyright 2011, Sebastian Tschan
  * https://blueimp.net
  *
  * Licensed under the MIT license:
  * http://www.opensource.org/licenses/MIT
  *
  * Based on
  * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
  * Digest Algorithm, as defined in RFC 1321.
  * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009
  * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
  * Distributed under the BSD License
  * See http://pajhome.org.uk/crypt/md5 for more info.
  */

  /*jslint bitwise: true */
  /*global unescape, define */

  (function ($) {
    'use strict';

    /*
     * Add integers, wrapping at 2^32. This uses 16-bit operations internally
     * to work around bugs in some JS interpreters.
     */
    function safe_add(x, y) {
      var lsw = (x & 0xFFFF) + (y & 0xFFFF),
          msw = (x >> 16) + (y >> 16) + (lsw >> 16);
      return (msw << 16) | (lsw & 0xFFFF);
    }

    /*
     * Bitwise rotate a 32-bit number to the left.
     */
    function bit_rol(num, cnt) {
      return (num << cnt) | (num >>> (32 - cnt));
    }

    /*
     * These functions implement the four basic operations the algorithm uses.
     */
    function md5_cmn(q, a, b, x, s, t) {
      return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s), b);
    }
    function md5_ff(a, b, c, d, x, s, t) {
      return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);
    }
    function md5_gg(a, b, c, d, x, s, t) {
      return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);
    }
    function md5_hh(a, b, c, d, x, s, t) {
      return md5_cmn(b ^ c ^ d, a, b, x, s, t);
    }
    function md5_ii(a, b, c, d, x, s, t) {
      return md5_cmn(c ^ (b | (~d)), a, b, x, s, t);
    }

    /*
     * Calculate the MD5 of an array of little-endian words, and a bit length.
     */
    function binl_md5(x, len) {
      /* append padding */
      x[len >> 5] |= 0x80 << (len % 32);
      x[(((len + 64) >>> 9) << 4) + 14] = len;

      var i, olda, oldb, oldc, oldd,
          a = 1732584193,
          b = -271733879,
          c = -1732584194,
          d = 271733878;

      for (i = 0; i < x.length; i += 16) {
        olda = a;
        oldb = b;
        oldc = c;
        oldd = d;

        a = md5_ff(a, b, c, d, x[i], 7, -680876936);
        d = md5_ff(d, a, b, c, x[i + 1], 12, -389564586);
        c = md5_ff(c, d, a, b, x[i + 2], 17, 606105819);
        b = md5_ff(b, c, d, a, x[i + 3], 22, -1044525330);
        a = md5_ff(a, b, c, d, x[i + 4], 7, -176418897);
        d = md5_ff(d, a, b, c, x[i + 5], 12, 1200080426);
        c = md5_ff(c, d, a, b, x[i + 6], 17, -1473231341);
        b = md5_ff(b, c, d, a, x[i + 7], 22, -45705983);
        a = md5_ff(a, b, c, d, x[i + 8], 7, 1770035416);
        d = md5_ff(d, a, b, c, x[i + 9], 12, -1958414417);
        c = md5_ff(c, d, a, b, x[i + 10], 17, -42063);
        b = md5_ff(b, c, d, a, x[i + 11], 22, -1990404162);
        a = md5_ff(a, b, c, d, x[i + 12], 7, 1804603682);
        d = md5_ff(d, a, b, c, x[i + 13], 12, -40341101);
        c = md5_ff(c, d, a, b, x[i + 14], 17, -1502002290);
        b = md5_ff(b, c, d, a, x[i + 15], 22, 1236535329);

        a = md5_gg(a, b, c, d, x[i + 1], 5, -165796510);
        d = md5_gg(d, a, b, c, x[i + 6], 9, -1069501632);
        c = md5_gg(c, d, a, b, x[i + 11], 14, 643717713);
        b = md5_gg(b, c, d, a, x[i], 20, -373897302);
        a = md5_gg(a, b, c, d, x[i + 5], 5, -701558691);
        d = md5_gg(d, a, b, c, x[i + 10], 9, 38016083);
        c = md5_gg(c, d, a, b, x[i + 15], 14, -660478335);
        b = md5_gg(b, c, d, a, x[i + 4], 20, -405537848);
        a = md5_gg(a, b, c, d, x[i + 9], 5, 568446438);
        d = md5_gg(d, a, b, c, x[i + 14], 9, -1019803690);
        c = md5_gg(c, d, a, b, x[i + 3], 14, -187363961);
        b = md5_gg(b, c, d, a, x[i + 8], 20, 1163531501);
        a = md5_gg(a, b, c, d, x[i + 13], 5, -1444681467);
        d = md5_gg(d, a, b, c, x[i + 2], 9, -51403784);
        c = md5_gg(c, d, a, b, x[i + 7], 14, 1735328473);
        b = md5_gg(b, c, d, a, x[i + 12], 20, -1926607734);

        a = md5_hh(a, b, c, d, x[i + 5], 4, -378558);
        d = md5_hh(d, a, b, c, x[i + 8], 11, -2022574463);
        c = md5_hh(c, d, a, b, x[i + 11], 16, 1839030562);
        b = md5_hh(b, c, d, a, x[i + 14], 23, -35309556);
        a = md5_hh(a, b, c, d, x[i + 1], 4, -1530992060);
        d = md5_hh(d, a, b, c, x[i + 4], 11, 1272893353);
        c = md5_hh(c, d, a, b, x[i + 7], 16, -155497632);
        b = md5_hh(b, c, d, a, x[i + 10], 23, -1094730640);
        a = md5_hh(a, b, c, d, x[i + 13], 4, 681279174);
        d = md5_hh(d, a, b, c, x[i], 11, -358537222);
        c = md5_hh(c, d, a, b, x[i + 3], 16, -722521979);
        b = md5_hh(b, c, d, a, x[i + 6], 23, 76029189);
        a = md5_hh(a, b, c, d, x[i + 9], 4, -640364487);
        d = md5_hh(d, a, b, c, x[i + 12], 11, -421815835);
        c = md5_hh(c, d, a, b, x[i + 15], 16, 530742520);
        b = md5_hh(b, c, d, a, x[i + 2], 23, -995338651);

        a = md5_ii(a, b, c, d, x[i], 6, -198630844);
        d = md5_ii(d, a, b, c, x[i + 7], 10, 1126891415);
        c = md5_ii(c, d, a, b, x[i + 14], 15, -1416354905);
        b = md5_ii(b, c, d, a, x[i + 5], 21, -57434055);
        a = md5_ii(a, b, c, d, x[i + 12], 6, 1700485571);
        d = md5_ii(d, a, b, c, x[i + 3], 10, -1894986606);
        c = md5_ii(c, d, a, b, x[i + 10], 15, -1051523);
        b = md5_ii(b, c, d, a, x[i + 1], 21, -2054922799);
        a = md5_ii(a, b, c, d, x[i + 8], 6, 1873313359);
        d = md5_ii(d, a, b, c, x[i + 15], 10, -30611744);
        c = md5_ii(c, d, a, b, x[i + 6], 15, -1560198380);
        b = md5_ii(b, c, d, a, x[i + 13], 21, 1309151649);
        a = md5_ii(a, b, c, d, x[i + 4], 6, -145523070);
        d = md5_ii(d, a, b, c, x[i + 11], 10, -1120210379);
        c = md5_ii(c, d, a, b, x[i + 2], 15, 718787259);
        b = md5_ii(b, c, d, a, x[i + 9], 21, -343485551);

        a = safe_add(a, olda);
        b = safe_add(b, oldb);
        c = safe_add(c, oldc);
        d = safe_add(d, oldd);
      }
      return [a, b, c, d];
    }

    /*
     * Convert an array of little-endian words to a string
     */
    function binl2rstr(input) {
      var i,
          output = '';
      for (i = 0; i < input.length * 32; i += 8) {
        output += String.fromCharCode((input[i >> 5] >>> (i % 32)) & 0xFF);
      }
      return output;
    }

    /*
     * Convert a raw string to an array of little-endian words
     * Characters >255 have their high-byte silently ignored.
     */
    function rstr2binl(input) {
      var i,
          output = [];
      output[(input.length >> 2) - 1] = undefined;
      for (i = 0; i < output.length; i += 1) {
        output[i] = 0;
      }
      for (i = 0; i < input.length * 8; i += 8) {
        output[i >> 5] |= (input.charCodeAt(i / 8) & 0xFF) << (i % 32);
      }
      return output;
    }

    /*
     * Calculate the MD5 of a raw string
     */
    function rstr_md5(s) {
      return binl2rstr(binl_md5(rstr2binl(s), s.length * 8));
    }

    /*
     * Calculate the HMAC-MD5, of a key and some data (raw strings)
     */
    function rstr_hmac_md5(key, data) {
      var i,
          bkey = rstr2binl(key),
          ipad = [],
          opad = [],
          hash;
      ipad[15] = opad[15] = undefined;
      if (bkey.length > 16) {
        bkey = binl_md5(bkey, key.length * 8);
      }
      for (i = 0; i < 16; i += 1) {
        ipad[i] = bkey[i] ^ 0x36363636;
        opad[i] = bkey[i] ^ 0x5C5C5C5C;
      }
      hash = binl_md5(ipad.concat(rstr2binl(data)), 512 + data.length * 8);
      return binl2rstr(binl_md5(opad.concat(hash), 512 + 128));
    }

    /*
     * Convert a raw string to a hex string
     */
    function rstr2hex(input) {
      var hex_tab = '0123456789abcdef',
          output = '',
          x,
          i;
      for (i = 0; i < input.length; i += 1) {
        x = input.charCodeAt(i);
        output += hex_tab.charAt((x >>> 4) & 0x0F) + hex_tab.charAt(x & 0x0F);
      }
      return output;
    }

    /*
     * Encode a string as utf-8
     */
    function str2rstr_utf8(input) {
      return unescape(encodeURIComponent(input));
    }

    /*
     * Take string arguments and return either raw or hex encoded strings
     */
    function raw_md5(s) {
      return rstr_md5(str2rstr_utf8(s));
    }
    function hex_md5(s) {
      return rstr2hex(raw_md5(s));
    }
    function raw_hmac_md5(k, d) {
      return rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d));
    }
    function hex_hmac_md5(k, d) {
      return rstr2hex(raw_hmac_md5(k, d));
    }

    function md5(string, key, raw) {
      if (!key) {
        if (!raw) {
          return hex_md5(string);
        }
        return raw_md5(string);
      }
      if (!raw) {
        return hex_hmac_md5(key, string);
      }
      return raw_hmac_md5(key, string);
    }

    if (typeof define === 'function' && define.amd) {
      define(function () {
        return md5;
      });
    } else {
      $.md5 = md5;
    }
  }(this));

  /**
   * base64 function wrap
   * usage: base64.encode(str); base64.decode(base64_str);
   */
  var base64 = {
    encodeChars : 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrs' +
                  'tuvwxyz0123456789+/',
    decodeChars : [
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
    52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1,
    -1, 0, 1, 2, 3,  4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
    15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
    -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
    41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1],

    encodeFunction: function(str) {
      var out = '',
        len = str.length,
        i = 0,
        c1,
        c2,
        c3;
    
      while(i < len) {
        c1 = str.charCodeAt(i++) & 0xff;
        if(i === len) {
          out += this.encodeChars.charAt(c1 >> 2);
          out += this.encodeChars.charAt((c1 & 0x3) << 4);
          out += "==";
          break;
        }
        c2 = str.charCodeAt(i++);
        if(i === len) {
          out += this.encodeChars.charAt(c1 >> 2);
          out += this.encodeChars.charAt(((c1 & 0x3)<< 4) | 
              ((c2 & 0xF0) >> 4));
          out += this.encodeChars.charAt((c2 & 0xF) << 2);
          out += "=";
          break;
        }
        c3 = str.charCodeAt(i++);
        out += this.encodeChars.charAt(c1 >> 2);
        out += this.encodeChars.charAt(((c1 & 0x3)<< 4) |
            ((c2 & 0xF0) >> 4));
        out += this.encodeChars.charAt(((c2 & 0xF) << 2) |
            ((c3 & 0xC0) >>6));
        out += this.encodeChars.charAt(c3 & 0x3F);
      }
      return out;
    },

    decodeFunction: function(str) {
      var c1,
        c2,
        c3,
        c4,
        len = str.length,
        out = '',
        i = 0;

      while(i < len) {
        do {
          c1 = this.decodeChars[str.charCodeAt(i++) & 0xff];
        } while(i < len && c1 === -1);
        if(c1 === -1) {
          break;
        }

        do {
          c2 = this.decodeChars[str.charCodeAt(i++) & 0xff];
        } while(i < len && c2 === -1);
        if(c2 === -1) {
          break;
        }
        out += String.fromCharCode((c1 << 2) |
            ((c2 & 0x30) >> 4));
        
        do { 
          c3 = str.charCodeAt(i++) & 0xff;
          if(c3 === 61) {
            return out;
          }
          c3 = this.decodeChars[c3];
        } while(i < len && c3 === -1);
        if(c3 === -1) {
          break;
        }
        out += String.fromCharCode(((c2 & 0XF) << 4) |
            ((c3 & 0x3C) >> 2));

        do { 
          c4 = str.charCodeAt(i++) & 0xff;
          if(c4 === 61) {
            return out;
          }
          c4 = this.decodeChars[c4];
        } while(i < len && c4 === -1);
        if(c4 === -1) {
          break;
        }
        out += String.fromCharCode(((c3 & 0x03) << 6) | c4);
      };
      return out;
    },

    utf16to8: function(str) {
      var out = '',
        len = str.length,
        i,
        c;

      for(i = 0; i < len; i++) {
        c = str.charCodeAt(i);
        if ((c >= 0x0001) && (c <= 0x007F)) {
          out += str.charAt(i);
        } else if (c > 0x07FF) {
          out += String.fromCharCode(0xE0 | ((c >> 12) & 0x0F));
          out += String.fromCharCode(0x80 | ((c >> 6) & 0x3F));
          out += String.fromCharCode(0x80 | ((c >> 0) & 0x3F));
        } else {
          out += String.fromCharCode(0xC0 | ((c >> 6) & 0x1F));
          out += String.fromCharCode(0x80 | ((c >> 0) & 0x3F));
        }
      }
      return out;
    },

    utf8to16: function(str) {
    var out = '',
          len = str.length,
          i = 0,
          c,
          char2,
          char3;
    
      while(i < len) {
        c = str.charCodeAt(i++);
        switch(c >> 4) {
        // 0xxxxxxx
        case 0:
        case 1:
        case 2:
        case 3:
        case 4:
        case 5:
        case 6:
        case 7:
          out += str.charAt(i - 1);
          break;
        // 110x xxxx  10xx xxxx
        case 12: case 13:
          char2 = str.charCodeAt(i++);
          out += String.fromCharCode(((c & 0x1F) << 6) |
              (char2 & 0x3F));
          break;
        // 1110 xxxx 10xx xxxx 10xx xxxx
        case 14:
          char2 = str.charCodeAt(i++);
          char3 = str.charCodeAt(i++);
          out += String.fromCharCode(((c & 0x0F) << 12) |
            ((char2 & 0x3F) << 6) | ((char3 & 0x3F) << 0));
          break;
        }
      }
      return out;
    },

    // This is encode/decode wrap, which convert chars between UTF-8
    // and UTF-16;
    encode: function(str) {
      return this.encodeFunction(this.utf16to8(str));
    },

    decode: function(str) {
      return this.utf8to16(this.decodeFunction(str));
    },
  };


  /**
   * Create a new <style> tag with str as its content.
   * @param string styleText
   *   - The <style> tag content.
   */
  var addStyle = function(styleText) {
    console.log('addStyle() --');
    var style = document.createElement('style');
    if (document.head) {
      document.head.appendChild(style);
      style.innerHTML = styleText;
    }
  };

  /**
   * Split query parameters in url and convert to object
   */
  var getQueryVariable = function(query) {
    var vars = query.split('&'),
        params = {},
        param,
        i;

    for (i = 0; i < vars.length; i += 1) {
      param = vars[i].split('=');
      params[param[0]] = param[1];
    }
    return params;
  };

  /**
   * Convert string to xml
   * @param string str
   *  - the string to be converted.
   * @return object xml
   *  - the converted xml object.
   */
  var parseXML = function(str) {
    if (document.implementation &&
        document.implementation.createDocument) {
      xmlDoc = new DOMParser().parseFromString(str, 'text/xml');
    } else {
      console.log('parseXML() error: not support current web browser!');
      return null;
    }
    return xmlDoc;
  };

  /**
   * UI functions.
   * create UI which has multiples files per video.
   */
  var multiFiles = {

    // videos is an object containing these fields:
    // title, video title
    // formats, title of each video format
    // links, list containing video links of each duration
    videos: null,

    run: function(videos) {
      console.log('multiFiles.run() --');
      this.videos = videos;
      if ((!videos.formats) || (videos.formats.length === 0)) {
        console.error('Error: no video formats specified!');
        return;
      }
      this.removeOldPanels();
      this.createPanel();
    },

    removeOldPanels: function() {
      console.log('removeOldPanels() --');
      var panels = document.querySelectorAll('.monkey-videos-panel'),
          panel,
          i;

      for (i = 0; panel = panels[i]; i += 1) {
        panel.parentElement.removeChild(panel);
      }
    },

    /**
     * Create the control panel.
     */
    createPanel: function() {
      console.log('createPanel() --');
      var panel = document.createElement('div'),
          div,
          form,
          label,
          input,
          span,
          a,
          i,
          playlistWrap,
          playlistToggle,
          that = this;

      addStyle([
        '.monkey-videos-panel {',
          'position: fixed;',
          'right: 10px;',
          'bottom: 0px;',
          'z-index: 99999;',
          'border: 2px solid #ccc;',
          'border-top-left-radius: 14px;',
          'margin: 10px 0px 0px 0px;',
          'padding: 10px 10px 0px 10px;',
          'background-color: #fff;',
          'overflow-y: hidden;',
          'max-height: 90%;',
          'min-width: 100px;',
          'text-align: left;',
        '}',
        '.monkey-videos-panel:hover {',
          'overflow-y: auto;',
        '}',
        '.monkey-videos-panel label {',
          'margin-right: 10px;',
        '}',
        '.monkey-videos-panel .playlist-item {',
          'display: block;',
          'margin-top: 8px;',
        '}',
        '.monkey-videos-panel #playlist-toggle {',
          'height: 10px;',
          'margin-top: 10px;',
        '}',
        '.monkey-videos-panel #playlist-toggle:hover {',
          'cursor: pointer;',
        '}',
        '.monkey-videos-panel .playlist-show {',
          'background-color: #8b82a2;',
          //'border-radius: 0px 0px 5px 5px;',
        '}',
        '.monkey-videos-panel .playlist-hide {',
          'background-color: #462093;',
          //'border-radius: 5px 5px 0px 0px;',
        '}',
      ].join(''));

      panel.className = 'monkey-videos-panel';
      document.body.appendChild(panel);

      playlistWrap = document.createElement('div');
      playlistWrap.className = 'playlist-wrap';
      panel.appendChild(playlistWrap);

      div = document.createElement('div');
      div.className = 'playlist-nav';
      playlistWrap.appendChild(div);

      form = document.createElement('form');
      form.className = 'playlist-format';
      playlistWrap.appendChild(form);
      for (i = 0; i < this.videos.formats.length; i += 1) {
        label = document.createElement('label');
        form.appendChild(label);
        input = document.createElement('input');
        label.appendChild(input);
        input.type = 'radio';
        input.name = 'monkey-videos-format';
        span = document.createElement('span');
        label.appendChild(span);
        span.innerHTML = this.videos.formats[i];

        (function(input, pos) {
          input.addEventListener('change', function() {
            that.modifyList(pos);
            GM_setValue('format', pos);
          }, false);
        })(input, i);
      }
      
      // playlist m3u (with url data schema)
      a = document.createElement('a');
      a.className = 'playlist-m3u';
      a.innerHTML = '播放列表';
      a.title = a.innerHTML;
      a.download = this.videos.title + '.m3u';
      a.href = '';
      form.appendChild(a);

      div = document.createElement('div');
      div.className = 'playlist';
      playlistWrap.appendChild(div);

      playlistToggle = document.createElement('div');
      playlistToggle.id = 'playlist-toggle';
      playlistToggle.title = '隐藏';
      playlistToggle.className = 'playlist-show';
      panel.appendChild(playlistToggle);
      playlistToggle.addEventListener('click', function(event) {
        var wrap = document.querySelector('.monkey-videos-panel .playlist-wrap');
        if (wrap.style.display === 'none') {
          wrap.style.display = 'block';
          event.target.className = 'playlist-show';
          event.target.title = '隐藏';
          GM_setValue('hidePlaylist', false);
        } else {
          wrap.style.display = 'none';
          event.target.title = '显示';
          event.target.className = 'playlist-hide';
          GM_setValue('hidePlaylist', true);
        }
      }, false);

      if (GM_getValue('hidePlaylist', false)) {
        playlistToggle.click();
      }

      this.loadDefault();
    },

    loadDefault: function() {
      console.log('loadDefault() --');
      // Load default type of playlist.
      var currPos = GM_getValue('format', 0),
          formats = this.videos.formats,
          currPlaylist;

      console.log('currPos: ', currPos);
      if (formats.length <= currPos) {
        currPos = formats.length - 1;
      }
      console.log('currPos: ', currPos);

      currPlaylist = document.querySelectorAll(
          '.monkey-videos-panel .playlist-format input')[currPos];

      if (currPlaylist) {
        currPlaylist.checked = true;
        this.modifyList(currPos);
      }
    },

    /**
     * Modify the playlist content.
     *
     * Empty playlist first, and add new links of specific video format.
     */
    modifyList: function(pos) {
      console.log('modifyList(), pos = ', pos);
      var playlist = document.querySelector('.monkey-videos-panel .playlist'),
          url,
          a,
          i;
      
      // Clear its content first
      playlist.innerHTML = '';

      for (i = 0; url = this.videos.links[pos][i]; i += 1) {
        a = document.createElement('a');
        playlist.appendChild(a);
        a.className = 'playlist-item',
        a.href = url;
        if (this.videos.links[pos].length == 1) {
          a.innerHTML = this.videos.title;
        }
        else if (i < 9) {
          a.innerHTML = this.videos.title + '(0' + String(i + 1) + ')';
        } else {
          a.innerHTML = this.videos.title + '(' + String(i + 1) + ')';
        }
        a.title = a.innerHTML;
      }

      // Refresh m3u playlist file.
      document.querySelector('.playlist-m3u').href = this.plsDataScheme();
    },

    /**
     * Generate Playlist using base64 and Data URI scheme.
     * So that we can download directly and same it as a pls file using HTML.
     * URL:http://en.wikipedia.org/wiki/Data_URI_scheme
     * @return string
     *  - Data scheme containting playlist.
     */
    plsDataScheme: function() {
      console.log('plsDataSchema() --');
      return 'data:audio/x-m3u;charset=UTF-8;base64,' +
        base64.encode(this.generatePls());
    },

    /**
     * Generate pls - a multimedia playlist file, like m3u.
     * @return string
     * - playlist content.
     */
    generatePls: function() {
      console.log('generatePls() --');
      var output = [],
          links = document.querySelectorAll('.monkey-videos-panel .playlist-item'),
          a,
          i;

      output.push('#EXTM3U');
      for (i = 0; a = links[i]; i += 1) {
        output.push('#EXTINF:81, ' + a.innerHTML);
        output.push(a.href);
      }
      return output.join('\n');
    },
  };


  var singleFile = {
    // videos is an object containing video info.
    //
    // @title, string, video title
    // @formats, string list, format name of each video
    // @links, string list, video link
    // @msg, string 
    // @ok, bool, is ok is false, @msg will be displayed on playlist-panel
    videos: null,

    run: function(videos) {
      console.log('run() -- ');
      this.videos = videos;
      this.createPanel();
      this.createPlaylist();
    },

    createPanel: function() {
      console.log('createPanel() --');
      var panel = document.createElement('div'),
          playlist = document.createElement('div'),
          playlistToggle = document.createElement('div');

      addStyle([
        '.monkey-videos-panel {',
          'position: fixed;',
          'right: 10px;',
          'bottom: 0px;',
          'z-index: 99999;',
          'border: 2px solid #ccc;',
          'border-top-left-radius: 14px;',
          'margin: 10px 0px 0px 0px;',
          'padding: 10px 10px 0px 10px;',
          'background-color: #fff;',
          'overflow-y: hidden;',
          'max-height: 90%;',
          'min-width: 100px;',
        '}',
        '.monkey-videos-panel:hover {',
          'overflow-y: auto;',
        '}',
        '.monkey-videos-panel label {',
          'margin-right: 10px;',
        '}',
        '.monkey-videos-panel .playlist-item {',
          'display: block;',
        '}',
        '.monkey-videos-panel #playlist-toggle {',
          'height: 10px;',
          'width: 100%;',
          'margin-top: 10px;',
        '}',
        '.monkey-videos-panel #playlist-toggle:hover {',
          'cursor: pointer;',
        '}',
        '.monkey-videos-panel .playlist-show {',
          'background-color: #8b82a2;',
          //'border-radius: 0px 0px 5px 5px;',
        '}',
        '.monkey-videos-panel .playlist-hide {',
          'background-color: #462093;',
          //'border-radius: 5px 5px 0px 0px;',
        '}',
      ].join(''));

      panel.className = 'monkey-videos-panel';
      document.body.appendChild(panel);

      playlist= document.createElement('div');
      playlist.className = 'playlist-wrap';
      panel.appendChild(playlist);

      playlistToggle = document.createElement('div');
      playlistToggle.id = 'playlist-toggle';
      playlistToggle.title = '隐藏';
      playlistToggle.className = 'playlist-show';
      panel.appendChild(playlistToggle);
      playlistToggle.addEventListener('click', function(event) {
        var wrap = document.querySelector('.monkey-videos-panel .playlist-wrap');

        if (wrap.style.display === 'none') {
          wrap.style.display = 'block';
          event.target.className = 'playlist-show';
          event.target.title = '隐藏';
          GM_setValue('hidePlaylist', false);
        } else {
          wrap.style.display = 'none';
          event.target.title = '显示';
          event.target.className = 'playlist-hide';
          GM_setValue('hidePlaylist', true);
        }
      }, false);

      if (GM_getValue('hidePlaylist', false)) {
        playlistToggle.click();
      }
    },

    createPlaylist: function() {
      console.log('createPlayList() -- ');
      var playlist = document.querySelector('.monkey-videos-panel .playlist-wrap'),
          a,
          i;

      if (!this.videos.ok) {
        console.error(this.videos.msg);
        a = document.createElement('span');
        a.title = this.videos.msg;
        a.innerHTML = this.videos.msg;
        playlist.appendChild(a);
        return;
      }

      for (i = 0; i < this.videos.links.length; i += 1) {
        a = document.createElement('a');
        a.className = 'playlist-item';
        a.innerHTML = this.videos.title + '(' + this.videos.formats[i] + ')';
        a.title = a.innerHTML;
        a.href = this.videos.links[i];
        playlist.appendChild(a);
      }
    },
  };

////////////////////////////////////////////////////////////////////////
//// Define video handlers
////////////////////////////////////////////////////////////////////////


/**
 * 56.com
 */
var monkey_56 = {
  title: '',
  id: '',
  json: null,
  videos: {
    'normal': '',
    'clear': '',
    'super': '',
  },

  run: function() {
    console.log('run() --');
    this.getID();
    if (this.id.length > 0) {
      this.getPlaylist();
    } else {
      console.error('Failed to get video id!');
      return;
    }
  },

  /**
   * Get video id
   */
  getID: function() {
    console.log('getID() --');
    var url = location.href,
        idReg = /\/v_(\w+)\.html/,
        idMatch = idReg.exec(url),
        albumIDReg = /_vid-(\w+)\.html/,
        albumIDMatch = albumIDReg.exec(url);

    console.log(idMatch);
    console.log(albumIDMatch);
    if (idMatch && idMatch.length === 2) {
      this.id = idMatch[1]; 
    } else if (albumIDMatch && albumIDMatch.length === 2) {
      this.id = albumIDMatch[1];
    }
    console.log(this);
  },

  /**
   * Get video playlist from a json object
   */
  getPlaylist: function() {
    console.log('getPlaylist() --');
    var url = 'http://vxml.56.com/json/' + this.id + '/?src=out',
        that = this;

    console.log('url: ', url);
    GM_xmlhttpRequest({
      method: 'get',
      url: url,
      onload: function(response) {
        console.log('response:', response);
        var txt = response.responseText,
            json = JSON.parse(txt),
            video,
            i;

        that.json = json;
        if (json.msg == 'ok' && json.status == '1') {
          that.title = json.info.Subject;
          for (i = 0; video = json.info.rfiles[i]; i = i + 1) {
            that.videos[video.type] = video.url;
          }
        }
        that.createUI();
      },
    });
  },

  createUI: function() {
    console.log('createUI() --');
    console.log(this);
    var videos = {
          title: this.title,
          formats: [],
          links: [],
          ok: true,
          msg: '',
        },
        formats = ['normal', 'clear', 'super'],
        format_names = ['标清', '高清', '超清'],
        format,
        link,
        i;

    for (i = 0; format = formats[i]; i += 1) {
      if (format in this.videos && this.videos[format].length > 0) {
        videos.links.push([this.videos[format]]);
        videos.formats.push(format_names[i]);
      } else {
        console.error('This video type is not supported: ', format);
      }
    }
    console.log(videos);
    multiFiles.run(videos);
  },
}


monkey.extend('www.56.com', [
  'http://www.56.com/u',
  'http://www.56.com/w',
], monkey_56);

/**
 * acfun.tv
 */
var monkey_acfun = {
  vid: '',
  origUrl: '',

  run: function() {
    this.getVid();
    if (this.vid.length === 0) {
      console.error('Failed to get video id!');
      this.createUI();
    } else {
      this.getVideoLink();
    }
  },

  getVid: function() {
    console.log('getVid()');
    var videos = document.querySelectorAll(
          'div#area-part-view div.l a'),
        video,
        i;

    console.log('videos: ', videos);
    for (i = 0; video = videos[i]; i += 1) {
      if (video.className.search('active') > 0) {
        this.vid = video.getAttribute('data-vid');
        return;
      }
    }
    console.error('Failed to get vid');
  },

  /**
   * Get video link from a json object
   */
  getVideoLink: function() {
    console.log('getVideoLink()');
    console.log(this);
    //var url = 'http://www.acfun.tv/api/getVideoByID.aspx?vid=' + this.vid,
    var url = 'http://www.acfun.tv/video/getVideo.aspx?id=' + this.vid,
        that = this;

    GM_xmlhttpRequest({
      method: 'GET',
      url: url,
      onload: function(response) {
        console.log('response:', response);
        var json = JSON.parse(response.responseText);

        if (json.success && json.sourceUrl.startsWith('http')) {
          that.origUrl = json.sourceUrl;
        }
        that.createUI();
      },
    });
  },

  createUI: function() {
    console.log('createUI() --');
    console.log(this);
    var videos = {
          title: '原始地址',
          formats: [],
          links: [],
          ok: true,
          msg: '',
        };

    if (this.origUrl.length === 0) {
      videos.ok = false;
      videos.msg = '暂不支持';
      singleFile.run(videos);
    } else {
      videos.formats.push(' ');
      videos.links.push(this.origUrl);
      singleFile.run(videos);
    }
  },
}

monkey.extend('www.acfun.tv', [
  'http://www.acfun.tv/v/ac',
], monkey_acfun);

/**
 * bilibili.tv
 */
var monkey_bili = {
  cid: '',
  title: '',
  oriurl: '',

  run: function() {
    console.log('run() --');
    this.getTitle();
    this.getCid();
  },

  /**
   * Get video title
   */
  getTitle: function() {
    console.log('getTitle()');
    var metas = document.querySelectorAll('meta'),
        meta,
        i;

    for (i = 0; meta = metas[i]; i += 1) {
      if (meta.hasAttribute('name') &&
          meta.getAttribute('name') === 'title') {
        this.title = meta.getAttribute('content');
        return;
      }
    }
    this.title = document.title;
  },

  /**
   * 获取 content ID.
   */
  getCid: function() {
    console.log('getCid()');
    var iframe = document.querySelector('iframe'),
        flashvar = document.querySelector('div#bofqi embed'),
        reg = /cid=(\d+)&aid=(\d+)/,
        match;


    if (iframe) {
      match = reg.exec(iframe.src);
    } else if (flashvar) {
      console.log(flashvar.getAttribute('flashvars'));
      match = reg.exec(flashvar.getAttribute('flashvars'));
    }
    console.log('match:', match);
    if (match && match.length === 3) {
      this.cid = match[1];
      this.getVideos();
    } else {
      console.error('Failed to get cid!');
    }
  },

  /**
   * Get original video links from interface.bilibili.cn
   */
  getVideos: function() {
    console.log('getVideos() -- ');
    var url = 'http://interface.bilibili.cn/player?cid=' + this.cid,
        that = this;

    console.log('url:', url);
    GM_xmlhttpRequest({
      method: 'GET',
      url: url,
      onload: function(response) {
        var reg = /<oriurl>(.+)<\/oriurl>/g,
            txt = response.responseText,
            match = reg.exec(txt);

        if (match && match.length === 2) {
          that.oriurl = match[1];
          that.createUI();
        }
      },
    });
  },

  createUI: function() {
    console.log('createUI() --');
    console.log(this);
    var videos = {
          title: '视频的原始地址',
          formats: [''],
          links: [],
          ok: true,
          msg: '',
        };

    videos.formats.push('');
    videos.links.push(this.oriurl);

    singleFile.run(videos);
  },
}

monkey.extend('www.bilibili.com', [
  'http://www.bilibili.com/video/av',
], monkey_bili);


/**
 * cntv.cn
 */
var monkey_cntv = {
  pid: '',
  title: '',
  json: [],
  videos: {
    chapters: [],
    chapters2: [],
  },

  run: function() {
    console.log('run() --');
    this.router();
  },

  router: function() {
    console.log('router() --');
    var href = location.href,
        schema;
    if (href.search('search.cctv.com/playVideo.php?') > -1) {
      schema = this.hashToObject(location.search.substr(1));
      this.pid = schema.detailsid;
      this.title = decodeURI(schema.title);
      this.getVideoInfo();
    } else if (href.search('tv.cntv.cn/video/') > -1) {
      this.pid = href.match(/\/([^\/]+)$/)[1];
      this.title = document.title.substring(0, document.title.length - 8);
      this.getVideoInfo();
    } else {
      this.getPidFromSource();
    }
  },

  /**
   * Get video pid from html source file
   */
  getPidFromSource: function() {
    console.log('getPidFromSource() --');
    var that = this;

    GM_xmlhttpRequest({
      url: location.href,
      method: 'GET',
      onload: function(response) {
        console.log('response:', response);
        that.parsePid(response.responseText);
      },
    });
  },

  /**
   * Parse txt and get pid of video
   */
  parsePid: function(txt) {
    console.log('parsePid() --');
    var pidReg = /code\.begin-->([^<]+)/,
        pidMatch = pidReg.exec(txt),
        titleReg = /title\.begin-->([^<]+)/,
        titleMatch = titleReg.exec(txt);

    if (titleMatch && titleMatch.length === 2) {
      this.title = titleMatch[1];
    } else {
      this.title = document.title;
    }

    console.log('pidMatch:', pidMatch);
    if (pidMatch && pidMatch.length === 2) {
      this.pid = pidMatch[1];
      this.getVideoInfo();
    } else {
      console.error('Failed to get Pid');
      return;
    }
  },

  /**
   * Get video info, including formats and uri
   */
  getVideoInfo: function() {
    console.log('getVideoInfo() --');
    var url = [
          'http://vdn.apps.cntv.cn/api/getHttpVideoInfo.do?',
          'tz=-8&from=000tv&idlr=32&modified=false&idl=32&pid=',
          this.pid,
          '&url=',
          location.href,
        ].join(''),
        that = this;

    console.log('url:', url);
    GM_xmlhttpRequest({
      url: url,
      method: 'GET',
      onload: function(response) {
        console.log('response: ', response);
        that.json = JSON.parse(response.responseText);
        console.log('that: ', that);
        that.parseVideos();
      },
    });
  },

  /**
   * Parse video info from json object.
   */
  parseVideos: function() {
    console.log('parseVideos() --');
    var chapter;

    for (chapter in this.json.video) {
      if (chapter.startsWith('chapters')) {
        this.parseChapter(chapter);
      }
    }

    this.createUI();
  },

  /**
   * Parse specified chapter, list of video links.
   */
  parseChapter: function(chapter) {
    console.log('parseChapter() --');
    var item,
        i;

    for (i = 0; item = this.json.video[chapter][i]; i += 1) {
      if (this.videos[chapter] === undefined) {
        this.videos[chapter] = [];
      }
      this.videos[chapter].push(item.url);
    }
  },

  /**
   * Call multiFiles.js to construct UI widgets.
   */
  createUI: function() {
    console.log('createUI() --');
    console.log('this: ', this);
    var videos = {
          title: this.title,
          formats: [],
          links: [],
      };

    if (this.videos.chapters.length > 0) {
      videos.formats.push('标清');
      videos.links.push(this.videos.chapters);
    }
    if (this.videos.chapters2.length > 0) {
      videos.formats.push('高清');
      videos.links.push(this.videos.chapters2);
    }

    multiFiles.run(videos);
  },

  /**
   * Create a new <style> tag with str as its content.
   * @param string styleText
   *   - The <style> tag content.
   */
  addStyle: function(styleText) {
    var style = document.createElement('style');
    if (document.head) {
      document.head.appendChild(style);
      style.innerHTML = styleText;
    }
  },

  /**
   * 将URL中的hash转为了个对象/字典, 用于解析链接;
   */
  hashToObject: function(hashTxt) {
    var list = hashTxt.split('&'),
        output = {},
        len = list.length,
        i = 0,
        tmp = '';

    for (i = 0; i < len; i += 1) {
      tmp = list[i].split('=')
      output[tmp[0]] = tmp[1];
    }
    return output;
  },
}

monkey.extend('tv.cntv.cn', [
  'http://tv.cntv.cn/video/',
], monkey_cntv);

monkey.extend('search.cctv.com', [
  'http://search.cctv.com/playVideo.php',
], monkey_cntv);

/**
 * funshion.com
 */
var monkey_funshion = {
  title: '',
  mediaid: '',       // 专辑ID;
  number: '',        // 第几集, 从1计数;
  jobs: 0,

  formats: {
    327680: '标清版',
    491520: '高清版',
    737280: '超清版',
  },
  videos: {
    327680: '',
    491520: '',
    737280: '',
  },

  run: function() {
    console.log('run() --');
    this.router();
  },
  
  /**
   * router control
   */
  router: function() {
    var url = location.href;

    if (url.search('subject/play/') > 1 ||
        url.search('/vplay/') > 1 ) {
      this.getVid();
    } else if (url.search('subject/') > 1) {
      this.addLinks();
    } else if (url.search('uvideo/play/') > 1) {
      this.getUGCID();
    } else {
      console.error('Error: current page is not supported!');
    }
  },

  /**
   * Get UGC video ID.
   * For uvideo/play/'.
   */
  getUGCID: function() {
    console.log('getUGCID() --');
    var urlReg = /uvideo\/play\/(\d+)$/,
        urlMatch = urlReg.exec(location.href);

    console.log('urlMatch: ', urlMatch);
    if (urlMatch.length === 2) {
      this.mediaid = urlMatch[1];
      this.getUGCVideoInfo();
    } else {
      console.error('Failed to parse video ID!');
    }
  },

  getUGCVideoInfo: function() {
    console.log('getUGCVideoInfo() --');
    var url = 'http://api.funshion.com/ajax/get_media_data/ugc/' + this.mediaid,
        that = this;

    console.log('url: ', url);
    GM_xmlhttpRequest({
      url: url,
      method: 'GET',
      onload: function(response) {
        console.log('response: ', response);
        that.json = JSON.parse(response.responseText);
        console.log('json: ', that.json);
        that.decodeUGCVideoInfo();
      },
    });
  },

  decodeUGCVideoInfo: function() {
    console.log('decodeUGCVideoInfo() --');
    var url = [
          'http://jobsfe.funshion.com/query/v1/mp4/',
          this.json.data.hashid,
          '.json?file=',
          this.json.data.filename,
        ].join(''),
        that = this;

    console.log('url: ', url);
    GM_xmlhttpRequest({
      url: url,
      method: 'GET',
      onload: function(response) {
        console.log('response: ', response);
        that.appendUGCVideo(JSON.parse(response.responseText));
      },
    });
  },

  appendUGCVideo: function(videoJson) {
    console.log('appendUGCVideo() --');
    console.log('this: ', this);
    console.log('videoJson:', videoJson);
    var fileformat = this.fileformats[videoJson.playlist[0].bits];

    info = {
      title: this.json.data.name_cn,
      href: videoJson.playlist[0].urls[0],
    };
    console.log('info: ', info);

    this._appendVideo(info);
  },


  /**
   * Get video ID.
   * For subject/play/'.
   */
  getVid: function() {
    console.log('getVid() --');
    var url = location.href,
        urlReg = /subject\/play\/(\d+)\/(\d+)$/,
        urlMatch = urlReg.exec(url),
        urlReg2 = /\/vplay\/m-(\d+)/,
        urlMatch2 = urlReg2.exec(url);

    console.log('urlMatch: ', urlMatch);
    console.log('urlMatch2: ', urlMatch2);
    if (urlMatch && urlMatch.length === 3) {
      this.mediaid = urlMatch[1];
      this.number = parseInt(urlMatch[2]);
    } else if (urlMatch2 && urlMatch2.length === 2) {
      this.mediaid = urlMatch2[1];
      this.number = 1;
    } else {
      console.error('Failed to parse video ID!');
      return;
    }
    this.getVideoInfo();
  },

  /**
   * Download a json file containing video info
   */
  getVideoInfo: function() {
    console.log('getVideoInfo() --');
    var url = [
          'http://api.funshion.com/ajax/get_web_fsp/',
          this.mediaid,
          '/mp4',
        ].join(''),
        that = this;

    console.log('url: ', url);
    GM_xmlhttpRequest({
      url: url,
      method: 'GET',
      onload: function(response) {
        console.log('response: ', response);
        var json = JSON.parse(response.responseText),
            format;
        console.log('json: ', json);
        that.title = json.data.name_cn || that.getTitle();
        if ((! json.data.fsps) || (! json.data.fsps.mult) ||
            (json.data.fsps.mult.length === 0) ||
            (! json.data.fsps.mult[0].cid)) {
          that.createUI();
        }

        that.mediaid = json.data.fsps.mult[0].cid;
        for (format in that.formats) {
          that.jobs = that.jobs + 1;
          that.getVideoLink(format);
        }
      },
    });
  },

  /**
   * Get title from document.tiel
   */
  getTitle: function() {
    console.log('getTitle() --');
    var title = document.title,
        online = title.search(' - 在线观看');

    if (online > -1) {
      return title.substr(0, online);
    } else {
      return title.substr(0, 12) + '..';
    }
  },

  /**
   * Get Video source link.
   */
  getVideoLink: function(format) {
    console.log('getVideoLink() --');
    var url = [
      'http://jobsfe.funshion.com/query/v1/mp4/',
      this.mediaid,
      '.json?bits=',
      format,
      ].join(''),
      that = this;

    console.log('url: ', url);
    GM_xmlhttpRequest({
      method: 'GET',
      url: url,
      onload: function(response) {
        console.log('response: ', response);
        var json = JSON.parse(response.responseText);
        console.log('json: ', json);
        that.videos[format] = json.playlist[0].urls[0];
        that.jobs = that.jobs - 1;
        if (that.jobs === 0) {
          that.createUI();
        }
      },
    });
  },

  createUI: function() {
    console.log('createUI() --');
    console.log(this);
    var videos = {
          title: this.title,
          formats: [],
          links: [],
          ok: true,
          msg: '',
        },
        format;

    if (this.videos[327680].length > 0) {
      videos.links.push([this.videos[327680]]);
      videos.formats.push(this.formats[327680]);
    }
    if (this.videos[491520].length > 0) {
      videos.links.push([this.videos[491520]]);
      videos.formats.push(this.formats[491520]);
    }
    if (this.videos[737280].length > 0) {
      videos.links.push([this.videos[737280]]);
      videos.formats.push(this.formats[737280]);
    }

    if (videos.links.length === 0) {
      videos.ok = false;
      videos.msg = 'Video source is not available.';
    }
    multiFiles.run(videos);
  },
}

monkey.extend('fun.tv', [
  'http://fun.tv/vplay/',
], monkey_funshion);

monkey.extend('www.fun.tv', [
  'http://www.fun.tv/vplay/',
], monkey_funshion);

/**
 * ifeng.com
 */
var monkey_ifeng = {
  id: '',
  title: '',
  links: [],

  run: function() {
    console.log('run() --');
    this.getId();
  },

  getId: function() {
    console.log('getId() --');
    var reg = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/,
        match = reg.exec(location.href);

    console.log('match: ', match);
    if (match && match.length === 2) {
      this.id = match[1];
      this.downloadById();
    } else {
      console.error('Failed to get video id');
    }
  },

  downloadById: function() {
    console.log('downloadById() --');
    var length = this.id.length,
        url = [
          'http://v.ifeng.com/video_info_new/',
          this.id[length-2],
          '/',
          this.id.substr(length-2),
          '/',
          this.id,
          '.xml'
         ].join(''),
         that = this;

    console.log('url:', url);
    GM_xmlhttpRequest({
      method: 'GET',
      url: url,
      onload: function(response) {
        var xml = new DOMParser().parseFromString(response.responseText,
                                                  'text/xml'),
            item = xml.querySelector('item');

        that.title = item.getAttribute('Name');
        that.links.push(item.getAttribute('VideoPlayUrl'));
        that.createUI();
      },
    });
  },

  createUI: function() {
    console.log('createUI() --');
    console.log(this);
    var videos = {
          title: this.title,
          formats: [],
          links: [],
          ok: true,
          msg: '',
        };
    videos.formats.push('标清');
    videos.links.push([this.links]);
    multiFiles.run(videos);
  },
};

monkey.extend('v.ifeng.com', [
  'http://v.ifeng.com/',
], monkey_ifeng);

monkey.extend('phtv.ifeng.com', [
  'http://phtv.ifeng.com/program/',
], monkey_ifeng);

/**
 * iqiyi.com
 */
var monkey_iqiyi = {
  title: '',
  vid: '',  // default vid, data-player-videoid
  uid: '',  // generated uuid/user id
  aid: '',  // album id, data-player-albumid
  tvid: '', // data-player-tvid
  type: 0,  // default type
  rcOrder: [96, 1, 2, 3, 4, 5, 10],
  vip: false, // this video is for VIP only
  rc: {
    96: {name: '240P', links: []},
    1: {name: '320P', links: []},
    2: {name: '480P', links: []},
    3: {name: 'super', links: []},
    4: {name: '720P', links: []},
    5: {name: '1080P', links: []},
    10: {name: '4K', links: []},
  },
  jobs: 0,

  run: function() {
    console.log('run() --');
    this.getTitle();
    this.getVid();
  },

  getTitle: function() {
    console.log('getTitle() --');
    var nav = unsafeWindow.document.querySelector('#navbar em'),
        id,
        title;

    if (nav) {
      title = nav.innerHTML;
    } else {
      title = unsafeWindow.document.title.split('-')[0];
    }
    this.title = title.trim();
  },

  getVid: function() {
    console.log('getVid() --');
    var videoPlay = unsafeWindow.document.querySelector('div#flashbox');
    if (videoPlay && videoPlay.hasAttribute('data-player-videoid')) {
      this.vid = videoPlay.getAttribute('data-player-videoid');
      this.aid = videoPlay.getAttribute('data-player-aid');
      this.tvid = videoPlay.getAttribute('data-player-tvid');
      this.uid = this.hex_guid();
      this.getVideoUrls();
    } else {
      console.error('Error: failed to get video id');
      return;
    }
  },

  getVideoUrls: function() {
    console.log('getVideoUrls() --');
    var tm = this.randint(1000, 2000),
        enc = md5('ts56gh' + tm + this.tvid),
        url = [
          'http://cache.video.qiyi.com/vms?key=fvip&src=p',
          '&tvId=', this.tvid,
          '&vid=', this.vid,
          '&vinfo=1&tm=', tm,
          '&enc=', enc,
          '&qyid=', this.uid,
          '&tn=', Math.random(),
        ].join(''),
        that = this;

    GM_xmlhttpRequest({
      method: 'GET',
      url: url,
      onload: function(response) {
        var json = JSON.parse(response.responseText),
            formats,
            format,
            vlink,
            vlink_parts,
            key,
            url,
            i,
            j;

        that.title = json.data.vi.vn;
        if (! json.data.vp.tkl) {
          that.vip = true;
          that.createUI();
        }

        formats = json.data.vp.tkl[0].vs;
        for (i = 0; format = formats[i]; i += 1) {
          if (! that.rc[format.bid]) {
            console.error('Current video type not supported: ', format.bid);
            continue;
          }
          for (j = 0; j < format.fs.length; j += 1) {
            vlink = format.fs[j].l;
            if (! vlink.startsWith('/')) {
              vlink = that.getVrsEncodeCode(vlink);
            }
            vlink_parts = vlink.split('/');
            that.getDispathKey(
                vlink_parts[vlink_parts.length - 1].split('.')[0],
                format.bid, vlink, j);
          }
        }
      },
    });
  },

  getVrsEncodeCode: function(vlink) {
    var loc6 = 0,
        loc2 = [],
        loc3 = vlink.split('-'),
        loc4 = loc3.length,
        i;

    for (i = loc4 - 1; i >= 0; i -= 1) {
      loc6 = this.getVRSXORCode(parseInt(loc3[loc4 - i - 1], 16), i);
      loc2.push(String.fromCharCode(loc6));
    }
    return loc2.reverse().join('');
  },

  getVRSXORCode: function(arg1, arg2) {
    var loc3 = arg2 % 3;
    if (loc3 === 1) {
      return arg1 ^ 121;
    } else if (loc3 === 2) {
      return arg1 ^ 72
    } else {
      return arg1 ^ 103;
    }
  },

  getDispathKey: function(rid, bid, vlink, i) {
    var tp =  ")(*&^flash@#$%a",
        that = this;

    GM_xmlhttpRequest({
      method: 'GET',
      url: 'http://data.video.qiyi.com/t?tn=' + Math.random(),
      onload: function(response) {
        var json = JSON.parse(response.responseText),
            time = json.t,
            t = Math.floor(parseInt(time) / 600.0).toString(),
            key = md5(t + tp + rid),
            url = [
              'http://data.video.qiyi.com/', key, '/videos', vlink,
              '?su=', that.uid,
              '&client=&z=&bt=&ct=&tn=', that.randint(10000, 20000),
              ].join('');

          that.rc[bid].links.push('');
          that.jobs += 1;
          that.getFinalURL(bid, i, url);
      },
    });
  },

  getFinalURL: function(bid, i, url) {
    var that = this;
    
    GM_xmlhttpRequest({
      method: 'GET',
      url: url,
      onload: function(response) {
        var json = JSON.parse(response.responseText);

        that.rc[bid].links[i] = json.l;
        that.jobs -= 1;
        if (that.jobs === 0) {
          that.createUI();
        }
      },
    });
  },

  createUI: function() {
    console.log('createUI() --');
    console.log(this);
    var i,
        video,
        videos = {
          title: this.title,
          formats: [],
          links: [],
        },
        links,
        link,
        j;

    if (this.vip) {
      unsafeWindow.alert('VIP only!');
      return;
    }
    for (i = 0; i < this.rcOrder.length; i += 1) {
      video = this.rc[this.rcOrder[i]];
      if (video.links.length > 0) {
        videos.formats.push(video.name);

        // append KEY/UUID string to end of each video link
        links = [];
        for (j = 0; j < video.links.length; j += 1) {
          link = [video.links[j], '?', video.key].join(''); 
          links.push(link);
        }
        videos.links.push(links);
      }
    }
    multiFiles.run(videos);
  },

  /**
   * Convert string to xml
   * @param string str
   *  - the string to be converted.
   * @return object xml
   *  - the converted xml object.
   */
  parseXML: function(str) {
    if (unsafeWindow.document.implementation &&
        unsafeWindow.document.implementation.createDocument) {
      xmlDoc = new DOMParser().parseFromString(str, 'text/xml');
    } else {
      console.log('parseXML() error: not support current web browser!');
      return null;
    }
    return xmlDoc;
  },

  /**
   * Generate a UUID string
   */
  hex_guid: function() {
    function s4() {
      return Math.floor((1 + Math.random()) * 0x10000)
                 .toString(16)
                 .substring(1);
    }
    return [s4(), s4(), s4(), s4(), s4(), s4(), s4(), s4()].join('');
  },

  randint: function(start, stop) {
    return parseInt(Math.random() * (stop - start)) + start;
  },
};

monkey.extend('www.iqiyi.com', [
  'http://www.iqiyi.com/v_',
  'http://www.iqiyi.com/jilupian/',
], monkey_iqiyi);

/**
 * letv.com
 */
var monkey_letv = {
  pid: '',
  vid: '',   // video id
  title: '',
  stime: 0, // server timestamp
  tkey: 0,  // time key
  jobs: 0,

  videoUrl: {
    '9': '',
    '21': '',
    '13': '',
  },
  videoFormats: {
    '9': '流畅',
    '13': '超清',
    '21': '高清',
  },

  run: function() {
    console.log('run() -- ');
    var url = location.href;
    this.title = document.title.substr(0, document.title.length-12);

    if (url.search('yuanxian.letv') !== -1) {
      // movie info page.
      this.addLinkToYuanxian();
    } else if (url.search('ptv/pplay/') > 1 ||
               url.search('ptv/vplay/' > 1)) {
      this.getVid();
    } else {
      console.error('I do not know what to do!');
    }
  },

  /**
   * Show original video link in video index page.
   */
  addLinkToYuanxian: function() {
    console.log('addLinkToYuanxian() --');
    var pid = __INFO__.video.pid,
        url = 'http://www.letv.com/ptv/pplay/' + pid + '.html',
        titleLink = document.querySelector('dl.w424 dt a');

    titleLink.href = url;
  },

  /**
   * Get video id
   */
  getVid: function() {
    console.log('getVid() --')
    var input = document.querySelector('.add input'),
        vidReg = /\/(\d+)\.html$/,
        vidMatch;

    console.log(input);
    if (input && input.hasAttribute('value')) {
      vidMatch = vidReg.exec(input.getAttribute('value'));
    } else {
      console.error('Failed to get input element');
      return;
    }

    console.log('vidMatch: ', vidMatch);
    if (vidMatch && vidMatch.length === 2) {
      this.vid = vidMatch[1];
      this.getTimestamp();
    } else {
      console.error('Failed to get video ID!');
      return;
    }
  },

  /**
   * Get timestamp from server
   */
  getTimestamp: function() {
    console.log('getTimestamp() --');
    var tn = Math.random(),
        url = 'http://api.letv.com/time?tn=' + tn.toString(),
        that = this;

    console.log('url:', url);
    GM_xmlhttpRequest({
      method: 'GET',
      url: url,
      onload: function(response) {
        console.log('response:', response);
        var obj = JSON.parse(response.responseText);
        that.stime = parseInt(obj.stime);
        that.tkey = that.getKey(that.stime);
        that.getVideoXML();
      },
    });
  },

  /**
   * Get time key
   * @param integer t, server time
   */
  getKey: function(t) {
    console.log('getKey() --', t);
    for(var e = 0, s = 0; s < 8; s += 1){
            e = 1 & t;
            t >>= 1;
            e <<= 31;
            t += e;
    }
    return t ^ 185025305;
  },

  /**
   * Get video info from an xml file
   */
  getVideoXML: function() {
    console.log('getVideoXML() --');
    var url = [
          'http://api.letv.com/mms/out/common/geturl?',
          'platid=3&splatid=301&playid=0&vtype=9,13,21,28&version=2.0',
          '&tss=no',
          '&vid=', this.vid,
          '&tkey=', this.tkey,
          '&domain=http%3A%2F%2Fwww.letv.com'
          ].join(''),
        that = this;

    console.log('videoXML url: ', url);
    GM_xmlhttpRequest({
      method: 'GET',
      url: url,
      onload: function(response) {
        console.log('response: ', response);
        var json = JSON.parse(response.responseText);
        that.getVideoUrl(json.data[0].infos);
      },
    });
  },

  /**
   * Parse video url
   */
  getVideoUrl: function(videos) {
    console.log('getVideoUrl() --');
    var video,
        i,
        url;


    for (i = 0; video = videos[i]; i = i + 1) {
      url = [
        video.mainUrl,
        '&ctv=pc&m3v=1&termid=1&format=1&hwtype=un&ostype=Linux&tag=letv',
        '&sign=letv&expect=3&pay=0&iscpn=f9051&rateid=1300',
        '&tn=', Math.random()
        ].join('');
      this.getFinalUrl(url, video.vtype);
      this.jobs += 1;
    }
  },

  /**
   * Get final video links
   * @param url, video link,
   * @param type, video type
   */
  getFinalUrl: function(url, type) {
    console.log('getFinalUrl() --', type);
    var that = this;

    GM_xmlhttpRequest({
      url: url,
      method: 'GET',
      onload: function(response) {
        var json = JSON.parse(response.responseText);
        that.videoUrl[type] = json.location;
        that.jobs -= 1;
        if (that.jobs === 0) {
          that.createUI();
        }
      },
    });
  },

  /**
   * construct ui widgets.
   */
  createUI: function() {
    console.log('createUI() --');
    console.log(this);
    var videos = {
          title: this.title,
          formats: [],
          links: [],
          ok: true,
          msg: '',
        },
        types = ['9', '21', '13'],
        type,
        url,
        i;
  
    for (i = 0; type = types[i]; i += 1) {
      url = this.videoUrl[type];
      if (url) {
        videos.links.push([this.videoUrl[type]]);
        videos.formats.push(this.videoFormats[type]);
      }
    }

    multiFiles.run(videos);
  },
};

monkey.extend('www.letv.com', [
  'http://www.letv.com/ptv/vplay/',
], monkey_letv);


/**
 * justing.com.cn
 */
var monkey_justing = {
  title: '',
  link: '',

  run: function() {
    console.log('run() --');
    this.getTitle();
    this.createUI();
  },

  getTitle: function() {
    var titleElem = unsafeWindow.document.querySelector('div#title');
    if (titleElem) {
      this.title = titleElem.innerHTML;
      this.link = encodeURI([
          'http://dl.justing.com.cn/page/',
          this.title,
          '.mp3'].join(''));
    }
  },

  createUI: function() {
    console.log('createUI() --');
    console.log(this);
    var videos = {
          title: this.title,
          links: [],
          formats: [],
          ok: true,
          msg: '',
        };
    if (this.title.length === 0) {
      videos.ok = false;
      videos.msg = 'Failed to get mp3 link';
    } else {
      videos.links.push([this.link]);
      videos.formats.push('mp3');
    }
    multiFiles.run(videos);
  },


};

monkey.extend('www.justing.com.cn', [
  'http://www.justing.com.cn/page/',
], monkey_justing);

/**
 * ku6.com
 */
var monkey_ku6 = {

  vid: '',
  title: '',
  links: [],
  type: '标清',

  run: function() {
    console.log('run() --');
    this.getVid();
  },

  /**
   * 获取video id, 用于构建下载链接.
   */
  getVid: function() {
    console.log('getVid() --');
    var url = location.href,
        vid_reg = /\/([^\/]+)\.html/,
        vid_match = vid_reg.exec(url);

    console.log(vid_match);
    if (vid_match && vid_match.length == 2) {
      this.vid = vid_match[1];
      this.getVideo();
    }
  },

  getVideo: function() {
    console.log('getVideo() --');
    var url = 'http://v.ku6.com/fetchVideo4Player/' + this.vid + '.html',
        that = this;

    console.log('url:', url);
    GM_xmlhttpRequest({
      url: url,
      method: 'GET',
      onload: function(response) {
        console.log('response:', response);
        var video_obj = JSON.parse(response.responseText);
        console.log(video_obj);
        that.title = video_obj.data.t;
        that.links = video_obj.data.f.split(',');
        that.createUI();
      },
    });
  },

  createUI: function() {
    console.log('createUI() --');
    console.log(this);
    var videos = {
          title: this.title,
          formats: [],
          links: [],
        };

    if (this.links.length > 0) {
      videos.formats.push(this.type);
      videos.links.push(this.links);
      multiFiles.run(videos);
    } else {
      console.error('this.video is empty');
    }
  },
};

monkey.extend('v.ku6.com', [
  'http://v.ku6.com/',
], monkey_ku6);

/**
 * 163.com
 */
var monkey_netease = {

  plid: '',  // playlist id
  mid: '',   // video id
  raw_vid: '',
  title: '',
  pl_title: '', // playlist title
  videos: {
    sd: '',
    hd: '',
    shd: '',
  },
  types: {
    sd: '标清',
    hd: '高清',
    shd: '超清',
  },
  subs: {
  },

  run: function() {
    console.log('run() --');

    this.getTitle();
    if (document.title.search('网易公开课') > -1) {
      this.getOpenCourseSource();
    } else {
      this.getSource();
    }
  },

  getTitle: function() {
    console.log('getTitle() --');
    this.title = document.title;
  },

  getOpenCourseSource: function() {
    console.log('getOpenCourseSource() --');
    var url = document.location.href.split('/'),
        urlMatch = /([A-Z0-9]{9})_([A-Z0-9]{9})/,
        match = urlMatch.exec(url),
        length = url.length,
        xmlUrl,
        that = this;

    if (! match || match.length !== 3) {
      console.error('Failed to get mid!', match);
      return;
    }
    this.raw_vid = match[0];
    this.plid = match[1];
    this.mid = match[2];
    xmlUrl = [
      'http://live.ws.126.net/movie',
      url[length - 3],
      url[length - 2],
      '2_' + this.raw_vid + '.xml',
      ].join('/');
    console.log('xmlUrl: ', xmlUrl);

    GM_xmlhttpRequest({
      method: 'GET',
      url: xmlUrl,
      onload: function(response) {
        var xml = parseXML(response.responseText),
            type,
            video,
            subs,
            sub,
            subName,
            i;

        //that.title = xml.querySelector('all title').innerHTML;
        that.title = that.title.replace('_网易公开课', '');
        for (type in that.videos) {
          video = xml.querySelector('playurl ' + type +' mp4');
          if (video) {
            that.videos[type] = video.firstChild.data;
            continue;
          }
          video = xml.querySelector('playurl ' + type.toUpperCase() +' mp4');
          if (video) {
            that.videos[type] = video.firstChild.data;
          }
        }
        subs = xml.querySelectorAll('subs sub');
        for (i = 0; sub = subs[i]; i += 1) {
          subName = sub.querySelector('name').innerHTML + '字幕';
          that.subs[subName] = sub.querySelector('url').innerHTML;
        }
        that.getMobileOpenCourse();
      },
    });
  },

  /**
   * AES ECB decrypt is too large to embed, so use another way.
   */
  getMobileOpenCourse: function() {
    console.log('getMobileOpenCourse() --');
    var url = 'http://mobile.open.163.com/movie/' + this.plid + '/getMoviesForAndroid.htm',
        that = this;

    console.log('url: ', url);
    GM_xmlhttpRequest({
      method: 'GET',
      url: url,
      onload: function(response) {
        var json = JSON.parse(response.responseText),
            video,
            i;

        for (i = 0; i < json.videoList.length; i += 1) {
          video = json.videoList[i];
          console.log('video:', video);
          if (video.mid === that.mid) {
            that.videos.sd = video.repovideourlmp4Origin;
            that.videos.hd = video.repovideourl;
            that.title = video.title;
            that.pl_title = json.title;
            break;
          }
        }
        that.createUI();
      },
    });
  },

  getSource: function() {
    console.log('getSource() --');
    var scripts = document.querySelectorAll('script'),
        script,
        reg = /<source[\s\S]+src="([^"]+)"/,
        match,
        m3u8Reg = /appsrc\:\s*'([\s\S]+)\.m3u8'/,
        m3u8Match,
        i;

    for (i = 0; script = scripts[i]; i += 1) {
      match = reg.exec(script.innerHTML);
      console.log(match);
      if (match && match.length > 1) {
        this.videos.sd = match[1].replace('-mobile.mp4', '.flv');
        this.createUI();
        return true;
      }
      m3u8Match = m3u8Reg.exec(script.innerHTML);
      console.log(m3u8Match);
      if (m3u8Match && m3u8Match.length > 1) {
        this.videos.sd = m3u8Match[1].replace('-list', '') + '.mp4';
        this.createUI();
        return true;
      }
    }
  },

  createUI: function() {
    console.log('createUI() --');
    console.log(this);
    var videos = {
          title: this.title,
          formats: [],
          links: [],
          ok: true,
          msg: '',
        },
        formats = ['sd', 'hd', 'shd'],
        format,
        url,
        subName,
        i;

    if (this.pl_title.length > 0) {
      videos.title = this.title;
    }

    for (i = 0; format = formats[i]; i += 1) {
      url = this.videos[format];
      if (url.length > 0) {
        videos.links.push([url]);
        videos.formats.push(this.types[format]);
      }
    }
    for (subName in this.subs) {
      videos.links.push([this.subs[subName]]);
      videos.formats.push(subName);
    }
    multiFiles.run(videos);
  },
};


monkey.extend('v.163.com', [
  'http://v.163.com/',
], monkey_netease);

monkey.extend('open.163.com', [
  'http://open.163.com/',
], monkey_netease);


/**
 * pps.tv
 */
var monkey_pps = {
  vid: '',
  title: '',
  types: {
    1: '高清',
    2: '标清',
    3: '流畅',
  },
  videoUrl: {
    1: '',
    2: '',
    3: '',
  },
  jobs: 3,
  fromIqiyi: false,

  run: function() {
    console.log('run()');
    if (location.href.search('pps.tv/play_') !== -1) {
      this.getId();
    } else {
      console.error('Failed to get vid!');
    }
  },

  getId: function() {
    console.log('getId() -- ');
    var vidReg = /play_([\s\S]+)\.html/,
        vidMatch = vidReg.exec(document.location.href),
        titleReg = /([\s\S]+)-在线观看/,
        titleMatch = titleReg.exec(document.title);
    if (vidMatch) {
      this.vid = vidMatch[1];
    }
    if (titleMatch) {
      this.title = titleMatch[1];
    }
    if (this.vid.length > 0) {
      this.getUrl(1); // 高清
      this.getUrl(2); // 标清
      this.getUrl(3); // 流畅
    }
  },

  getUrl: function(type) {
    console.log('getUrl()');
    var url = [
      'http://dp.ppstv.com/get_play_url_cdn.php?sid=',
      this.vid,
      '&flash_type=1&type=',
      type,
      ].join(''),
      that = this;

    console.log('url: ', url);
    GM_xmlhttpRequest({
      method: 'GET',
      url: url,
      onload: function(response) {
        console.log(response);
        var txt = response.responseText;

        if (txt.search('.pfv?') > 0) {
          that.videoUrl[type] = txt.substr(0, txt.search('.pfv?') + 4);
        }
        that.jobs -= 1;
        if (that.jobs === 0) {
          that.createUI();
        }
      },
    });
  },

  createUI: function() {
    console.log('createUI() --');
    console.log(this);
    var videos = {
          title: this.title,
          formats: [],
          links: [],
        },
        types = [3, 2, 1],
        type,
        i;

    for (i = 0; type = types[i]; i += 1) {
      if (this.videoUrl[type]) {
        videos.links.push([this.videoUrl[type]]);
        videos.formats.push(this.types[type]);
      }
    }
    multiFiles.run(videos);
  },
}

monkey.extend('v.pps.tv', [
  'http://v.pps.tv/play_',
], monkey_pps);

monkey.extend('ipd.pps.tv', [
    'http://ipd.pps.tv/play_',
], monkey_pps);


/**
 * sina.com.cn
 */
var monkey_sina = {
  title: '',
  jobs: 0,
  video: {
    format: '标清',
    vid: '',
    url: '',
    links: [],
  },
  hdVideo: {
    format: '高清',
    vid: '',
    url: '',
    links: [],
  },

  run: function() {
    var loc = location.href;
    if (loc.search('/vlist/') > -1) {
      this.getVlist();
    } else if (loc.search('video.sina.com.cn') > -1 ||
               loc.search('open.sina.com.cn') > -1) {
      this.getVid(loc);
    } else {
      console.error('This page is not supported!');
      return;
    }
  },

  /**
   * e.g.
   * http://video.sina.com.cn/vlist/news/zt/topvideos1/?opsubject_id=top12#118295074
   * http://video.sina.com.cn/news/vlist/zt/chczlj2013/?opsubject_id=top12#109873117
   */
  getVlist: function() {
    console.log('getVlist() --');
    var h4s = document.querySelectorAll('h4.video_btn'),
        h4,
        i,
        lis = document.querySelectorAll('ul#video_list li'),
        li,
        As,
        A,
        j,
        that = this;

    if (h4s && h4s.length > 0) {
      this.getVlistItem(h4s[0].parentElement);
      for (i = 0; i < h4s.length; i += 1) {
        h4 = h4s[i];
        h4.addEventListener('click', function(event) {
          that.getVlistItem(event.target.parentElement);
        }, false);
      }

    } else if (lis && lis.length > 0) {
      this.getVlistItem(lis[0]);
      for (i = 0; i < lis.length; i += 1) {
        li = lis[i];
        As = li.querySelectorAll('a.btn_play');
        for (j = 0; A = As[j]; j += 1) {
          A.href= li.getAttribute('vurl');
        }
        li.addEventListener('click', function(event) {
          that.getVlistItem(event.target);
          event.preventDefault();
          return;
        }, false);
      }
    }
  },

  getVlistItem: function(div) {
    console.log('getVlistItem() --', div);
    if (div.hasAttribute('data-url')) {
      this.getVid(div.getAttribute('data-url'));
    } else if (div.nodeName === 'A' && div.className === 'btn_play') {
      this.getVid(div.parentElement.parentElement.parentElement.getAttribute('vurl'));
    } else if (div.nodeName === 'IMG') {
      this.getVid(div.parentElement.parentElement.parentElement.parentElement.getAttribute('vurl'));
    } else if (div.hasAttribute('vurl')) {
      this.getVid(div.getAttribute('vurl'));
    } else {
      console.error('Failed to get vid!', div);
      return;
    }
  },

  /**
   * Get Video vid and hdVid.
   */
  getVid: function(url) {
    console.log('getVid() --', url);
    var that = this;

    GM_xmlhttpRequest({
      method: 'GET',
      url: url,
      onload: function(response) {
        console.log(response);
        var reg = /vid:['"](\d{5,})['"]/,
            txt = response.responseText,
            match = reg.exec(txt),
            hdReg = /hd_vid:'(\d{5,})'/,
            hdMatch = hdReg.exec(txt),
            titleReg = /\s+title:'([^']+)'/,
            titleMatch = titleReg.exec(txt);
            title2Reg = /VideoTitle : "([^"]+)"/,
            title2Match = title2Reg.exec(txt);

        if (titleMatch) {
          that.title = titleMatch[1];
        } else if (title2Match) {
          that.title = title2Match[1];
        }
        if (hdMatch && hdMatch.length > 1) {
          that.hdVideo.vid = hdMatch[1];
          that.jobs += 1;
          that.getVideoByVid(that.hdVideo);
        }
        if (match && match.length > 1) {
          that.video.vid = match[1];
          that.jobs += 1;
          that.getVideoByVid(that.video);
        }
      },
    });
  },

  /**
   * Calcuate video information url
   */
  getURLByVid: function(vid) {
    console.log('getURLByVid() -- ', vid);
    var randInt = parseInt(Math.random() * 1000),
        time = parseInt(Date.now() / 1000) >> 6,
        key = '';

    key = [
      vid,
      'Z6prk18aWxP278cVAH',
      time,
      randInt,
      ].join('');
    key = md5(key);
    console.log('key: ', key);
    key = key.substring(0, 16) + time;
    console.log('key: ', key);

    return [
      'http://v.iask.com/v_play.php?',
      'vid=', vid,
      '&uid=null',
      '&pid=null',
      '&tid=undefined',
      '&plid=4001',
      '&prid=ja_7_4993252847',
      '&referer=',
      '&ran=', randInt,
      '&r=video.sina.com.cn',
      '&v=4.1.42.35',
      '&p=i',
      '&k=', key,
      ].join('');
  },

  /**
   * Get video info specified by vid.
   */
  getVideoByVid: function(container) {
    console.log('getVideoByVid() --', container);
    console.log(this);
    var that = this;
    container.url = this.getURLByVid(container.vid),

    GM_xmlhttpRequest({
      method: 'GET',
      url: container.url,
      onload: function(response) {
        console.log('response: ', response);
        var reg = /<url>.{9}([^\]]+)/g,
            txt = response.responseText,
            match = reg.exec(txt);

        while (match) {
          container.links.push(match[1]);
          match = reg.exec(txt);
        }

        that.jobs -= 1;
        if (that.jobs === 0) {
          that.createUI();
        }
      },
    });
  },

  createUI: function() {
    console.log('createUI() --');
    console.log(this);
    var videos = {
          formats: [],
          links: [],
          title: this.title,
        };

    if (this.video.links.length > 0) {
      videos.formats.push(this.video.format);
      videos.links.push(this.video.links);
    }
    if (this.hdVideo.links.length > 0) {
      videos.formats.push(this.hdVideo.format);
      videos.links.push(this.hdVideo.links);
    }
    console.log('videos: ', videos);
    multiFiles.run(videos);
  },
}


monkey.extend('video.sina.com.cn', [
  'http://video.sina.com.cn/',
], monkey_sina);

monkey.extend('open.sina.com.cn', [
  'http://open.sina.com.cn/course/',
], monkey_sina);

/**
 * sohu.com
 */
var monkey_sohu = {
  title: '',
  vid: '',
  plid: '',
  referer: '',
  jobs: 0,
  formats: {
    p1: '标清',
    p2: '高清',
    p3: '超清',
    p4: '原画质'
  },

  p1: {
    json: [],
    su: [],
    clipsURL: [],
    ip: '',
    vid: 0,
    reserveIp: [],
    videos: [],
    params: [],
  },

  p2: {
    json: [],
    su: [],
    vid: 0,
    clipsURL: [],
    ip: '',
    reserveIp: [],
    videos: [],
    params: [],
  },

  p3: {
    json: [],
    su: [],
    clipsURL: [],
    vid: 0,
    ip: '',
    reserveIp: [],
    videos: [],
    params: [],
  },

  p4: {
    json: [],
    su: [],
    clipsURL: [],
    vid: 0,
    ip: '',
    reserveIp: [],
    videos: [],
    params: [],
  },

  run: function() {
    console.log('run() --');
    this.router();
  },

  router: function() {
    console.log('router() -- ');
    var host = document.location.hostname;
    if (host === 'my.tv.sohu.com') {
      this.getUGCId();
    } else if (host === 'tv.sohu.com') {
      this.getId();
    } else {
      console.error('Error: this page is not supported');
    }
  },

  /**
   * Get video id for UGC video
   */
  getUGCId: function() {
    console.log('getUGCId() -- ');
    var scripts = document.querySelectorAll('script'),
        script,
        vidReg = /var vid\s+=\s+'(\d+)'/,
        vidMatch,
        titleReg = /,title:\s+'([^']+)'/,
        titleMatch,
        txt,
        i;

    for (i = 0; script = scripts[i]; i += 1) {
      if (script.innerHTML.search('var vid') > -1) {
        txt = script.innerHTML;
        vidMatch = vidReg.exec(txt);
        console.log('vidMatch: ', vidMatch);
        if (vidMatch && vidMatch.length === 2) {
          this.vid = vidMatch[1];
        }
        console.log('titleMatch: ', titleMatch);
        titleMatch = titleReg.exec(txt);
        if (titleMatch && titleMatch.length === 2) {
          this.title = titleMatch[1];
        }
        break;
      }
    }
    if (this.vid.length > 0) {
      this.referer = escape(location.href);
      this.p2.vid = this.vid;
      this.getUGCVideoJSON('p2');
    } else {
      console.error('Error: failed to get video id!');
    }
  },

  /**
   * Get UGC video info
   */
  getUGCVideoJSON: function(fmt) {
    console.log('getUGCVideoJSON() -- ');
    var that = this,
        url = 'http://my.tv.sohu.com/videinfo.jhtml?m=viewtv&vid=' + this.vid;

    console.log('url: ', url);
    GM_xmlhttpRequest({
      method: 'GET',
      url: url,
      onload: function(response) {
        var json = JSON.parse(response.responseText);

        console.log('json: ', json);
        that[fmt].json = json;
        that[fmt].su = json.data.su;
        that[fmt].clipsURL = json.data.clipsURL;

        if (fmt === 'p2') {
          if (json.data.norVid) {
            that.p1.vid = json.data.norVid;
            that.getUGCVideoJSON('p1');
          }
          if (json.data.superVid) {
            that.p3.vid = json.data.superVid;
            that.getUGCVideoJSON('p3');
          }
          if (json.data.oriVid) {
            that.p4.vid = json.data.oriVid;
            that.getUGCVideoJSON('p4');
          }
        }
        that.decUGCVideo(fmt);
      },
    });
  },

  /**
   * Decode UGC video url
   */
  decUGCVideo: function(fmt) {
    console.log('decUGCVideo() -- ');
    var url,
        json = this[fmt].json,
        i;

    for (i = 0; i < json.data.clipsURL.length; i += 1) {
      url = [
        'http://', json.allot, '/' ,
        '?prot=', json.prot, 
        '&file=', json.data.clipsURL[i],
        '&new=', json.data.su[i],
      ].join('');
      console.log('url: ', url);
      this[fmt].videos.push('');
      this.jobs += 1;
      this.decUGCVideo2(fmt, url, i);
    }
  },

  decUGCVideo2: function(fmt, url, i) {
    console.log('decUGCVideo2() -- ');
    var that = this;

    GM_xmlhttpRequest({
      method: 'GET',
      url: url,
      onload: function(response) {
        console.log('response:', response);
        var params = response.responseText.split('|');

        that[fmt].params = params;
        that[fmt].videos[i] = [
          params[0],
          that[fmt].su[i],
          '?key=',
          params[3],
        ].join('');
        
        that.jobs -= 1;
        if (that.jobs === 0) {
          that.createUI();
        }
      },
    });
  },

  /**
   * Get video id
   */
  getId: function() {
    console.log('getId() --');
    var scripts = document.querySelectorAll('script'),
        script,
        vidReg = /var vid="(\d+)";/,
        vidMatch,
        playlistReg = /var playlistId="(\d+)";/,
        playlistMatch,
        i;
    for (i = 0; script = scripts[i]; i += 1) {
      if (script.innerHTML.search('var vid') > -1) {
        txt = script.innerHTML;
        vidMatch = vidReg.exec(txt);
        console.log('vidMatch: ', vidMatch);
        if (vidMatch && vidMatch.length === 2) {
          this.vid = vidMatch[1];
        }
        playlistMatch = playlistReg.exec(txt);
        if (playlistMatch && playlistMatch.length === 2) {
          this.plid = playlistMatch[1];
          break;
        }
      }
    }

    if (this.vid) {
      this.p2.vid = this.vid;
      this.title = document.title.split('-')[0].trim();
      this.referer = escape(location.href);
      this.jobs += 1;
      this.getVideoJSON('p2');
    } else {
      console.error('Failed to get vid!');
    }
  },

  /**
   * Get video info.
   * e.g. http://hot.vrs.sohu.com/vrs_flash.action?vid=1109268&plid=5028903&referer=http%3A//tv.sohu.com/20130426/n374150509.shtml
   */
  getVideoJSON: function(fmt) {
    console.log('getVideoJSON() --');
    console.log('fmt: ', fmt);
    var pref = 'http://hot.vrs.sohu.com/vrs_flash.action',
        url = '',
        that = this;

    // If vid is unset, just return it.
    if (this[fmt].vid === 0) {
      return;
    }

    url = [
      pref, 
      '?vid=', this[fmt].vid,
      '&plid=', this.plid,
      '&out=0',
      '&g=8',
      '&referer=', this.referer,
      '&r=1',
      ].join('');
    console.log('url: ', url);

    GM_xmlhttpRequest({
      method: 'GET',
      url: url,
      onload: function(response) {
        console.log('response: ', response);
        var i = 0;

        console.log(that);
        that.jobs -= 1;
        that[fmt].json = JSON.parse(response.responseText);
        //that.title = that[fmt].json.data.tvName;
        that[fmt].clipsURL = that[fmt].json.data.clipsURL;
        that[fmt].su = that[fmt].json.data.su;
        that.p1.vid = that[fmt].json.data.norVid;
        that.p2.vid = that[fmt].json.data.highVid;
        that.p3.vid = that[fmt].json.data.superVid;
        that.p4.vid = that[fmt].json.data.oriVid;
        that[fmt].ip = that[fmt].json.allot;
        that[fmt].reserveIp = that[fmt].json.reserveIp.split(';');
        for (i in that[fmt].clipsURL) {
          url = [
            'http://', that[fmt].ip, '/',
            '?prot=', that[fmt].json.prot,
            '&file=', that[fmt].clipsURL[i],
            '&new=', that[fmt].su[i],
            ].join('');
          that.jobs += 1;
          that[fmt].videos.push('');
          that.getRealUrl(fmt, url, that[fmt].su[i], i);
        }

        if (fmt === 'p2') {
          if (that.p1.vid > 0) {
            that.jobs += 1;
            that.getVideoJSON('p1');
          }
          if (that.p3.vid > 0) {
            that.jobs += 1;
            that.getVideoJSON('p3');
          }
          if (that.p4.vid > 0) {
            that.jobs += 1;
            that.getVideoJSON('p4');
          }
        }

      },
    });
  },

  /**
   * Get final url from content like this:
   *   http://61.54.26.168/sohu/s26h23eab6/6/|145|182.112.229.55|G69w1ucoqFNj8ww74DxHN-6ZQTkgJZ90FlnqBA..|1|0|2|1518|1
   */
  getRealUrl: function(fmt, url, new_, i) {
    console.log('getRealUrl() --', fmt, url, new_, i);
    var that = this;

    GM_xmlhttpRequest({
      method: 'GET',
      url: url,
      onload: function(response) {
        var txt = response.responseText,
            params = txt.split('|'),
            start = params[0],
            key = params[3],
            url = [
              start.substr(0, start.length-1), new_,
              '?key=', key,
            ].join('');

        that[fmt].videos[i] = url;
        that.jobs -= 1;

        // Display UI when all processes ended
        if (that.jobs === 0) {
          that.createUI();
        }
      },
    });
  },

  /**
   * Construct UI widgets
   */
  createUI: function() {
    console.log('createUI() --');
    console.log(this);
    var videos = {
          title: this.title,
          links: [],
          formats: [],
        },
        type,
        i;

    for (type in this.formats) {
      console.log('type: ', type);
      if (this[type].videos.length > 0) {
        videos.links.push(this[type].videos);
        videos.formats.push(this.formats[type]);
      }
    }
    if (videos.formats.length > 0) {
      multiFiles.run(videos);
    }
  },
};

monkey.extend('tv.sohu.com', [
  'http://tv.sohu.com/',
], monkey_sohu);


/**
 * tucao.cc
 */
var monkey_tucao = {

  url: '',
  title: '',
  playerId: '',
  key: '',
  timestamp: '',
  vid: '',
  type: '',
  vids: [],
  pos: 0,
  videos: [],
  format: '标清',
  redirect: false,
  types: {
    sina: 'sina',
    tudou: false,  // redirect to original url
    youku: false,  // redirect to original url
  },

  run: function() {
    console.log('run()');
    this.getVid();
  },

  /**
   * Get video id
   */
  getVid: function() {
    console.log('getVid() -- ');
    var playerCode = document.querySelectorAll(
          'ul#player_code li');

    if (playerCode && playerCode.length === 2) {
      this.vids = playerCode[0].firstChild.nodeValue.split('**');
      if (this.vids[this.vids.length - 1] == '') {
        // remove empty vid
        this.vids.pop();
      }
      this.playerId = playerCode[1].innerHTML;
      this.getTitle();
    }
  },

  /**
   * Get video title
   */
  getTitle: function() {
    console.log('getTitle()');
    var params;

    if (this.vids.length === 1 || location.hash === '') {
      this.pos = 0;
      this.url = location.href;
    } else {
      // hash starts with 1, not 0
      this.pos = parseInt(location.hash.replace('#', '')) - 1;
      this.url = location.href.replace(location.hash, '');
    }
    params = getQueryVariable(this.vids[this.pos].split('|')[0]);
    this.vid = params.vid;
    this.type = params.type;
    if (this.vids.length === 1) {
      this.title = document.title.substr(0, document.title.length - 16);
    } else {
      this.title = this.vids[this.pos].split('|')[1];
    }
    this.getUrl();
  },

  /**
   * Get original url
   */
  getUrl: function(type) {
    console.log('getUrl()');
    var url,
        params,
        that = this;

    if (this.types[this.type] === false) {
      this.redirectTo();
      return;
    }

    this.calcKey();
    url = [
      'http://www.tucao.cc/api/playurl.php',
      '?type=',
      this.type,
      '&vid=',
      this.vid,
      '&key=', this.key,
      '&r=', this.timestamp
      ].join('');

    console.log('url: ', url);
    GM_xmlhttpRequest({
      method: 'GET',
      url: url,
      onload: function(response) {
        console.log(response);
        var xml = parseXML(response.responseText),
            durls = xml.querySelectorAll('durl'),
            durl,
            url,
            i;

        for (i = 0; durl = durls[i]; i += 1) {
          url = durl.querySelector('url'); 
          that.videos.push(
            url.innerHTML.replace('<![CDATA[', '').replace(']]>', ''));
        }

        that.createUI();
      },
    });
  },

  /**
   * 计算这个请求的授权key.
   * 算法来自于: http://www.cnbeining.com/2014/05/serious-businesstucao-cc-c-video-resolution/
   * @return [key, timestamp]
   */
  calcKey: function() {
    console.log('calcKey () --');
    var time = new Date().getTime();

    this.timestamp = Math.round(time / 1000);
    var local3 = this.timestamp ^ 2774181285;
    var local4 = parseInt(this.vid, 10);
    var local5 = local3 + local4;
    local5 = (local5 < 0) ? (-(local5) >> 0) : (local5 >> 0);
    this.key = 'tucao' + local5.toString(16) + '.cc';
  },

  /**
   * Redirect to original url
   */
  redirectTo: function() {
    console.log('redirectTo() --');
    console.log(this);
    var urls = {
          tudou: function(vid) {
            return 'http://www.tudou.com/programs/view/' + vid + '/';
          },
          youku: function(vid) {
            return 'http://v.youku.com/v_show/id_' + vid + '.html';
          },
        };

    this.redirect = true;
    this.videos.push(urls[this.type](this.vid));
    this.formats.push('原始地址');
    this.createUI();
  },

  /**
   * Construct ui widgets
   */
  createUI: function() {
    console.log('createUI() -- ');
    console.log(this);
    var videos = {
          title: this.title,
          formats: [],
          links: [],
          ok: true,
          msg: '',
        },
        video,
        i;

    videos.links.push(this.videos);
    videos.formats.push(this.format);
    multiFiles.run(videos);
  },

}


monkey.extend('www.tucao.cc', [
  'http://www.tucao.cc/play/',
], monkey_tucao);

/**
 * tudou.com
 */
var monkey_tudou = {

  url:'',   // document.location.href
  title: '',
  iid: '',
  vcode: '',
  segs: {},
  totalJobs: 0,
  formats: {
    2: '240P',       // 流畅
    3: '360P',       // 清晰
    4: '480P',       // 高清
    5: '720P',       // 超清
    52: '240P(mp4)',
    53: '360P(mp4)',
    54: '480P(mp4)',
    99: '原画质'     // 原画质
  },
  links: {
  },

  run: function() {
    console.log('run() --');
    this.router();
  },

  /**
   * Page router control
   */
  router: function() {
    console.log('router() --');
    var scripts = document.querySelectorAll('script'),
        script,
        titleReg = /kw:\s*['"]([^'"]+)['"]/,
        titleMatch,
        iidReg = /iid\s*[:=]\s*(\d+)/,
        iidMatch,
        vcodeReg = /vcode: '([^']+)'/,
        vcodeMatch,
        i;

    for (i = 0; script = scripts[i]; i += 1) {
      if (this.vcode.length === 0) {
        vcodeMatch = vcodeReg.exec(script.innerHTML);
        console.log('vcodeMatch:', vcodeMatch);
        if (vcodeMatch && vcodeMatch.length > 1) {
          this.vcode = vcodeMatch[1];
          this.redirectToYouku();
          return;
        }
      }

      if (this.title.length === 0) {
        titleMatch = titleReg.exec(script.innerHTML);
        console.log('titleMatch:', titleMatch);
        if (titleMatch) {
          this.title = titleMatch[1];
        }
      }

      if (this.iid.length === 0) {
        iidMatch = iidReg.exec(script.innerHTML);
        console.log('iidMatch:', iidMatch);
        if (iidMatch) {
          this.iid = iidMatch[1];
          this.getByIid();
          return;
        }
      }
    }
    //this.getPlayList();
  },

  /**
   * Get video info by vid
   */
  getByIid: function() {
    console.log('getByIid()');
    console.log(this);

    var that = this,
        url = 'http://www.tudou.com/outplay/goto/getItemSegs.action?iid=' +
            this.iid;

    GM_xmlhttpRequest({
      method: 'GET',
      url: url,
      onload: function(response) {
        console.log('respone:', response);
        that.segs = JSON.parse(response.responseText);
        that.getAllVideos();
      },
    });
  },

//  getPlayList: function() {
//    console.log('getPlayList()');
//    console.log(this);
//  },

  /**
   * Get all video links
   */
  getAllVideos: function() {
    console.log('getAllVideos() --');
    console.log(this);
    var key,
        videos,
        video,
        i;

    for (key in this.segs) {
      videos = this.segs[key];
      for (i = 0; video = videos[i]; i += 1) {
        console.log(key, video);
        this.links[key] = [];
        this.totalJobs += 1;
        this.getVideoUrl(key, video['k'], video['no']);
      }
    }
  },


  /**
   * Get video url
   */
  getVideoUrl: function(key, k, num) {
    console.log('getVideoUrl() --');
    var url = 'http://ct.v2.tudou.com/f?id=' + k,
        that = this;

    GM_xmlhttpRequest({
      method: 'GET',
      url: url,
      onload: function(response) { 
        console.log('response:', response);
        var reg = /<f[^>]+>([^<]+)</,
            match = reg.exec(response.responseText);

        if (match && match.length > 1) {
          that.links[key][num] = match[1];
          that.totalJobs -= 1;
          if (that.totalJobs === 0) {
            that.createUI();
          }
        }
      },
    });
  },

  /**
   * Redirect url to youku.com.
   * Because tudou.com use youku.com as video source on /albumplay/ page.
   */
  redirectToYouku: function() {
    var url = 'http://v.youku.com/v_show/id_' + this.vcode + '.html';
    console.log('redirectToYouku:', url);
    this.createUI();
  },

  /**
   * Construct UI widgets
   */
  createUI: function() {
    console.log('createUI()');
    console.log(this);
    var videos = {
          title: this.title,
          formats: [],
          links: [],
          ok: true,
          msg: '',
        },
        type,
        i;

    if (this.vcode.length > 0) {
      videos.title = '原始地址';
      videos.links.push('http://v.youku.com/v_show/id_' + this.vcode + '.html');
      videos.formats.push('');
      singleFile.run(videos);
    } else {
      for (type in this.links) {
        videos.links.push(this.links[type]);
        videos.formats.push(this.formats[type]);
      }
      console.log('videos: ', videos);
      multiFiles.run(videos);
    }
  },

};

monkey.extend('www.tudou.com', [
  'http://www.tudou.com/albumplay/',
  'http://www.tudou.com/listplay/',
  'http://www.tudou.com/programs/view/',
], monkey_tudou);

/**
 * wasu.cn
 */
var monkey_wasu = {

  id: '',
  key: '',
  url: '',
  title: '',
  link: '',
  format: '高清',

  run: function() {
    console.log('run() --');
    this.getTitle();
  },

  getTitle: function() {
    console.log('getTitle() --');
    var h3 = document.querySelector('div.play_movie div.play_site div.l h3');
    if (h3) {
      this.title = h3.innerHTML;
    } else {
      this.title = document.title.replace(
          '高清电影全集在线观看-正版高清电影-华数TV', '').replace(
          ' 正版高清电影', '');
    }
    this.getVid();
  },

  /**
   * Get video id
   */
  getVid: function() {
    console.log('getVid()--');
    var reg = /show\/id\/(\d+)/,
        match = reg.exec(location.href),
        url,
        that = this;

    if (!match || match.length !== 2) {
      console.error('Failed to get vid!');
      return
    }
    this.vid = match[1];
    url = 'http://www.wasu.cn/wap/play/show/id/' + this.vid,

    console.log('url: ', url);
    GM_xmlhttpRequest({
      method: 'GET',
      url: url,
      onload: function(response) {
        var txt = response.responseText,
            keyReg = /'key'\s*:\s*'([^']+)'/,
            urlReg = /'url'\s*:\s*'([^']+)'/,
            keyMatch,
            urlMatch;

        keyMatch = keyReg.exec(txt);
        if (! keyMatch || keyMatch.length !== 2) {
          console.error('Failed to get key: ', keyMatch);
          return;
        }
        that.key = keyMatch[1];
        urlMatch = urlReg.exec(txt);
        that.url = urlMatch[1];
        that.getVideoInfo();
      },
    });
  },

  /**
   * Get video information
   */
  getVideoInfo: function() {
    console.log('getVideoInfo() --');
    var url = [
          'http://www.wasu.cn/wap/Api/getVideoUrl/id/', this.vid,
          '/key/', this.key,
          '/url/', this.url,
          '/type/txt',
        ].join(''),
        that = this;

    console.log('video info link: ', url);
    GM_xmlhttpRequest({
      method: 'GET',
      url: url,
      onload: function(response) {
        that.link = response.responseText;
        that.createUI();
      },
    });
  },

  createUI: function() {
    console.log('createUI() --');
    console.log(this);
    var videos = {
          title: this.title,
          formats: [],
          links: [],
          ok: true,
          msg: '',
        };

    if (this.link.length === 0) {
      videos.ok = false;
      videos.msg = 'Failed to get video link';
    } else {
      videos.formats.push(this.format);
      videos.links.push([this.link]);
    }
    multiFiles.run(videos);
  },

};

monkey.extend('www.wasu.cn', [
  'http://www.wasu.cn/Play/show/id/',
  'http://www.wasu.cn/play/show/id/',
  'http://www.wasu.cn/wap/Play/show/id/',
  'http://www.wasu.cn/wap/play/show/id/',
], monkey_wasu);


/**
 * weiqitv.com
 */
var monkey_weiqitv = {
  sid: '',
  vid: '',
  title: '',
  videos: {},
  formats: {
    'default': '标清flv',  // 640x360
          '2': '高清flv',  // 960x540
          '3': '超清flv',  // 1280x720
          '4': '高清mp4',  // 850x480
          '5': '超清mp4',  // 1280x720
  },

  run: function() {
    console.log('run() -- ');
    this.title = document.title.replace('围棋TV - ', '');
    this.getVid();
  },

  getVid: function() {
    console.log('getVid() --');
    var vidReg = /vid:(\d+),/,
        vidMatch,
        sidReg = /sid:(\d+)\s*/,
        sidMatch,
        scripts = document.querySelectorAll('script'),
        script,
        i;

    for (i = 0; i < scripts.length; i += 1) {
      script = scripts[i];
      vidMatch = vidReg.exec(script.innerHTML);
      if (vidMatch && vidMatch.length === 2) {
        this.vid = vidMatch[1];
        sidMatch = sidReg.exec(script.innerHTML);
        this.sid = sidMatch[1];
        break;
      }
    }
    if (this.vid.length === 0) {
      console.error('Failed to get vid!');
    } else {
      this.getVideoInfo();
    }
  },

  getVideoInfo: function() {
    var that = this,
        url = [
          'http://www.yunsp.com.cn:8080/dispatch/videoPlay/getInfo?',
          'vid=', this.vid,
          '&sid=', this.sid,
          '&isList=0&ecode=notexist',
        ].join('');

    console.log('url: ', url);
    GM_xmlhttpRequest({
      method: 'GET',
      url: url,
      onload: function(response) {
        var json = JSON.parse(response.responseText),
            videoInfo = json[0].videoInfo,
            format;

        for (format in that.formats) {
          if (format in videoInfo) {
            that.videos[format] = videoInfo[format].url;
          }
        }
        that.createUI();
      },
    });
  },

  /**
   * construct ui widgets.
   */
  createUI: function() {
    console.log('createUI() --');
    console.log(this);
    var videos = {
          title: this.title,
          formats: [],
          links: [],
          ok: true,
          msg: '',
        },
        types = ['default', '4', '2', '5', '3'],
        type,
        url,
        i;
  
    for (i = 0; type = types[i]; i += 1) {
      url = this.videos[type];
      if (url && url.length > 0) {
        videos.links.push([url]);
        videos.formats.push(this.formats[type]);
      }
    }

    multiFiles.run(videos);
  },
};

monkey.extend('www.weiqitv.com', [
  'http://www.weiqitv.com/index/live_back?videoId=',
  'http://www.weiqitv.com/index/video_play?videoId=',
], monkey_weiqitv);

/**
 * youku.com
 */
var monkey_youku = {
  // store xhr result, with json format
  rs: null,
  brs: null,

  // store video formats and its urls
  data: null,
  title: '',
  videoId: '',

  run: function() {
    console.log('run() --');
    this.getVideoId();
  },

  /**
   * Get video id, and stored in yk.videoId.
   *
   * Page url for playing page almost like this:
   *   http://v.youku.com/v_show/id_XMjY1OTk1ODY0.html
   *   http://v.youku.com/v_playlist/f17273995o1p0.html
   */
  getVideoId: function() {
    console.log('getVideoId() --');
    var url = location.href,
        idReg = /(?:id_)(.*)(?:.html)/, 
        idMatch = idReg.exec(url),
        idReg2 = /(?:v_playlist\/f)(.*)(?:o1p\d.html)/,
        idMatch2 = idReg2.exec(url);

    console.log('idMatch: ', idMatch);
    console.log('idMatch2: ', idMatch2);
    if (idMatch && idMatch.length === 2) {
      this.videoId = idMatch[1];
      this.getPlayListMeta();
    } else if (idMatch2 && idMatch2.length === 2) {
      this.videoId = idMatch2[1];
      this.getPlayListMeta();
    } else {
      error('Failed to get video id!');
    }
  },

  /**
   * Get metadata of video playlist
   */
  getPlayListMeta: function() {
    console.log('getPlaylistMeta() --');
    var url = 'http://v.youku.com/player/getPlayList/VideoIDS/' + this.videoId,
        url2 = url + '/Pf/4/ctype/12/ev/1',
        that = this;

    console.log('url2:', url2);
    GM_xmlhttpRequest({
      method: 'GET',
      url: url2,
      onload: function(response) {
        var json = JSON.parse(response.responseText);
        if (json.data.length === 0) {
          console.error('Error: video not found!');
          return;
        }
        that.rs = json.data[0];
        that.title = that.rs.title;
        that.parseVideo();
      }
    });

    GM_xmlhttpRequest({
      method: 'GET',
      url: url,
      onload: function(response) {
        var json = JSON.parse(response.responseText);
        if (json.data.length === 0) {
          console.error('Error: video not found!');
          return;
        }
        that.brs = json.data[0];
        that.parseVideo();
      }
    });
  },

  parseVideo: function() {
    console.log('parseVideo() --');
    if (! this.rs || ! this.brs) {
      return;
    }

    var streamtypes = this.rs.streamtypes,
        streamfileids = this.rs.streamfileids,
        data = {},
        seed = this.rs.seed,
        segs = this.rs.segs,
        key,
        value,
        k,
        v,
        ip = this.rs.ip,
        bsegs = this.brs.segs,
        sid,
        token,
        i,
        k,
        number,
        fileId0,
        fileId,
        ep,
        pass1 = 'becaf9be',
        pass2 = 'bf7e5f01',
        typeArray = {
          'flv': 'flv', 'mp4': 'mp4', 'hd2': 'flv', '3gphd': 'mp4',
          '3gp': 'flv', 'hd3': 'flv'
          },
        sharpness = {
          'flv': 'normal', 'flvhd': 'normal', 'mp4': 'high',
          'hd2': 'super', '3gphd': 'high', '3g': 'normal',
          'hd3': 'original'
        },
        filetype;

    [sid, token] = this.yk_e(pass1, this.yk_na(this.rs.ep)).split('_');
    
    for (key in segs) {
      value = segs[key];
      if (streamtypes.indexOf(key) > -1) {
        for (i in value) {
          v = value[i];
          number = parseInt(v.no, 10).toString(16).toUpperCase();
          if (number.length === 1) {
            number = '0'.concat(number);
          }
          // 构建视频地址K值
          k = v.k;
          if (!k || k === -1) {
            console.log(bsegs, bsegs[key], bsegs[key][i]);
            k = bsegs[key][i]['k'];
          }
          fileId0 = this.getFileId(streamfileids[key], seed);
          fileId = fileId0.substr(0, 8) + number + fileId0.substr(10);
          ep = encodeURIComponent(this.yk_d(
                this.yk_e(pass2, [sid, fileId, token].join('_'))));

          // 判断后缀类型, 获得后缀
          filetype = typeArray[key];
          data[key] = data[key] || [];
          data[key].push([
            'http://k.youku.com/player/getFlvPath/sid/', sid,
            '_00/st/', filetype,
            '/fileid/', fileId,
            '?K=', k,
            '&ctype=12&ev=1&token=', token,
            '&oip=', ip,
            '&ep=', ep,
            ].join(''));
        }
      }
    }
    this.data = data;
    this.createUI();
  },

  /**
   * Get file id of each video file.
   *
   * @param string seed
   *  - the file seed number.
   * @param string fileId
   *  - file Id.
   * @return string
   *  - return decrypted file id.
   */
  getFileId: function(fileId, seed) {
    console.log('getFileId() --');
    function getFileIdMixed(seed) {
      var mixed = [],
          source = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOP' +
            'QRSTUVWXYZ/\\:._-1234567890',
          len = source.length,
          index,
          i;
    
      for (i = 0; i < len; i += 1) {
          seed = (seed * 211 + 30031) % 65536;
          index = Math.floor(seed / 65536 * source.length);
          mixed.push(source.charAt(index));
          source = source.replace(source.charAt(index), '');
      }
      return mixed;
    }

    var mixed = getFileIdMixed(seed),
        ids = fileId.split('\*'),
        len = ids.length - 1,
        realId = '',
        idx,
        i;

    for (i = 0; i < len; i += 1) {
      idx = parseInt(ids[i]);
      realId += mixed[idx];
    }
    return realId;
  },

  /**
   * Timestamp
   */
  getSid: function() {
    return String((new Date()).getTime()) + '01';
  },

  /**
   * Decryption
   */
  yk_d: function(s) {
    var len = s.length,
        i = 0,
        result = [],
        e = 0,
        g = 0,
        h,
        chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';

    if (len === 0) {
      return '';
    }

    while (i < len) {
      e = s.charCodeAt(i) & 255;
      i = i + 1;
      if (i === len) {
        result.push(chars.charAt(e >> 2));
        result.push(chars.charAt((e & 3) << 4));
        result.push('==');
        break
      }
      g = s.charCodeAt(i);
      i = i + 1;
      if (i === len) {
        result.push(chars.charAt(e >> 2));
        result.push(chars.charAt((e & 3) << 4 | (g & 240) >> 4));
        result.push(chars.charAt((g & 15) << 2));
        result.push('=');
        break
      }
      h = s.charCodeAt(i);
      i = i + 1;
      result.push(chars.charAt(e >> 2));
      result.push(chars.charAt((e & 3) << 4 | (g & 240) >> 4));
      result.push(chars.charAt((g & 15) << 2 | (h & 192) >> 6));
      result.push(chars.charAt(h & 63));
    }
    return result.join('');
  },

  yk_na: function(a) {
    if (! a) {
      return '';
    }

    var h = [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,62,-1,-1,-1,63,52,53,54,55,56,57,58,59,60,61,-1,-1,-1,-1,-1,-1,-1,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1,-1,-1,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,-1,-1,-1,-1,-1],
        i = a.length,
        e = [],
        f = 0,
        b,
        c;

    while (f < i) {
      do {
        c = h[a.charCodeAt(f++) & 255];
      } while (f < i && c === -1);
      if (c === -1) {
        break;
      }

      do {
        b = h[a.charCodeAt(f++) & 255];
      } while (f < i && b === -1);
      if (b === -1) {
        break;
      }
      e.push(String.fromCharCode(c << 2 | (b & 48) >> 4));

      do {
        c = a.charCodeAt(f++) & 255;
        if (c === 61) {
          return e.join('');
        }
        c = h[c];
      } while (f < i && c === -1);
      if (c === -1) {
        break;
      }
      e.push(String.fromCharCode((b & 15) << 4 | (c & 60) >> 2));

      do {
        b = a.charCodeAt(f) & 255;
        f = f + 1;
        if (b === 61) {
          return e.join('');
        }
        b = h[b];
      } while (f < i && b === -1);
      if (b === -1) {
        break;
      }
      e.push(String.fromCharCode((c & 3) << 6 | b));
    }

    return e.join('');
  },

  yk_e: function(a, c) {
    var f = 0,
        i = '',
        e = [],
        q = 0,
        h = 0,
        b = {};
    for (h = 0; h < 256; h = h + 1) {
      b[h] = h;
    }
    for (h = 0; h < 256; h = h + 1) {
      f = ((f + b[h]) + a.charCodeAt(h % a.length)) % 256;
      i = b[h];
      b[h] = b[f];
      b[f] = i;
    }
    for (q = 0, f = 0, h = 0; q < c.length; q = q + 1) {
      h = (h + 1) % 256;
      f = (f + b[h]) % 256;
      i = b[h];
      b[h] = b[f];
      b[f] = i;
      e.push(String.fromCharCode(c.charCodeAt(q) ^ b[(b[h] + b[f]) % 256]));
    }
    return e.join('');
  },

  /**
   * construct video data and create UI widgets.
   */
  createUI: function() {
    console.log('createUI() --');
    console.log(this);
    var videos = {
          title: this.title,
          formats: [],
          links: [],
        },
        types = {
          '3gp': '3G',
          '3gphd': '3G高清',
          flv: '标清',
          flvhd: '高清Flv',
          mp4: '高清',
          hd2: '超清',
          hd3: '1080P',
        },
        type;

    for(type in types) {
      if (type in this.data) {
        videos.formats.push(types[type]);
        videos.links.push(this.data[type]);
      }
    }

    multiFiles.run(videos);
  },
};

monkey.extend('v.youku.com', [
  'http://v.youku.com/v_show/id_',
  'http://v.youku.com/v_playlist/',
], monkey_youku);


/**
 * youtube.com
 */
var monkey_youtube = {
  videoId: '',
  videoInfoUrl: null,
  videoTitle: '',
  stream: null,
  adaptive_fmts: null,
  urlInfo: false,

  // format list comes from https://github.com/rg3/youtube-dl
  formats:  {
    '5': {ext: 'flv', width: 400, height: 240, resolution: '240p'},
    '6': {ext: 'flv', width: 450, height: 270, resolution: '270p'},
    '13': {ext: '3gp', resolution: 'unknown'},
    '17': {ext: '3gp', width: 176, height: 144, resolution: '144p'},
    '18': {ext: 'mp4', width: 640, height: 360, resolution: '360p'},
    '22': {ext: 'mp4', width: 1280, height: 720, resolution: '720p'},
    '34': {ext: 'flv', width: 640, height: 360, resolution: '360p'},
    '35': {ext: 'flv', width: 854, height: 480, resolution: '720p'},
    '36': {ext: '3gp', width: 320, height: 240, resolution: '240p'},
    '37': {ext: 'mp4', width: 1920, height: 1080, resolution: '1080p'},
    '38': {ext: 'mp4', width: 4096, height: 3072, resolution: '4k'},
    '43': {ext: 'webm', width: 640, height: 360, resolution: '360p'},
    '44': {ext: 'webm', width: 854, height: 480, resolution: '480p'},
    '45': {ext: 'webm', width: 1280, height: 720, resolution: '720p'},
    '46': {ext: 'webm', width: 1920, height: 1080, resolution: '1080p'},


    // 3d videos
    '82': {'ext': 'mp4', 'height': 360, 'resolution': '360p', 'format_note': '3D', 'preference': -20},
    '83': {'ext': 'mp4', 'height': 480, 'resolution': '480p', 'format_note': '3D', 'preference': -20},
    '84': {'ext': 'mp4', 'height': 720, 'resolution': '720p', 'format_note': '3D', 'preference': -20},
    '85': {'ext': 'mp4', 'height': 1080, 'resolution': '1080p', 'format_note': '3D', 'preference': -20},
    '100': {'ext': 'webm', 'height': 360, 'resolution': '360p', 'format_note': '3D', 'preference': -20},
    '101': {'ext': 'webm', 'height': 480, 'resolution': '480p', 'format_note': '3D', 'preference': -20},
    '102': {'ext': 'webm', 'height': 720, 'resolution': '720p', 'format_note': '3D', 'preference': -20},

    // Apple HTTP Live Streaming
    '92': {'ext': 'mp4', 'height': 240, 'resolution': '240p', 'format_note': 'HLS', 'preference': -10},
    '93': {'ext': 'mp4', 'height': 360, 'resolution': '360p', 'format_note': 'HLS', 'preference': -10},
    '94': {'ext': 'mp4', 'height': 480, 'resolution': '480p', 'format_note': 'HLS', 'preference': -10},
    '95': {'ext': 'mp4', 'height': 720, 'resolution': '720p', 'format_note': 'HLS', 'preference': -10},
    '96': {'ext': 'mp4', 'height': 1080, 'resolution': '1080p', 'format_note': 'HLS', 'preference': -10},
    '132': {'ext': 'mp4', 'height': 240, 'resolution': '240p', 'format_note': 'HLS', 'preference': -10},
    '151': {'ext': 'mp4', 'height': 72, 'resolution': '72p', 'format_note': 'HLS', 'preference': -10},

    // DASH mp4 video
    '133': {'ext': 'mp4', 'width': 400, 'height': 240, 'resolution': '240p', 'format_note': 'DASH video', 'preference': -40},
    '134': {'ext': 'mp4', 'width': 640, 'height': 360, 'resolution': '360p', 'format_note': 'DASH video', 'preference': -40},
    '135': {'ext': 'mp4', 'width': 854, 'height': 480, 'resolution': '480p', 'format_note': 'DASH video', 'preference': -40},
    '136': {'ext': 'mp4', 'width': 1280, 'height': 720, 'resolution': '720p', 'format_note': 'DASH video', 'preference': -40},
    '137': {'ext': 'mp4', 'width': 1920, 'height': 1080, 'resolution': '1080p', 'format_note': 'DASH video', 'preference': -40},
    '138': {'ext': 'mp4', 'width': 1921, 'height': 1081, 'resolution': '>1080p', 'format_note': 'DASH video', 'preference': -40},
    '160': {'ext': 'mp4', 'width': 256, 'height': 192, 'resolution': '192p', 'format_note': 'DASH video', 'preference': -40},
    '264': {'ext': 'mp4', 'width': 1920, 'height': 1080, 'resolution': '1080p', 'format_note': 'DASH video', 'preference': -40},

    // Dash mp4 audio
    '139': {'ext': 'm4a', 'format_note': 'DASH audio', 'vcodec': 'none', 'abr': 48, 'preference': -50},
    '140': {'ext': 'm4a', 'format_note': 'DASH audio', 'vcodec': 'none', 'abr': 128, 'preference': -50},
    '141': {'ext': 'm4a', 'format_note': 'DASH audio', 'vcodec': 'none', 'abr': 256, 'preference': -50},

    // Dash webm
    '167': {'ext': 'webm', 'height': 360, 'width': 640, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'VP8', 'acodec': 'none', 'preference': -40, resolution: '360p'},
    '168': {'ext': 'webm', 'height': 480, 'width': 854, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'VP8', 'acodec': 'none', 'preference': -40, resolution: '480p'},
    '169': {'ext': 'webm', 'height': 720, 'width': 1280, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'VP8', 'acodec': 'none', 'preference': -40, resolution: '720p'},
    '170': {'ext': 'webm', 'height': 1080, 'width': 1920, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'VP8', 'acodec': 'none', 'preference': -40, resolution: '1080p'},
    '218': {'ext': 'webm', 'height': 480, 'width': 854, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'VP8', 'acodec': 'none', 'preference': -40, resolution: '480p'},
    '219': {'ext': 'webm', 'height': 480, 'width': 854, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'VP8', 'acodec': 'none', 'preference': -40, resolution: '480p'},
    '242': {'ext': 'webm', 'height': 240, 'resolution': '240p', 'format_note': 'DASH webm', 'preference': -40},
    '243': {'ext': 'webm', 'height': 360, 'resolution': '360p', 'format_note': 'DASH webm', 'preference': -40},
    '244': {'ext': 'webm', 'height': 480, 'resolution': '480p', 'format_note': 'DASH webm', 'preference': -40},
    '245': {'ext': 'webm', 'height': 480, 'resolution': '480p', 'format_note': 'DASH webm', 'preference': -40},
    '246': {'ext': 'webm', 'height': 480, 'resolution': '480p', 'format_note': 'DASH webm', 'preference': -40},
    '247': {'ext': 'webm', 'height': 720, 'resolution': '720p', 'format_note': 'DASH webm', 'preference': -40},
    '248': {'ext': 'webm', 'height': 1080, 'resolution': '1080p', 'format_note': 'DASH webm', 'preference': -40},

    // Dash webm audio
    '171': {'ext': 'webm', 'vcodec': 'none', 'format_note': 'DASH webm audio', 'abr': 48, 'preference': -50},
    '172': {'ext': 'webm', 'vcodec': 'none', 'format_note': 'DASH webm audio', 'abr': 256, 'preference': -50},

    // RTMP (unnamed)
    '_rtmp': {'protocol': 'rtmp'},
  },

  run: function() {
    console.log('run() --');
    this.getURLInfo();
  },

  /**
   * parse location.href
   */
  getURLInfo: function() {
    this.urlInfo = this.parseURI(unsafeWindow.location.href);
    if (document.location.href.contains('/embed/')) {
      window.location.href = this.urlInfo.replace('/embed/', '/watch?v=');
    } else {
      this.getVideo();
    }
  },

  /**
   * Get video url info:
   */
  getVideo: function () {
    console.log('getVideo()--');
    var that = this;

    if (!this.urlInfo.params['v']) {
      return;
    }

    this.videoId = this.urlInfo.params['v'];
    this.videoInfoUrl = [
      '/get_video_info',
      '?video_id=', this.videoId,
      //'&el=player_embeded&hl=en&gl=US',
      '&el=html5&hl=en&gl=US',
      '&eurl=https://youtube.googleapis.com/v/', this.videoId,
      ].join('');
    this.videoTitle = unsafeWindow.document.title.substr(
        0, unsafeWindow.document.title.length - 10);

    GM_xmlhttpRequest({
      method: 'GET',
      url: this.videoInfoUrl,
      onload: function(response) {
        console.log('xhr response: ', response);
        that.parseStream(response.responseText);
      },
    });
  },

  /**
   * Parse stream info from xhr text:
   */
  parseStream: function(rawVideoInfo) {
    console.log('parseStream() ---');
    var that = this;

    /**
     * Parse the stream text to Object
     */
    function _parseStream(rawStream){
      var a = decodeURIComponent(rawStream).split(',');
      return a.map(that.urlHashToObject);
    }

    this.videoInfo = this.urlHashToObject(rawVideoInfo);
    this.stream = _parseStream(this.videoInfo.url_encoded_fmt_stream_map);
    this.adaptive_fmts = _parseStream(this.videoInfo.adaptive_fmts)
    this.createUI();
  },

  /**
   * Create download list:
   */
  createUI: function() {
    console.log('createUI() -- ');
    console.log('this: ', this);
    var videos = {
          title: this.videoTitle,
          formats: [],
          links: [],
          ok: true,
          msg: '',
        },
        video,
        format,
        formatName,
        url,
        streams = this.stream.concat(this.adaptive_fmts),
        i;

    for (i = 0; video = streams[i]; i += 1) {
      format = this.formats[video['itag']];
      if (! format) {
        console.error('current format not supported: ', video);
        continue;
      }
      formatName = []
      if ('format_note' in format) {
        formatName.push(format.format_note);
      }
      if ('resolution' in format) {
        if (formatName.length > 0) {
          formatName.push('-');
        }
        formatName.push(format.resolution);
      }
      if ('ext' in format) {
        formatName.push('.');
        formatName.push(format.ext);
      }
      formatName = formatName.join('');
      if (videos.formats.indexOf(formatName) >= 0) {
        continue;
      }
      videos.formats.push(formatName);
      url = decodeURIComponent(video.url);
      if ('sig' in video) {
        url = url + '&signature=' + video.sig
      }
      videos.links.push(url);
    }

    if (videos.links.length === 0) {
      videos.ok = false;
      videos.msg = 'This video does not allowed to download';
    }
    singleFile.run(videos);
  },

  /**
   * Parse URL hash and convert to Object.
   */
  urlHashToObject: function(hashText) {
    var list = hashText.split('&'),
        output = {},
        len = list.length,
        i = 0,
        tmp = '';

    for (i = 0; i < len; i += 1) {
      tmp = list[i].split('=')
      output[tmp[0]] = tmp[1];
    }
    return output;
  },

  /**
   * FROM: http://james.padolsey.com/javascript/parsing-urls-with-the-dom/
   * This function creates a new anchor element and uses location
   * properties (inherent) to get the desired URL data. Some String
   * operations are used (to normalize results across browsers).
   */
  parseURI: function(url) {
    var a =  unsafeWindow.document.createElement('a');
    a.href = url;
    return {
      source: url,
      protocol: a.protocol.replace(':',''),
      host: a.hostname,
      port: a.port,
      query: a.search,
      params: (function(){
        var ret = {},
            seg = a.search.replace(/^\?/,'').split('&'),
            len = seg.length,
            i = 0,
            s;

        for (i = 0; i< len; i += 1) {
          if (seg[i]) {
            s = seg[i].split('=');
            ret[s[0]] = s[1];
          }
        }
        return ret;
      })(),
      file: (a.pathname.match(/\/([^\/?#]+)$/i) || [,''])[1],
      hash: a.hash.replace('#',''),
      path: a.pathname.replace(/^([^\/])/,'/$1'),
      relative: (a.href.match(/tps?:\/\/[^\/]+(.+)/) || [,''])[1],
      segments: a.pathname.replace(/^\//,'').split('/')
    };
  },
};


monkey.extend('www.youtube.com', [
  'http://www.youtube.com/watch?v=',
  'https://www.youtube.com/watch?v=',
  'http://www.youtube.com/embed/',
  'https://www.youtube.com/embed/',
], monkey_youtube);

  // In the end, get video handler and call it
  monkey.run();

}).call(this);