NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Bandcamp Collection Filters // @version 1.0.5 // @description List items in a collection or wishlist that match certain filters (free, in common, etc) // @namespace 289690-squeek502 // @license 0BSD // @match http*://bandcamp.com/* // @include http*://bandcamp.com/* // @grant GM_xmlhttpRequest // ==/UserScript== if (!document.querySelector('#collection-grid') || !document.querySelector('#wishlist-grid')) return; var collectionSummary; var pageData; var isOwner = document.querySelector('#fan-banner').classList.contains('owner'); var LOAD_URL_FORMAT = "https://bandcamp.com/api/fancollection/1/{}_items"; var pageTypes = ['collection', 'wishlist']; var buttonTypes = ['free', 'purchased', 'wishlisted']; var inCommonSeparator = document.createElement('span'); inCommonSeparator.style.color = '#828282'; inCommonSeparator.style.marginRight = '16px'; inCommonSeparator.textContent = 'in common:'; var separatorsAfter = {'free': inCommonSeparator}; var buttons = {}; var results = {}; var started = {}; pageTypes.forEach(function(type) { buttons[type] = {}; results[type] = {}; var buttonContainer = document.createElement('div'); buttonContainer.style.marginTop = '0px'; buttonContainer.classList.add('wishlist-controls'); buttonContainer.classList.add('owner-controls'); var grid = document.querySelector('#'+type+'-grid'); var itemsContainer = grid.querySelector('#'+type+'-items-container') || grid.querySelector(':scope > .inner'); // not all collection pages always have both a collection and a wishlist, so bail if this fails if (!itemsContainer) { return; } var items = itemsContainer.querySelector('#'+type+'-items'); var resultContainer = document.createElement('div'); buttonTypes.forEach(function(button) { var buttonElement = document.createElement('a'); buttonElement.style.marginRight = '16px'; buttonElement.innerHTML = button; buttonContainer.appendChild(buttonElement); var list = document.createElement('ul'); list.style.display = 'none'; resultContainer.appendChild(list); buttons[type][button] = buttonElement; results[type][button] = list; if (separatorsAfter[button]) { buttonContainer.appendChild(separatorsAfter[button].cloneNode(true)); } }); itemsContainer.insertBefore(buttonContainer, items); itemsContainer.insertBefore(resultContainer, items); }); var onSummaryError = function(errmsg) { pageTypes.forEach(function(type) { buttonTypes.forEach(function(button) { // free doesn't depend on summary if (button == 'free') return; results[type][button].innerHTML = errmsg; }); }); }; var handleItems = function(items, type) { items.forEach(function(item) { var isFree = item.price === 0; var lookupKey = item.tralbum_type + "" + item.tralbum_id; var tralbum = collectionSummary && collectionSummary.tralbum_lookup[lookupKey]; var isPurchased = tralbum !== undefined && tralbum.purchased !== undefined && tralbum.purchased; var isWishlisted = tralbum !== undefined && !tralbum.purchased; if (!(isFree || isPurchased || isWishlisted)) return; var li = document.createElement('li'); var a = document.createElement('a'); a.href = item.item_url; a.textContent = item.band_name + ' - ' + item.item_title; li.appendChild(a); if (isFree) { results[type].free.appendChild(li.cloneNode(true)); } if (isPurchased) { results[type].purchased.appendChild(li.cloneNode(true)); } if (isWishlisted) { results[type].wishlisted.appendChild(li.cloneNode(true)); } }); buttonTypes.forEach(function(button) { buttons[type][button].textContent = button + " (" + results[type][button].childElementCount + ")"; }); }; var get = function(url, cb) { var opts = { method: 'GET', url: url, onload: function (res) { cb(res.status, res.responseText, res.finalUrl || url); } }; GM_xmlhttpRequest(opts); }; var 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); }; var getNext = function(fan_id, older_than_token, type) { var url = LOAD_URL_FORMAT.replace('{}', type); post(url, '{"fan_id":'+fan_id+',"older_than_token":"'+older_than_token+'","count":40}', function(status, res, url) { if (status != 200) { console.error("failed to get next " + type, status, res, url); return; } var parsed = JSON.parse(res); if (parsed.error) { console.error("error when getting next " + type, parsed.error_message, parsed); return; } handleItems(parsed.items, type); if (parsed.more_available) { // we should be able to use parsed.last_token here, but there is currently a bug // in the collection_items endpoint in that it always gives the 20th item's token // in last_token even if count is different than 20, so we need to get the actual // last item's token var last_token = parsed.items[parsed.items.length - 1].token; getNext(fan_id, last_token, type); } }); }; var setState = function(type, button, state) { if (state) { // hide all of the same type buttonTypes.forEach(function(other) { if (other == button) return; setState(type, other, false); }); results[type][button].style.display = 'block'; buttons[type][button].style.fontWeight = 'bold'; buttons[type][button].style.textDecoration = 'underline'; } else { results[type][button].style.display = 'none'; buttons[type][button].style.fontWeight = 'normal'; buttons[type][button].style.textDecoration = 'none'; } }; var getState = function(type, button) { return results[type][button].style.display != 'none'; }; var onclick = function(type, button, e) { if (!started[type]) { if (!pageData) { pageData = JSON.parse(document.querySelector('#pagedata').getAttribute('data-blob')); } var start = function() { var now = Math.floor(Date.now()/1000); var nowToken = pageData[type+'_data'].last_token.replace(/^\d+/, now); getNext(pageData.fan_data.fan_id, nowToken, type); }; if (!collectionSummary) { get('https://bandcamp.com/api/fan/2/collection_summary', function(status, res, url) { if (status != 200) { console.warn("unexpected response from " + url, status, res); onSummaryError("unexpected http status code: " + status); } var parsedRes = JSON.parse(res); if (parsedRes.error) { onSummaryError(parsedRes.error_message); } else { collectionSummary = parsedRes.collection_summary; } start(); }); } else { start(); } setState(type, button, true); started[type] = true; } else { setState(type, button, !getState(type, button)); } }; pageTypes.forEach(function(type) { buttonTypes.forEach(function(button) { buttons[type][button].addEventListener("click", onclick.bind(null, type, button)); }); });