maleficus032 / Metal-Archives PassTheHeadphones

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

// ==UserScript==
// @name Metal-Archives PassTheHeadphones
// @description Search PassTheHeadphones from Metal-Archives
// @updateURL https://openuserjs.org/meta/maleficus032/Metal-Archives_PassTheHeadphones.meta.js
// @version 1.0.1
// @require http://code.jquery.com/jquery-3.1.0.min.js
// @include /^http:\/\/www\.metal-archives\.com\/search\?.*\&type\=(band_name|album_title)/
// @include /^http:\/\/www\.metal-archives\.com\/bands\/.*/
// @grant GM_xmlhttpRequest
// @license MPL 2.0;http://mozilla.org/MPL/2.0/
// ==/UserScript==

/*jshint esversion: 6 */

// Use noConflict mode since Metal-Archives includes its own copy of jQuery
this.$ = this.jQuery = jQuery.noConflict(true);

// Check if user is logged in to Metal-Archives because an extra column will be
// displayed
var maLoggedIn = $(".member_logout").length > 0;

/* Constants */
const PTH_BASE_URL = 'https://passtheheadphones.me/';
const PTH_GROUP_ID = 'torrents.php?id=';
const PTH_ADV_QUERY = 'torrents.php?artistname=AR_REPLACE' +
                       '&groupname=AL_REPLACE&order_by=time&order_way=desc' +
                       '&group_results=1&action=advanced&searchsubmit=1';
const PTH_ARTIST_API = 'ajax.php?action=artist&artistname=AR_REPLACE' +
                        '&artistreleases=true';
const ARTIST_NAME = 'artistname';
const COLLAGE_LABEL_QUERY = 'https://passtheheadphones.me/collages.php?action=search' +
                            '&search=REPLACE&tags=&tags_type=1&cats%5B4%5D=1' +
                            '&type=c.name&order_by=Time&order_way=Descending';

/**
 * The `elements` module defines functions for creating common DOM elements
 * used by the view controllers.
 */
var elements = {

  /**
   * Return a table header object for PassTheHeadphones columns.
   *
   * @returns {jQuery}
   */
  pthHeader: function() {
    return $('<th>', {
      id: 'pth-header',
      text: 'PTH'
    }).css('width', '50px');
  },

  /**
   * Return a PassTheHeadphones search link for a given artist and album.
   *
   * @param String artist name of artist
   * @param String album name of album
   * @returns {jQuery}
   */
  searchLink: function(artist, album) {
    return $('<a>', {
      text: 'Search',
      href:  PTH_BASE_URL + PTH_ADV_QUERY
        .replace('AR_REPLACE', encodeURIComponent(artist))
        .replace('AL_REPLACE', encodeURIComponent(album)),
      target: '_blank'
    });
  },

  /**
   * Return a PassTheHeadphones link to view a given group
   *
   * @param String groupId groupId of torrent
   * @returns {jQuery}
   */
  viewLink: function(groupId) {
    return $('<a>', {
      text: 'View',
      href: PTH_BASE_URL + PTH_GROUP_ID + groupId,
      target: '_blank'
    });
  },

  /**
   * Get the value passed as a query parameter to the URL.
   *
   * @param String param query search key
   * @returns {String|null}
   */
  queryString: function(param) {
    var string = new RegExp('[\?&]' + param + '=([^&#]*)')
      .exec(window.location.href);
    return string ? string[1] : null;
  }

};

/**
 * The `searchResultsViewController` module defines functions for manipulating
 * the /search view on Metal-Archives.
 */
var searchResultsViewController = {

  /**
   * Add search links to results in 'Artist' and 'Album' searches.
   */
  addLinksToSearchResults: function(queryType) {
    // Return if there's only two rows in the table, which are the "no matches
    // found" row and a hidden row
    if ($('#searchResults tr').length == 2) return;

    // Add PassTheHeadphones header if it isn't already on the page
    if (!$('#pth-header').length) {
      elements.pthHeader().insertBefore($("#searchResults th").first());
    }

    // Add links to search results, which are stored in rows with class names
    // "even" and "odd"
    $('#searchResults .even, .odd').each(function() {
      var artistCell = $(this).children().first();
      var artist = artistCell.children().first().text();
      var album = "";
      if (queryType ==  'album_title') {
        album = $(this).children("td:eq(1)").text().trim();
      }

      var link = elements.searchLink(artist, album);
      var cell = $('<td>');
      cell.append(link);
      cell.insertBefore(artistCell);
    });
  }

};

/**
 * The `artistViewController` module defines functions for manipulating the
 * /bands view on Metal-Archives.
 */
var artistViewController = {

  /**
   * Add all search links to artist view.
   */
  addLinksToArtistView: function() {
    // Check if table is fully loaded and return if it isn't
    if (!$('.releaseCol').length) return;

    var artist = $('.band_name').children().first().text();
    artistViewController.addCollageLabelSearch();
    pthApiController.getReleasesForArtist(artist,
      artistViewController.addLinksToArtistReleases
    );
  },

  /**
   * Add search links to releases listed on an artist's page.
   *
   * @param Array pthGroups array of groupName/groupId pairs
   */
  addLinksToArtistReleases: function(pthGroups) {
    // Add PassTheHeadphones header if it isn't already on the page, and return if it
    // already exists (e.g., user switched tabs)
    if ($('#pth-header').length) {
      return;
    } else {
      elements.pthHeader().insertBefore($(".releaseCol").first());
    }

    var artist = $('.band_name').children().first().text();

    // Add links to 'Complete Discography', skipping the first header row
    $('.discog tr').slice(1).each(function() {
      var releaseCell = maLoggedIn ? $(this).children().first().next()
                                   : $(this).children().first();
      var release = releaseCell.children().first().text();
      var link = null;

      // Attempt to match torrents on PassTheHeadphones with releases on Metal-Archives
      if (pthGroups) {
        for (var i = 0; i < pthGroups.length; i++) {
          // Use a faked text area to decode the HTML entities provided by
          // PassTheHeadphones's API (breaks foreign characters)
          var pthRelease = $('<textarea />')
            .html(pthGroups[i].groupName).text();
          if (pthRelease.toLowerCase() == release.toLowerCase()) {
            link = elements.viewLink(pthGroups[i].groupId.toString());
            break;
          }
        }
      }

      // Add a "Search" link if the album wasn't found--maybe someone uploaded
      // it with a slightly different name
      if (!link) {
        link = elements.searchLink(artist, release);
      }

      var cell = $('<td>');
      cell.append(link);
      cell.insertBefore(releaseCell);
    });
  },

  /**
   * Add 'Search Collages' link to an artist's page to search PassTheHeadphones Collages
   * for their current label.
   */
  addCollageLabelSearch: function() {
    // Return if the DOM already has this element, as switching tabs will cause
    // the function to trigger again
    if ($('#collage-label-search').length) return;

    var label = $('#band_stats')
      .find('.float_right')
      .first()
      .children("dd")
      .last();

    // Return if there's no link to a label in the description, meaning
    // "unsigned and/or independent"
    if (!label.children("a").length) return;

    labelLink = $('<a>', {
      text: 'Search Collages', 
      href: COLLAGE_LABEL_QUERY
        .replace('REPLACE', label.children("a").first().text()),
      target: '_blank'
    });
    labelLink.attr('id', 'collage-label-search');
    label.append(document.createTextNode(" | "), labelLink);
  }

};

/**
 * The `pthApiController` module defines functions for searching the PassTheHeadphones
 * API.
 */
var pthApiController = {

  /**
   * Get the releases available on PassTheHeadphones for the given artist.
   *
   * @param String artist artist to search
   * @returns {Array}
   */
  getReleasesForArtist: function(artist, callback) {
    var url = PTH_BASE_URL + PTH_ARTIST_API.replace('AR_REPLACE', artist);
    return GM_xmlhttpRequest({
      method: "GET",
      url: url,
      onload: function(data) {
        if (data.status == 200) {
          // Get groupNames and groupIds and add links to results
          var payload = $.parseJSON(data.response);
          var groups = payload.response.torrentgroup.map(function(release) {
            return {
              'groupName': release.groupName,
              'groupId': release.groupId
            };
          });
          callback(groups);
        } else {
          // Bad response, probably not logged in. Add generic "Search" links
          // to results
          callback(null);
        }
      },
      onerror: function() {
        console.log('Metal-Archives PassTheHeadphones: An error occurred ' +
                    'searching PassTheHeadphones for ' + artist);
      }
    });
  }

};

/**
 * Main logic
 */
(function() {
  var callback = null;
  var target = null;
  var config = { childList: true };
  var queryType = elements.queryString('type');
  var obs = null;

  if (queryType !== null) {
    callback = searchResultsViewController.addLinksToSearchResults;
    target = document.getElementById('searchResults');
  } else {
    // No query in URL, must be looking at an artist page
    callback = artistViewController.addLinksToArtistView;
    target = document.getElementById('band_tab_discography');
    config.subtree = true;
  }

  // Register a mutation observer for results because the tables are
  // populated via an AJAX request
  if (target !== null) {
    obs = new MutationObserver(function(mutations) {
      callback(queryType);
    });
    obs.observe(target, config);
  } else {
    console.log("Metal-Archives PassTheHeadphones: Couldn't find target");
  }
})();