MusicBrainz: Fix featured artists

Tries to detect artist names in artist and track fields and allows you to extract those. Found entries are added to the corresponding editor for fast adding.

目前為 2014-10-24 提交的版本,檢視 最新版本

// ==UserScript==
// @name        MusicBrainz: Fix featured artists
// @description Tries to detect artist names in artist and track fields and allows you to extract those. Found entries are added to the corresponding editor for fast adding.
// @supportURL  https://github.com/JensBee/userscripts
// @namespace   http://www.jens-bertram.net/userscripts/fix-featured-artists
// @icon        https://wiki.musicbrainz.org/-/images/3/39/MusicBrainz_Logo_Square_Transparent.png
// @license     MIT
// @version     2.3beta
//
// @require     https://gf.qytechs.cn/scripts/5140-musicbrainz-function-library/code/MusicBrainz%20function%20library.js?version=21997
//
// @grant       none
// @include     *://musicbrainz.org/recording/*/edit
// @include     *://*.musicbrainz.org/recording/*/edit
// @include     *://musicbrainz.org/recording/create
// @include     *://*.musicbrainz.org/recording/create
// @include     *://musicbrainz.org/release/*/edit
// @include     *://*.musicbrainz.org/release/*/edit
// @include     *://musicbrainz.org/release-group/*/edit
// @include     *://*.musicbrainz.org/release-group/*/edit
// @include     *://musicbrainz.org/release/add
// @include     *://*.musicbrainz.org/release/add
// @include     *://musicbrainz.org/artist/*/edit
// @include     *://*.musicbrainz.org/artist/*/edit
// @include     *://musicbrainz.org/artist/*/split
// @include     *://*.musicbrainz.org/artist/*/split
// ==/UserScript==
//**************************************************************************//
var mbz = mbz || {};
var sharedMbz = unsafeWindow.mbz || {};
mbz.fix_feat = {
  splitPoints: [ // order matters
    '\\s&\\s',
    '\\s+\\s',
    ',\\s',
    '\\s/\\s',
    '\\s\\(?and\\s',
    '\\s\\(?with\\s',
    '\\s\\(?meets\\s',
    '\\s\\(?feat\\.\\s',
    '\\s\\(?ft\\.\\s',
    '\\s\\(?featuring\\s',
  ],
  splitPointsRx : [],
  btn: {
    add: '<button class="nobutton add-artist-credit mbz-fix-feat-add-credit" '
      + 'type="button" title="Add Artist Credit">'
      + '<div class="add-item icon img" title="Add artist credit"></div>'
      + '</button>',
    addAll: '<button class="nobutton add-artist-credit '
      + 'mbz-fix-feat-add-all-credits" type="button" title="Add all artist credits">'
      + '<div class="add-item icon img" title="Add all new artist credits"></div>'
      + '</button>',
    remove: '<button class="icon remove-item mbz-fix-feat-remove-credit" '
      + 'type="button" title="Remove Artist Credit">'
      + '<div class="remove-item icon img" title="Remove this credit"></div>'
      + '</button>',
    removeAll: '<button class="icon remove-item mbz-fix-feat-remove-all-credits" '
      + 'type="button" title="Remove all new artist credits">'
      + '<div class="remove-item icon img" title="Remove all credits"></div>'
      + '</button>',
    trigger: '<button>Try detect artists</button>',
    triggerShort: '<button title="Try detect artists">FixFeat</button>',
    triggerTrackList: '<button title="Try detect artists in track name" '
      + 'class="icon mbz-fix-feat">'
  },
  rowDiv: '<div class="row"><label>Fix featured artists:</label></div>',
  rowTab: '<tr><td><label>Fix featured artists:</label></td></tr>',

  _init: function() {
    // create RegEx objects
    for (let splitPoint of this.splitPoints) {
      this.splitPointsRx.push(new RegExp(splitPoint, 'gi'));
    }
    // append style
    MBZ.Html.addStyle(''
      + '#release-editor #track-ac-bubble {width:66%!important}'
      + 'tr.MBZ-FixFeat-Ruler td {height:2px;padding:0!important;}'
      + 'tr.MBZ-FixFeat-Ruler td:nth-child(2),'
      +	'#track-ac-bubble tr.MBZ-FixFeat-Ruler td {border-top:1px dotted #666;}'
      + '#track-ac-bubble .MBZ-FixFeat-Item .mbz-fix-feat-add-credit {'
        +	'width:170px;'
        + 'margin-left:0!important;'
      + '}'
      + '#track-ac-bubble .MBZ-FixFeat-ItemAddAll td {text-align:right;}'
      + '#track-ac-bubble .MBZ-FixFeat-ItemAddAll .mbz-fix-feat-add-credit {'
        + 'width:auto;'
        + 'margin-left:0!important;'
      + '}'
      + 'input.MBZ-FixFeat-MaySplit {background-color:#FFFFD0;}'
    );
  },

  /**
    * Check, if there's something to split
    */
  hasSplitPoints: function(str) {
    var cnt = 0;
    str = str.replace(/\s+/, ' ');
    for (let splitPoint of this.splitPoints) {
      if (str.match(splitPoint)) {
        cnt++;
      }
    }
    return cnt;
  },

  /**
    * Split something.
    */
  splitArtists: function(str) {
    var artists = [];
    var artistsCleaned = [];
    str = str.replace(/\s+/, ' ');
    for (let splitPointRx of this.splitPointsRx) {
      str = str.replace(splitPointRx, '|SPLT|');
    }
    artists = str.split('|SPLT|');
    for (let idx in artists) {
      var artist = artists[idx].trim();
      // skip empty and dupes
      if (artist != '' && artistsCleaned.indexOf(artist) == -1) {
        artistsCleaned.push(artist);
      }
      if (idx == artistsCleaned.length -1) {
        // remove possibly unbalanced parenthesis
        artistsCleaned[idx] = artistsCleaned[idx].replace(/\)$/, '');
      }
    }
    return artistsCleaned;
  }
};

mbz.fix_feat.BubbleEditor = function(bubbleEditorApi) {
  var b = null;
  var bubbleApi = null;
  var initialized = false;
  var self = this;

  this.getBubble = function() {
    if (!b || b.length == 0) {
      return null;
    }
    return b;
  };

  this.getBubbleApi = function() {
    return bubbleApi;
  };

  this.setBubble = function(bubble) {
    if (!bubble || bubble.length == 0) {
      console.err("No bubble.");
    } else {
      b = bubble;
      bubbleApi = bubbleEditorApi;
      initialized = true;

      b.on('click', 'button', function() {
        var btn = $(this);
        if (btn.hasClass('mbz-fix-feat-remove-credit')) {
          // remove row
          self.removeButtonRow.call(self, btn);
          return false;
        } else if (btn.hasClass('mbz-fix-feat-add-credit')) {
          // add artist
          bubbleApi.addArtist(btn.data('artist'), true);
          // remove row
          self.removeButtonRow.call(self, btn);
          return false;
        } else if (btn.hasClass('mbz-fix-feat-add-all-credits')) {
          btn.remove();
          b.find('.MBZ-FixFeat-Item button.mbz-fix-feat-add-credit').click();
          self.clear();
          return false;
        } else if (btn.hasClass('mbz-fix-feat-remove-all-credits')) {
          self.clear();
          return false;
        }
      });
    }
  }
};
mbz.fix_feat.BubbleEditor.prototype = {
  removeButtonRow: function(btn) {
    var b = this.getBubble();

    btn.remove();
    // remove lefotovers
    $.each(b.find('.MBZ-FixFeat-Item'), function() {
      // if one button removed itself, remove the whole item
      if ($(this).find('button.mbz-fix-feat-add-credit').length == 0
          || $(this).find('button.mbz-fix-feat-remove-credit').length == 0) {
        $(this).remove();
      }
    });

    var items = b.find('.MBZ-FixFeat-Item');
    if (items.length == 0) {
      // remove other elements, if no credit is left
      this.clear();
    } else if (items.length == 1) {
      b.find('tr.MBZ-FixFeat-ItemAddAll').remove();
    }
  },

  /**
    * Attach the list of found entities.
    * @artists Array of artists to attach
    * @return true if something was added
    */
  attachArtists: function(artists) {
    // check, if there's something to add
    if (artists.length == 0) {
      console.debug("No artists to attach.");
      return false;
    }

    var b = this.getBubble();
    if (!b) {
      console.debug("No bubble.");
      return;
    }
    var api = this.getBubbleApi();

    // clear any previous attached items
    this.clear();

    // show bubble
    api.tryOpen($('#open-ac'));

    var rows = [];
    var ruler = '';

    switch(api.type) {
      case MBZ.BubbleEditor.types.artistCredits:
        rows = b.find('.row-form tr');
        ruler = '<tr class="MBZ-FixFeat MBZ-FixFeat-Ruler">'
          + '<td></td><td colspan="2"></td></tr>';
        break;
      case MBZ.BubbleEditor.types.trackArtistCredits:
        rows = api.getCreditRows();
        ruler = '<tr class="MBZ-FixFeat MBZ-FixFeat-Ruler">'
          + '<td colspan="3"></td></tr>';
        break;
    }

    if (rows.length > 0) {
      artists.reverse();
      var self = this;

      var addButtons = function(target, idx) {
        var artist = artists[idx];
        // add button
        var btnAdd = $(mbz.fix_feat.btn.add);
        btnAdd.data('artist', artist);
        target.append(btnAdd.prepend(artist)).append(
          // remove button
          $(mbz.fix_feat.btn.remove)
        );
      };

      var target = null;
      switch(api.type) {
        case MBZ.BubbleEditor.types.artistCredits:
          // target is last row
          target = $(rows.get(rows.length -1));
          // append a row for each entity
          for (let idx in artists) {
            var aCell = $('<td colspan="3">');
            addButtons(aCell, idx);
            target.after($('<tr class="MBZ-FixFeat MBZ-FixFeat-Item">')
              .append(aCell));
          }
          break;
        case MBZ.BubbleEditor.types.trackArtistCredits:
          target = b.find('tr:has(button.add-item)');
          var rowCode = '<tr class="MBZ-FixFeat">';
          var row = $(rowCode);
          // append a row for two entries
          target.after(row);
          for (let idx in artists) {
            var aCell = $('<td class="MBZ-FixFeat-Item">');
            aCell.data('id', idx);
            addButtons(aCell, idx);
            if ((idx + 1) < artists.length && ((idx + 1) % 2) == 0) {
              var oldRow = row;
              row = $(rowCode);
              oldRow.after(row);
            }
            row.append(aCell);
          }
          break;
      }

      // append add all button, if more than one artist is present
      if (artists.length > 1) {
        target.after($('<tr class="MBZ-FixFeat MBZ-FixFeat-ItemAddAll">').append(
          $('<td colspan="3">').append(
            $(mbz.fix_feat.btn.addAll).prepend('<b>All new credits</b>')
          ).append($(mbz.fix_feat.btn.removeAll))
        ));
      }

      // append ruler before/after attached items
      var aItems = b.find('.MBZ-FixFeat');
      aItems.first().before(ruler)
      aItems.last().after(ruler);
    }
    return true;
  },

  clear: function() {
    var b = this.getBubble();
    if (b) {
      b.find('.MBZ-FixFeat').remove();
    }
  },
};

/**
  * Show only a link to the split artist editor on single artist edit page,
  * if we are able to split the name.
  */
mbz.fix_feat.artistPage = {
  init: function() {
    var strEl = $('#id-edit-artist\\.name');
    var str = strEl.val();
    var lnk = 'Please use the <a href="'
      + window.location.toString().replace(/\/edit/, '/split') + '">'
      + 'split artist editor</a>.';
    if (mbz.fix_feat.hasSplitPoints(str)) {
      var row = $('<div class="row"></div>');
      row.append($(mbz.fix_feat.rowDiv)).append(lnk);
      strEl.after(row);
    }
  }
};

/**
  * Parse entries on release pages.
  */
mbz.fix_feat.Release = function () {
  var initialized = false;
  var bubbleEditor = null;
  var currentBubbleRow = null;
  var entryCode = {
    item: '<span class="MBZ-FixFeat-Item" style="display:block">',
    row: '<tr class="MBZ-FixFeat"><td colspan="3"></td></tr>'
  };

  this.init = function() {
    if (initialized) {
      return;
    }
    initialized = true;
    var strEl = $('#release-artist');
    var canSplit = false;

    // init ac-bubble editor, if release artist name is splitable
    if (strEl.length > 0 && mbz.fix_feat.hasSplitPoints(strEl.val())) {
      var acbEdit = new mbz.fix_feat.BubbleEditor(
        MBZ.BubbleEditor.ArtistCredits);
      MBZ.BubbleEditor.ArtistCredits.onAppear({cb: acbEdit.setBubble});
      var row = $(mbz.fix_feat.rowTab);
      var btn = $(mbz.fix_feat.btn.trigger);
      btn.click(function(){
        btn.text('Rescan');
        var artists = mbz.fix_feat.splitArtists(strEl.val());
        if (artists.length > 0) {
          if (!acbEdit.attachArtists(
            MBZ.BubbleEditor.ArtistCredits.removePresentArtists(artists)
          )) {
            btn.remove();
            row.find('td').last().append('<em>No new artists found.</em>');
          }
        }
        return false;
      });
      row.append($('<td colspan="2"></td>').append(btn));
      strEl.parentsUntil('table').filter('tr').next().after(row);
    }

    var trackList = MBZ.TrackList.getList();
    if (trackList) {
      trackList.on('click', 'button', function(){
        if ($(this).parent().hasClass('credits-button')) {
          currentBubbleRow = $(this).parents('tr');
        };
      });

      // check rows that may be splitted
      var stoppedTyping;
      trackList.on('keypress', 'input[type="text"]', function() {
        if (stoppedTyping) clearTimeout(stoppedTyping);
        var el = $(this);
        stoppedTyping = setTimeout(function() {
          scanRow(el.parentsUntil('table').filter('tr'));
        }, 1000);
      });
    }

    // re-check rows that may be splitted on row changes
    MBZ.TrackList.onContentChange({cb: scanRows});

    // now initialize the track artists credits bubble editor
    bubbleEditor = new mbz.fix_feat.BubbleEditor(
      MBZ.BubbleEditor.TrackArtistCredits);
    MBZ.BubbleEditor.TrackArtistCredits.onAppear({cb: bubbleAppears});
  };

  function bubbleAppears(bubble) {
    bubbleEditor.setBubble(bubble);
    var btn = $(mbz.fix_feat.btn.triggerShort);
    btn.click(function(){
      scanBubble(btn);
      return false;
    });
    btn.attr('type', 'button');
    bubble.find('div.buttons').first().append(btn);
  };

  var creditEditor = {
    addAll: function(row) {
      var items = row.find('.MBZ-FixFeat-Item');
      if (items.length > 0) {
        var artists = [];
        $.each(items, function() {
          artists.push($(this).text().trim());
        });
      }
    },

    checkCreditsCount: function(row) {
      if (row.hasClass('MBZ-FixFeat')
          && row.find('.MBZ-FixFeat-Item').length == 0) {
        creditEditor.clear(row);
      }
    },

    clear: function(row) {
      row.find('.MBZ-FixFeat-Item').remove();
      if (row.next().hasClass('MBZ-FixFeat')) {
        row.next().remove();
      }
    }
  };

  function scanBubble(btn) {
    var artistsSplitted = [];
    // add all artist credits listed
    for (let artist of MBZ.BubbleEditor.TrackArtistCredits.getArtistCredits()) {
      artistsSplitted = artistsSplitted.concat(
        mbz.fix_feat.splitArtists(artist));
    }
    // add value from current track title
    if (currentBubbleRow) {
      var fromTrackTitle = mbz.fix_feat.splitArtists(
        currentBubbleRow.find('td.title input').val());
      if (fromTrackTitle.length > 1) { // first entry should be track title
        fromTrackTitle.shift();
        artistsSplitted = artistsSplitted.concat(fromTrackTitle);
      }
    }
    // attach all artist we gathered
    bubbleEditor.attachArtists(
      MBZ.BubbleEditor.TrackArtistCredits.removePresentArtists(artistsSplitted)
    );
  };

  function scanRow(row) {
    row = $(row);
    if (row.hasClass('track')) {
      var title = row.find('td.title input');
      if (mbz.fix_feat.hasSplitPoints(title.val())) {
        title.addClass('MBZ-FixFeat-MaySplit');
      } else {
        title.removeClass('MBZ-FixFeat-MaySplit');
      }
    }
  };

  function scanRows(tl, mutations) {
    if (mutations) {
      MBZ.Util.Mutations.forAddedTagName(mutations, 'tr', scanRow);
    }
  };
};

/**
  * Generic way to catch artist credit bubble editors.
  */
mbz.fix_feat.acBubble = {
  /**
    * Initialize the editor.
    * @strEl jQuery Element containing the artists name.
    */
  init: function(strEl) {
    if (strEl.length > 0 && mbz.fix_feat.hasSplitPoints(strEl.val())) {
      var bEdit = new mbz.fix_feat.BubbleEditor(MBZ.BubbleEditor.ArtistCredits);
      MBZ.BubbleEditor.ArtistCredits.onAppear({cb: bEdit.setBubble});
      var row = $(mbz.fix_feat.rowDiv);
      var btn = $(mbz.fix_feat.btn.trigger);
      btn.click(function(){
        btn.text('Rescan');
        var artists = mbz.fix_feat.splitArtists(strEl.val());
        if (artists.length > 0) {
          bEdit.attachArtists(
            MBZ.BubbleEditor.ArtistCredits.removePresentArtists(artists)
          );
        }
        return false;
      });
      row.append(btn);
      $('#open-ac').parent().after(row);
    }
  }
};

/**
  * Main initializer function.
  */
mbz.fix_feat.init = function() {
  mbz.fix_feat._init();
  var pageType = MBZ.Util.getMbzPageType();
  if (pageType.indexOf("artist") > -1) {
    if (pageType.indexOf("split") > -1) {
      mbz.fix_feat.acBubble.init($('#entity-artist'));
    } else {
      mbz.fix_feat.artistPage.init();
    }
  } else if (pageType.indexOf("recording") > -1) {
    mbz.fix_feat.acBubble.init($('#entity-artist'));
  } else if (pageType.indexOf("release") > -1) {
    // init observer, since component may need time to load
    var instance = new mbz.fix_feat.Release();
    MBZ.BubbleEditor.ArtistCredits.onAppear({cb: instance.init});
  } else if (pageType.indexOf("release-group") > -1) {
    mbz.fix_feat.acBubble.init($('#entity-artist'));
  }
};

mbz.fix_feat.init();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址