From 6d18a67817b309b0b92534f417ded668de67ab33 Mon Sep 17 00:00:00 2001 From: Viktini <59123723+Viktini@users.noreply.github.com> Date: Wed, 5 Apr 2023 18:23:19 +0100 Subject: [PATCH 1/8] add submit folksonomy tag plugin --- plugins/submit_folksonomy_tags/README.md | 31 ++ plugins/submit_folksonomy_tags/__init__.py | 442 ++++++++++++++++++++ plugins/submit_folksonomy_tags/ui_config.py | 145 +++++++ 3 files changed, 618 insertions(+) create mode 100644 plugins/submit_folksonomy_tags/README.md create mode 100644 plugins/submit_folksonomy_tags/__init__.py create mode 100644 plugins/submit_folksonomy_tags/ui_config.py diff --git a/plugins/submit_folksonomy_tags/README.md b/plugins/submit_folksonomy_tags/README.md new file mode 100644 index 00000000..f186b478 --- /dev/null +++ b/plugins/submit_folksonomy_tags/README.md @@ -0,0 +1,31 @@ +# Submit Folksonomy Tags - Picard Plugin + +A plugin that lets the user submit tags from their tracks' `genre` and `mood` tags to their respective MusicBrainz pages' folksonomy tags via MusicBrainz Picard. Useful for music geeks who are meticulous with their genre tagging. + +To use, right click on a track or release, then go to _Plugins > Submit tags to MusicBrainz_ - there are multiple options depending on if you want to submit tags to the recording, release or release group associated with the track/s or album/s you've right-clicked The tags will be applied from the `genre` and `mood` tags on your tracks. + +Uses code from rswift's "Submit ISRC" plugin (specifically, the handling of the network response) + +# How to Install +This plugin requires that you log into MusicBrainz via Picard. The option to do so is in _Options > Options > General_. + +Download the contents of this repository as a .zip then drop it into the Picard plugins folder. **Do not rename the .zip file as Picard will have trouble loading it if you do.** + +Alternatively, you may decide to extract the contents of the plugin into a folder. + +# Features +It does what it says on the tin: submits any tags you have in the genre tags of whichever files you drop into Picard to the respective pages. Right now the following entities are supported: + +- recordings +- releases +- release groups +- artists (by release) + +The plugin can also replace certain tags if your tags don't match up with MusicBrainz's standard tags, notably with their allowed genre list (e.g. if you use "synthpop" and not "synth-pop", or you use the full name "electronic dance music" and not the abbreviated "edm"). + +# Limitations +Right now, this plugin only submits tags. No tags are _retrieved_ for comparison yet, meaning I've opted to implement two modes based on how the MusicBrainz API works: maintain the tags that are already saved or overwrite _all_ of your tags. For anyone using the MusicBrainz API, choosing to keep your tags is basically sending the "upvote" attribute with every user tag, and choosing to overwrite doesn't do that, which MusicBrainz will respond by clearing old tags. See the [tags section of the MusicBrainz API for more details.](https://musicbrainz.org/doc/MusicBrainz_API#tags) + +Submitting for releases, release artists and release groups will also trigger an alert if your tags are not consistently the same across all tracks in an album. This is to prevent spamming of tags, purposeful or accidental, and is based on the standard already set for years by digital music sites and CD ripper utilities where an album would have the same genres tagged across all tracks. + +Submitting to artists by recording is not handled right now, see #3. \ No newline at end of file diff --git a/plugins/submit_folksonomy_tags/__init__.py b/plugins/submit_folksonomy_tags/__init__.py new file mode 100644 index 00000000..4cc55663 --- /dev/null +++ b/plugins/submit_folksonomy_tags/__init__.py @@ -0,0 +1,442 @@ +PLUGIN_NAME = "Submit Folksonomy Tags" +PLUGIN_AUTHOR = "Flaky" +PLUGIN_DESCRIPTION = """ +A plugin allowing submission of specific tags on tracks you own (defaults to genre and mood) as folksonomy tags on MusicBrainz. Supports submitting to recording, release, release group and release artist entities. + +A MusicBrainz login is required to use this plugin. Log in first by going to the General options. Then, to use, right click on a track or release then go to Plugins and depending on what you want to submit, choose the option you want. + +Uses code from rswift's "Submit ISRC" plugin (specifically, the handling of the network response) +""" +PLUGIN_VERSION = '0.2.4' +PLUGIN_API_VERSIONS = ['2.2'] +PLUGIN_LICENSE = "GPL-2.0" +PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.txt" + +from picard import config, log +from picard.album import Album, Track +from picard.ui.itemviews import (BaseAction, + register_album_action, + register_track_action + ) +from picard.ui.options import OptionsPage, register_options_page +from picard.webservice.api_helpers import MBAPIHelper, _wrap_xml_metadata +from .ui_config import TagSubmitPluginOptionsUI +import re +import html +import functools +from PyQt5 import QtCore +from PyQt5.QtWidgets import QMessageBox + +# List of Qt network error codes. +# From "Submit ISRC" plugin - credit to rswift. +q_error_codes = { + 0: 'No error', + 1: "The remote server refused the connection (the server is not accepting requests).", + 2: "The remote server closed the connection prematurely, before the entire reply was received and processed.", + 3: "The remote host name was not found (invalid hostname).", + 4: "The connection to the remote server timed out.", + 5: "The operation was canceled via calls to abort() or close() before it was finished.", + 6: "The SSL/TLS handshake failed and the encrypted channel could not be established. The sslErrors() signal should have been emitted.", + 7: "The connection was broken due to disconnection from the network, however the system has initiated roaming to another access point. The request should be resubmitted and will be processed as soon as the connection is re-established.", + 8: "The connection was broken due to disconnection from the network or failure to start the network.", + 9: "The background request is not currently allowed due to platform policy.", + 10: "While following redirects, the maximum limit was reached.", + 11: "While following redirects, the network access API detected a redirect from a encrypted protocol (https) to an unencrypted one (http).", + 99: "An unknown network-related error was detected.", + 101: "The connection to the proxy server was refused (the proxy server is not accepting requests).", + 102: "The proxy server closed the connection prematurely, before the entire reply was received and processed.", + 103: "The proxy host name was not found (invalid proxy hostname).", + 104: "The connection to the proxy timed out or the proxy did not reply in time to the request sent.", + 105: "The proxy requires authentication in order to honour the request but did not accept any credentials offered (if any).", + 199: "An unknown proxy-related error was detected.", + 201: "The access to the remote content was denied (similar to HTTP error 403).", + 202: "The operation requested on the remote content is not permitted.", + 203: "The remote content was not found at the server (similar to HTTP error 404).", + 204: "The remote server requires authentication to serve the content but the credentials provided were not accepted (if any).", + 205: "The request needed to be sent again, but this failed for example because the upload data could not be read a second time.", + 206: "The request could not be completed due to a conflict with the current state of the resource.", + 207: "The requested resource is no longer available at the server.", + 299: "An unknown error related to the remote content was detected.", + 301: "The Network Access API cannot honor the request because the protocol is not known.", + 302: "The requested operation is invalid for this protocol.", + 399: "A breakdown in protocol was detected (parsing error, invalid or unexpected responses, etc.).", + 401: "The server encountered an unexpected condition which prevented it from fulfilling the request.", + 402: "The server does not support the functionality required to fulfill the request.", + 403: "The server is unable to handle the request at this time.", + 499: "An unknown error related to the server response was detected.", +} + +# Some internal settings. +# Don't change these unless you know what you're doing. +# You can change the tags you want to submit in the settings. +client_params = { + "client": f"picard_plugin_{PLUGIN_NAME.replace(' ', '_')}-v{PLUGIN_VERSION}" +} +default_tags_to_submit = ['genre', 'mood'] + +# The options as saved in Picard.ini +config.BoolOption("setting", 'tag_submit_plugin_destructive', False) +config.BoolOption("setting", 'tag_submit_plugin_destructive_alert_acknowledged', False) +config.BoolOption("setting", 'tag_submit_plugin_aliases_enabled', False) +config.ListOption("setting", 'tag_submit_plugin_alias_list', []) +config.ListOption("setting", 'tag_submit_plugin_tags_to_submit', default_tags_to_submit) + +def tag_submit_handler(document, reply, error, tagger): + """ + The function handling the network response from MusicBrainz + or QtNetwork, showing a message box if an error had occurred. + + Uses the network response handler code from rswift's "Submit ISRC" + plugin. + """ + if error: + # Error handling from rswift's Submit ISRC plugin + xml_text = str(document, 'UTF-8') if isinstance(document, (bytes, bytearray, QtCore.QByteArray)) else str(document) + + # Build error text message from returned xml payload + err_text = '' + matches = re.findall(r'(.*?)', xml_text) + if matches: + err_text = '\n'.join(matches) + else: + err_text = '' + + if not err_text: + err_text = q_error_codes[error] if error in q_error_codes else 'There was no error message provided.' + + error = QMessageBox() + error.setStandardButtons(QMessageBox.Ok) + error.setDefaultButton(QMessageBox.Ok) + error.setIcon(QMessageBox.Critical) + error.setText(f"

An error has occurred submitting the tags to MusicBrainz.

{err_text}

") + error.exec_() + else: + tagger.window.set_statusbar_message( + "Successfully submitted tags to MusicBrainz." + ) + +def process_tag_aliases(tag_input): + """ + Retrieves a string as input, and searches the tag alias tuple list + for a match. + """ + matched_tag_index = next( + (tag for tag, + tag_tuple in enumerate(config.setting['tag_submit_plugin_alias_list']) + if tag_tuple[0] == tag_input.lower()), + None + ) + if matched_tag_index is not None: + resolved_tag = config.setting['tag_submit_plugin_alias_list'][matched_tag_index][1] + return resolved_tag + else: + return tag_input + +def process_objs_to_track_list(objs): + """ + Creates a track list out of Album/Track objects + """ + track_list = [] + for item in objs: + if isinstance(item, Track): + track_list.append(item) + elif isinstance(item, Album): + if len(item.tracks) > 0: + for track in item.tracks: + track_list.append(track) + return track_list + +# TODO handle artist +def handle_submit_process(tagger, track_list, target_tag): + """ + Does some pre-processing before submitting tags. Handles tag deduplication + and halting when inconsistent tagging is detected (i.e. the user is trying + to submit tags to a release with the submitted track tags not being + consistent.) + """ + + dict_key = "" + tags_to_search = config.setting['tag_submit_plugin_tags_to_submit'] + + # Variable to enable when inconsistent tagging is detected, which can be problematic for anything other than recordings. + alert_inconsistent = True + inconsistent_detected = False + # Variable to enable alert if multiple MBIDs are associated, must be toggled. + alert_multiple_mbids = False + + # TODO when Windows Picard updates with Python 3.10, use case/switch. + if target_tag == "musicbrainz_recordingid": + dict_key = "recording" + alert_inconsistent = False + elif target_tag == "musicbrainz_albumid": + dict_key = "release" + elif target_tag == "musicbrainz_releasegroupid": + dict_key = "release-group" + elif target_tag == "musicbrainz_albumartistid" or target_tag == "musicbrainz_artistid": + dict_key = "artist" + + data = {dict_key: {}} + + last_tags = {"mbid": ""} + banned_mbids = { + # Any artist entities that can be applied to multiple artists go here. + # SPAs generally fit the bill here. + "f731ccc4-e22a-43af-a747-64213329e088", # artist: [anonymous] + "33cf029c-63b0-41a0-9855-be2a3665fb3b", # artist: [data] + "314e1c25-dde7-4e4d-b2f4-0a7b9f7c56dc", # artist: [dialogue] + "eec63d3c-3b81-4ad4-b1e4-7c147d4d2b61", # artist: [no artist] + "9be7f096-97ec-4615-8957-8d40b5dcbc41", # artist: [traditional] + "125ec42a-7229-4250-afc5-e057484327fe", # artist: [unknown] + "89ad4ac3-39f7-470e-963a-56509c546377", # artist: Various Artists + "66ea0139-149f-4a0c-8fbf-5ea9ec4a6e49", # artist: [Disney] + "a0ef7e1d-44ff-4039-9435-7d5fefdeecc9", # artist: [theatre] + "80a8851f-444c-4539-892b-ad2a49292aa9", # artist: [language instruction] + "", # blank + } + + for track in track_list: + if track.files: + for file in track.files: + mbid_list = [html.escape(tag.strip().lower()) for tag + in re.split(";|/|,", file.metadata[target_tag])] + if len(mbid_list) > 1: + alert_multiple_mbids = True + for mbid in mbid_list: + if mbid not in banned_mbids: + processed_tags = [] + for tag in tags_to_search: + if file.metadata[tag]: + if tag not in last_tags: + pass + else: + # Flip the switch when current tag didn't match last tag on current mbid, on an entity that needs this checked. + if (last_tags[tag] != file.metadata[tag]) and (last_tags["mbid"] == file.metadata[target_tag]) and alert_inconsistent: + inconsistent_detected = True + # in any case, process the tags in case the user intends to go with it. + processed_tags.extend([html.escape(tag.strip().lower()) for tag + in re.split(";|/|,", file.metadata[tag])]) + last_tags[tag] = file.metadata[tag] + last_tags["mbid"] = file.metadata[target_tag] + # If a track has multiple files associated to it, there may be duplicate tags. + processed_tags = list(set(processed_tags)) + if processed_tags and mbid in data[dict_key]: + data[dict_key][mbid].extend(processed_tags) + else: + data[dict_key][mbid] = processed_tags + else: + log.info(f"Not submitting MBID {track.metadata[target_tag]} as it was found on 'do not submit' MBID set.") + + # Send an alert when, at the end of it all, inconsistent tagging was detected. + if inconsistent_detected or alert_multiple_mbids: + warning = QMessageBox() + warning.setStandardButtons(QMessageBox.Ok|QMessageBox.Cancel) + warning.setDefaultButton(QMessageBox.Cancel) + warning_title = "" + warning_message = "" + if inconsistent_detected and alert_multiple_mbids: + warning.setIcon(QMessageBox.Warning) + warning.setText(""" +

WARNING: INCONSISTENT TAGGING AND SUBMISSION TO MULTIPLE MBIDS DETECTED.

+

You are trying to apply different tags to multiple MusicBrainz entities.

+

This isn't a use case this plugin supports whatsoever due to the potential for + wrong tags to be unintentionally assigned, but detects and warns just in case.

+

If this was intentional, click OK. Otherwise, click Cancel.

+ """) + elif inconsistent_detected: + warning.setIcon(QMessageBox.Warning) + warning.setText(""" +

WARNING: INCONSISTENT TAGGING DETECTED.

+

You are trying to apply multiple tags to one entity, which benefits more from + having the same tags across all tracks when submitting tags via this plugin.

+

If you intended to have tracks in a release to have different submitted tags, + it's better to cancel this attempt and choose the recording option. + If you didn't, you should apply the same tag across all tracks.

+

If this was intentional, click OK. Otherwise, click Cancel.

+ """) + elif alert_multiple_mbids: + warning.setIcon(QMessageBox.Warning) + warning.setText(""" +

MULTIPLE MBIDS DETECTED.

+

You are trying to apply a tag to multiple MusicBrainz entities.

+

This isn't a use case this plugin supports whatsoever due to the potential for + wrong tags to be unintentionally assigned, but detects and warns just in case.

+

If this was intentional, click OK. Otherwise, click Cancel.

+ """) + result = warning.exec_() + if result == QMessageBox.Ok: + upload_tags_to_mbz(data, tagger) + else: + tagger.window.set_statusbar_message( + "Tag submission halted by user request." + ) + else: + upload_tags_to_mbz(data, tagger) + +def upload_tags_to_mbz(data, tagger): + """ + Generates the XML from the data retrieved, and then uploads it to MusicBrainz. + """ + helper = MBAPIHelper(tagger.webservice) + + empty_data = { + "", + "", + "", + "" + } + + xml_data = [] + upvote_tag_fill = ' vote="upvote"' if not config.setting['tag_submit_plugin_destructive'] else '' + for key in data: + # start the list + xml_data.append(f"<{key}-list>") + for mbid in data[key]: + # deduplicate the list of genres so no redundant tags are sent. + data[key][mbid] = list(set(data[key][mbid])) + # start the user tag list + xml_data.extend([f'<{key} id="{mbid}">', ""]) + # add the tags + for tag in data[key][mbid]: + xml_data.append(f'{process_tag_aliases(tag.lower())}') + # close the user tag list + xml_data.extend(["", f""]) + # close the list + xml_data.append(f"") + # make the string that would become our submitted XML. + final_xml = ''.join(xml_data) + log.info(final_xml) + + if final_xml not in empty_data: + # post it to MusicBrainz + tagger.window.set_statusbar_message( + "Submitting tags to MusicBrainz..." + ) + submitted_xml = _wrap_xml_metadata(''.join(xml_data)) + helper.post( + ['tag'], + submitted_xml, + functools.partial(tag_submit_handler, tagger=tagger), + priority=True, + queryargs=client_params, + parse_response_type="xml", + request_mimetype="application/xml; charset=utf-8" + ) + else: + tagger.window.set_statusbar_message( + "Not submitting to MusicBrainz due to empty data." + ) + + +class TagSubmitPlugin_OptionsPage(OptionsPage): + NAME = "tag_submit_plugin" + TITLE = "Tag Submission Plugin" + PARENT = "plugins" + HELP_URL = "" + + def __init__(self, parent=None): + super().__init__() + self.ui = TagSubmitPluginOptionsUI(self) + self.destructive_acknowledgement = config.setting['tag_submit_plugin_destructive_alert_acknowledged'] + self.ui.overwrite_radio_button.clicked.connect(self.on_destructive_selected) + + def on_destructive_selected(self): + if not config.setting['tag_submit_plugin_destructive_alert_acknowledged']: + warning = QMessageBox() + warning.setStandardButtons(QMessageBox.Ok) + warning.setDefaultButton(QMessageBox.Ok) + warning.setIcon(QMessageBox.Warning) + warning.setText("

WARNING: BY SELECTING TO OVERWRITE ALL TAGS, THIS MEANS ALL TAGS.

By enabling this option, you acknowledge that you may lose the tags already saved online from the tracks you process via this plugin. This alert will only be displayed once before you save.

If you do not want this behaviour, select the maintain option.

") + warning.exec_() + config.setting['tag_submit_plugin_destructive_alert_acknowledged'] = True + + def load(self): + # Destructive option + if config.setting['tag_submit_plugin_destructive']: + self.ui.overwrite_radio_button.setChecked(True) + else: + self.ui.keep_radio_button.setChecked(True) + + self.ui.tags_to_save_textbox.setText( + '; '.join(config.setting['tag_submit_plugin_tags_to_submit']) + ) + + # Aliases enabled option + self.ui.tag_alias_groupbox.setChecked( + config.setting['tag_submit_plugin_aliases_enabled'] + ) + + # Alias list + if 'tag_submit_plugin_alias_list' in config.setting: + log.info("Alias list exists! Let's populate the table.") + for alias_tuple in config.setting['tag_submit_plugin_alias_list']: + self.ui.add_row(alias_tuple[0], alias_tuple[1]) + self.ui.tag_alias_table.resizeColumnsToContents() + + config.setting['tag_submit_plugin_destructive_alert_acknowledged'] = self.destructive_acknowledgement + + def save(self): + config.setting['tag_submit_plugin_destructive'] = self.ui.overwrite_radio_button.isChecked() + config.setting['tag_submit_plugin_aliases_enabled'] = self.ui.tag_alias_groupbox.isChecked() + + tag_textbox_text = self.ui.tags_to_save_textbox.text() + if tag_textbox_text: + config.setting['tag_submit_plugin_tags_to_submit'] = [ + tag.strip() for tag in tag_textbox_text.split(';') + ] + else: + config.setting['tag_submit_plugin_tags_to_submit'] = default_tags_to_submit + + if config.setting['tag_submit_plugin_aliases_enabled']: + new_alias_list = self.ui.rows_to_tuple_list() + log.info(new_alias_list) + config.setting['tag_submit_plugin_alias_list'] = new_alias_list + +class SubmitTrackTagsMenuAction(BaseAction): + NAME = 'Submit tags to MusicBrainz (recording)' + + def callback(self, objs): + handle_submit_process( + objs[0].tagger, + process_objs_to_track_list(objs), + "musicbrainz_recordingid" + ) + +class SubmitReleaseTagsMenuAction(BaseAction): + NAME = 'Submit tags to MusicBrainz (release)' + + def callback(self, objs): + handle_submit_process( + objs[0].tagger, + process_objs_to_track_list(objs), + "musicbrainz_albumid" + ) + +class SubmitRGTagsMenuAction(BaseAction): + NAME = 'Submit tags to MusicBrainz (release group)' + + def callback(self, objs): + handle_submit_process( + objs[0].tagger, + process_objs_to_track_list(objs), + "musicbrainz_releasegroupid" + ) + +class SubmitRATagsMenuAction(BaseAction): + NAME = 'Submit tags to MusicBrainz (release artist)' + + def callback(self, objs): + handle_submit_process( + objs[0].tagger, + process_objs_to_track_list(objs), + "musicbrainz_albumartistid" + ) + +register_options_page(TagSubmitPlugin_OptionsPage) +register_album_action(SubmitTrackTagsMenuAction()) +register_track_action(SubmitTrackTagsMenuAction()) +register_album_action(SubmitReleaseTagsMenuAction()) +register_track_action(SubmitReleaseTagsMenuAction()) +register_album_action(SubmitRGTagsMenuAction()) +register_track_action(SubmitRGTagsMenuAction()) +register_album_action(SubmitRATagsMenuAction()) +register_track_action(SubmitRATagsMenuAction()) diff --git a/plugins/submit_folksonomy_tags/ui_config.py b/plugins/submit_folksonomy_tags/ui_config.py new file mode 100644 index 00000000..6e6a166f --- /dev/null +++ b/plugins/submit_folksonomy_tags/ui_config.py @@ -0,0 +1,145 @@ +from PyQt5.QtCore import ( + QSize, + Qt + ) + +from PyQt5.QtWidgets import ( + QGridLayout, + QGroupBox, + QHBoxLayout, + QLabel, + QPushButton, + QRadioButton, + QSizePolicy, + QTableWidget, + QTableWidgetItem, + QVBoxLayout, + QAbstractItemView, + QLineEdit + ) + +class TagSubmitPluginOptionsUI(): + + def __init__(self, page): + self.main_container = QVBoxLayout() + + sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + + # Group box: tag saving + self.tag_save_groupbox = QGroupBox() + sizePolicy.setHeightForWidth(self.tag_save_groupbox.sizePolicy().hasHeightForWidth()) + self.tag_save_groupbox.setSizePolicy(sizePolicy) + self.tag_save_groupbox_layout = QGridLayout(self.tag_save_groupbox) + # Label: tag saving description + self.tag_save_description = QLabel(self.tag_save_groupbox) + sizePolicy.setHeightForWidth(self.tag_save_description.sizePolicy().hasHeightForWidth()) + self.tag_save_description.setSizePolicy(sizePolicy) + self.tag_save_description.setMinimumSize(QSize(0, 56)) + self.tag_save_description.setWordWrap(True) + self.tag_save_groupbox_layout.addWidget(self.tag_save_description, 0, 0, 1, 1) + self.tag_save_groupbox.setTitle("Tag Saving") + self.tag_save_description.setText(u"

There are two modes to tag saving via this plugin right now: keep all existing saved tags (only adding on tags) or overwrite them. If you are not in a position where replacing your saved tags online is a good idea, it is recommended to keep this option unchanged.

") + self.main_container.addWidget(self.tag_save_groupbox) + self.tag_save_groupbox_layout.addWidget(self.tag_save_description, 0, 0, 1, 1) + + self.tags_to_save_groupbox = QGroupBox() + sizePolicy.setHeightForWidth(self.tags_to_save_groupbox.sizePolicy().hasHeightForWidth()) + self.tags_to_save_groupbox.setSizePolicy(sizePolicy) + self.tags_to_save_groupbox_layout = QGridLayout(self.tags_to_save_groupbox) + self.tags_to_save_groupbox.setTitle("Tags to Submit") + self.tags_to_save_description = QLabel(self.tags_to_save_groupbox) + sizePolicy.setHeightForWidth(self.tags_to_save_description.sizePolicy().hasHeightForWidth()) + self.tags_to_save_textbox = QLineEdit() + self.tags_to_save_description.setText("

List the tags that you want to submit to MusicBrainz via the plugin in the text box below. Separate each tag with a semi-colon. (e.g. genre; mood)

") + self.tags_to_save_groupbox_layout.addWidget(self.tags_to_save_description) + self.tags_to_save_textbox.setPlaceholderText("Tags you want to submit (separated by semicolons - e.g. genre; mood)") + self.tags_to_save_groupbox_layout.addWidget(self.tags_to_save_textbox) + self.main_container.addWidget(self.tags_to_save_groupbox) + + # Radio buttons for tag saving options (on the plugin as "destructive") + self.keep_radio_button = QRadioButton(self.tag_save_groupbox) + self.keep_radio_button.setText("Keep existing online saved tags") + self.tag_save_groupbox_layout.addWidget(self.keep_radio_button, 1, 0, 1, 1) + self.overwrite_radio_button = QRadioButton(self.tag_save_groupbox) + self.overwrite_radio_button.setText("Overwrite all online saved tags") + self.tag_save_groupbox_layout.addWidget(self.overwrite_radio_button, 2, 0, 1, 1) + + # Group box: tag aliases + self.tag_alias_groupbox = QGroupBox() + sizePolicy1 = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding) + sizePolicy1.setHorizontalStretch(0) + sizePolicy1.setVerticalStretch(0) + sizePolicy1.setHeightForWidth(self.tag_alias_groupbox.sizePolicy().hasHeightForWidth()) + self.tag_alias_groupbox.setSizePolicy(sizePolicy1) + self.tag_alias_groupbox.setCheckable(True) + self.tag_alias_groupbox.setChecked(False) + self.tag_alias_groupbox_layout = QVBoxLayout(self.tag_alias_groupbox) + self.tag_alias_groupbox.setTitle("Tag Aliases") + self.tag_alias_description = QLabel() + self.tag_alias_description.setText("

There may be cases where you prefer one tag on your files to be saved as another on MusicBrainz (e.g. if your genre tags don't align with MusicBrainz's standard genre tags). In such cases, the plugin can substitute your tags with whichever tags you want when submitting.

Anything listed here is case-insensitive, as MusicBrainz will process them in lowercase anyway.

") + self.tag_alias_description.setMinimumSize(QSize(0, 42)) + self.tag_alias_description.setWordWrap(True) + self.tag_alias_groupbox_layout.addWidget(self.tag_alias_description) + + # Tag alias table + self.tag_alias_table = QTableWidget() + self.tag_alias_table.setColumnCount(2) + __find_column = QTableWidgetItem() + __find_column.setText("Find...") + self.tag_alias_table.setHorizontalHeaderItem(0, __find_column) + __replace_column = QTableWidgetItem() + __replace_column.setText("Replace...") + self.tag_alias_table.setHorizontalHeaderItem(1, __replace_column) + self.tag_alias_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) + self.tag_alias_groupbox_layout.addWidget(self.tag_alias_table) + + # Tag alias buttons + self.table_button_layout = QHBoxLayout() + self.add_row_button = QPushButton() + self.delete_row_button = QPushButton() + self.add_row_button.setText("Add tag alias") + self.add_row_button.clicked.connect(self.add_row) + self.delete_row_button.setText("Delete selected tag aliases") + self.delete_row_button.clicked.connect(self.delete_rows) + self.table_button_layout.addWidget(self.add_row_button) + self.table_button_layout.addWidget(self.delete_row_button) + self.tag_alias_groupbox_layout.addLayout(self.table_button_layout) + self.main_container.addWidget(self.tag_alias_groupbox) + + page.setLayout(self.main_container) + + def add_row(self, find_entry="", replace_entry=""): + """ + Adds a row to the table. Accepts input. + """ + row_pos = self.tag_alias_table.rowCount() + self.tag_alias_table.insertRow(row_pos) + + find_tableitem = QTableWidgetItem(find_entry) + replace_tableitem = QTableWidgetItem(replace_entry) + + self.tag_alias_table.setItem(row_pos, 0, find_tableitem) + self.tag_alias_table.setItem(row_pos, 1, replace_tableitem) + + def delete_rows(self): + """ + Self-explanatory - removes the selected rows. + """ + for row in self.tag_alias_table.selectionModel().selectedRows(): + self.tag_alias_table.removeRow(row.row()) + + def rows_to_tuple_list(self): + """ + Converts filled in rows to a list of tuples for the alias list setting. + """ + + tuple_list = [] + row_count = self.tag_alias_table.rowCount() + for row in range(row_count): + find_tableitem = self.tag_alias_table.item(row, 0).text() + replace_tableitem = self.tag_alias_table.item(row, 1).text() + if find_tableitem and replace_tableitem: + tuple_list.append((find_tableitem, replace_tableitem)) + return tuple_list From 984c7614c9312b48cafa5ccb4c5b0109127f8cfb Mon Sep 17 00:00:00 2001 From: Flaky <59123723+Viktini@users.noreply.github.com> Date: Wed, 5 Apr 2023 18:27:41 +0100 Subject: [PATCH 2/8] Update README.md --- plugins/submit_folksonomy_tags/README.md | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/plugins/submit_folksonomy_tags/README.md b/plugins/submit_folksonomy_tags/README.md index f186b478..f108e03b 100644 --- a/plugins/submit_folksonomy_tags/README.md +++ b/plugins/submit_folksonomy_tags/README.md @@ -1,17 +1,12 @@ # Submit Folksonomy Tags - Picard Plugin -A plugin that lets the user submit tags from their tracks' `genre` and `mood` tags to their respective MusicBrainz pages' folksonomy tags via MusicBrainz Picard. Useful for music geeks who are meticulous with their genre tagging. +A plugin that lets the user submit tags from their tracks' tags - defaults to `genre` and `mood` - to their respective MusicBrainz pages' folksonomy tags via MusicBrainz Picard. Useful for music geeks who are meticulous with their genre tagging. -To use, right click on a track or release, then go to _Plugins > Submit tags to MusicBrainz_ - there are multiple options depending on if you want to submit tags to the recording, release or release group associated with the track/s or album/s you've right-clicked The tags will be applied from the `genre` and `mood` tags on your tracks. +**This plugin requires that you log into MusicBrainz via Picard.** The option to do so is in _Options > Options > General_. -Uses code from rswift's "Submit ISRC" plugin (specifically, the handling of the network response) - -# How to Install -This plugin requires that you log into MusicBrainz via Picard. The option to do so is in _Options > Options > General_. - -Download the contents of this repository as a .zip then drop it into the Picard plugins folder. **Do not rename the .zip file as Picard will have trouble loading it if you do.** +To use, right click on a track or release, then go to _Plugins > Submit [x] tags to MusicBrainz_ - there are multiple options depending on if you want to submit tags to the recording, release, release group or release artist associated with the track/s or album/s you've right-clicked The tags will be applied from the `genre` and `mood` tags on your tracks. -Alternatively, you may decide to extract the contents of the plugin into a folder. +Uses code from rswift's "Submit ISRC" plugin (specifically, the handling of the network response) # Features It does what it says on the tin: submits any tags you have in the genre tags of whichever files you drop into Picard to the respective pages. Right now the following entities are supported: @@ -27,5 +22,3 @@ The plugin can also replace certain tags if your tags don't match up with MusicB Right now, this plugin only submits tags. No tags are _retrieved_ for comparison yet, meaning I've opted to implement two modes based on how the MusicBrainz API works: maintain the tags that are already saved or overwrite _all_ of your tags. For anyone using the MusicBrainz API, choosing to keep your tags is basically sending the "upvote" attribute with every user tag, and choosing to overwrite doesn't do that, which MusicBrainz will respond by clearing old tags. See the [tags section of the MusicBrainz API for more details.](https://musicbrainz.org/doc/MusicBrainz_API#tags) Submitting for releases, release artists and release groups will also trigger an alert if your tags are not consistently the same across all tracks in an album. This is to prevent spamming of tags, purposeful or accidental, and is based on the standard already set for years by digital music sites and CD ripper utilities where an album would have the same genres tagged across all tracks. - -Submitting to artists by recording is not handled right now, see #3. \ No newline at end of file From d7225d14b1f1a2012ffe0d50c2cb6816474fec91 Mon Sep 17 00:00:00 2001 From: Flaky <59123723+Viktini@users.noreply.github.com> Date: Wed, 5 Apr 2023 18:50:20 +0100 Subject: [PATCH 3/8] removed unused variables and whitespace --- plugins/submit_folksonomy_tags/__init__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/plugins/submit_folksonomy_tags/__init__.py b/plugins/submit_folksonomy_tags/__init__.py index 4cc55663..fe61f45b 100644 --- a/plugins/submit_folksonomy_tags/__init__.py +++ b/plugins/submit_folksonomy_tags/__init__.py @@ -164,7 +164,7 @@ def handle_submit_process(tagger, track_list, target_tag): # Variable to enable alert if multiple MBIDs are associated, must be toggled. alert_multiple_mbids = False - # TODO when Windows Picard updates with Python 3.10, use case/switch. + # TODO when Windows Picard updates with Python 3.10, use case/switch. if target_tag == "musicbrainz_recordingid": dict_key = "recording" alert_inconsistent = False @@ -176,7 +176,7 @@ def handle_submit_process(tagger, track_list, target_tag): dict_key = "artist" data = {dict_key: {}} - + last_tags = {"mbid": ""} banned_mbids = { # Any artist entities that can be applied to multiple artists go here. @@ -231,8 +231,6 @@ def handle_submit_process(tagger, track_list, target_tag): warning = QMessageBox() warning.setStandardButtons(QMessageBox.Ok|QMessageBox.Cancel) warning.setDefaultButton(QMessageBox.Cancel) - warning_title = "" - warning_message = "" if inconsistent_detected and alert_multiple_mbids: warning.setIcon(QMessageBox.Warning) warning.setText(""" From 1bd1aa9b139ce843c3110dee52992bc96ffeddca Mon Sep 17 00:00:00 2001 From: Flaky <59123723+Viktini@users.noreply.github.com> Date: Wed, 5 Apr 2023 18:57:43 +0100 Subject: [PATCH 4/8] update submit_folksonomy_tags README based on initial checks from codacy https://github.com/metabrainz/picard-plugins/runs/12546220957 I'm new to this, so... bleh. --- plugins/submit_folksonomy_tags/README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/plugins/submit_folksonomy_tags/README.md b/plugins/submit_folksonomy_tags/README.md index f108e03b..b33bbfbd 100644 --- a/plugins/submit_folksonomy_tags/README.md +++ b/plugins/submit_folksonomy_tags/README.md @@ -4,21 +4,21 @@ A plugin that lets the user submit tags from their tracks' tags - defaults to `g **This plugin requires that you log into MusicBrainz via Picard.** The option to do so is in _Options > Options > General_. -To use, right click on a track or release, then go to _Plugins > Submit [x] tags to MusicBrainz_ - there are multiple options depending on if you want to submit tags to the recording, release, release group or release artist associated with the track/s or album/s you've right-clicked The tags will be applied from the `genre` and `mood` tags on your tracks. +To use, right click on a track or release, then go to _Plugins > Submit [x] tags to MusicBrainz_ - there are multiple options depending on if you want to submit tags to the recording, release, release group or release artist associated with the track/s or album/s you've right-clicked. The tags will be applied from the track tags you have configured. Uses code from rswift's "Submit ISRC" plugin (specifically, the handling of the network response) -# Features +## Features It does what it says on the tin: submits any tags you have in the genre tags of whichever files you drop into Picard to the respective pages. Right now the following entities are supported: -- recordings -- releases -- release groups -- artists (by release) + - recordings + - releases + - release groups + - artists (by release) The plugin can also replace certain tags if your tags don't match up with MusicBrainz's standard tags, notably with their allowed genre list (e.g. if you use "synthpop" and not "synth-pop", or you use the full name "electronic dance music" and not the abbreviated "edm"). -# Limitations +## Limitations Right now, this plugin only submits tags. No tags are _retrieved_ for comparison yet, meaning I've opted to implement two modes based on how the MusicBrainz API works: maintain the tags that are already saved or overwrite _all_ of your tags. For anyone using the MusicBrainz API, choosing to keep your tags is basically sending the "upvote" attribute with every user tag, and choosing to overwrite doesn't do that, which MusicBrainz will respond by clearing old tags. See the [tags section of the MusicBrainz API for more details.](https://musicbrainz.org/doc/MusicBrainz_API#tags) Submitting for releases, release artists and release groups will also trigger an alert if your tags are not consistently the same across all tracks in an album. This is to prevent spamming of tags, purposeful or accidental, and is based on the standard already set for years by digital music sites and CD ripper utilities where an album would have the same genres tagged across all tracks. From b254136fb0053daa845140abec6ca55b5941dad4 Mon Sep 17 00:00:00 2001 From: Flaky <59123723+Viktini@users.noreply.github.com> Date: Wed, 5 Apr 2023 19:24:48 +0100 Subject: [PATCH 5/8] another README.md fix for submit-folksonomy-tags Codacy really doesn't like the README.md (namely the list-item indicies), fixing it again https://github.com/metabrainz/picard-plugins/runs/12546981858 --- plugins/submit_folksonomy_tags/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/submit_folksonomy_tags/README.md b/plugins/submit_folksonomy_tags/README.md index b33bbfbd..18f0dc23 100644 --- a/plugins/submit_folksonomy_tags/README.md +++ b/plugins/submit_folksonomy_tags/README.md @@ -4,17 +4,17 @@ A plugin that lets the user submit tags from their tracks' tags - defaults to `g **This plugin requires that you log into MusicBrainz via Picard.** The option to do so is in _Options > Options > General_. -To use, right click on a track or release, then go to _Plugins > Submit [x] tags to MusicBrainz_ - there are multiple options depending on if you want to submit tags to the recording, release, release group or release artist associated with the track/s or album/s you've right-clicked. The tags will be applied from the track tags you have configured. +To use, right click on a track or release, then go to _Plugins > Submit **x** tags to MusicBrainz_ - there are multiple options depending on if you want to submit tags to the recording, release, release group or release artist associated with the track/s or album/s you've right-clicked. The tags will be applied from the track tags you have configured. Uses code from rswift's "Submit ISRC" plugin (specifically, the handling of the network response) ## Features It does what it says on the tin: submits any tags you have in the genre tags of whichever files you drop into Picard to the respective pages. Right now the following entities are supported: - - recordings - - releases - - release groups - - artists (by release) + - recordings + - releases + - release groups + - artists (by release) The plugin can also replace certain tags if your tags don't match up with MusicBrainz's standard tags, notably with their allowed genre list (e.g. if you use "synthpop" and not "synth-pop", or you use the full name "electronic dance music" and not the abbreviated "edm"). From e5f10572fddef3c4e52c937d2343216561bdd8d0 Mon Sep 17 00:00:00 2001 From: Flaky <59123723+Viktini@users.noreply.github.com> Date: Wed, 5 Apr 2023 19:32:41 +0100 Subject: [PATCH 6/8] Remove redundant warning message lines --- plugins/submit_folksonomy_tags/__init__.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/plugins/submit_folksonomy_tags/__init__.py b/plugins/submit_folksonomy_tags/__init__.py index fe61f45b..934ba5c0 100644 --- a/plugins/submit_folksonomy_tags/__init__.py +++ b/plugins/submit_folksonomy_tags/__init__.py @@ -225,14 +225,14 @@ def handle_submit_process(tagger, track_list, target_tag): data[dict_key][mbid] = processed_tags else: log.info(f"Not submitting MBID {track.metadata[target_tag]} as it was found on 'do not submit' MBID set.") - + # Send an alert when, at the end of it all, inconsistent tagging was detected. if inconsistent_detected or alert_multiple_mbids: warning = QMessageBox() warning.setStandardButtons(QMessageBox.Ok|QMessageBox.Cancel) warning.setDefaultButton(QMessageBox.Cancel) + warning.setIcon(QMessageBox.Warning) if inconsistent_detected and alert_multiple_mbids: - warning.setIcon(QMessageBox.Warning) warning.setText("""

WARNING: INCONSISTENT TAGGING AND SUBMISSION TO MULTIPLE MBIDS DETECTED.

You are trying to apply different tags to multiple MusicBrainz entities.

@@ -241,7 +241,6 @@ def handle_submit_process(tagger, track_list, target_tag):

If this was intentional, click OK. Otherwise, click Cancel.

""") elif inconsistent_detected: - warning.setIcon(QMessageBox.Warning) warning.setText("""

WARNING: INCONSISTENT TAGGING DETECTED.

You are trying to apply multiple tags to one entity, which benefits more from @@ -252,7 +251,6 @@ def handle_submit_process(tagger, track_list, target_tag):

If this was intentional, click OK. Otherwise, click Cancel.

""") elif alert_multiple_mbids: - warning.setIcon(QMessageBox.Warning) warning.setText("""

MULTIPLE MBIDS DETECTED.

You are trying to apply a tag to multiple MusicBrainz entities.

@@ -375,7 +373,7 @@ def load(self): def save(self): config.setting['tag_submit_plugin_destructive'] = self.ui.overwrite_radio_button.isChecked() config.setting['tag_submit_plugin_aliases_enabled'] = self.ui.tag_alias_groupbox.isChecked() - + tag_textbox_text = self.ui.tags_to_save_textbox.text() if tag_textbox_text: config.setting['tag_submit_plugin_tags_to_submit'] = [ From c05a3d396c3f1e6a7f07473767f827d02d8df42b Mon Sep 17 00:00:00 2001 From: Flaky <59123723+Viktini@users.noreply.github.com> Date: Wed, 5 Apr 2023 23:22:53 +0100 Subject: [PATCH 7/8] correct credit --- plugins/submit_folksonomy_tags/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/submit_folksonomy_tags/__init__.py b/plugins/submit_folksonomy_tags/__init__.py index 934ba5c0..0ebdf994 100644 --- a/plugins/submit_folksonomy_tags/__init__.py +++ b/plugins/submit_folksonomy_tags/__init__.py @@ -5,7 +5,7 @@ A MusicBrainz login is required to use this plugin. Log in first by going to the General options. Then, to use, right click on a track or release then go to Plugins and depending on what you want to submit, choose the option you want. -Uses code from rswift's "Submit ISRC" plugin (specifically, the handling of the network response) +Uses code from rdswift's "Submit ISRC" plugin (specifically, the handling of the network response) """ PLUGIN_VERSION = '0.2.4' PLUGIN_API_VERSIONS = ['2.2'] @@ -28,7 +28,7 @@ from PyQt5.QtWidgets import QMessageBox # List of Qt network error codes. -# From "Submit ISRC" plugin - credit to rswift. +# From "Submit ISRC" plugin - credit to rdswift. q_error_codes = { 0: 'No error', 1: "The remote server refused the connection (the server is not accepting requests).", @@ -86,11 +86,11 @@ def tag_submit_handler(document, reply, error, tagger): The function handling the network response from MusicBrainz or QtNetwork, showing a message box if an error had occurred. - Uses the network response handler code from rswift's "Submit ISRC" + Uses the network response handler code from rdswift's "Submit ISRC" plugin. """ if error: - # Error handling from rswift's Submit ISRC plugin + # Error handling from rdswift's Submit ISRC plugin xml_text = str(document, 'UTF-8') if isinstance(document, (bytes, bytearray, QtCore.QByteArray)) else str(document) # Build error text message from returned xml payload From 7d13ff70b48a26cc65f506161b1c2b04116b5248 Mon Sep 17 00:00:00 2001 From: Flaky <59123723+Viktini@users.noreply.github.com> Date: Thu, 13 Apr 2023 23:08:00 +0100 Subject: [PATCH 8/8] Change + move escape, use getall to get MBIDs uses `xml.sax.saxutils` instead of `html` to provide XML escaping, and move said escaping into the function for generating XMLs instead of handling tags. I've also used `.getall()` for grabbing MBIDs as phw suggested. --- plugins/submit_folksonomy_tags/__init__.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/plugins/submit_folksonomy_tags/__init__.py b/plugins/submit_folksonomy_tags/__init__.py index 0ebdf994..a07d6d95 100644 --- a/plugins/submit_folksonomy_tags/__init__.py +++ b/plugins/submit_folksonomy_tags/__init__.py @@ -22,8 +22,8 @@ from picard.webservice.api_helpers import MBAPIHelper, _wrap_xml_metadata from .ui_config import TagSubmitPluginOptionsUI import re -import html import functools +from xml.sax.saxutils import escape from PyQt5 import QtCore from PyQt5.QtWidgets import QMessageBox @@ -197,8 +197,7 @@ def handle_submit_process(tagger, track_list, target_tag): for track in track_list: if track.files: for file in track.files: - mbid_list = [html.escape(tag.strip().lower()) for tag - in re.split(";|/|,", file.metadata[target_tag])] + mbid_list = file.metadata.getall(target_tag) if len(mbid_list) > 1: alert_multiple_mbids = True for mbid in mbid_list: @@ -213,7 +212,7 @@ def handle_submit_process(tagger, track_list, target_tag): if (last_tags[tag] != file.metadata[tag]) and (last_tags["mbid"] == file.metadata[target_tag]) and alert_inconsistent: inconsistent_detected = True # in any case, process the tags in case the user intends to go with it. - processed_tags.extend([html.escape(tag.strip().lower()) for tag + processed_tags.extend([tag.strip().lower() for tag in re.split(";|/|,", file.metadata[tag])]) last_tags[tag] = file.metadata[tag] last_tags["mbid"] = file.metadata[target_tag] @@ -293,7 +292,7 @@ def upload_tags_to_mbz(data, tagger): xml_data.extend([f'<{key} id="{mbid}">', ""]) # add the tags for tag in data[key][mbid]: - xml_data.append(f'{process_tag_aliases(tag.lower())}') + xml_data.append(f'{escape(process_tag_aliases(tag.lower()))}') # close the user tag list xml_data.extend(["", f""]) # close the list