Greasy Fork

Greasy Fork is available in English.

Bandcamp Collection Filters

List items in a collection or wishlist that match certain filters (free, in common, etc)

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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));
  });
});