// ==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&',
'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);