// ==UserScript==
// @name Bandcamp Supporters You Follow
// @version 1.0.3
// @description Show supporters of an album/track that you follow
// @namespace 289690-squeek502
// @license 0BSD
// @match http*://*.bandcamp.com/*
// @include http*://*.bandcamp.com/*
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// ==/UserScript==
// messy code ahead
var OPTION_AUTOLOAD = 'autoload';
var SMALL_SIZE = '28px';
var LARGE_SIZE = 'auto';
var DEFAULT_SIZE = SMALL_SIZE;
var OPTION_SIZE = 'size';
var DEFAULT_AUTOLOAD = false;
var collectedByContainer = document.querySelector('.collected-by.tralbum.collectors');
if (!collectedByContainer)
return;
var get; var post;
var loadedimgs = [];
var existingMessage = collectedByContainer.querySelector('.message');
var optionsMenu = document.createElement('div');
optionsMenu.style.display = 'none';
optionsMenu.style.marginTop = '10px';
optionsMenu.style.marginBottom = '10px';
optionsMenu.style.position = 'relative';
var toggleAutoload = document.createElement('input');
toggleAutoload.setAttribute('type', 'checkbox');
toggleAutoload.checked = GM_getValue("autoload", false);
toggleAutoload.id = 'supporters-you-follow-autoload';
var toggleAutoloadLabel = document.createElement('label');
toggleAutoloadLabel.setAttribute('for', 'supporters-you-follow-autoload');
toggleAutoloadLabel.textContent = 'autoload';
toggleAutoloadLabel.style.fontWeight = 'bold';
var toggleAutoloadContainer = document.createElement('div');
toggleAutoloadContainer.style.display = 'inline-block';
toggleAutoloadContainer.style.marginRight = '30px';
toggleAutoloadContainer.appendChild(toggleAutoload);
toggleAutoloadContainer.appendChild(toggleAutoloadLabel);
toggleAutoload.addEventListener('change', function() {
GM_setValue(OPTION_AUTOLOAD, this.checked);
});
var onSizeChanged = function() {
if (this.checked) {
GM_setValue(OPTION_SIZE, this.value);
loadedimgs.forEach(function(img) {
img.style.width = this.value;
img.style.height = this.value;
}.bind(this));
}
};
var curSizeSetting = GM_getValue(OPTION_SIZE, DEFAULT_SIZE);
var sizeSmall = document.createElement('input');
sizeSmall.setAttribute('type', 'radio');
sizeSmall.name = 'supporters-you-follow-size';
sizeSmall.value = SMALL_SIZE;
sizeSmall.id = 'supporters-you-follow-size-small';
sizeSmall.style.marginLeft = '10px';
sizeSmall.checked = curSizeSetting == SMALL_SIZE;
sizeSmall.addEventListener('change', onSizeChanged);
var sizeSmallLabel = document.createElement('label');
sizeSmallLabel.setAttribute('for', 'supporters-you-follow-size-small');
sizeSmallLabel.textContent = 'small';
sizeSmallLabel.style.marginRight = '10px';
sizeSmallLabel.style.fontWeight = 'bold';
var sizeLarge = document.createElement('input');
sizeLarge.setAttribute('type', 'radio');
sizeLarge.name = 'supporters-you-follow-size';
sizeLarge.value = LARGE_SIZE;
sizeLarge.id = 'supporters-you-follow-size-large';
sizeLarge.checked = curSizeSetting == LARGE_SIZE;
sizeLarge.addEventListener('change', onSizeChanged);
var sizeLargeLabel = document.createElement('label');
sizeLargeLabel.setAttribute('for', 'supporters-you-follow-size-large');
sizeLargeLabel.textContent = 'large';
sizeLargeLabel.style.fontWeight = 'bold';
var sizeContainer = document.createElement('div');
sizeContainer.style.display = 'inline-block';
sizeContainer.appendChild(sizeSmall);
sizeContainer.appendChild(sizeSmallLabel);
sizeContainer.appendChild(sizeLarge);
sizeContainer.appendChild(sizeLargeLabel);
optionsMenu.appendChild(toggleAutoloadContainer);
optionsMenu.appendChild(document.createTextNode("thumb size: "));
optionsMenu.appendChild(sizeContainer);
optionsMenu.appendChild(document.createElement('hr'));
var optionsLink = document.createElement('a');
optionsLink.textContent = 'options';
optionsLink.style.float = 'right';
collectedByContainer.insertBefore(optionsLink, existingMessage);
optionsLink.addEventListener('click', function() {
var wasHidden = optionsMenu.style.display == 'none';
optionsMenu.style.display = wasHidden ? 'block' : 'none';
optionsLink.style.fontWeight = wasHidden ? 'bold' : 'normal';
});
var message = existingMessage.cloneNode(true);
message.textContent = 'supporters you follow';
collectedByContainer.insertBefore(message, existingMessage);
collectedByContainer.insertBefore(optionsMenu, existingMessage);
var existingDeets = collectedByContainer.querySelector('.deets');
var deets = document.createElement('div');
deets.style.marginBottom = '16px';
deets.style.marginTop = '10px';
deets.style.lineHeight = '0';
var statusElement = document.createElement('div');
statusElement.style.lineHeight = '100%';
statusElement.style.opacity = '0.5';
statusElement.textContent = 'loading';
statusElement.style.display = 'none';
deets.appendChild(statusElement);
collectedByContainer.insertBefore(deets, existingMessage);
var makeThumb = function(fan) {
var a = document.createElement('a');
a.href = fan.url;
a.title = fan.name;
a.style.margin = '0 7px 7px 0px';
a.style.display = 'inline-block';
var img = document.createElement('img');
img.src = "https://f4.bcbits.com/img/" + fan.image_id.toString().padStart(10, '0') + "_42.jpg";
img.setAttribute('alt', fan.name + " thumbnail");
img.style.display = 'block';
var size = GM_getValue("size", DEFAULT_SIZE);
img.style.width = size;
img.style.height = size;
a.appendChild(img);
loadedimgs.push(img);
return a;
};
var handleSupporter = function(supporter) {
if (this[supporter.fan_id]) {
if (statusElement.parentNode) {
deets.removeChild(statusElement);
}
deets.appendChild(makeThumb(supporter));
}
};
var onEnd = function() {
if (loadedimgs.length === 0) {
statusElement.textContent = 'none found';
} else if (statusElement.parentNode) {
deets.removeChild(statusElement);
}
};
var onError = function(err) {
statusElement.textContent = err;
};
var getNext = function(tralbum_type, tralbum_id, token, lookup) {
var data;
if (token !== undefined) {
data = '{"tralbum_type":"'+tralbum_type+'","tralbum_id":'+tralbum_id+',"token":"'+token+'","count":80}';
} else {
data = '{"tralbum_type":"'+tralbum_type+'","tralbum_id":'+tralbum_id+',"count":80}';
}
var url = (new URL("/api/tralbumcollectors/2/thumbs", document.location)).href;
post(url, data, function(status, res, url) {
if (status != 200) {
console.warn("failed to get more supporters", status, res, url);
onError("unexpected http status: " + status);
return;
}
var parsed = JSON.parse(res);
if (parsed.error) {
console.warn("error when getting more supporters", parsed.error_message, parsed);
onError(parsed.error_message);
return;
}
parsed.results.forEach(handleSupporter.bind(lookup));
if (parsed.more_available) {
var lastToken = parsed.results[parsed.results.length - 1].token;
getNext(tralbum_type, tralbum_id, lastToken, lookup);
} else {
onEnd();
}
});
};
var onSummary = function(err, summary) {
if (err) {
onError(err);
return;
}
var collectorsData = JSON.parse(document.querySelector('#collectors-data').getAttribute('data-blob'));
var supporters = collectorsData.thumbs;
var lookup = summary.follows.following;
var pageData = JSON.parse(document.querySelector('#pagedata').getAttribute('data-blob'));
var tralbum = pageData.fan_tralbum_data;
// collectorsData.thumbs has only minimal data now, so we have to re-get the first page using no token
getNext(tralbum.tralbum_type, tralbum.tralbum_id, undefined, lookup);
};
var go = function() {
statusElement.style.display = 'block';
var url = (new URL("/api/fan/2/collection_summary", document.location)).href;
get(url, function(status, res, url) {
if (status != 200) {
onSummary("unexpected http status code: " + status);
return;
}
var parsedRes = JSON.parse(res);
onSummary(parsedRes.error_message, parsedRes.collection_summary);
});
};
get = function(url, cb) {
var opts = {
method: 'GET',
url: url,
onload: function (res) {
cb(res.status, res.responseText, res.finalUrl || url);
}
};
GM_xmlhttpRequest(opts);
};
post = function(url, data, cb) {
var opts = {
method: 'POST',
url: url,
data: data,
headers: {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
},
onload: function (res) {
cb(res.status, res.responseText, res.finalUrl || url);
}
};
GM_xmlhttpRequest(opts);
};
if (GM_getValue("autoload", false)) {
go();
} else {
var loadLink = document.createElement('a');
loadLink.textContent = 'load...';
loadLink.style.lineHeight = '100%';
var handleAutoloadChecked;
var load = function() {
go();
deets.removeChild(loadLink);
toggleAutoload.removeEventListener('change', handleAutoloadChecked);
};
loadLink.addEventListener('click', function(e) {
load();
e.preventDefault();
e.stopImmediatePropagation();
}, true);
deets.appendChild(loadLink);
handleAutoloadChecked = function() {
if(this.checked && loadLink.parentNode) {
load();
}
};
toggleAutoload.addEventListener('change', handleAutoloadChecked);
}