// ==UserScript==
// @name NWCD Populate Label Collage
// @namespace notwhat.cd
// @description Finds missing releases in label collages
// @version 1.0.2
// @match https://*.notwhat.cd/collage*.php*id=*
// @grant none
// ==/UserScript==
var ORIGINAL = 0;
var REMASTER = 1;
// Ajax handler
var ajx = {
numReqs: 0,
queue: [],
processing: false,
get: function (req) {
ajx.queue[req.priority ? 'unshift' : 'push'](req);
if (!ajx.processing) {
ajx.processing = true;
ajx.doNext();
}
},
doNext: function () {
if (ajx.queue[0]) {
if (ajx.numReqs < 5) {
if (ajx.numReqs === 0) setTimeout(ajx.newPeriod, 10000);
var req = ajx.queue.shift();
if (req.test && !req.test.call(req)) {
ajx.doNext();
} else {
ajx.numReqs++;
ajx.send(req);
setTimeout(ajx.doNext, 400);
}
}
} else {
ajx.processing = false;
}
},
newPeriod: function () {
ajx.numReqs = 0;
ajx.doNext();
},
send: function (req) {
var xhr = new XMLHttpRequest();
xhr.open('GET', req.url, true);
xhr.responseType = 'json';
xhr.originalRequest = req;
xhr.context = req.context;
xhr.onload = ajx.onLoad;
xhr.onerror = req.onError || null;
if (req.timeout) {
xhr.ontimeout = req.onTimeout || xhr.onerror;
xhr.timeout = req.timeout;
}
xhr.send(null);
},
onLoad: function () {
if (this.response) {
var req = this.originalRequest;
if (this.response.status == 'success') {
req.onLoad.call(this);
return;
} else if (this.response.error == 'rate limit exceeded') {
setTimeout(function () { ajx.get(req); }, 2000);
return;
}
}
if (this.onerror) this.onerror.call(this);
}
};
// DOM methods
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); },
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);
if (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;
},
par: function (tag, el) {
do el = el.parentNode;
while (el && el.tagName != tag.toUpperCase());
return el;
}
};
var categoryIsLabel = !!dom.qs('.box_category a[href$="cats[4]=1"]');
if (categoryIsLabel) {
// Set the search string from the name of the collage
var searchString = dom.tag('h2')[0].textContent.
// specific words:
replace(/\b(?:^an?\s+|the|and|label|record(?:ing)?s|musi(?:[ck]|que)|produ[ck]tions?|publications|entertainment|media|group|ltd|limited|inc|incorporated|international|publishing|company|co|discography|releases|albums|discs|series|compilations?|(?:its +)?sublabels)\b/ig, ' ').
// anything in parentheses or brackets, except at the beginning:
replace(/(.)(?:\([^)]*\)|\[[^\]]*\])/g, '$1 ').
// special characters:
replace(/[&+.,:;!?\-_/\\=%*#|~()[\]"'`´\u2122]/g, ' ').
// extra spaces:
replace(/\s{2,}/g, ' ').trim();
// Create the box
var box = dom.mk('div', {className: 'box'},
dom.mk('div', {className: 'head'},
dom.mk('strong', null, 'Find missing groups')),
dom.mk('div', {className: 'pad'},
dom.mk('form', null,
dom.mk('div', {className: 'field_div'},
dom.mk('input', {type: 'text', size: 20, value: searchString})),
dom.mk('div', {className: 'submit_div'},
dom.mk('input', {type: 'submit', value: 'Search'})))));
var elem = dom.cl('box_addtorrent')[0];
elem.parentNode.insertBefore(box, elem);
dom.tag('form', box)[0].addEventListener('submit', function (e) {
e.preventDefault();
var field = dom.qs('.field_div > input', box);
searchString = field.value.trim();
if (searchString) startSearch();
else window.alert('The search string is too short!');
});
// Add CSS
var updateCSS = function () {
function addStyle(css) {
dom.app(document.head, dom.mk('style', {type: 'text/css'}, css));
}
var boxStyle = getComputedStyle(box);
var col = boxStyle.color;
var fw = parseInt(boxStyle.fontWeight, 10);
addStyle([
'#plc_progress { height: 3px; width: 96%;',
'border: 1px solid ', col, '; border-radius: 2px; }',
'#plc_progress > div { height: 100%; width: 0; background-color: ', col, ';}',
'#plc_progress.plc_error { border-color: #ce1717; }',
'#plc_progress.plc_error > div { background-color: #ce1717; }',
'#plc_cont > div { margin-bottom: 10px; }',
'#plc_cont > div:first-child { margin-bottom: 16px; }',
'#plc_table td:first-child { width: 12px; }',
'.plc_present > td { background-color: rgba(57, 225, 20, 0.05) !important; }',
'.plc_label { margin-top: 4px; }',
'.plc_label, .plc_viewall {',
'float: right;', fw < 400 ? '}' : 'font-weight: normal !important; }',
'.plc_tag0 { font-size: 10px; opacity: 0.7; }',
'.plc_tag1 { font-size: 11px; }',
'.plc_tag2 { font-size: 12px; }',
'.plc_tag3 { font-size: 13px; }',
'.plc_tag4 { font-size: 14px; }',
'.plc_tag5 { font-size: 15px; }',
'.plc_tag6 { font-size: 16px; }',
'.plc_tag7 { font-size: 17px; }',
'.plc_tag8 { font-size: 18px; }',
'.plc_tag9 { font-size: 19px; }',
'.plc_tag10 { font-size: 20px; }'
].join(''));
var done = false;
return function () {
if (done) return;
var elem = dom.qs('#plc_table .group_info');
if (elem) {
if (getComputedStyle(elem).display == 'table-cell') {
addStyle('#plc_table .group_info { width: 650px; }');
elem = dom.qs('#plc_table img');
if (elem) {
var s = getComputedStyle(elem);
var w = parseInt(s.width, 10) + parseInt(s.borderLeftWidth, 10) +
parseInt(s.borderRightWidth, 10);
addStyle('#plc_table .group_image { min-width: ' + w + 'px; }');
}
}
done = true;
}
};
}();
}
// Finished setting things up
// Initiate a new search
function startSearch() {
// Update number of selected/found groups in the status box
function refreshStatus() {
var numSel = 0;
var elems = dom.qsa('.group input', table);
for (var i = elems.length; i--; ) {
if (elems[i].checked) numSel++;
}
cont.children[1].textContent = ['Selected: ', numSel, ' / ', elems.length].join('');
cont.children[2].disabled = !numSel; // "Add" button
}
// Update the search progress bar
function updateProgress(pct) {
pct = Math.floor(pct * 100);
cont.children[0].children[0].style.width = pct + '%';
}
// Things that should happen when all searches have loaded
function onSearchFinished() {
var numGroups = dom.cl('group', table).length;
if (numGroups === 0) {
dom.app(table.children[0], dom.mk('tr', null,
dom.mk('td', {className: 'center', colSpan: 2}, 'Found no missing groups.')));
} else if (numGroups < 4) {
showAllLabels();
} else {
dom.app(table.rows[0].cells[1],
dom.mk('a', {className: 'plc_viewall brackets', href: '#'}, 'View all labels'));
}
}
// Show all the labels
function showAllLabels() {
var links = dom.cl('plc_viewlabel', table);
if (links[0]) links[0].click();
for (var i = links.length; i--; ) {
links[i].click();
}
}
// Submit all selected groups to the server
function addSelectedToCollage() {
var urls = [];
var base = 'https://notwhat.cd/torrents.php?id=';
var elems = dom.qsa('.group input', table);
for (var i = elems.length; i--; ) {
if (elems[i].checked) urls.push(base + elems[i].value);
}
if (urls.length) {
var form = dom.qs('.add_form[name="torrents"]');
dom.tag('textarea', form)[0].value = urls.join('\n');
form.submit();
}
}
// Cancel all pending requests and restore the page
function cancelSearch() {
searchIsActive = false;
box.removeChild(cont);
table.parentNode.removeChild(table);
box.lastElementChild.classList.remove('hidden');
}
// Load search results
var loadSearch = function () {
function testRequest() {
// this also handles the "too many pages" site bug
return searchIsActive && this.context.page <= numPages[this.context.edition];
}
function onFailSearch() {
cont.children[0].classList.add('plc_error');
}
var onLoadSearch = function () {
var processedPages = 0;
var foundGroups = {}; // used to test for dupes
return function () {
var page = this.context.page;
var edition = this.context.edition;
var groups = this.response.response.results;
var respPages = this.response.response.pages;
if (groups.length) {
if (page == 1) {
for (var i = 2; i <= respPages; i++) {
loadSearch({ edition: edition, page: i });
}
}
var foundSomething = false;
for (var i = 0, il = groups.length; i < il; i++) {
var id = 'group_' + groups[i].groupId;
if (!foundGroups[id] && !dom.id(id)) {
foundGroups[id] = foundSomething = true;
appendGroup(groups[i]);
}
}
processedPages++;
// beware the "too many pages" bug:
if (respPages < numPages[edition]) numPages[edition] = respPages;
if (foundSomething) {
refreshStatus();
updateCSS();
}
} else {
if (numPages[edition] == Infinity) numPages[edition] = 0;
}
var totalPages = numPages[ORIGINAL] + numPages[REMASTER];
var progress = totalPages ? processedPages / totalPages : 1;
updateProgress(progress);
if (progress == 1) onSearchFinished();
};
}();
var search = encodeURIComponent(searchString);
var numPages = [Infinity, Infinity];
return function (param) {
param.page = param.page || 1;
ajx.get({
url: ['ajax.php?action=browse', param.edition == ORIGINAL ? '&' : '&remaster',
'recordlabel=', search, '&page=', param.page].join(''),
context: param,
onLoad: onLoadSearch,
onError: onFailSearch,
test: testRequest
});
};
}(); // loadSearch
// Load torrent group info
var loadGroup = function () {
function testRequest() {
return searchIsActive;
}
function onFailGroup() {
this.context.textContent = 'Loading failed';
}
function onLoadGroup() {
var grp = this.response.response.group;
var tor = this.response.response.torrents;
var labelKeys = [];
var labelStrings = [];
var addLabel = function (label, cat) {
// get rid of duplicates
var key = (label + cat).replace(/\s+/g, '').toLowerCase();
if (key && labelKeys.indexOf(key) < 0) {
labelKeys.push(key);
labelStrings.push((label || '(none)') + (cat ? ' / ' + cat : ''));
}
};
addLabel(grp.recordLabel, grp.catalogueNumber);
for (var i = 0, il = tor.length; i < il; i++) {
addLabel(tor[i].remasterRecordLabel, tor[i].remasterCatalogueNumber);
}
var elem = this.context;
elem.innerHTML = labelStrings.join('<br>');
// look for more artists
var row = dom.par('tr', elem);
if (!row.classList.contains('plc_present') && grp.musicInfo) {
var artists = grp.musicInfo.dj.concat(grp.musicInfo.with,
grp.musicInfo.remixedBy, grp.musicInfo.producer);
if (testArtists(artists)) {
row.classList.add('plc_present');
}
}
}
return function (elem) {
var row = dom.par('tr', elem);
var id = row.cells[0].children[0].value;
ajx.get({
url: 'ajax.php?action=torrentgroup&id=' + id,
context: elem,
priority: true,
onLoad: onLoadGroup,
onError: onFailGroup,
test: testRequest
});
};
}(); // loadGroup
// Test if any of the artists are present in the collage,
// as far as we know (they could be hidden under Various Artists)
var testArtists = function () {
var artistIds = {};
var artistLinks = dom.qsa('#discog_table a[href*="artist.php"], .box_artists a');
for (var i = artistLinks.length; i--; ) {
var id = artistLinks[i].href.split('id=')[1];
artistIds[id] = true;
}
return function (artists) {
if (artists && artistLinks.length) {
return artists.some(function (artist) {
return artistIds[artist.id];
});
}
return false;
};
}();
// Create a new table row for this group
var appendGroup = function () {
function makeImg(src) {
src = src || 'static/common/noartwork/music.png';
var suffix = /ptpimg(?!.*_thumb)/.test(src) ? '_thumb' :
/imgur.*\/(\w{5}|\w{7})\.\w+$/.test(src) ? 'm' : '';
var thumb = suffix ? src.replace(/\.\w+$/, suffix + '$&') : src;
return dom.mk('img', {src: thumb, alt: 'Cover', width: 90, height: 90});
}
function brkt(str) {
return str ? ' [' + str + ']' : '';
}
var makeTagDiv = function () {
function getClass(score) {
for (var i = 0; i < 11; i++) {
if (score*10 <= i) return 'plc_tag' + i;
}
}
function ci(num, total) {
if (num === 0 || total === 0) return 0;
var z = 1.96;
var p = num / total;
return (p + z*z / (2*total) - z * Math.sqrt((p*(1 - p) +
z*z / (4*total)) / total)) / (1 + z*z / total);
}
var tagCls = {}; // cache of tag classes
var colTable = dom.id('discog_table');
var totalTor = dom.cl('group', colTable).length;
return function (tags) {
// if torrent has no tags, the array contains one empty string
var div = dom.mk('div', {className: 'tags'});
for (var i = 0; i < tags.length; i++) {
var t = tags[i];
if (t) {
if (totalTor && !(t in tagCls)) {
var numTor = dom.qsa('a[href$="taglist=' + t + '"]', colTable).length;
tagCls[t] = getClass(ci(numTor, totalTor));
}
dom.app(div, i ? ', ' : null, dom.mk('a', {className: tagCls[t] || ''}, t));
}
}
return div;
};
}();
return function (grp) {
// if the category is not music, grp.torrents is undefined
var artHere = grp.torrents && testArtists(grp.torrents[0].artists);
dom.app(table.children[0],
dom.mk('tr', {className: 'group discog' + (artHere ? ' plc_present' : '')},
dom.mk('td', {className: 'center'},
dom.mk('input', {type: 'checkbox', value: grp.groupId})),
dom.mk('td', {className: 'big_info'},
dom.mk('div', {className: 'group_image float_left clear'},
makeImg(grp.cover)),
dom.mk('div', {className: 'group_info clear'},
dom.mk('strong', null,
dom.mk('a', {href: 'torrents.php?id=' + grp.groupId, target: '_blank'},
dom.mk('span', {innerHTML: grp.artist || '', dir: 'ltr'}),
grp.artist ? ' - ' : null,
dom.mk('span', {innerHTML: grp.groupName, dir: 'ltr'})),
brkt(grp.groupYear) + brkt(grp.releaseType) + brkt(grp.category)),
makeTagDiv(grp.tags),
dom.mk('div', {className: 'plc_label'},
dom.mk('a', {className: 'plc_viewlabel brackets', href: '#'},
'View label'))))));
};
}(); // appendGroup
// Create the container
var cont = dom.mk('div', {id: 'plc_cont', className: 'pad'},
dom.mk('div', {id: 'plc_progress'}, dom.mk('div')),
dom.mk('div', null, 'Selected: 0 / 0'),
dom.mk('input', {type: 'button', value: 'Add', disabled: true}), ' ',
dom.mk('input', {type: 'button', value: 'Cancel'}));
cont.addEventListener('click', function (e) {
if (e.target.tagName == 'INPUT') {
if (e.target.value == 'Add') addSelectedToCollage();
else cancelSearch();
}
}, false);
box.lastElementChild.classList.add('hidden');
dom.app(box, cont);
// Create the table
var table = dom.mk('table', {id: 'plc_table', className: 'torrent_table'},
dom.mk('tbody', null,
dom.mk('tr', {className: 'colhead'},
dom.mk('td', {className: 'center'},
dom.mk('input', {type: 'checkbox'})),
dom.mk('td', null,
dom.mk('strong', null, 'Missing torrent groups')))));
table.addEventListener('change', function (e) {
if (dom.par('tr', e.target).rowIndex === 0) {
var elems = dom.qsa('.group input', table);
for (var i = elems.length; i--; ) {
elems[i].checked = e.target.checked;
}
}
refreshStatus();
}, false);
table.addEventListener('click', function (e) {
if (e.target.classList.contains('plc_viewlabel')) {
e.preventDefault();
loadGroup(e.target.parentNode);
e.target.parentNode.textContent = 'Loading...';
}
if (e.target.classList.contains('plc_viewall')) {
e.preventDefault();
showAllLabels();
e.target.style.opacity = '0.4';
setTimeout(function () { e.target.style.display = 'none'; }, 80);
}
if (e.target.tagName == 'IMG') {
if (window.lightbox && lightbox.init) {
var src = e.target.src.
replace(/(ptpimg.*)_thumb(\.\w+)$/, '$1$2').
replace(/(imgur.*\/(?:\w{5}|\w{7}))m(\.\w+)$/, '$1$2');
lightbox.init(src, 90);
}
}
}, false);
var parent = dom.cl('main_column')[0];
parent.insertBefore(table, parent.firstChild);
// Begin searching
var searchIsActive = true;
loadSearch({ edition: ORIGINAL });
loadSearch({ edition: REMASTER });
}