您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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.
// ==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();