redacted.ch :: Group Top 10

Groups torrents from the same album in Top 10 lists

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name           redacted.ch :: Group Top 10
// @namespace      passtheheadphones.me
// @description    Groups torrents from the same album in Top 10 lists
// @version        1.1
// @include        https://redacted.ch/top10.php*
// @include        https://redacted.ch/user.php*action=edit*
// @exclude        https://redacted.ch/top10.php*type=users*
// @exclude        https://redacted.ch/top10.php*type=tags*
// @exclude        https://redacted.ch/top10.php*type=votes*
// @exclude        https://redacted.ch/top10.php*type=lastfm*
// @exclude        https://redacted.ch/top10.php*type=donors*
// @exclude        https://redacted.ch/top10.php*type=request_contest*
// @grant          GM_addStyle
// @grant          GM_xmlhttpRequest
// @run-at         document-start
// ==/UserScript==


function main() {

  function doTop10() {

    function doTable(table) {

      if (!table.rows[1].classList.contains('torrent')) return; // Server busy/Nothing found


      // Get sorting criterion

      var columns = { Data: 4, Snatched: 5, Seeded: 6, Active: 8 };
      var head = table.previousElementSibling.firstChild;
      var sortCol = columns[head.textContent.trim().split(' ')[3]];


      // Populate arrays

      var allRows = {}, ids = [];
      var bookmarks = dom.qsa('.add_bookmark, .remove_bookmark', table);
      var rl = table.rows.length;
      for (var i = 1; i < rl; ++i) {
        var row = table.rows[i];
        var id = bookmarks[i-1].firstElementChild.id.split('_')[2];
        if (!(id in allRows)) {
          ids.push(id);
          allRows[id] = [row.cloneNode(true)];
        }
        allRows[id].push(row);
      }


      // Sum up stats

      for (var i = ids.length; i--; ) {
        var rows = allRows[ids[i]], rl = rows.length, stats = [];
        if (rl > 2) {

          for (var j = 1; j < rl; ++j) {
            if (rows[j].classList.contains('snatched_torrent')) {
              rows[0].classList.add('snatched_torrent'); // do this now to save us a loop later
            }

            for (var c = 3; c < 9; ++c) {
              stats[c] = (stats[c] || 0) + cell.getNum(rows[j], c);
            }
          }

          for (var c = 3; c < 9; ++c) {
            if (options.avgStats) {
              stats[c] = Math.round(stats[c] / (rl-1));
            }
            cell.setNum(rows[0], c, stats[c]);
          }

        }
      }


      // Sort rows, format them, and rebuild table

      ids.sort(function (a, b) {
        return cell.getNum(allRows[b][0], sortCol) - cell.getNum(allRows[a][0], sortCol);
      });

      var formatRegex = /\[(.+?)[\]\s\/-]*$/;
      var rows, row, rowClasses = ['rowa', 'rowb'];
      var tbod = dom.mk('tbody', null, table.rows[0]);
      var numGroups = Math.min(ids.length, maxGroups);
      var il = options.useFilter ? ids.length : numGroups;

      for (var i = 0; i < il; ++i) {
        var rows = allRows[ids[i]], rl = rows.length, snatched = false;

        for (var j = 0; j < rl; ++j) {
          row = rows[j];
          var infoDiv = dom.cl('group_info', row)[0];
          var strongs = dom.tag('strong', infoDiv);
          var link = strongs[0].lastElementChild;
          var dl = strongs[0].previousElementSibling;


          if (j === 0) {

            // Modify grouprow

            row.cells[0].firstElementChild.textContent = i + 1;
            infoDiv.removeChild(dl);
            var node = strongs[1]; // "Snatched!" / "Reported"
            while (node && (node.nodeType == 3 || node.nodeName == 'STRONG')) {
              var nextNode = node.nextSibling;
              infoDiv.removeChild(node);
              node = nextNode;
            }
            strongs[0].nextSibling.textContent = rl > 2 ? ' (' + (rl - 1) + ') ' : ' ';
            link.href = link.pathname.substr(1) + link.search.split('&')[0];

            if (row.classList.contains('snatched_torrent')) {
              row.className = row.className.replace('snatched_torrent', 'snatched_group');
              snatched = true;
            }

            if (link.className.indexOf('wcds_') > -1) {
              link.className = link.className.replace(/wcds_[^b]\w+/g, '');
            }


          } else {

            // Modify torrentrow

            var format = strongs[0].nextSibling.textContent.match(formatRegex);
            if (format) link.textContent = format[1];
            if (strongs[1]) { // "Snatched!" / "Reported"
              for (var k = 1; k < strongs.length; ++k) {
                dom.app(link, ' / ', strongs[k]);
              }
            }

            infoDiv = infoDiv.cloneNode(false);
            dom.app(infoDiv, dl, ' \u00BB ', link);

            if (!options.expandAll) row.style.display = 'none';
            row.className += ' gt10_torrent' + (snatched ? ' snatched_group' : '');
            row.cells[1].classList.remove('cats_col');
            cell.setText(row, 1, cell.getText(row, 0));
            cell.setText(row, 0, '');
            cell.setText(row, 2, '');
            dom.app(row.cells[2], infoDiv);

            link.classList.remove('group_snatched');
            link.classList.remove('wcds_bookmark');

          }

          row.className = row.className.replace(rowClasses[(i+1)%2], rowClasses[i%2]);
          if (i >= maxGroups) row.classList.add('gt10_filtered');
          dom.app(tbod, row);

        }
      }

      table.replaceChild(tbod, table.firstElementChild);
      table.addEventListener('click', makeClickHandler(), false);

      head.textContent = head.textContent.replace(/\d+/, numGroups).
          replace('Torrents', 'Album' + (numGroups !=1 ? 's' : ''));

    } // doTable



    function makeClickHandler() {

      var expanded = options.expandAll;

      return function (e) {
        var clickedRow = dom.par(e.target, 'tr', true);
        if (!clickedRow) return; // clicked the table border

        // Column header: expand/collapse all groups
        if (clickedRow.rowIndex === 0) {
          var disp = expanded ? 'none' : '';
          var rows = dom.cl('gt10_torrent', clickedRow.parentNode);
          for (var i = rows.length; i--; ) {
            rows[i].style.display = disp;
          }
          expanded = !expanded;
          return;
        }

        var cell = dom.par(e.target, 'td', true);
        if (!cell || cell.cellIndex > 1) return;

        // Expand/collapse clicked group
        for (var gRow = clickedRow; gRow.classList.contains('gt10_torrent');
             gRow = gRow.previousSibling);
        var row = gRow.nextSibling;
        var disp = row.style.display ? '' : 'none';
        while (row && row.classList.contains('gt10_torrent')) {
          row.style.display = disp;
          row = row.nextSibling;
        }
      };
    }



    function replaceTable(num, newTable) {
      var oldTable = dom.cl('torrent_table')[num];
      oldTable.previousElementSibling.className = 'gt10_loaded';
      newTable.className = oldTable.className;
      oldTable.parentNode.replaceChild(newTable, oldTable);
      doTable(newTable);

      var links = dom.qsa('a[class*="wcds_"], .group_snatched', oldTable);
      for (var i = links.length; i--; ) {
        var newLink = dom.qs('a[href$="' + links[i].search + '"]', newTable);
        if (newLink) newLink.className = links[i].className;
      }
    }



    function loadTable(num) {

      function onLoad(response) {
        if (response.status == '200') {
          var match = /<table.*?torrent_table.*?>([\s\S]*?)<\/table>/.
              exec(response.responseText);

          if (match) {
            var newTable = dom.mk('table', { innerHTML: match[1] });
            if (newTable.rows.length > 2) {
              replaceTable(num, newTable);
              if (options.useFilter && filter.text()) filter.apply();
              cache.set(num, match[1]);
              return;
            }
          }
        }
        onError();
      }

      function onError() {
        dom.tag('h3')[num].className = 'gt10_failed';
      }

      var listTypes = ['day', 'week', 'month', 'year', 'overall', 'snatched', 'data', 'seeded'];
      var url = ['https://', wl.host, '/top10.php?type=torrents&limit=100&details=',
                 listTypes[num]].join('');

      GM_xmlhttpRequest({
        method: 'GET',
        url: url,
        headers: { Accept: 'text/xml' },
        onload: onLoad,
        onerror: onError
      });
    }



    var convert = function () {
      function toNum(val, isSize) {
        return isSize ? toBytes(val) : remComma(val);
      }
      function fromNum(num, isBytes) {
        return isBytes ? toSize(num) : insComma(num);
      }
      function insComma(num) {
        return num.toString().replace(regex, ',');
      }
      function remComma(str) {
        return parseFloat(str.replace(',', ''));
      }
      function toSize(bytes) {
        if (bytes <= 0) return '0 B';
        var e = Math.min(Math.floor(Math.log(bytes)/Math.log(1024)), 4);
        var sizeNum = (Math.round(bytes * 1000 / Math.pow(1024, e)) / 1000).
                       toFixed(Math.max(e-1, 2)); // use three decimals for TB
        return insComma(sizeNum) + ' ' + prefixes.charAt(e).replace(' ', '') + 'B';
      }
      function toBytes(size) {
        var e = prefixes.indexOf(size.charAt(size.length-2));
        return Math.round(remComma(size) * Math.pow(1024, e));
      }
      var prefixes = ' KMGT', regex = /\B(?=(\d{3})(?!\d))/;
      return { textToNum: toNum, numToText: fromNum };
    }();


    var cell = {
      getText: function (row, c) {
        return row.cells[c].textContent;
      },
      setText: function (row, c, txt) {
        row.cells[c].textContent = txt;
      },
      getNum: function (row, c) {
        return convert.textToNum(this.getText(row, c), c < 5);
      },
      setNum: function (row, c, num) {
        this.setText(row, c, convert.numToText(num, c < 5));
      }
    };



    var aCell = dom.qs('.torrent > td');
    var cellStyle = aCell && getComputedStyle(aCell);
    if (cellStyle && cellStyle.borderBottomStyle != 'none') {
      var borderStyle = [cellStyle.borderBottomWidth, cellStyle.borderBottomStyle,
                         cellStyle.borderBottomColor].join(' ');
      GM_addStyle([
        '.torrent_table { border-bottom: ', borderStyle, '; }',
        '.torrent, .torrent > td { border-top: ', borderStyle, '; }'
      ].join(''));
    }

    GM_addStyle([
      cellStyle && parseInt(cellStyle.fontWeight, 10) < 400 ? '' :
          '.gt10_torrent, .gt10_torrent a { font-weight: normal !important; }',
      '.torrent, .torrent > td { border-bottom-style: none !important; }',
      '.gt10_torrent, .gt10_torrent > td { border-top-style: none !important; }',
      '.colhead, .torrent > td:first-child, .torrent > td:first-child + td { cursor: pointer; }',
      '.cats_col { min-width: 18px; }'
    ].join(''));

    var ssLink = dom.qs('link[rel="stylesheet"][title]', document.head || dom.tag('head')[0]);
    if (ssLink && ssLink.title.indexOf('mono') > -1) {
      GM_addStyle('.gt10_torrent span { float: right; }');
    }


    var tables = dom.cl('torrent_table');
    var maxGroups = tables.length > 1 ? 10 : 250;
    var advanced = wl.search.indexOf('advanced=1') > -1;
    for (var i = 0, il = tables.length; i < il; ++i) {
      doTable(tables[i]);
    }


    // Load more?
    if (tables.length > 1 && !advanced) {
      var queueTable = function () {
        var delay = 500;
        return function (num) {
          setTimeout(function () { loadTable(num); }, delay);
          delay += 1500;
        };
      }();

      GM_addStyle([
        '.gt10_loading, .gt10_loading + .torrent_table td { cursor: progress !important; }',
        '.gt10_loading_status { float: right; margin: 2px 10px 0 0; }',
        '.gt10_loaded > .gt10_loading_status, .gt10_loading .important_text,',
            '.gt10_failed .important_text_alt { display: none; }'
      ].join(''));

      for (var i = 0, il = options.getMore.length; i < il; ++i) {
        if (options.getMore[i]) {

          if (cache.test(i)) {
            replaceTable(i, dom.mk('table', { innerHTML: cache.get(i) }));

          } else {
            var head = dom.tag('h3')[i];
            head.className = 'gt10_loading';
            dom.app(head,
              dom.mk('small', {className: 'gt10_loading_status'},
                dom.mk('strong', {className: 'important_text_alt'}, 'Loading...'),
                dom.mk('strong', {className: 'important_text'}, 'Failed')));
            queueTable(i);
          }
        }
      }
    } // load more



    if (!options.useFilter) return;

    var filter = {
      textField: dom.mk('input', {type: 'text', size: 75, spellcheck: false}),
      text: function () { return this.textField.value.trim(); },

      getWords: function () {
        return this.text().toLowerCase().split(/[ ,]+/).
            map(function (word) {
              var prefix = word.indexOf('!') > -1 ? '!' : '';
              if (word.indexOf('*') > -1) prefix += '*';
              return prefix ? prefix + word.replace(/[!*]/g, '') : word;
            }).
            filter(function (word) {
              return /[^!*]/.test(word);
            });
      },

      applyDelayed: function () {
        clearTimeout(filter.timer);
        filter.timer = setTimeout(filter.apply, 400);
      },
      clear: function () {
        filter.textField.value = '';
        filter.apply();
      },
      saveDefault: function () {
        options = stor.getOptions();
        options.filter = filter.getWords().join(', ');
        stor.set('gt10_options', options);
      },
      restoreDefault: function () {
        if (typeof options.filter == 'string' && options.filter != filter.text()) {
          filter.textField.value = options.filter;
          filter.apply();
        }
      },

      apply: function () {

        function isFiltered(row) {

          function testWords(words) {
            for (var i = words.length; i--; ) {
              var fuzzy = words[i][0] == '*';
              var word = fuzzy ? words[i].slice(1) : words[i];
              var tags = fuzzy ? tagStr : tagArr;
              if (tags.indexOf(word) > -1) return true;
            }
            return false;
          }

          var tagStr = dom.cl('tags', row)[0].textContent.trim();
          var tagArr = tagStr.split(/[ ,]+/);

          if (testWords(excl)) return true;
          if (testWords(incl)) return false;
          return incl.length > 0;
        }


        var incl = [], excl = [];
        var words = filter.getWords();
        for (var i = words.length; i--; ) {
          if (words[i][0] != '!') incl.push(words[i]);
          else excl.push(words[i].slice(1));
        }

        var hide, numFiltered = 0;
        var groupNum = 0, numShown = 0;
        var cls = ['rowa', 'rowb'];
        var rows = dom.cl('torrent');

        for (var i = 0, il = rows.length; i < il; ++i) {
          if (!rows[i].classList.contains('gt10_torrent')) { // a group row
            if (rows[i].rowIndex == 1) groupNum = numShown = 0;
            ++groupNum;
            hide = numShown >= maxGroups || isFiltered(rows[i]);
            if (!hide) ++numShown;
            else if (groupNum <= maxGroups) ++numFiltered;
          }

          if (hide) {
            rows[i].classList.add('gt10_filtered');
          } else {
            rows[i].classList.remove('gt10_filtered');
            rows[i].className = rows[i].className.replace(cls[numShown%2], cls[(numShown+1)%2]);
          }
        }

        dom.id('gt10_numfilt').textContent = numFiltered ? '-' + numFiltered : '';
      }
    };


    GM_addStyle([
      '.gt10_filtered { display: none; }',
      '#gt10_numfilt, #gt10_buttons > input:last-child { margin-left: 14px; }'
    ].join(''));

    var form = dom.mk('form', {className: 'search_form'},
      dom.mk('table', {className: 'layout border', width: '100%',
                       cellSpacing: 1, cellPadding: 6, border: 0},
        dom.mk('tbody', null,
          dom.mk('tr', null,
            dom.mk('td', {className: 'label'}, 'Tag filter: '),
            dom.mk('td', {className: 'ft_taglist'},
              filter.textField,
              dom.mk('strong', {id: 'gt10_numfilt'}))),
          dom.mk('tr', null,
            dom.mk('td', {id: 'gt10_buttons', className: 'center', colSpan: 2},
              dom.mk('input', {type: 'button', value: 'Clear'}), ' ',
              dom.mk('input', {type: 'button', value: 'Restore'}), ' ',
              dom.mk('input', {type: 'button', value: 'Make default'}))))));

    var elem = dom.cl('header')[0].nextElementSibling;
    elem.parentNode.insertBefore(form, elem);
    form.addEventListener('submit', function (e) { e.preventDefault(); });

    // Hide regular elite+ filter box, add toggle
    var oldForm = form.nextElementSibling;
    if (oldForm.tagName == 'FORM') {
      dom.togCl('hidden', advanced ? form : oldForm);
      var toggleLink = dom.mk('a', {className: 'brackets', href: '#'}, 'Toggle filterbox');
      var linkBox = oldForm.nextElementSibling;
      linkBox.insertBefore(toggleLink, linkBox.firstChild);
      toggleLink.addEventListener('click', function (e) {
        e.preventDefault();
        dom.togCl('hidden', form, oldForm);
      }, false);
    }

    var buttons = dom.id('gt10_buttons');
    buttons.children[0].addEventListener('click', filter.clear, false);
    buttons.children[1].addEventListener('click', filter.restoreDefault, false);
    buttons.children[2].addEventListener('click', filter.saveDefault, false);
    filter.textField.addEventListener('input', filter.applyDelayed, false);

    if (!advanced) filter.restoreDefault();

  } // doTop10




  function doSettings() {

    function makeOption(name, descr) {
      var id = name.split('_'), opt = options[id[0]];
      if (id[1]) opt = opt[+id[1]];
      return dom.mk('li', null,
        dom.mk('label', null,
          dom.mk('input', {id: 'gt10_' + name, type: 'checkbox', checked: opt}),
          ' ' + descr));
    }

    function updateBoxes() {
      var boxes = dom.tag('input', newRow);
      for (var i = 1, il = boxes.length; i < il; ++i) {
        boxes[i].disabled = !options.groupEm;
      }
    }


    GM_addStyle([
      '#gt10_options { position: relative; }',
      '#gt10_options p { margin: 8px 5px 6px; }',
      '#gt10_options p > span { font-size: 0.8em; }',
      '#gt10_saving { position: absolute; right: 5px; top: 0; }'
    ].join(''));

    var table = dom.id('torrent_settings');
    var thatRow = dom.par(dom.id('showtags'), 'tr') || table.rows[9];

    var newRow = dom.mk('tr', null,
      dom.mk('td', {className: 'label'},
        dom.mk('strong', null, 'Top 10')),
      dom.mk('td', null,
        dom.mk('div', {id: 'gt10_options'},
          dom.mk('ul', {className: 'options_list nobullet'},
            makeOption('groupEm', 'Group torrents'),
            makeOption('expandAll', 'Expand groups by default'),
            makeOption('avgStats', 'Use averages in group stats'),
            makeOption('useFilter', 'Enable real-time tag filtering')),
          dom.mk('p', null,
            'On the Top 10 index page, make the following lists more accurate: ',
            dom.mk('a', {className: 'brackets', href: '#', onclick: function () {
              dom.togCl('hidden', dom.par(this).nextSibling, this.firstChild, this.lastChild);
              return false;
            }},
              dom.mk('span', null, 'Show'),
              dom.mk('span', {className: 'hidden'}, 'Hide'))),
          dom.mk('div', {className: 'hidden'},
            dom.mk('ul', {className: 'options_list nobullet'},
              makeOption('getMore_0', 'Most Active Torrents Uploaded in the Past Day'),
              makeOption('getMore_1', 'Most Active Torrents Uploaded in the Past Week'),
              makeOption('getMore_2', 'Most Active Torrents Uploaded in the Past Month'),
              makeOption('getMore_3', 'Most Active Torrents Uploaded in the Past Year')),
            dom.mk('p', null,
              dom.mk('span', null,
                'Selected lists will automatically load the top 100 torrents.'))),
          dom.mk('strong', {id: 'gt10_saving', className: 'important_text_alt hidden'},
            'Saving settings...'))));

    updateBoxes();
    table.tBodies[0].insertBefore(newRow, thatRow);

    var timer;
    newRow.addEventListener('change', function (e) {
      options = stor.getOptions();
      var id = e.target.id.split('_');

      if (id[2]) options[id[1]][+id[2]] = e.target.checked;
      else options[id[1]] = e.target.checked;
      if ('groupEm' == id[1]) updateBoxes();

      dom.id('gt10_saving').classList.remove('hidden');
      clearTimeout(timer);
      timer = setTimeout(function () {
        dom.id('gt10_saving').classList.add('hidden');
      }, 900);

      stor.set('gt10_options', options);

    }, false);


    cache.purgeOld();

  } // doSettings





  var stor = {
    get: function (key, def) {
      var val = window.localStorage && localStorage.getItem(key);
      return val ? JSON.parse(val) : def;
    },
    set: function (key, val) {
      try { localStorage.setItem(key, JSON.stringify(val)); } catch (e) {}
    },
    getOptions: function () {
      var opt = this.get('gt10_options', { groupEm: true, expandAll: false, avgStats: false,
                                           useFilter: true, getMore: [true, true] });
      if (typeof opt.useFilter == 'undefined') opt.useFilter = true;
      if (opt.getMore.length > 4) opt.getMore = opt.getMore.slice(0, 4);
      return opt;
    }
  };


  var dom = {
    id: function (id) { return document.getElementById(id); },
    qs: function (s, p) { return (p || document).querySelector(s); },
    qsa: function (s, p) { return (p || document).querySelectorAll(s); },
    cl: function (cl, p) { return (p || document).getElementsByClassName(cl); },
    tag: function (tag, p) { return (p || document).getElementsByTagName(tag); },
    par: function (elem, tag, inclSelf) {
      if (!inclSelf) elem = elem && elem.parentNode;
      while (elem && tag && elem.nodeName !== tag.toUpperCase()) elem = elem.parentNode;
      return elem;
    },
    txt: function (txt) { return document.createTextNode(txt); },
    app: function (parent, var_args) {
      for (var i = 1, il = arguments.length; i < il; ++i) {
        var child = arguments[i];
        if (typeof child == 'string') child = this.txt(child);
        parent.appendChild(child);
      }
    },
    mk: function (tag, attr, var_args) {
      var elem = document.createElement(tag);
      if (attr) for (var a in attr) if (attr.hasOwnProperty(a)) elem[a] = attr[a];
      if (arguments.length > 2) {
        var args = Array.prototype.slice.call(arguments, 2);
        args.unshift(elem);
        this.app.apply(this, args);
      }
      return elem;
    },
    togCl: function (cl, var_args) {
      for (var i = 1, il = arguments.length; i < il; ++i) {
        arguments[i].classList.toggle(cl);
      }
    }
  };


  var cache = {
    data: stor.get('gt10_cache', []),

    get: function (num) {
      return this.expand(this.data[num].html);
    },

    set: function (num, htm) {
      this.data[num] = { time: Math.floor(Date.now() / 60000), html: this.shorten(htm) };
      stor.set('gt10_cache', this.data);
    },

    test: function (num) {
      return this.data[num] && this.data[num].time + 15 > Date.now() / 60000;
    },

    pats: [
      /<strong><a href="artist\.php\?id=([\d]+)" dir="ltr">/g,
      /<a href="torrents\.php\?([^"]+)" class="tooltip" title="View torrent" dir="ltr">/g,
      /<a href="torrents\.php\?taglist=([^"]+)">[^<]+<\/a>/g,
      /<td class="number_column">([\d]+)<\/td>/g,
      /<td class="number_column nobr">([^<]+)<\/td>/g,
      new RegExp([
        '<span class="add_bookmark float_right"> <a href="#" id="bookmarklink_torrent_([\\d]+)" ',
        'class="bookmarklink_torrent_[\\d]+ brackets" onclick="Bookmark\\(\'torrent\', [\\d]+, ',
        '\'Remove bookmark\'\\); return false;">Bookmark</a> </span> <div class="tags"'
      ].join(''), 'g'),
      new RegExp([
        '<div class="group_info clear"> <span><a href="torrents\\.php\\?action=download&amp;',
        'id=([^"]+)" title="Download" class="brackets tooltip">DL</a></span>'
      ].join(''), 'g'),

      [new RegExp(['<td class="big_info"> <div class="group_image float_left clear"> ',
          '<img src="[^"]+" width="90" height="90" alt="Cover" ',
          'onclick="lightbox.init\\(\'([^\']+)\', 90\\)" /> </div>'].join(''), 'g'),
      function (m, p) {
        return ['<td class="big_info"> <div class="group_image float_left clear"> <img src="',
            cache.thumb(p), '" width="90" height="90" alt="Cover" onclick="lightbox.init(\'',
            p, '\', 90)" /> </div>'].join('');
      }]
    ],

    thumb: function (src) {
      var suffix = /ptpimg(?!.*_thumb)/.test(src) ? '_thumb' :
                   /imgur.*\/(\w{5}|\w{7})\.\w+$/.test(src) ? 'm' : '';
      return src.replace(/\.\w+$/, suffix + '$&');
    },

    shorten: function (htm) {
      htm = htm.trim().replace(/\s{2,}/g, ' ');
      for (var i = this.pats.length; i--; ) {
        var regex = Array.isArray(this.pats[i]) ? this.pats[i][0]: this.pats[i];
        htm = htm.replace(regex, '<' + i + '=$1>');
      }
      return htm;
    },

    expand: function (htm) {
      var p = /\(?\[[^\]]+\]\+\)?/g;
      for (var i = this.pats.length; i--; ) {
        var replacer = Array.isArray(this.pats[i]) ? this.pats[i][1] :
            this.pats[i].toString().slice(1, -2).replace(p, '$$1').replace(/\\/g, '');
        htm = htm.replace(new RegExp('<' + i + '=([^>]+)>', 'g'), replacer);
      }
      return htm;
    },

    purgeOld: function () {
      var updated = false;
      for (var i = 0; i < this.data.length; ++i) {
        if (this.data[i] && !this.test(i)) {
          delete this.data[i];
          updated = true;
        }
      }
      if (updated) stor.set('gt10_cache', this.data);
    }
  };


  var options = stor.getOptions();

  var wl = window.location;
  if (wl.pathname == '/user.php') doSettings();
  else if (options.groupEm) doTop10();


} // main


(function hideBody() {
  var head = document.head || document.getElementsByTagName('head')[0];
  if (head) GM_addStyle('#top10:not(.gt10_ready) { display: none; }');
  else setTimeout(hideBody, 100);
})();

document.addEventListener('DOMContentLoaded', function () {
  try { main(); }
  finally { document.body.classList.add('gt10_ready'); }
}, false);