diff --git a/picard/config_upgrade.py b/picard/config_upgrade.py index 354d3d5f1..04d565876 100644 --- a/picard/config_upgrade.py +++ b/picard/config_upgrade.py @@ -10,6 +10,7 @@ # Copyright (C) 2016 Suhas # Copyright (C) 2016-2017 Sambhav Kothari # Copyright (C) 2021 Gabriel Ferreira +# Copyright (C) 2021 Bob Swift # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -428,6 +429,10 @@ def upgrade_to_v2_7_0_dev_4(config): if _s["artist_script_exception"]: _s["artist_script_exceptions"] = [_s["artist_script_exception"]] _s.remove("artist_script_exception") + ListOption("setting", "artist_locales", ['en']) + if _s["artist_locale"]: + _s["artist_locales"] = [_s["artist_locale"]] + _s.remove("artist_locale") def rename_option(config, old_opt, new_opt, option_type, default): diff --git a/picard/mbjson.py b/picard/mbjson.py index e5e93a1a4..7509d12f4 100644 --- a/picard/mbjson.py +++ b/picard/mbjson.py @@ -10,6 +10,7 @@ # Copyright (C) 2020 David Kellner # Copyright (C) 2020 dukeyin # Copyright (C) 2021 Vladislav Karbovskii +# Copyright (C) 2021 Bob Swift # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -192,9 +193,6 @@ def _translate_artist_node(node): config = get_config() transl, translsort = None, None if config.setting['translate_artist_names']: - locale = config.setting["artist_locale"] - lang = locale.split("_")[0] - if config.setting['translate_artist_names_script_exception']: threshhold = config.setting["artist_script_exception_weighting"] / 100 detected_scripts = list_script_weighted(node["name"], threshhold) @@ -202,39 +200,45 @@ def _translate_artist_node(node): if script_id in detected_scripts: return node['name'], node['sort-name'] - if "aliases" in node: - result = (-1, (None, None)) - for alias in node['aliases']: - if not alias["primary"]: - continue - if "locale" not in alias: - continue - parts = [] - if alias['locale'] == locale: - score = 0.8 - elif alias['locale'] == lang: - score = 0.6 - elif alias['locale'].split("_")[0] == lang: - score = 0.4 - else: - continue - parts.append((score, 5)) - if alias["type"] == "Artist name": - score = 0.8 - elif alias["type"] == "Legal Name": - score = 0.5 - else: - # as 2014/09/19, only Artist or Legal names should have the - # Primary flag - score = 0.0 - parts.append((score, 5)) - comb = linear_combination_of_weights(parts) - if comb > result[0]: - result = (comb, (alias['name'], alias["sort-name"])) - transl, translsort = result[1] - if not transl: - translsort = node['sort-name'] - transl = translate_from_sortname(node['name'] or "", translsort) + for locale in config.setting["artist_locales"]: + lang = locale.split("_")[0] + + if "aliases" in node: + result = (-1, (None, None)) + for alias in node['aliases']: + if not alias["primary"]: + continue + if "locale" not in alias: + continue + parts = [] + if alias['locale'] == locale: + score = 0.8 + elif alias['locale'] == lang: + score = 0.6 + elif alias['locale'].split("_")[0] == lang: + score = 0.4 + else: + continue + parts.append((score, 5)) + if alias["type"] == "Artist name": + score = 0.8 + elif alias["type"] == "Legal Name": + score = 0.5 + else: + # as 2014/09/19, only Artist or Legal names should have the + # Primary flag + score = 0.0 + parts.append((score, 5)) + comb = linear_combination_of_weights(parts) + if comb > result[0]: + result = (comb, (alias['name'], alias["sort-name"])) + transl, translsort = result[1] + if transl: + # Matching locale found. Skip further processing. + return (transl, translsort) + if not transl: + translsort = node['sort-name'] + transl = translate_from_sortname(node['name'] or "", translsort) else: transl, translsort = node['name'], node['sort-name'] return (transl, translsort) diff --git a/picard/profile.py b/picard/profile.py index 6f85accf5..22d0c9112 100644 --- a/picard/profile.py +++ b/picard/profile.py @@ -43,7 +43,7 @@ class UserProfileGroups(): SettingDesc("va_name", N_("Various Artists name"), ["va_name"]), SettingDesc("nat_name", N_("Non-album tracks name"), ["nat_name"]), SettingDesc("translate_artist_names", N_("Translate artist names"), ["translate_artist_names"]), - SettingDesc("artist_locale", N_("Translation locale"), ["artist_locale"]), + SettingDesc("artist_locales", N_("Translation locales"), ["selected_locales"]), SettingDesc("translate_artist_names_script_exception", N_("Translate artist names exception"), ["translate_artist_names_script_exception"]), SettingDesc("artist_script_exceptions", N_("Translation script exceptions"), ["ignore_tx_scripts"]), SettingDesc("artist_script_exception_weighting", N_("Exception script weighting"), ["minimum_weighting"]), diff --git a/picard/ui/options/metadata.py b/picard/ui/options/metadata.py index a0e98ec85..111d24185 100644 --- a/picard/ui/options/metadata.py +++ b/picard/ui/options/metadata.py @@ -10,6 +10,7 @@ # Copyright (C) 2014 Wieland Hoffmann # Copyright (C) 2017 Sambhav Kothari # Copyright (C) 2021 Vladislav Karbovskii +# Copyright (C) 2021 Bob Swift # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -43,10 +44,13 @@ from picard.const import ( SCRIPTS, ) +from picard.ui import PicardDialog +from picard.ui.moveable_list_view import MoveableListView from picard.ui.options import ( OptionsPage, register_options_page, ) +from picard.ui.ui_multi_locale_selector import Ui_MultiLocaleSelector from picard.ui.ui_options_metadata import Ui_MetadataOptionsPage @@ -79,7 +83,7 @@ class MetadataOptionsPage(OptionsPage): options = [ TextOption("setting", "va_name", "Various Artists"), TextOption("setting", "nat_name", "[non-album tracks]"), - TextOption("setting", "artist_locale", "en"), + ListOption("setting", "artist_locales", ["en"]), BoolOption("setting", "translate_artist_names", False), BoolOption("setting", "translate_artist_names_script_exception", False), ListOption("setting", "artist_script_exceptions", []), @@ -98,21 +102,15 @@ class MetadataOptionsPage(OptionsPage): self.ui.setupUi(self) self.ui.va_name_default.clicked.connect(self.set_va_name_default) self.ui.nat_name_default.clicked.connect(self.set_nat_name_default) + self.ui.select_locales.clicked.connect(self.open_locale_selector) self.ui.translate_artist_names.stateChanged.connect(self.set_enabled_states) self.ui.translate_artist_names_script_exception.stateChanged.connect(self.set_enabled_states) def load(self): config = get_config() self.ui.translate_artist_names.setChecked(config.setting["translate_artist_names"]) - - combo_box = self.ui.artist_locale - current_locale = config.setting["artist_locale"] - for i, (locale, name, level) in enumerate(iter_sorted_locales(ALIAS_LOCALES)): - label = " " * level + name - combo_box.addItem(label, locale) - if locale == current_locale: - combo_box.setCurrentIndex(i) - + self.current_locales = config.setting["artist_locales"] + self.make_locales_text() self.ui.translate_artist_names_script_exception.setChecked(config.setting["translate_artist_names_script_exception"]) self.ui.ignore_tx_scripts.clear() for script_id in SCRIPTS: @@ -135,10 +133,14 @@ class MetadataOptionsPage(OptionsPage): self.set_enabled_states() + def make_locales_text(self): + locales = list(ALIAS_LOCALES[key] for key in self.current_locales) + self.ui.selected_locales.setText('; '.join(locales)) + def save(self): config = get_config() config.setting["translate_artist_names"] = self.ui.translate_artist_names.isChecked() - config.setting["artist_locale"] = self.ui.artist_locale.itemData(self.ui.artist_locale.currentIndex()) + config.setting["artist_locales"] = self.current_locales config.setting["translate_artist_names_script_exception"] = self.ui.translate_artist_names_script_exception.isChecked() script_exceptions = [] for idx in range(self.ui.ignore_tx_scripts.count()): @@ -171,9 +173,78 @@ class MetadataOptionsPage(OptionsPage): def set_enabled_states(self): translate_checked = self.ui.translate_artist_names.isChecked() translate_exception_checked = self.ui.translate_artist_names_script_exception.isChecked() - self.ui.artist_locale.setEnabled(translate_checked) + self.ui.select_locales.setEnabled(translate_checked) + self.ui.selected_locales.setEnabled(translate_checked) self.ui.translate_artist_names_script_exception.setEnabled(translate_checked) self.ui.ignore_script_frame.setEnabled(translate_checked and translate_exception_checked) + def open_locale_selector(self): + dialog = MultiLocaleSelector(self) + dialog.show() + + +class MultiLocaleSelector(PicardDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.ui = Ui_MultiLocaleSelector() + self.ui.setupUi(self) + self.ui.button_box.accepted.connect(self.save_changes) + self.ui.button_box.rejected.connect(self.reject) + self.move_view = MoveableListView(self.ui.selected_locales, self.ui.move_up, self.ui.move_down) + self.ui.add_locale.clicked.connect(self.add_locale) + self.ui.remove_locale.clicked.connect(self.remove_locale) + self.ui.selected_locales.currentRowChanged.connect(self.set_button_state) + self.load() + + def load(self): + self.ui.selected_locales.clear() + for locale in self.parent().current_locales: + label = (" " if '_' in locale else "") + ALIAS_LOCALES[locale] + item = QtWidgets.QListWidgetItem(label) + item.setData(QtCore.Qt.UserRole, locale) + self.ui.selected_locales.addItem(item) + self.ui.selected_locales.setCurrentRow(0) + + self.ui.available_locales.clear() + for (locale, name, level) in iter_sorted_locales(ALIAS_LOCALES): + label = " " * level + name + item = QtWidgets.QListWidgetItem(label) + item.setData(QtCore.Qt.UserRole, locale) + self.ui.available_locales.addItem(item) + self.ui.available_locales.setCurrentRow(0) + + self.set_button_state() + + def add_locale(self): + item = self.ui.available_locales.currentItem() + if item is None: + return + locale = item.data(QtCore.Qt.UserRole) + for row in range(self.ui.selected_locales.count()): + selected_item = self.ui.selected_locales.item(row) + if selected_item.data(QtCore.Qt.UserRole) == locale: + return + new_item = item.clone() + new_item.setText(new_item.text().strip()) + self.ui.selected_locales.addItem(new_item) + self.ui.selected_locales.setCurrentRow(self.ui.selected_locales.count() - 1) + + def remove_locale(self): + row = self.ui.selected_locales.currentRow() + self.ui.selected_locales.takeItem(row) + + def set_button_state(self): + enabled = self.ui.selected_locales.count() > 1 + self.ui.remove_locale.setEnabled(enabled) + + def save_changes(self): + locales = [] + for row in range(self.ui.selected_locales.count()): + selected_item = self.ui.selected_locales.item(row) + locales.append(selected_item.data(QtCore.Qt.UserRole)) + self.parent().current_locales = locales + self.parent().make_locales_text() + self.accept() + register_options_page(MetadataOptionsPage) diff --git a/picard/ui/ui_options_metadata.py b/picard/ui/ui_options_metadata.py index fe6c2b60e..24191fba5 100644 --- a/picard/ui/ui_options_metadata.py +++ b/picard/ui/ui_options_metadata.py @@ -27,9 +27,17 @@ class Ui_MetadataOptionsPage(object): self.translate_artist_names = QtWidgets.QCheckBox(self.metadata_groupbox) self.translate_artist_names.setObjectName("translate_artist_names") self.verticalLayout_3.addWidget(self.translate_artist_names) - self.artist_locale = QtWidgets.QComboBox(self.metadata_groupbox) - self.artist_locale.setObjectName("artist_locale") - self.verticalLayout_3.addWidget(self.artist_locale) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setContentsMargins(-1, -1, -1, 0) + self.horizontalLayout.setObjectName("horizontalLayout") + self.selected_locales = QtWidgets.QLineEdit(self.metadata_groupbox) + self.selected_locales.setReadOnly(True) + self.selected_locales.setObjectName("selected_locales") + self.horizontalLayout.addWidget(self.selected_locales) + self.select_locales = QtWidgets.QPushButton(self.metadata_groupbox) + self.select_locales.setObjectName("select_locales") + self.horizontalLayout.addWidget(self.select_locales) + self.verticalLayout_3.addLayout(self.horizontalLayout) self.translate_artist_names_script_exception = QtWidgets.QCheckBox(self.metadata_groupbox) self.translate_artist_names_script_exception.setObjectName("translate_artist_names_script_exception") self.verticalLayout_3.addWidget(self.translate_artist_names_script_exception) @@ -147,8 +155,7 @@ class Ui_MetadataOptionsPage(object): self.retranslateUi(MetadataOptionsPage) QtCore.QMetaObject.connectSlotsByName(MetadataOptionsPage) - MetadataOptionsPage.setTabOrder(self.translate_artist_names, self.artist_locale) - MetadataOptionsPage.setTabOrder(self.artist_locale, self.translate_artist_names_script_exception) + MetadataOptionsPage.setTabOrder(self.translate_artist_names, self.translate_artist_names_script_exception) MetadataOptionsPage.setTabOrder(self.translate_artist_names_script_exception, self.standardize_artists) MetadataOptionsPage.setTabOrder(self.standardize_artists, self.standardize_instruments) MetadataOptionsPage.setTabOrder(self.standardize_instruments, self.convert_punctuation) @@ -163,7 +170,8 @@ class Ui_MetadataOptionsPage(object): def retranslateUi(self, MetadataOptionsPage): _translate = QtCore.QCoreApplication.translate self.metadata_groupbox.setTitle(_("Metadata")) - self.translate_artist_names.setText(_("Translate artist names to this locale where possible:")) + self.translate_artist_names.setText(_("Translate artist names to these locales where possible:")) + self.select_locales.setText(_("Select")) self.translate_artist_names_script_exception.setText(_("Ignore artist name translation for these scripts:")) self.label_2.setText(_("
Only scripts where the percentage of the artist name that is written using that script are above the specified minimum weighting will be considered.
")) self.label.setText(_("Minimum Weighting:")) diff --git a/test/test_acoustid.py b/test/test_acoustid.py index 57da56dae..13caca47c 100644 --- a/test/test_acoustid.py +++ b/test/test_acoustid.py @@ -6,6 +6,7 @@ # Copyright (C) 2018 Wieland Hoffmann # Copyright (C) 2019-2020 Philipp Wolfer # Copyright (C) 2020 Ray Bouchard +# Copyright (C) 2021 Bob Swift # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -38,7 +39,7 @@ settings = { "standardize_artists": False, "standardize_releases": False, "translate_artist_names": True, - "artist_locale": 'en', + "artist_locales": ['en'], "translate_artist_names_script_exception": False, } diff --git a/test/test_mbjson.py b/test/test_mbjson.py index 63189b7a6..003c4da2b 100644 --- a/test/test_mbjson.py +++ b/test/test_mbjson.py @@ -6,6 +6,7 @@ # Copyright (C) 2017, 2019 Laurent Monin # Copyright (C) 2018 Wieland Hoffmann # Copyright (C) 2018-2021 Philipp Wolfer +# Copyright (C) 2021 Bob Swift # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -55,7 +56,7 @@ settings = { "standardize_instruments": True, "release_ars": True, "preferred_release_countries": [], - "artist_locale": 'en', + "artist_locales": ['en'], } diff --git a/ui/options_metadata.ui b/ui/options_metadata.ui index 558fba09f..c7431ff21 100644 --- a/ui/options_metadata.ui +++ b/ui/options_metadata.ui @@ -35,12 +35,30 @@