NWCD Populate Label Collage

Finds missing releases in label collages

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


}