NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==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.3.1beta // // @require https://greasyfork.org/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.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();