NWCD Populate Label Collage

Finds missing releases in label collages

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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 });


}