Example #1
0
class TagsOptionsPage(OptionsPage):

    NAME = "tags"
    TITLE = N_("Tags")
    PARENT = None
    SORT_ORDER = 30
    ACTIVE = True

    options = [
        config.BoolOption("setting", "dont_write_tags", False),
        config.BoolOption("setting", "preserve_timestamps", False),
        config.BoolOption("setting", "clear_existing_tags", False),
        config.BoolOption("setting", "remove_id3_from_flac", False),
        config.BoolOption("setting", "remove_ape_from_mp3", False),
        config.TextOption("setting", "preserved_tags", ""),
    ]

    def __init__(self, parent=None):
        super().__init__(parent)
        self.ui = Ui_TagsOptionsPage()
        self.ui.setupUi(self)
        self.completer = QtWidgets.QCompleter(sorted(TAG_NAMES.keys()), self)
        self.completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
        self.completer.setWidget(self.ui.preserved_tags)
        self.ui.preserved_tags.textEdited.connect(self.preserved_tags_edited)
        self.completer.activated.connect(self.completer_activated)

    def load(self):
        self.ui.write_tags.setChecked(not config.setting["dont_write_tags"])
        self.ui.preserve_timestamps.setChecked(
            config.setting["preserve_timestamps"])
        self.ui.clear_existing_tags.setChecked(
            config.setting["clear_existing_tags"])
        self.ui.remove_ape_from_mp3.setChecked(
            config.setting["remove_ape_from_mp3"])
        self.ui.remove_id3_from_flac.setChecked(
            config.setting["remove_id3_from_flac"])
        self.ui.preserved_tags.setText(config.setting["preserved_tags"])

    def save(self):
        config.setting["dont_write_tags"] = not self.ui.write_tags.isChecked()
        config.setting[
            "preserve_timestamps"] = self.ui.preserve_timestamps.isChecked()
        clear_existing_tags = self.ui.clear_existing_tags.isChecked()
        if clear_existing_tags != config.setting["clear_existing_tags"]:
            config.setting["clear_existing_tags"] = clear_existing_tags
            self.tagger.window.metadata_box.update()
        config.setting[
            "remove_ape_from_mp3"] = self.ui.remove_ape_from_mp3.isChecked()
        config.setting[
            "remove_id3_from_flac"] = self.ui.remove_id3_from_flac.isChecked()
        config.setting["preserved_tags"] = re.sub(
            r"[,\s]+$", "", self.ui.preserved_tags.text())
        self.tagger.window.enable_tag_saving_action.setChecked(
            not config.setting["dont_write_tags"])

    def preserved_tags_edited(self, text):
        prefix = text[:self.ui.preserved_tags.cursorPosition()].split(",")[-1]
        self.completer.setCompletionPrefix(prefix.strip())
        if prefix:
            self.completer.complete()
        else:
            self.completer.popup().hide()

    def completer_activated(self, text):
        input_field = self.ui.preserved_tags
        current = input_field.text()
        cursor_pos = input_field.cursorPosition()
        prefix_len = len(self.completer.completionPrefix())
        leading_text = current[:cursor_pos - prefix_len].rstrip()
        trailing_text = current[cursor_pos:].lstrip()
        # Replace the autocompletion prefix with the autocompleted text,
        # append a comma so the user can easily enter the next entry
        replacement = ("%s %s, " % (leading_text, text)).lstrip()
        input_field.setText(replacement + trailing_text)
        # Set cursor position to end of autocompleted input
        input_field.setCursorPosition(len(replacement))
Example #2
0
class FileBrowser(QtWidgets.QTreeView):

    options = [
        config.TextOption("persist", "current_browser_path",
                          _default_current_browser_path),
        config.BoolOption("persist", "show_hidden_files", False),
    ]

    def __init__(self, parent):
        super().__init__(parent)
        self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
        self.setDragEnabled(True)
        self.move_files_here_action = QtWidgets.QAction(
            _("&Move Tagged Files Here"), self)
        self.move_files_here_action.triggered.connect(self.move_files_here)
        self.addAction(self.move_files_here_action)
        self.toggle_hidden_action = QtWidgets.QAction(_("Show &Hidden Files"),
                                                      self)
        self.toggle_hidden_action.setCheckable(True)
        self.toggle_hidden_action.setChecked(
            config.persist["show_hidden_files"])
        self.toggle_hidden_action.toggled.connect(self.show_hidden)
        self.addAction(self.toggle_hidden_action)
        self.set_as_starting_directory_action = QtWidgets.QAction(
            _("&Set as starting directory"), self)
        self.set_as_starting_directory_action.triggered.connect(
            self.set_as_starting_directory)
        self.addAction(self.set_as_starting_directory_action)
        self.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
        self.focused = False
        self._set_model()

    def _set_model(self):
        self.model = QtWidgets.QFileSystemModel()
        self.model.layoutChanged.connect(self._layout_changed)
        self.model.setRootPath("")
        self._set_model_filter()
        filters = []
        for exts, name in supported_formats():
            filters.extend("*" + e for e in exts)
        self.model.setNameFilters(filters)
        # Hide unsupported files completely
        self.model.setNameFilterDisables(False)
        self.model.sort(0, QtCore.Qt.AscendingOrder)
        self.setModel(self.model)
        if IS_MACOS:
            self.setRootIndex(self.model.index("/Volumes"))
        header = self.header()
        header.hideSection(1)
        header.hideSection(2)
        header.hideSection(3)
        header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
        header.setStretchLastSection(False)
        header.setVisible(False)

    def _set_model_filter(self):
        model_filter = QtCore.QDir.AllDirs | QtCore.QDir.Files | QtCore.QDir.Drives | QtCore.QDir.NoDotAndDotDot
        if config.persist["show_hidden_files"]:
            model_filter |= QtCore.QDir.Hidden
        self.model.setFilter(model_filter)

    def _layout_changed(self):
        def scroll():
            # XXX The currentIndex seems to change while QFileSystemModel is
            # populating itself (so setCurrentIndex in __init__ won't last).
            # The time it takes to load varies and there are no signals to find
            # out when it's done. As a workaround, keep restoring the state as
            # long as the layout is updating, and the user hasn't focused yet.
            if not self.focused:
                self._restore_state()
            self.scrollTo(self.currentIndex())

        QtCore.QTimer.singleShot(0, scroll)

    def scrollTo(self,
                 index,
                 scrolltype=QtWidgets.QAbstractItemView.EnsureVisible):
        # QTreeView.scrollTo resets the horizontal scroll position to 0.
        # Reimplemented to instead scroll to horizontal parent position.
        level = -1
        super().scrollTo(index, scrolltype)
        parent = self.currentIndex().parent()
        root = self.rootIndex()
        while parent.isValid() and parent != root:
            parent = parent.parent()
            level += 1
        pos_x = max(self.indentation() * level, 0)
        self.horizontalScrollBar().setValue(pos_x)

    def mousePressEvent(self, event):
        index = self.indexAt(event.pos())
        if index.isValid():
            self.selectionModel().setCurrentIndex(
                index, QtCore.QItemSelectionModel.NoUpdate)
        super().mousePressEvent(event)

    def focusInEvent(self, event):
        self.focused = True
        super().focusInEvent(event)

    def show_hidden(self, state):
        config.persist["show_hidden_files"] = state
        self._set_model_filter()

    def save_state(self):
        indexes = self.selectedIndexes()
        if indexes:
            path = self.model.filePath(indexes[0])
            config.persist["current_browser_path"] = os.path.normpath(path)

    def restore_state(self):
        pass

    def _restore_state(self):
        if config.setting["starting_directory"]:
            path = config.setting["starting_directory_path"]
            scrolltype = QtWidgets.QAbstractItemView.PositionAtTop
        else:
            path = config.persist["current_browser_path"]
            scrolltype = QtWidgets.QAbstractItemView.PositionAtCenter
        if path:
            index = self.model.index(find_existing_path(path))
            self.setCurrentIndex(index)
            self.expand(index)
            self.scrollTo(index, scrolltype)

    def _get_destination_from_path(self, path):
        destination = os.path.normpath(path)
        if not os.path.isdir(destination):
            destination = os.path.dirname(destination)
        return destination

    def move_files_here(self):
        indexes = self.selectedIndexes()
        if not indexes:
            return
        path = self.model.filePath(indexes[0])
        config.setting["move_files_to"] = self._get_destination_from_path(path)

    def set_as_starting_directory(self):
        indexes = self.selectedIndexes()
        if indexes:
            path = self.model.filePath(indexes[0])
            config.setting[
                "starting_directory_path"] = self._get_destination_from_path(
                    path)
Example #3
0
class InterfaceOptionsPage(OptionsPage):

    NAME = "interface"
    TITLE = N_("User Interface")
    PARENT = None
    SORT_ORDER = 80
    ACTIVE = True
    SEPARATOR = '—' * 5
    TOOLBAR_BUTTONS = {
        'add_directory_action': {
            'label': N_('Add Folder'),
            'icon': 'folder'
        },
        'add_files_action': {
            'label': N_('Add Files'),
            'icon': 'document-open'
        },
        'cluster_action': {
            'label': N_('Cluster'),
            'icon': 'picard-cluster'
        },
        'autotag_action': {
            'label': N_('Lookup'),
            'icon': 'picard-auto-tag'
        },
        'analyze_action': {
            'label': N_('Scan'),
            'icon': 'picard-analyze'
        },
        'browser_lookup_action': {
            'label': N_('Lookup in Browser'),
            'icon': 'lookup-musicbrainz'
        },
        'save_action': {
            'label': N_('Save'),
            'icon': 'document-save'
        },
        'view_info_action': {
            'label': N_('Info'),
            'icon': 'picard-edit-tags'
        },
        'remove_action': {
            'label': N_('Remove'),
            'icon': 'list-remove'
        },
        'submit_acoustid_action': {
            'label': N_('Submit AcoustIDs'),
            'icon': 'acoustid-fingerprinter'
        },
        'play_file_action': {
            'label': N_('Open in Player'),
            'icon': 'play-music'
        },
        'cd_lookup_action': {
            'label': N_('Lookup CD...'),
            'icon': 'media-optical'
        },
    }
    ACTION_NAMES = set(TOOLBAR_BUTTONS.keys())
    options = [
        config.BoolOption("setting", "toolbar_show_labels", True),
        config.BoolOption("setting", "toolbar_multiselect", False),
        config.BoolOption("setting", "builtin_search", False),
        config.BoolOption("setting", "use_adv_search_syntax", False),
        config.BoolOption("setting", "quit_confirmation", True),
        config.TextOption("setting", "ui_language", ""),
        config.BoolOption("setting", "starting_directory", False),
        config.TextOption("setting", "starting_directory_path",
                          _default_starting_dir),
        config.TextOption("setting", "load_image_behavior", "append"),
        config.ListOption("setting", "toolbar_layout", [
            'add_directory_action',
            'add_files_action',
            'separator',
            'cluster_action',
            'separator',
            'autotag_action',
            'analyze_action',
            'browser_lookup_action',
            'separator',
            'save_action',
            'view_info_action',
            'remove_action',
            'separator',
            'cd_lookup_action',
            'separator',
            'submit_acoustid_action',
        ]),
    ]

    def __init__(self, parent=None):
        super().__init__(parent)
        self.ui = Ui_InterfaceOptionsPage()
        self.ui.setupUi(self)
        self.ui.ui_language.addItem(_('System default'), '')
        language_list = [(l[0], l[1], _(l[2])) for l in UI_LANGUAGES]
        fcmp = lambda x: locale.strxfrm(x[2])
        for lang_code, native, translation in sorted(language_list, key=fcmp):
            if native and native != translation:
                name = '%s (%s)' % (translation, native)
            else:
                name = translation
            self.ui.ui_language.addItem(name, lang_code)
        self.ui.starting_directory.stateChanged.connect(
            partial(enabledSlot, self.ui.starting_directory_path.setEnabled))
        self.ui.starting_directory.stateChanged.connect(
            partial(enabledSlot, self.ui.starting_directory_browse.setEnabled))
        self.ui.starting_directory_browse.clicked.connect(
            self.starting_directory_browse)
        self.ui.add_button.clicked.connect(self.add_to_toolbar)
        self.ui.insert_separator_button.clicked.connect(self.insert_separator)
        self.ui.remove_button.clicked.connect(self.remove_action)
        self.move_view = MoveableListView(self.ui.toolbar_layout_list,
                                          self.ui.up_button,
                                          self.ui.down_button,
                                          self.update_action_buttons)
        self.update_buttons = self.move_view.update_buttons

    def load(self):
        self.ui.toolbar_show_labels.setChecked(
            config.setting["toolbar_show_labels"])
        self.ui.toolbar_multiselect.setChecked(
            config.setting["toolbar_multiselect"])
        self.ui.builtin_search.setChecked(config.setting["builtin_search"])
        self.ui.use_adv_search_syntax.setChecked(
            config.setting["use_adv_search_syntax"])
        self.ui.quit_confirmation.setChecked(
            config.setting["quit_confirmation"])
        current_ui_language = config.setting["ui_language"]
        self.ui.ui_language.setCurrentIndex(
            self.ui.ui_language.findData(current_ui_language))
        self.ui.starting_directory.setChecked(
            config.setting["starting_directory"])
        self.ui.starting_directory_path.setText(
            config.setting["starting_directory_path"])
        self.populate_action_list()
        self.ui.toolbar_layout_list.setCurrentRow(0)
        self.update_buttons()

    def save(self):
        config.setting[
            "toolbar_show_labels"] = self.ui.toolbar_show_labels.isChecked()
        config.setting[
            "toolbar_multiselect"] = self.ui.toolbar_multiselect.isChecked()
        config.setting["builtin_search"] = self.ui.builtin_search.isChecked()
        config.setting[
            "use_adv_search_syntax"] = self.ui.use_adv_search_syntax.isChecked(
            )
        config.setting[
            "quit_confirmation"] = self.ui.quit_confirmation.isChecked()
        self.tagger.window.update_toolbar_style()
        new_language = self.ui.ui_language.itemData(
            self.ui.ui_language.currentIndex())
        if new_language != config.setting["ui_language"]:
            config.setting["ui_language"] = self.ui.ui_language.itemData(
                self.ui.ui_language.currentIndex())
            dialog = QtWidgets.QMessageBox(
                QtWidgets.QMessageBox.Information, _('Language changed'),
                _('You have changed the interface language. You have to restart Picard in order for the change to take effect.'
                  ), QtWidgets.QMessageBox.Ok, self)
            dialog.exec_()
        config.setting[
            "starting_directory"] = self.ui.starting_directory.isChecked()
        config.setting["starting_directory_path"] = os.path.normpath(
            self.ui.starting_directory_path.text())
        self.update_layout_config()

    def restore_defaults(self):
        super().restore_defaults()
        self.update_buttons()

    def starting_directory_browse(self):
        item = self.ui.starting_directory_path
        path = QtWidgets.QFileDialog.getExistingDirectory(
            self, "", item.text())
        if path:
            path = os.path.normpath(path)
            item.setText(path)

    def _get_icon_from_name(self, name):
        return self.TOOLBAR_BUTTONS[name]['icon']

    def _insert_item(self, action, index=None):
        list_item = ToolbarListItem(action)
        list_item.setToolTip(_('Drag and Drop to re-order'))
        if action in self.TOOLBAR_BUTTONS:
            list_item.setText(_(self.TOOLBAR_BUTTONS[action]['label']))
            list_item.setIcon(
                icontheme.lookup(self._get_icon_from_name(action),
                                 icontheme.ICON_SIZE_MENU))
        else:
            list_item.setText(self.SEPARATOR)
        if index is not None:
            self.ui.toolbar_layout_list.insertItem(index, list_item)
        else:
            self.ui.toolbar_layout_list.addItem(list_item)
        return list_item

    def _all_list_items(self):
        return [
            self.ui.toolbar_layout_list.item(i).action_name
            for i in range(self.ui.toolbar_layout_list.count())
        ]

    def _added_actions(self):
        actions = self._all_list_items()
        return set(action for action in actions if action != 'separator')

    def populate_action_list(self):
        self.ui.toolbar_layout_list.clear()
        for name in config.setting['toolbar_layout']:
            if name in self.ACTION_NAMES or name == 'separator':
                self._insert_item(name)

    def update_action_buttons(self):
        self.ui.add_button.setEnabled(
            self._added_actions() != self.ACTION_NAMES)

    def add_to_toolbar(self):
        display_list = set.difference(self.ACTION_NAMES, self._added_actions())
        selected_action, ok = AddActionDialog.get_selected_action(
            display_list, self)
        if ok:
            list_item = self._insert_item(
                selected_action,
                self.ui.toolbar_layout_list.currentRow() + 1)
            self.ui.toolbar_layout_list.setCurrentItem(list_item)
        self.update_buttons()

    def insert_separator(self):
        insert_index = self.ui.toolbar_layout_list.currentRow() + 1
        self._insert_item('separator', insert_index)

    def remove_action(self):
        item = self.ui.toolbar_layout_list.takeItem(
            self.ui.toolbar_layout_list.currentRow())
        del item
        self.update_buttons()

    def update_layout_config(self):
        config.setting['toolbar_layout'] = self._all_list_items()
        self._update_toolbar()

    def _update_toolbar(self):
        widget = self.parent()
        while not isinstance(widget, QtWidgets.QMainWindow):
            widget = widget.parent()
        # Call the main window's create toolbar method
        widget.create_action_toolbar()
        widget.set_tab_order()
Example #4
0
class CoverOptionsPage(OptionsPage):

    NAME = "cover"
    TITLE = N_("Cover Art")
    PARENT = None
    SORT_ORDER = 35
    ACTIVE = True

    options = [
        config.BoolOption("setting", "save_images_to_tags", True),
        config.BoolOption("setting", "save_only_front_images_to_tags", True),
        config.BoolOption("setting", "save_images_to_files", False),
        config.TextOption("setting", "cover_image_filename", "cover"),
        config.BoolOption("setting", "save_images_overwrite", False),
        config.BoolOption("setting", "ca_provider_use_amazon", True),
        config.BoolOption("setting", "ca_provider_use_caa", True),
        config.BoolOption("setting",
                          "ca_provider_use_caa_release_group_fallback", False),
        config.BoolOption("setting", "ca_provider_use_whitelist", True),
        config.BoolOption("setting", "caa_approved_only", True),
        config.BoolOption("setting", "caa_image_type_as_filename", False),
        config.IntOption("setting", "caa_image_size", 1),
        config.ListOption("setting", "caa_image_types", [u"front"]),
        config.BoolOption("setting", "caa_restrict_image_types", True),
    ]

    def __init__(self, parent=None):
        super(CoverOptionsPage, self).__init__(parent)
        self.ui = Ui_CoverOptionsPage()
        self.ui.setupUi(self)
        self.ui.save_images_to_files.clicked.connect(self.update_filename)
        self.ui.restrict_images_types.clicked.connect(self.update_caa_types)

    def load(self):
        self.ui.save_images_to_tags.setChecked(config.setting["save_images_to_tags"])
        self.ui.cb_embed_front_only.setChecked(config.setting["save_only_front_images_to_tags"])
        self.ui.save_images_to_files.setChecked(config.setting["save_images_to_files"])
        self.ui.cover_image_filename.setText(config.setting["cover_image_filename"])
        self.ui.save_images_overwrite.setChecked(config.setting["save_images_overwrite"])
        self.update_filename()
        self.ui.caprovider_amazon.setChecked(config.setting["ca_provider_use_amazon"])
        self.ui.caprovider_caa.setChecked(config.setting["ca_provider_use_caa"])
        self.ui.caprovider_caa_release_group.setChecked(
            config.setting["ca_provider_use_caa_release_group_fallback"])
        self.ui.caprovider_whitelist.setChecked(config.setting["ca_provider_use_whitelist"])
        self.ui.gb_caa.setEnabled(config.setting["ca_provider_use_caa"])

        self.ui.cb_image_size.setCurrentIndex(config.setting["caa_image_size"])
        self.ui.cb_approved_only.setChecked(config.setting["caa_approved_only"])
        self.ui.cb_type_as_filename.setChecked(config.setting["caa_image_type_as_filename"])
        self.connect(self.ui.caprovider_caa, QtCore.SIGNAL("toggled(bool)"),
                     self.ui.gb_caa.setEnabled)
        self.ui.select_caa_types.clicked.connect(self.select_caa_types)
        self.ui.restrict_images_types.setChecked(
            config.setting["caa_restrict_image_types"])
        self.update_caa_types()
        self.update_filename()

    def save(self):
        config.setting["save_images_to_tags"] = self.ui.save_images_to_tags.isChecked()
        config.setting["save_only_front_images_to_tags"] = self.ui.cb_embed_front_only.isChecked()
        config.setting["save_images_to_files"] = self.ui.save_images_to_files.isChecked()
        config.setting["cover_image_filename"] = unicode(self.ui.cover_image_filename.text())
        config.setting["ca_provider_use_amazon"] =\
            self.ui.caprovider_amazon.isChecked()
        config.setting["ca_provider_use_caa"] =\
            self.ui.caprovider_caa.isChecked()
        config.setting["ca_provider_use_caa_release_group_fallback"] =\
            self.ui.caprovider_caa_release_group.isChecked()
        config.setting["ca_provider_use_whitelist"] =\
            self.ui.caprovider_whitelist.isChecked()
        config.setting["caa_image_size"] =\
            self.ui.cb_image_size.currentIndex()
        config.setting["caa_approved_only"] =\
            self.ui.cb_approved_only.isChecked()
        config.setting["caa_image_type_as_filename"] = \
            self.ui.cb_type_as_filename.isChecked()

        config.setting["save_images_overwrite"] = self.ui.save_images_overwrite.isChecked()
        config.setting["caa_restrict_image_types"] = \
            self.ui.restrict_images_types.isChecked()

    def update_filename(self):
        enabled = self.ui.save_images_to_files.isChecked()
        self.ui.cover_image_filename.setEnabled(enabled)
        self.ui.save_images_overwrite.setEnabled(enabled)

    def update_caa_types(self):
        enabled = self.ui.restrict_images_types.isChecked()
        self.ui.select_caa_types.setEnabled(enabled)

    def select_caa_types(self):
        (types, ok) = CAATypesSelectorDialog.run(
            self, config.setting["caa_image_types"])
        if ok:
            config.setting["caa_image_types"] = types
Example #5
0
class RenamingOptionsPage(OptionsPage):

    NAME = "filerenaming"
    TITLE = N_("File Naming")
    PARENT = None
    SORT_ORDER = 40
    ACTIVE = True

    options = [
        config.BoolOption("setting", "windows_compatibility", True),
        config.BoolOption("setting", "ascii_filenames", False),
        config.BoolOption("setting", "rename_files", False),
        config.TextOption(
            "setting",
            "file_naming_format",
            _DEFAULT_FILE_NAMING_FORMAT,
        ),
        config.BoolOption("setting", "move_files", False),
        config.TextOption("setting", "move_files_to", _default_music_dir),
        config.BoolOption("setting", "move_additional_files", False),
        config.TextOption("setting", "move_additional_files_pattern",
                          "*.jpg *.png"),
        config.BoolOption("setting", "delete_empty_dirs", True),
    ]

    def __init__(self, parent=None):
        super().__init__(parent)
        self.ui = Ui_RenamingOptionsPage()
        self.ui.setupUi(self)

        self.ui.ascii_filenames.clicked.connect(self.update_examples)
        self.ui.windows_compatibility.clicked.connect(self.update_examples)
        self.ui.rename_files.clicked.connect(self.update_examples)
        self.ui.move_files.clicked.connect(self.update_examples)
        self.ui.move_files_to.editingFinished.connect(self.update_examples)

        self.ui.move_files.toggled.connect(
            partial(enabledSlot, self.toggle_file_moving))
        self.ui.rename_files.toggled.connect(
            partial(enabledSlot, self.toggle_file_renaming))
        self.ui.file_naming_format.textChanged.connect(self.check_formats)
        self.ui.file_naming_format_default.clicked.connect(
            self.set_file_naming_format_default)
        self.highlighter = TaggerScriptSyntaxHighlighter(
            self.ui.file_naming_format.document())
        self.ui.move_files_to_browse.clicked.connect(self.move_files_to_browse)

        textEdit = self.ui.file_naming_format
        self.textEditPaletteNormal = textEdit.palette()
        self.textEditPaletteReadOnly = QPalette(self.textEditPaletteNormal)
        disabled_color = self.textEditPaletteNormal.color(
            QPalette.Inactive, QPalette.Window)
        self.textEditPaletteReadOnly.setColor(QPalette.Disabled, QPalette.Base,
                                              disabled_color)

    def toggle_file_moving(self, state):
        self.ui.delete_empty_dirs.setEnabled(state)
        self.ui.move_files_to.setEnabled(state)
        self.ui.move_files_to_browse.setEnabled(state)
        self.ui.move_additional_files.setEnabled(state)
        self.ui.move_additional_files_pattern.setEnabled(state)

    def toggle_file_renaming(self, state):

        self.ui.file_naming_format.setEnabled(state)
        self.ui.file_naming_format_default.setEnabled(state)
        self.ui.ascii_filenames.setEnabled(state)
        self.ui.file_naming_format_group.setEnabled(state)
        if not IS_WIN:
            self.ui.windows_compatibility.setEnabled(state)

        if self.ui.file_naming_format.isEnabled():
            self.ui.file_naming_format.setPalette(self.textEditPaletteNormal)
        else:
            self.ui.file_naming_format.setPalette(self.textEditPaletteReadOnly)

    def check_formats(self):
        self.test()
        self.update_examples()

    def _example_to_filename(self, file):
        settings = SettingsOverride(
            config.setting, {
                'ascii_filenames': self.ui.ascii_filenames.isChecked(),
                'file_naming_format': self.ui.file_naming_format.toPlainText(),
                'move_files': self.ui.move_files.isChecked(),
                'move_files_to': os.path.normpath(
                    self.ui.move_files_to.text()),
                'rename_files': self.ui.rename_files.isChecked(),
                'windows_compatibility':
                self.ui.windows_compatibility.isChecked(),
            })

        try:
            if config.setting["enable_tagger_scripts"]:
                for s_pos, s_name, s_enabled, s_text in config.setting[
                        "list_of_scripts"]:
                    if s_enabled and s_text:
                        parser = ScriptParser()
                        parser.eval(s_text, file.metadata)
            filename = file._make_filename(file.filename, file.metadata,
                                           settings)
            if not settings["move_files"]:
                return os.path.basename(filename)
            return filename
        except ScriptError:
            return ""
        except TypeError:
            return ""

    def update_examples(self):
        # TODO: Here should be more examples etc.
        # TODO: Would be nice to show diffs too....
        example1 = self._example_to_filename(self.example_1())
        example2 = self._example_to_filename(self.example_2())
        self.ui.example_filename.setText(example1)
        self.ui.example_filename_va.setText(example2)

    def load(self):
        if IS_WIN:
            self.ui.windows_compatibility.setChecked(True)
            self.ui.windows_compatibility.setEnabled(False)
        else:
            self.ui.windows_compatibility.setChecked(
                config.setting["windows_compatibility"])
        self.ui.rename_files.setChecked(config.setting["rename_files"])
        self.ui.move_files.setChecked(config.setting["move_files"])
        self.ui.ascii_filenames.setChecked(config.setting["ascii_filenames"])
        self.ui.file_naming_format.setPlainText(
            config.setting["file_naming_format"])
        args = {
            "picard-doc-scripting-url": PICARD_URLS['doc_scripting'],
        }
        text = _('<a href="%(picard-doc-scripting-url)s">Open Scripting'
                 ' Documentation in your browser</a>') % args
        self.ui.file_naming_format_documentation.setText(text)
        self.ui.move_files_to.setText(config.setting["move_files_to"])
        self.ui.move_files_to.setCursorPosition(0)
        self.ui.move_additional_files.setChecked(
            config.setting["move_additional_files"])
        self.ui.move_additional_files_pattern.setText(
            config.setting["move_additional_files_pattern"])
        self.ui.delete_empty_dirs.setChecked(
            config.setting["delete_empty_dirs"])
        self.update_examples()

    def check(self):
        self.check_format()
        if self.ui.move_files.isChecked(
        ) and not self.ui.move_files_to.text().strip():
            raise OptionsCheckError(
                _("Error"),
                _("The location to move files to must not be empty."))

    def check_format(self):
        parser = ScriptParser()
        try:
            parser.eval(self.ui.file_naming_format.toPlainText())
        except Exception as e:
            raise OptionsCheckError("", str(e))
        if self.ui.rename_files.isChecked():
            if not self.ui.file_naming_format.toPlainText().strip():
                raise OptionsCheckError(
                    "", _("The file naming format must not be empty."))

    def save(self):
        config.setting[
            "windows_compatibility"] = self.ui.windows_compatibility.isChecked(
            )
        config.setting["ascii_filenames"] = self.ui.ascii_filenames.isChecked()
        config.setting["rename_files"] = self.ui.rename_files.isChecked()
        config.setting[
            "file_naming_format"] = self.ui.file_naming_format.toPlainText()
        self.tagger.window.enable_renaming_action.setChecked(
            config.setting["rename_files"])
        config.setting["move_files"] = self.ui.move_files.isChecked()
        config.setting["move_files_to"] = os.path.normpath(
            self.ui.move_files_to.text())
        config.setting[
            "move_additional_files"] = self.ui.move_additional_files.isChecked(
            )
        config.setting[
            "move_additional_files_pattern"] = self.ui.move_additional_files_pattern.text(
            )
        config.setting[
            "delete_empty_dirs"] = self.ui.delete_empty_dirs.isChecked()
        self.tagger.window.enable_moving_action.setChecked(
            config.setting["move_files"])

    def display_error(self, error):
        pass

    def set_file_naming_format_default(self):
        self.ui.file_naming_format.setText(self.options[3].default)


#        self.ui.file_naming_format.setCursorPosition(0)

    def example_1(self):
        file = File("ticket_to_ride.mp3")
        file.state = File.NORMAL
        file.metadata['album'] = 'Help!'
        file.metadata['title'] = 'Ticket to Ride'
        file.metadata['artist'] = 'The Beatles'
        file.metadata['artistsort'] = 'Beatles, The'
        file.metadata['albumartist'] = 'The Beatles'
        file.metadata['albumartistsort'] = 'Beatles, The'
        file.metadata['tracknumber'] = '7'
        file.metadata['totaltracks'] = '14'
        file.metadata['discnumber'] = '1'
        file.metadata['totaldiscs'] = '1'
        file.metadata['date'] = '1965-08-06'
        file.metadata['releasetype'] = ['album', 'soundtrack']
        file.metadata['~primaryreleasetype'] = ['album']
        file.metadata['~secondaryreleasetype'] = ['soundtrack']
        file.metadata['releasestatus'] = 'official'
        file.metadata['releasecountry'] = 'US'
        file.metadata['~extension'] = 'mp3'
        file.metadata[
            'musicbrainz_albumid'] = '2c053984-4645-4699-9474-d2c35c227028'
        file.metadata[
            'musicbrainz_albumartistid'] = 'b10bbbfc-cf9e-42e0-be17-e2c3e1d2600d'
        file.metadata[
            'musicbrainz_artistid'] = 'b10bbbfc-cf9e-42e0-be17-e2c3e1d2600d'
        file.metadata[
            'musicbrainz_recordingid'] = 'ed052ae1-c950-47f2-8d2b-46e1b58ab76c'
        file.metadata[
            'musicbrainz_releasetrackid'] = '7668a62a-2fac-3151-a744-5707ac8c883c'
        return file

    def example_2(self):
        file = File("track05.mp3")
        file.state = File.NORMAL
        file.metadata['album'] = "Coup d'État, Volume 1: Ku De Ta / Prologue"
        file.metadata['title'] = "I've Got to Learn the Mambo"
        file.metadata['artist'] = "Snowboy feat. James Hunter"
        file.metadata['artistsort'] = "Snowboy feat. Hunter, James"
        file.metadata['albumartist'] = config.setting['va_name']
        file.metadata['albumartistsort'] = config.setting['va_name']
        file.metadata['tracknumber'] = '5'
        file.metadata['totaltracks'] = '13'
        file.metadata['discnumber'] = '2'
        file.metadata['totaldiscs'] = '2'
        file.metadata['discsubtitle'] = "Beat Up"
        file.metadata['date'] = '2005-07-04'
        file.metadata['releasetype'] = ['album', 'compilation']
        file.metadata['~primaryreleasetype'] = 'album'
        file.metadata['~secondaryreleasetype'] = 'compilation'
        file.metadata['releasestatus'] = 'official'
        file.metadata['releasecountry'] = 'AU'
        file.metadata['compilation'] = '1'
        file.metadata['~multiartist'] = '1'
        file.metadata['~extension'] = 'mp3'
        file.metadata[
            'musicbrainz_albumid'] = '4b50c71e-0a07-46ac-82e4-cb85dc0c9bdd'
        file.metadata[
            'musicbrainz_recordingid'] = 'b3c487cb-0e55-477d-8df3-01ec6590f099'
        file.metadata[
            'musicbrainz_releasetrackid'] = 'f8649a05-da39-39ba-957c-7abf8f9012be'
        file.metadata[
            'musicbrainz_albumartistid'] = '89ad4ac3-39f7-470e-963a-56509c546377'
        file.metadata['musicbrainz_artistid'] = [
            '7b593455-d207-482c-8c6f-19ce22c94679',
            '9e082466-2390-40d1-891e-4803531f43fd'
        ]
        return file

    def move_files_to_browse(self):
        path = QtWidgets.QFileDialog.getExistingDirectory(
            self, "", self.ui.move_files_to.text())
        if path:
            path = os.path.normpath(path)
            self.ui.move_files_to.setText(path)

    def test(self):
        self.ui.renaming_error.setStyleSheet("")
        self.ui.renaming_error.setText("")
        try:
            self.check_format()
        except OptionsCheckError as e:
            self.ui.renaming_error.setStyleSheet(self.STYLESHEET_ERROR)
            self.ui.renaming_error.setText(e.info)
            return
Example #6
0
class GeneralOptionsPage(OptionsPage):

    NAME = "general"
    TITLE = N_("General")
    PARENT = None
    SORT_ORDER = 1
    ACTIVE = True

    options = [
        config.TextOption("setting", "server_host", MUSICBRAINZ_SERVERS[0]),
        config.IntOption("setting", "server_port", 443),
        config.TextOption("persist", "oauth_refresh_token", ""),
        config.BoolOption("setting", "analyze_new_files", False),
        config.BoolOption("setting", "ignore_file_mbids", False),
        config.TextOption("persist", "oauth_refresh_token", ""),
        config.TextOption("persist", "oauth_refresh_token_scopes", ""),
        config.TextOption("persist", "oauth_access_token", ""),
        config.IntOption("persist", "oauth_access_token_expires", 0),
        config.TextOption("persist", "oauth_username", ""),
        config.BoolOption("setting", "check_for_updates", True),
        config.IntOption("setting", "update_check_days", 7),
        config.IntOption("setting", "update_level", 0),
        config.IntOption("persist", "last_update_check", 0),
    ]

    def __init__(self, parent=None):
        super().__init__(parent)
        self.ui = Ui_GeneralOptionsPage()
        self.ui.setupUi(self)
        self.ui.server_host.addItems(MUSICBRAINZ_SERVERS)
        self.ui.login.clicked.connect(self.login)
        self.ui.logout.clicked.connect(self.logout)
        self.update_login_logout()

    def load(self):
        self.ui.server_host.setEditText(config.setting["server_host"])
        self.ui.server_port.setValue(config.setting["server_port"])
        self.ui.analyze_new_files.setChecked(
            config.setting["analyze_new_files"])
        self.ui.ignore_file_mbids.setChecked(
            config.setting["ignore_file_mbids"])
        if self.tagger.autoupdate_enabled:
            self.ui.check_for_updates.setChecked(
                config.setting["check_for_updates"])
            self.ui.update_level.clear()
            for level, description in PROGRAM_UPDATE_LEVELS.items():
                self.ui.update_level.addItem(_(description['title']), level)
            self.ui.update_level.setCurrentIndex(
                self.ui.update_level.findData(config.setting["update_level"]))
            self.ui.update_check_days.setValue(
                config.setting["update_check_days"])
        else:
            self.ui.update_check_groupbox.hide()

    def save(self):
        config.setting["server_host"] = self.ui.server_host.currentText(
        ).strip()
        config.setting["server_port"] = self.ui.server_port.value()
        config.setting[
            "analyze_new_files"] = self.ui.analyze_new_files.isChecked()
        config.setting[
            "ignore_file_mbids"] = self.ui.ignore_file_mbids.isChecked()
        if self.tagger.autoupdate_enabled:
            config.setting[
                "check_for_updates"] = self.ui.check_for_updates.isChecked()
            config.setting["update_level"] = self.ui.update_level.currentData(
                QtCore.Qt.UserRole)
            config.setting[
                "update_check_days"] = self.ui.update_check_days.value()

    def update_login_logout(self):
        if self.tagger.webservice.oauth_manager.is_logged_in():
            self.ui.logged_in.setText(
                _("Logged in as <b>%s</b>.") %
                config.persist["oauth_username"])
            self.ui.logged_in.show()
            self.ui.login.hide()
            self.ui.logout.show()
        else:
            self.ui.logged_in.hide()
            self.ui.login.show()
            self.ui.logout.hide()

    def login(self):
        scopes = "profile tag rating collection submit_isrc submit_barcode"
        authorization_url = self.tagger.webservice.oauth_manager.get_authorization_url(
            scopes)
        webbrowser2.open(authorization_url)
        authorization_code, ok = QInputDialog.getText(self,
                                                      _("MusicBrainz Account"),
                                                      _("Authorization code:"))
        if ok:
            self.tagger.webservice.oauth_manager.exchange_authorization_code(
                authorization_code, scopes, self.on_authorization_finished)

    def restore_defaults(self):
        super().restore_defaults()
        self.logout()

    def on_authorization_finished(self, successful):
        if successful:
            self.tagger.webservice.oauth_manager.fetch_username(
                self.on_login_finished)

    def on_login_finished(self, successful):
        self.update_login_logout()
        if successful:
            load_user_collections()

    def logout(self):
        self.tagger.webservice.oauth_manager.revoke_tokens()
        self.update_login_logout()
        load_user_collections()
Example #7
0
class FormatPerformerTagsOptionsPage(OptionsPage):

    NAME = "format_performer_tags"
    TITLE = "Format Performer Tags"
    PARENT = "plugins"

    options = [
        config.IntOption("setting", "format_group_additional", 3),
        config.IntOption("setting", "format_group_guest", 4),
        config.IntOption("setting", "format_group_solo", 3),
        config.IntOption("setting", "format_group_vocals", 2),
        config.TextOption("setting", "format_group_1_start_char", ''),
        config.TextOption("setting", "format_group_1_end_char", ' '),
        config.TextOption("setting", "format_group_1_sep_char", ''),
        config.TextOption("setting", "format_group_2_start_char", ', '),
        config.TextOption("setting", "format_group_2_end_char", ''),
        config.TextOption("setting", "format_group_2_sep_char", ''),
        config.TextOption("setting", "format_group_3_start_char", ' ('),
        config.TextOption("setting", "format_group_3_end_char", ')'),
        config.TextOption("setting", "format_group_3_sep_char", ''),
        config.TextOption("setting", "format_group_4_start_char", ' ('),
        config.TextOption("setting", "format_group_4_end_char", ')'),
        config.TextOption("setting", "format_group_4_sep_char", ''),
    ]

    def __init__(self, parent=None):
        super(FormatPerformerTagsOptionsPage, self).__init__(parent)
        self.ui = Ui_FormatPerformerTagsOptionsPage()
        self.ui.setupUi(self)
        self.ui.additional_rb_1.clicked.connect(self.update_examples)
        self.ui.additional_rb_2.clicked.connect(self.update_examples)
        self.ui.additional_rb_3.clicked.connect(self.update_examples)
        self.ui.additional_rb_4.clicked.connect(self.update_examples)
        self.ui.guest_rb_1.clicked.connect(self.update_examples)
        self.ui.guest_rb_2.clicked.connect(self.update_examples)
        self.ui.guest_rb_3.clicked.connect(self.update_examples)
        self.ui.guest_rb_4.clicked.connect(self.update_examples)
        self.ui.solo_rb_1.clicked.connect(self.update_examples)
        self.ui.solo_rb_2.clicked.connect(self.update_examples)
        self.ui.solo_rb_3.clicked.connect(self.update_examples)
        self.ui.solo_rb_4.clicked.connect(self.update_examples)
        self.ui.vocals_rb_1.clicked.connect(self.update_examples)
        self.ui.vocals_rb_2.clicked.connect(self.update_examples)
        self.ui.vocals_rb_3.clicked.connect(self.update_examples)
        self.ui.vocals_rb_4.clicked.connect(self.update_examples)
        self.ui.format_group_1_start_char.editingFinished.connect(
            self.update_examples)
        self.ui.format_group_2_start_char.editingFinished.connect(
            self.update_examples)
        self.ui.format_group_3_start_char.editingFinished.connect(
            self.update_examples)
        self.ui.format_group_4_start_char.editingFinished.connect(
            self.update_examples)
        self.ui.format_group_1_sep_char.editingFinished.connect(
            self.update_examples)
        self.ui.format_group_2_sep_char.editingFinished.connect(
            self.update_examples)
        self.ui.format_group_3_sep_char.editingFinished.connect(
            self.update_examples)
        self.ui.format_group_4_sep_char.editingFinished.connect(
            self.update_examples)
        self.ui.format_group_1_end_char.editingFinished.connect(
            self.update_examples)
        self.ui.format_group_2_end_char.editingFinished.connect(
            self.update_examples)
        self.ui.format_group_3_end_char.editingFinished.connect(
            self.update_examples)
        self.ui.format_group_4_end_char.editingFinished.connect(
            self.update_examples)

    def load(self):
        # Enable external link
        self.ui.format_description.setOpenExternalLinks(True)

        # Settings for Keyword: additional
        temp = config.setting["format_group_additional"]
        if temp > 3:
            self.ui.additional_rb_4.setChecked(True)
        elif temp > 2:
            self.ui.additional_rb_3.setChecked(True)
        elif temp > 1:
            self.ui.additional_rb_2.setChecked(True)
        else:
            self.ui.additional_rb_1.setChecked(True)

        # Settings for Keyword: guest
        temp = config.setting["format_group_guest"]
        if temp > 3:
            self.ui.guest_rb_4.setChecked(True)
        elif temp > 2:
            self.ui.guest_rb_3.setChecked(True)
        elif temp > 1:
            self.ui.guest_rb_2.setChecked(True)
        else:
            self.ui.guest_rb_1.setChecked(True)

        # Settings for Keyword: solo
        temp = config.setting["format_group_solo"]
        if temp > 3:
            self.ui.solo_rb_4.setChecked(True)
        elif temp > 2:
            self.ui.solo_rb_3.setChecked(True)
        elif temp > 1:
            self.ui.solo_rb_2.setChecked(True)
        else:
            self.ui.solo_rb_1.setChecked(True)

        # Settings for all vocal keywords
        temp = config.setting["format_group_vocals"]
        if temp > 3:
            self.ui.vocals_rb_4.setChecked(True)
        elif temp > 2:
            self.ui.vocals_rb_3.setChecked(True)
        elif temp > 1:
            self.ui.vocals_rb_2.setChecked(True)
        else:
            self.ui.vocals_rb_1.setChecked(True)

        # Settings for word group 1
        self.ui.format_group_1_start_char.setText(
            config.setting["format_group_1_start_char"])
        self.ui.format_group_1_end_char.setText(
            config.setting["format_group_1_end_char"])
        self.ui.format_group_1_sep_char.setText(
            config.setting["format_group_1_sep_char"])

        # Settings for word group 2
        self.ui.format_group_2_start_char.setText(
            config.setting["format_group_2_start_char"])
        self.ui.format_group_2_end_char.setText(
            config.setting["format_group_2_end_char"])
        self.ui.format_group_2_sep_char.setText(
            config.setting["format_group_2_sep_char"])

        # Settings for word group 3
        self.ui.format_group_3_start_char.setText(
            config.setting["format_group_3_start_char"])
        self.ui.format_group_3_end_char.setText(
            config.setting["format_group_3_end_char"])
        self.ui.format_group_3_sep_char.setText(
            config.setting["format_group_3_sep_char"])

        # Settings for word group 4
        self.ui.format_group_4_start_char.setText(
            config.setting["format_group_4_start_char"])
        self.ui.format_group_4_end_char.setText(
            config.setting["format_group_4_end_char"])
        self.ui.format_group_4_sep_char.setText(
            config.setting["format_group_4_sep_char"])
        self.update_examples()

        # TODO: Modify self.format_description in ui_options_format_performer_tags.py to include a placeholder
        #       such as {user_guide_url} so that the translated string can be formatted to include the value
        #       of PLUGIN_USER_GUIDE_URL to dynamically set the link while not requiring retranslation if the
        #       link changes.  Preliminary code something like:
        #
        #       temp = (self.ui.format_description.text).format(user_guide_url=PLUGIN_USER_GUIDE_URL,)
        #       self.ui.format_description.setText(temp)

    def save(self):
        self._set_settings(config.setting)

    def restore_defaults(self):
        super().restore_defaults()
        self.update_examples()

    def _set_settings(self, settings):

        # Process 'additional' keyword settings
        temp = 1
        if self.ui.additional_rb_2.isChecked(): temp = 2
        if self.ui.additional_rb_3.isChecked(): temp = 3
        if self.ui.additional_rb_4.isChecked(): temp = 4
        settings["format_group_additional"] = temp

        # Process 'guest' keyword settings
        temp = 1
        if self.ui.guest_rb_2.isChecked(): temp = 2
        if self.ui.guest_rb_3.isChecked(): temp = 3
        if self.ui.guest_rb_4.isChecked(): temp = 4
        settings["format_group_guest"] = temp

        # Process 'solo' keyword settings
        temp = 1
        if self.ui.solo_rb_2.isChecked(): temp = 2
        if self.ui.solo_rb_3.isChecked(): temp = 3
        if self.ui.solo_rb_4.isChecked(): temp = 4
        settings["format_group_solo"] = temp

        # Process all vocal keyword settings
        temp = 1
        if self.ui.vocals_rb_2.isChecked(): temp = 2
        if self.ui.vocals_rb_3.isChecked(): temp = 3
        if self.ui.vocals_rb_4.isChecked(): temp = 4
        settings["format_group_vocals"] = temp

        # Settings for word group 1
        settings[
            "format_group_1_start_char"] = self.ui.format_group_1_start_char.text(
            )
        settings[
            "format_group_1_end_char"] = self.ui.format_group_1_end_char.text(
            )
        settings[
            "format_group_1_sep_char"] = self.ui.format_group_1_sep_char.text(
            )

        # Settings for word group 2
        settings[
            "format_group_2_start_char"] = self.ui.format_group_2_start_char.text(
            )
        settings[
            "format_group_2_end_char"] = self.ui.format_group_2_end_char.text(
            )
        settings[
            "format_group_2_sep_char"] = self.ui.format_group_2_sep_char.text(
            )

        # Settings for word group 3
        settings[
            "format_group_3_start_char"] = self.ui.format_group_3_start_char.text(
            )
        settings[
            "format_group_3_end_char"] = self.ui.format_group_3_end_char.text(
            )
        settings[
            "format_group_3_sep_char"] = self.ui.format_group_3_sep_char.text(
            )

        # Settings for word group 4
        settings[
            "format_group_4_start_char"] = self.ui.format_group_4_start_char.text(
            )
        settings[
            "format_group_4_end_char"] = self.ui.format_group_4_end_char.text(
            )
        settings[
            "format_group_4_sep_char"] = self.ui.format_group_4_sep_char.text(
            )

    def update_examples(self):
        settings = {}
        self._set_settings(settings)
        word_dict = get_word_dict(settings)

        instruments_credits = {
            "guitar": ["Johnny Flux", "John Watson"],
            "guest guitar": ["Jimmy Page"],
            "additional guest solo guitar": ["Jimmy Page"],
        }
        instruments_example = self.build_example(instruments_credits,
                                                 word_dict, settings)
        self.ui.example_instruments.setText(instruments_example)

        vocals_credits = {
            "additional solo lead vocals": ["Robert Plant"],
            "additional solo guest lead vocals": ["Sandy Denny"],
        }
        vocals_example = self.build_example(vocals_credits, word_dict,
                                            settings)
        self.ui.example_vocals.setText(vocals_example)

    @staticmethod
    def build_example(credits, word_dict, settings):
        prefix = "performer:"
        metadata = Metadata()
        for key, values in credits.items():
            rewrite_tag(prefix + key, values, metadata, word_dict, settings)

        examples = []
        for key, values in metadata.rawitems():
            credit = "%s: %s" % (key, ", ".join(values))
            if credit.startswith(prefix):
                credit = credit[len(prefix):]
            examples.append(credit)
        return "\n".join(examples)
Example #8
0
class MetadataBox(QtGui.QTableWidget):

    options = (config.TextOption("persist", "metadata_box_sizes",
                                 "150 300 300"),
               config.BoolOption("persist", "show_changes_first", False))

    def __init__(self, parent):
        QtGui.QTableWidget.__init__(self, parent)
        self.parent = parent
        self.setAccessibleName(_("metadata view"))
        self.setAccessibleDescription(
            _("Displays original and new tags for the selected files"))
        self.setColumnCount(3)
        self.setHorizontalHeaderLabels(
            (_("Tag"), _("Original Value"), _("New Value")))
        self.horizontalHeader().setStretchLastSection(True)
        self.horizontalHeader().setClickable(False)
        self.verticalHeader().setDefaultSectionSize(21)
        self.verticalHeader().setVisible(False)
        self.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection)
        self.setTabKeyNavigation(False)
        self.setStyleSheet("QTableWidget {border: none;}")
        self.setAttribute(QtCore.Qt.WA_MacShowFocusRect, 1)
        self.colors = {
            TagStatus.NoChange: self.palette().color(QtGui.QPalette.Text),
            TagStatus.Removed: QtGui.QBrush(QtGui.QColor("red")),
            TagStatus.Added: QtGui.QBrush(QtGui.QColor("green")),
            TagStatus.Changed: QtGui.QBrush(QtGui.QColor("darkgoldenrod"))
        }
        self.files = set()
        self.tracks = set()
        self.objects = set()
        self.selection_mutex = QtCore.QMutex()
        self.selection_dirty = False
        self.editing = None  # the QTableWidgetItem being edited
        self.clipboard = [""]
        self.add_tag_action = QtGui.QAction(_(u"Add New Tag..."), parent)
        self.add_tag_action.triggered.connect(partial(self.edit_tag, ""))
        self.changes_first_action = QtGui.QAction(_(u"Show Changes First"),
                                                  parent)
        self.changes_first_action.setCheckable(True)
        self.changes_first_action.setChecked(
            config.persist["show_changes_first"])
        self.changes_first_action.toggled.connect(self.toggle_changes_first)

    def edit(self, index, trigger, event):
        if index.column() != 2:
            return False
        item = self.itemFromIndex(index)
        if item.flags() & QtCore.Qt.ItemIsEditable and \
           trigger in (QtGui.QAbstractItemView.DoubleClicked,
                       QtGui.QAbstractItemView.EditKeyPressed,
                       QtGui.QAbstractItemView.AnyKeyPressed):
            tag = self.tag_diff.tag_names[item.row()]
            values = self.tag_diff.new[tag]
            if len(values) > 1:
                self.edit_tag(tag)
                return False
            else:
                self.editing = item
                item.setText(values[0])
                return QtGui.QTableWidget.edit(self, index, trigger, event)
        return False

    def event(self, e):
        item = self.currentItem()
        if (item and e.type() == QtCore.QEvent.KeyPress
                and e.modifiers() == QtCore.Qt.ControlModifier):
            column = item.column()
            tag = self.tag_diff.tag_names[item.row()]
            if e.key() == QtCore.Qt.Key_C:
                if column == 1:
                    self.clipboard = list(self.tag_diff.orig[tag])
                elif column == 2:
                    self.clipboard = list(self.tag_diff.new[tag])
            elif e.key(
            ) == QtCore.Qt.Key_V and column == 2 and tag != "~length":
                self.set_tag_values(tag, list(self.clipboard))
        return QtGui.QTableWidget.event(self, e)

    def closeEditor(self, editor, hint):
        QtGui.QTableWidget.closeEditor(self, editor, hint)
        tag = self.tag_diff.tag_names[self.editing.row()]
        old = self.tag_diff.new[tag]
        new = [unicode(editor.text())]
        if old == new:
            self.editing.setText(old[0])
        else:
            self.set_tag_values(tag, new)
        self.editing = None
        self.update()

    def contextMenuEvent(self, event):
        menu = QtGui.QMenu(self)
        if self.objects:
            tags = self.selected_tags()
            if len(tags) == 1:
                edit_tag_action = QtGui.QAction(_(u"Edit..."), self.parent)
                edit_tag_action.triggered.connect(
                    partial(self.edit_tag,
                            list(tags)[0]))
                menu.addAction(edit_tag_action)
            removals = []
            useorigs = []
            for tag in tags:
                if self.tag_is_removable(tag):
                    removals.append(partial(self.remove_tag, tag))
                status = self.tag_diff.status[tag] & TagStatus.Changed
                if status == TagStatus.Changed or status == TagStatus.Removed:
                    for file in self.files:
                        objects = [file]
                        if file.parent in self.tracks and len(
                                self.files
                                & set(file.parent.linked_files)) == 1:
                            objects.append(file.parent)
                        orig_values = list(
                            file.orig_metadata.getall(tag)) or [""]
                        useorigs.append(
                            partial(self.set_tag_values, tag, orig_values,
                                    objects))
            if removals:
                remove_tag_action = QtGui.QAction(_(u"Remove"), self.parent)
                remove_tag_action.triggered.connect(
                    lambda: [f() for f in removals])
                menu.addAction(remove_tag_action)
            if useorigs:
                name = ungettext("Use Original Value", "Use Original Values",
                                 len(useorigs))
                use_orig_value_action = QtGui.QAction(name, self.parent)
                use_orig_value_action.triggered.connect(
                    lambda: [f() for f in useorigs])
                menu.addAction(use_orig_value_action)
                menu.addSeparator()
            if len(tags) == 1 or removals or useorigs:
                menu.addSeparator()
            menu.addAction(self.add_tag_action)
            menu.addSeparator()
        menu.addAction(self.changes_first_action)
        menu.exec_(event.globalPos())
        event.accept()

    def edit_tag(self, tag):
        EditTagDialog(self.parent, tag).exec_()

    def toggle_changes_first(self, checked):
        config.persist["show_changes_first"] = checked
        self.update()

    def set_tag_values(self, tag, values, objects=None):
        if objects is None:
            objects = self.objects
        self.parent.ignore_selection_changes = True
        if values != [""] or self.tag_is_removable(tag):
            for obj in objects:
                obj.metadata[tag] = values
                obj.update()
        self.update()
        self.parent.ignore_selection_changes = False

    def remove_tag(self, tag):
        self.set_tag_values(tag, [""])

    def remove_selected_tags(self):
        (self.remove_tag(tag) for tag in self.selected_tags()
         if self.tag_is_removable(tag))

    def tag_is_removable(self, tag):
        return self.tag_diff.status[tag] & TagStatus.NotRemovable == 0

    def selected_tags(self):
        tags = set(self.tag_diff.tag_names[item.row()]
                   for item in self.selectedItems())
        tags.discard("~length")
        return tags

    def _update_selection(self):
        files = set()
        tracks = set()
        objects = set()
        for obj in self.parent.selected_objects:
            if isinstance(obj, File):
                files.add(obj)
            elif isinstance(obj, Track):
                tracks.add(obj)
                files.update(obj.linked_files)
            elif isinstance(obj, Cluster) and obj.can_edit_tags():
                objects.add(obj)
                files.update(obj.files)
            elif isinstance(obj, Album):
                objects.add(obj)
                tracks.update(obj.tracks)
                for track in obj.tracks:
                    files.update(track.linked_files)
        objects.update(files)
        objects.update(tracks)
        self.selection_dirty = False

        self.selection_mutex.lock()
        self.files = files
        self.tracks = tracks
        self.objects = objects
        self.selection_mutex.unlock()

    @throttle(100)
    def update(self):
        if self.editing:
            return
        if self.selection_dirty:
            self._update_selection()
        thread.run_task(self._update_tags, self._update_items)

    def _update_tags(self):
        self.selection_mutex.lock()
        files = self.files
        tracks = self.tracks
        self.selection_mutex.unlock()

        if not (files or tracks):
            return None

        tag_diff = TagDiff()
        orig_tags = tag_diff.orig
        new_tags = tag_diff.new
        # existing_tags are orig_tags that would not be overwritten by
        # any new_tags, assuming clear_existing_tags is disabled.
        existing_tags = set()
        tag_diff.objects = len(files)

        clear_existing_tags = config.setting["clear_existing_tags"]

        for file in files:
            new_metadata = file.metadata
            orig_metadata = file.orig_metadata
            tags = set(new_metadata.keys() + orig_metadata.keys())

            for name in filter(lambda x: not x.startswith("~"), tags):
                new_values = new_metadata.getall(name)
                orig_values = orig_metadata.getall(name)

                if not ((new_values and not name in existing_tags)
                        or clear_existing_tags):
                    new_values = list(orig_values or [""])
                    existing_tags.add(name)

                tag_diff.add(name, orig_values, new_values,
                             clear_existing_tags)

            tag_diff.add("~length", str(orig_metadata.length),
                         str(new_metadata.length), False)

        for track in tracks:
            if track.num_linked_files == 0:
                for name, values in dict.iteritems(track.metadata):
                    if not name.startswith("~"):
                        tag_diff.add(name, values, values, True)

                length = str(track.metadata.length)
                tag_diff.add("~length", length, length, False)

                tag_diff.objects += 1

        all_tags = set(orig_tags.keys() + new_tags.keys())
        tag_names = COMMON_TAGS + sorted(all_tags.difference(COMMON_TAGS))

        if config.persist["show_changes_first"]:
            tags_by_status = {}

            for tag in tag_names:
                tags_by_status.setdefault(tag_diff.tag_status(tag),
                                          []).append(tag)

            for status in (TagStatus.Changed, TagStatus.Added,
                           TagStatus.Removed, TagStatus.NoChange):
                tag_diff.tag_names += tags_by_status.pop(status, [])
        else:
            tag_diff.tag_names = [
                tag for tag in tag_names
                if tag_diff.status[tag] != TagStatus.Empty
            ]

        return tag_diff

    def _update_items(self, result=None, error=None):
        if self.editing:
            return

        if not (self.files or self.tracks):
            result = None

        self.tag_diff = result

        if result is None:
            self.setRowCount(0)
            return

        self.setRowCount(len(result.tag_names))

        orig_flags = QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled
        new_flags = orig_flags | QtCore.Qt.ItemIsEditable

        for i, name in enumerate(result.tag_names):
            length = name == "~length"
            tag_item = self.item(i, 0)
            orig_item = self.item(i, 1)
            new_item = self.item(i, 2)
            if not tag_item:
                tag_item = QtGui.QTableWidgetItem()
                tag_item.setFlags(orig_flags)
                font = tag_item.font()
                font.setBold(True)
                tag_item.setFont(font)
                self.setItem(i, 0, tag_item)
            if not orig_item:
                orig_item = QtGui.QTableWidgetItem()
                orig_item.setFlags(orig_flags)
                self.setItem(i, 1, orig_item)
            if not new_item:
                new_item = QtGui.QTableWidgetItem()
                self.setItem(i, 2, new_item)
            tag_item.setText(display_tag_name(name))
            self.set_item_value(orig_item, self.tag_diff.orig, name)
            new_item.setFlags(orig_flags if length else new_flags)
            self.set_item_value(new_item, self.tag_diff.new, name)

            color = self.colors.get(result.tag_status(name),
                                    self.colors[TagStatus.NoChange])
            orig_item.setForeground(color)
            new_item.setForeground(color)

    def set_item_value(self, item, tags, name):
        text, italic = tags.display_value(name)
        item.setText(text)
        font = item.font()
        font.setItalic(italic)
        item.setFont(font)

    def restore_state(self):
        sizes = config.persist["metadata_box_sizes"].split(" ")
        header = self.horizontalHeader()
        try:
            for i in range(header.count()):
                size = max(int(sizes[i]), header.sectionSizeHint(i))
                header.resizeSection(i, size)
        except IndexError:
            pass
        self.shrink_columns()

    def save_state(self):
        sizes = []
        header = self.horizontalHeader()
        for i in range(header.count()):
            sizes.append(str(header.sectionSize(i)))
        config.persist["metadata_box_sizes"] = " ".join(sizes)

    def shrink_columns(self):
        header = self.horizontalHeader()
        cols = [header.sectionSize(i) for i in range(3)]
        width = sum(cols)
        visible_width = self.contentsRect().width()
        scroll = self.verticalScrollBar()
        if scroll.isVisible():
            visible_width -= scroll.width()
        if width > visible_width:
            diff = float(width - visible_width)
            for i in range(3):
                sub = int(diff * cols[i] / width) + 1
                header.resizeSection(i, cols[i] - sub)
Example #9
0
class InterfaceOptionsPage(OptionsPage):

    NAME = "interface"
    TITLE = N_("User Interface")
    PARENT = "advanced"
    SORT_ORDER = 40
    ACTIVE = True

    options = [
        config.BoolOption("setting", "toolbar_show_labels", True),
        config.BoolOption("setting", "toolbar_multiselect", False),
        config.BoolOption("setting", "use_adv_search_syntax", False),
        config.BoolOption("setting", "quit_confirmation", True),
        config.TextOption("setting", "ui_language", u""),
        config.BoolOption("setting", "starting_directory", False),
        config.TextOption("setting", "starting_directory_path", ""),
    ]

    def __init__(self, parent=None):
        super(InterfaceOptionsPage, self).__init__(parent)
        self.ui = Ui_InterfaceOptionsPage()
        self.ui.setupUi(self)
        self.ui.ui_language.addItem(_('System default'), '')
        language_list = [(l[0], l[1], _(l[2])) for l in UI_LANGUAGES]
        for lang_code, native, translation in sorted(
                language_list, key=operator.itemgetter(2), cmp=locale.strcoll):
            if native and native != translation:
                name = u'%s (%s)' % (translation, native)
            else:
                name = translation
            self.ui.ui_language.addItem(name, lang_code)
        self.ui.starting_directory.stateChanged.connect(
            partial(enabledSlot, self.ui.starting_directory_path.setEnabled))
        self.ui.starting_directory.stateChanged.connect(
            partial(enabledSlot, self.ui.starting_directory_browse.setEnabled))
        self.ui.starting_directory_browse.clicked.connect(
            self.starting_directory_browse)

    def load(self):
        self.ui.toolbar_show_labels.setChecked(
            config.setting["toolbar_show_labels"])
        self.ui.toolbar_multiselect.setChecked(
            config.setting["toolbar_multiselect"])
        self.ui.use_adv_search_syntax.setChecked(
            config.setting["use_adv_search_syntax"])
        self.ui.quit_confirmation.setChecked(
            config.setting["quit_confirmation"])
        current_ui_language = config.setting["ui_language"]
        self.ui.ui_language.setCurrentIndex(
            self.ui.ui_language.findData(current_ui_language))
        self.ui.starting_directory.setChecked(
            config.setting["starting_directory"])
        self.ui.starting_directory_path.setText(
            config.setting["starting_directory_path"])

    def save(self):
        config.setting[
            "toolbar_show_labels"] = self.ui.toolbar_show_labels.isChecked()
        config.setting[
            "toolbar_multiselect"] = self.ui.toolbar_multiselect.isChecked()
        config.setting[
            "use_adv_search_syntax"] = self.ui.use_adv_search_syntax.isChecked(
            )
        config.setting[
            "quit_confirmation"] = self.ui.quit_confirmation.isChecked()
        self.tagger.window.update_toolbar_style()
        new_language = self.ui.ui_language.itemData(
            self.ui.ui_language.currentIndex())
        if new_language != config.setting["ui_language"]:
            config.setting["ui_language"] = self.ui.ui_language.itemData(
                self.ui.ui_language.currentIndex())
            dialog = QtGui.QMessageBox(
                QtGui.QMessageBox.Information, _('Language changed'),
                _('You have changed the interface language. You have to restart Picard in order for the change to take effect.'
                  ), QtGui.QMessageBox.Ok, self)
            dialog.exec_()
        config.setting[
            "starting_directory"] = self.ui.starting_directory.isChecked()
        config.setting["starting_directory_path"] = os.path.normpath(
            unicode(self.ui.starting_directory_path.text()))

    def starting_directory_browse(self):
        item = self.ui.starting_directory_path
        path = QtGui.QFileDialog.getExistingDirectory(self, "", item.text())
        if path:
            path = os.path.normpath(unicode(path))
            item.setText(path)
Example #10
0
class GenresOptionsPage(OptionsPage):

    NAME = "genres"
    TITLE = N_("Genres")
    PARENT = "metadata"
    SORT_ORDER = 20
    ACTIVE = True

    options = [
        config.BoolOption("setting", "use_genres", False),
        config.IntOption("setting", "max_genres", 5),
        config.IntOption("setting", "min_genre_usage", 90),
        config.TextOption("setting", "genres_filter",
                          "-seen live\n-favorites\n-fixme\n-owned"),
        config.TextOption("setting", "join_genres", ""),
        config.BoolOption("setting", "only_my_genres", False),
        config.BoolOption("setting", "artists_genres", False),
        config.BoolOption("setting", "folksonomy_tags", False),
    ]

    def __init__(self, parent=None):
        super().__init__(parent)
        self.ui = Ui_GenresOptionsPage()
        self.ui.setupUi(self)

        self.ui.genres_filter.setToolTip(_(TOOLTIP_GENRES_FILTER))
        self.ui.genres_filter.textChanged.connect(
            self.update_test_genres_filter)

        self.ui.test_genres_filter.setToolTip(_(TOOLTIP_TEST_GENRES_FILTER))
        self.ui.test_genres_filter.textChanged.connect(
            self.update_test_genres_filter)

        #FIXME: colors aren't great from accessibility POV
        self.fmt_keep = QTextBlockFormat()
        self.fmt_keep.setBackground(Qt.green)

        self.fmt_skip = QTextBlockFormat()
        self.fmt_skip.setBackground(Qt.red)

        self.fmt_clear = QTextBlockFormat()
        self.fmt_clear.clearBackground()

    def load(self):
        self.ui.use_genres.setChecked(config.setting["use_genres"])
        self.ui.max_genres.setValue(config.setting["max_genres"])
        self.ui.min_genre_usage.setValue(config.setting["min_genre_usage"])
        self.ui.join_genres.setEditText(config.setting["join_genres"])
        self.ui.genres_filter.setPlainText(config.setting["genres_filter"])
        self.ui.only_my_genres.setChecked(config.setting["only_my_genres"])
        self.ui.artists_genres.setChecked(config.setting["artists_genres"])
        self.ui.folksonomy_tags.setChecked(config.setting["folksonomy_tags"])

    def save(self):
        config.setting["use_genres"] = self.ui.use_genres.isChecked()
        config.setting["max_genres"] = self.ui.max_genres.value()
        config.setting["min_genre_usage"] = self.ui.min_genre_usage.value()
        config.setting["join_genres"] = self.ui.join_genres.currentText()
        config.setting["genres_filter"] = self.ui.genres_filter.toPlainText()
        config.setting["only_my_genres"] = self.ui.only_my_genres.isChecked()
        config.setting["artists_genres"] = self.ui.artists_genres.isChecked()
        config.setting["folksonomy_tags"] = self.ui.folksonomy_tags.isChecked()

    def update_test_genres_filter(self):
        test_text = self.ui.test_genres_filter.toPlainText()

        filters = self.ui.genres_filter.toPlainText()
        tagfilter = TagGenreFilter(filters)

        #FIXME: very simple error reporting, improve
        self.ui.label_test_genres_filter_error.setText("\n".join([
            _("Error line %d: %s") % (lineno + 1, error)
            for lineno, error in tagfilter.errors.items()
        ]))

        def set_line_fmt(lineno, textformat):
            obj = self.ui.test_genres_filter
            if lineno < 0:
                #use current cursor position
                cursor = obj.textCursor()
            else:
                cursor = QTextCursor(obj.document().findBlockByNumber(lineno))
            obj.blockSignals(True)
            cursor.setBlockFormat(textformat)
            obj.blockSignals(False)

        set_line_fmt(-1, self.fmt_clear)
        for lineno, line in enumerate(test_text.splitlines()):
            line = line.strip()
            fmt = self.fmt_clear
            if line:
                if tagfilter.skip(line):
                    fmt = self.fmt_skip
                else:
                    fmt = self.fmt_keep
            set_line_fmt(lineno, fmt)
Example #11
0
class NetworkOptionsPage(OptionsPage):

    NAME = "network"
    TITLE = N_("Network")
    PARENT = "advanced"
    SORT_ORDER = 10
    ACTIVE = True
    HELP_URL = '/config/options_network.html'

    options = [
        config.BoolOption("setting", "use_proxy", False),
        config.TextOption("setting", "proxy_type", "http"),
        config.TextOption("setting", "proxy_server_host", ""),
        config.IntOption("setting", "proxy_server_port", 80),
        config.TextOption("setting", "proxy_username", ""),
        config.TextOption("setting", "proxy_password", ""),
        config.BoolOption("setting", "browser_integration", True),
        config.IntOption("setting", "browser_integration_port", 8000),
        config.BoolOption("setting", "browser_integration_localhost_only",
                          True),
        config.IntOption("setting", "network_transfer_timeout_seconds", 30),
    ]

    def __init__(self, parent=None):
        super().__init__(parent)
        self.ui = Ui_NetworkOptionsPage()
        self.ui.setupUi(self)

    def load(self):
        self.ui.web_proxy.setChecked(config.setting["use_proxy"])
        if config.setting["proxy_type"] == 'socks':
            self.ui.proxy_type_socks.setChecked(True)
        else:
            self.ui.proxy_type_http.setChecked(True)
        self.ui.server_host.setText(config.setting["proxy_server_host"])
        self.ui.server_port.setValue(config.setting["proxy_server_port"])
        self.ui.username.setText(config.setting["proxy_username"])
        self.ui.password.setText(config.setting["proxy_password"])
        self.ui.transfer_timeout.setValue(
            config.setting["network_transfer_timeout_seconds"])
        self.ui.browser_integration.setChecked(
            config.setting["browser_integration"])
        self.ui.browser_integration_port.setValue(
            config.setting["browser_integration_port"])
        self.ui.browser_integration_localhost_only.setChecked(
            config.setting["browser_integration_localhost_only"])

    def save(self):
        config.setting["use_proxy"] = self.ui.web_proxy.isChecked()
        if self.ui.proxy_type_socks.isChecked():
            config.setting["proxy_type"] = 'socks'
        else:
            config.setting["proxy_type"] = 'http'
        config.setting["proxy_server_host"] = self.ui.server_host.text()
        config.setting["proxy_server_port"] = self.ui.server_port.value()
        config.setting["proxy_username"] = self.ui.username.text()
        config.setting["proxy_password"] = self.ui.password.text()
        self.tagger.webservice.setup_proxy()
        transfer_timeout = self.ui.transfer_timeout.value()
        config.setting["network_transfer_timeout_seconds"] = transfer_timeout
        self.tagger.webservice.set_transfer_timeout(transfer_timeout)
        config.setting[
            "browser_integration"] = self.ui.browser_integration.isChecked()
        config.setting[
            "browser_integration_port"] = self.ui.browser_integration_port.value(
            )
        config.setting["browser_integration_localhost_only"] = \
            self.ui.browser_integration_localhost_only.isChecked()
        self.update_browser_integration()

    def update_browser_integration(self):
        if self.ui.browser_integration.isChecked():
            self.tagger.browser_integration.start()
        else:
            self.tagger.browser_integration.stop()
Example #12
0
class MetadataOptionsPage(OptionsPage):

    NAME = "metadata"
    TITLE = N_("Metadata")
    PARENT = None
    SORT_ORDER = 20
    ACTIVE = True
    HELP_URL = '/config/options_metadata.html'

    options = [
        config.TextOption("setting", "va_name", "Various Artists"),
        config.TextOption("setting", "nat_name", "[non-album tracks]"),
        config.TextOption("setting", "artist_locale", "en"),
        config.BoolOption("setting", "translate_artist_names", False),
        config.BoolOption("setting", "release_ars", True),
        config.BoolOption("setting", "track_ars", False),
        config.BoolOption("setting", "convert_punctuation", True),
        config.BoolOption("setting", "standardize_artists", False),
        config.BoolOption("setting", "standardize_instruments", True),
    ]

    def __init__(self, parent=None):
        super().__init__(parent)
        self.ui = Ui_MetadataOptionsPage()
        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)

    def load(self):
        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.ui.convert_punctuation.setChecked(config.setting["convert_punctuation"])
        self.ui.release_ars.setChecked(config.setting["release_ars"])
        self.ui.track_ars.setChecked(config.setting["track_ars"])
        self.ui.va_name.setText(config.setting["va_name"])
        self.ui.nat_name.setText(config.setting["nat_name"])
        self.ui.standardize_artists.setChecked(config.setting["standardize_artists"])
        self.ui.standardize_instruments.setChecked(config.setting["standardize_instruments"])

    def save(self):
        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["convert_punctuation"] = self.ui.convert_punctuation.isChecked()
        config.setting["release_ars"] = self.ui.release_ars.isChecked()
        config.setting["track_ars"] = self.ui.track_ars.isChecked()
        config.setting["va_name"] = self.ui.va_name.text()
        nat_name = self.ui.nat_name.text()
        if nat_name != config.setting["nat_name"]:
            config.setting["nat_name"] = nat_name
            if self.tagger.nats is not None:
                self.tagger.nats.update()
        config.setting["standardize_artists"] = self.ui.standardize_artists.isChecked()
        config.setting["standardize_instruments"] = self.ui.standardize_instruments.isChecked()

    def set_va_name_default(self):
        self.ui.va_name.setText(self.options[0].default)
        self.ui.va_name.setCursorPosition(0)

    def set_nat_name_default(self):
        self.ui.nat_name.setText(self.options[1].default)
        self.ui.nat_name.setCursorPosition(0)
Example #13
0
class AdvancedOptionsPage(OptionsPage):

    NAME = "advanced"
    TITLE = N_("Advanced")
    PARENT = None
    SORT_ORDER = 90
    ACTIVE = True

    options = [
        config.TextOption("setting", "ignore_regex", ""),
        config.BoolOption("setting", "ignore_hidden_files", False),
        config.BoolOption("setting", "recursively_add_files", True),
        config.IntOption("setting", "ignore_track_duration_difference_under",
                         2),
        config.BoolOption("setting", "completeness_ignore_videos", False),
        config.BoolOption("setting", "completeness_ignore_pregap", False),
        config.BoolOption("setting", "completeness_ignore_data", False),
        config.BoolOption("setting", "completeness_ignore_silence", False),
        config.ListOption("setting", "compare_ignore_tags", []),
    ]

    def __init__(self, parent=None):
        super().__init__(parent)
        self.ui = Ui_AdvancedOptionsPage()
        self.ui.setupUi(self)
        self.init_regex_checker(self.ui.ignore_regex, self.ui.regex_error)

    def load(self):
        self.ui.ignore_regex.setText(config.setting["ignore_regex"])
        self.ui.ignore_hidden_files.setChecked(
            config.setting["ignore_hidden_files"])
        self.ui.recursively_add_files.setChecked(
            config.setting["recursively_add_files"])
        self.ui.ignore_track_duration_difference_under.setValue(
            config.setting["ignore_track_duration_difference_under"])
        self.ui.completeness_ignore_videos.setChecked(
            config.setting["completeness_ignore_videos"])
        self.ui.completeness_ignore_pregap.setChecked(
            config.setting["completeness_ignore_pregap"])
        self.ui.completeness_ignore_data.setChecked(
            config.setting["completeness_ignore_data"])
        self.ui.completeness_ignore_silence.setChecked(
            config.setting["completeness_ignore_silence"])
        self.ui.compare_ignore_tags.update(
            config.setting["compare_ignore_tags"])
        self.ui.compare_ignore_tags.set_user_sortable(False)

    def save(self):
        config.setting["ignore_regex"] = self.ui.ignore_regex.text()
        config.setting[
            "ignore_hidden_files"] = self.ui.ignore_hidden_files.isChecked()
        config.setting[
            "recursively_add_files"] = self.ui.recursively_add_files.isChecked(
            )
        config.setting[
            "ignore_track_duration_difference_under"] = self.ui.ignore_track_duration_difference_under.value(
            )
        config.setting[
            "completeness_ignore_videos"] = self.ui.completeness_ignore_videos.isChecked(
            )
        config.setting[
            "completeness_ignore_pregap"] = self.ui.completeness_ignore_pregap.isChecked(
            )
        config.setting[
            "completeness_ignore_data"] = self.ui.completeness_ignore_data.isChecked(
            )
        config.setting[
            "completeness_ignore_silence"] = self.ui.completeness_ignore_silence.isChecked(
            )
        tags = list(self.ui.compare_ignore_tags.tags)
        if tags != config.setting["compare_ignore_tags"]:
            config.setting["compare_ignore_tags"] = tags

    def restore_defaults(self):
        self.ui.compare_ignore_tags.clear()
        super().restore_defaults()
Example #14
0
class OptionsDialog(PicardDialog, SingletonDialog):

    autorestore = False

    options = [
        config.TextOption("persist", "options_last_active_page", ""),
        config.ListOption("persist", "options_pages_tree_state", []),
        config.Option("persist", "options_splitter", QtCore.QByteArray()),
    ]

    def add_pages(self, parent, default_page, parent_item):
        pages = [(p.SORT_ORDER, p.NAME, p) for p in self.pages
                 if p.PARENT == parent]
        items = []
        for foo, bar, page in sorted(pages):
            item = HashableTreeWidgetItem(parent_item)
            item.setText(0, _(page.TITLE))
            if page.ACTIVE:
                self.item_to_page[item] = page
                self.page_to_item[page.NAME] = item
                self.ui.pages_stack.addWidget(page)
            else:
                item.setFlags(QtCore.Qt.ItemIsEnabled)
            self.add_pages(page.NAME, default_page, item)
            if page.NAME == default_page:
                self.default_item = item
            items.append(item)
        if not self.default_item and not parent:
            self.default_item = items[0]

    def __init__(self, default_page=None, parent=None):
        super().__init__(parent)
        self.setWindowModality(QtCore.Qt.ApplicationModal)
        self.setAttribute(QtCore.Qt.WA_DeleteOnClose)

        from picard.ui.ui_options import Ui_Dialog
        self.ui = Ui_Dialog()
        self.ui.setupUi(self)

        self.ui.reset_all_button = QtWidgets.QPushButton(
            _("&Restore all Defaults"))
        self.ui.reset_all_button.setToolTip(
            _("Reset all of Picard's settings"))
        self.ui.reset_button = QtWidgets.QPushButton(_("Restore &Defaults"))
        self.ui.reset_button.setToolTip(
            _("Reset all settings for current option page"))

        ok = StandardButton(StandardButton.OK)
        ok.setText(_("Make It So!"))
        self.ui.buttonbox.addButton(ok, QtWidgets.QDialogButtonBox.AcceptRole)
        self.ui.buttonbox.addButton(StandardButton(StandardButton.CANCEL),
                                    QtWidgets.QDialogButtonBox.RejectRole)
        self.ui.buttonbox.addButton(StandardButton(StandardButton.HELP),
                                    QtWidgets.QDialogButtonBox.HelpRole)
        self.ui.buttonbox.addButton(self.ui.reset_all_button,
                                    QtWidgets.QDialogButtonBox.ActionRole)
        self.ui.buttonbox.addButton(self.ui.reset_button,
                                    QtWidgets.QDialogButtonBox.ActionRole)

        self.ui.buttonbox.accepted.connect(self.accept)
        self.ui.buttonbox.rejected.connect(self.reject)
        self.ui.reset_all_button.clicked.connect(self.confirm_reset_all)
        self.ui.reset_button.clicked.connect(self.confirm_reset)
        self.ui.buttonbox.helpRequested.connect(self.help)

        self.pages = []
        for Page in page_classes:
            try:
                page = Page(self.ui.pages_stack)
                self.pages.append(page)
            except Exception:
                log.exception('Failed initializing options page %r', page)
        self.item_to_page = {}
        self.page_to_item = {}
        self.default_item = None
        if not default_page:
            default_page = config.persist["options_last_active_page"]
        self.add_pages(None, default_page, self.ui.pages_tree)

        # work-around to set optimal option pane width
        self.ui.pages_tree.expandAll()
        max_page_name = self.ui.pages_tree.sizeHintForColumn(
            0) + 2 * self.ui.pages_tree.frameWidth()
        self.ui.splitter.setSizes(
            [max_page_name,
             self.geometry().width() - max_page_name])

        self.ui.pages_tree.setHeaderLabels([""])
        self.ui.pages_tree.header().hide()
        self.ui.pages_tree.itemSelectionChanged.connect(self.switch_page)

        self.restoreWindowState()
        self.finished.connect(self.saveWindowState)

        for page in self.pages:
            try:
                page.load()
            except Exception:
                log.exception('Failed loading options page %r', page)
                self.disable_page(page.NAME)
        self.ui.pages_tree.setCurrentItem(self.default_item)

    def keyPressEvent(self, event):
        if event.matches(QtGui.QKeySequence.HelpContents):
            self.help()
        else:
            super().keyPressEvent(event)

    def switch_page(self):
        items = self.ui.pages_tree.selectedItems()
        if items:
            page = self.item_to_page[items[0]]
            config.persist["options_last_active_page"] = page.NAME
            self.ui.pages_stack.setCurrentWidget(page)

    def disable_page(self, name):
        item = self.page_to_item[name]
        item.setDisabled(True)

    def help(self):
        current_page = self.ui.pages_stack.currentWidget()
        url = current_page.HELP_URL
        # If URL is empty, use the first non empty parent help URL.
        while current_page.PARENT and not url:
            current_page = self.item_to_page[self.page_to_item[
                current_page.PARENT]]
            url = current_page.HELP_URL
        if not url:
            url = DOCS_BASE_URL
        elif url.startswith('/'):
            url = DOCS_BASE_URL + url
        webbrowser2.open(url)

    def accept(self):
        for page in self.pages:
            try:
                page.check()
            except OptionsCheckError as e:
                self._show_page_error(page, e)
                return
            except Exception as e:
                log.exception('Failed checking options page %r', page)
                self._show_page_error(page, e)
                return
        for page in self.pages:
            try:
                page.save()
            except Exception as e:
                log.exception('Failed saving options page %r', page)
                self._show_page_error(page, e)
                return
        super().accept()

    def _show_page_error(self, page, error):
        if not isinstance(error, OptionsCheckError):
            error = OptionsCheckError(_('Unexpected error'), str(error))
        self.ui.pages_tree.setCurrentItem(self.page_to_item[page.NAME])
        page.display_error(error)

    def saveWindowState(self):
        expanded_pages = []
        for page, item in self.page_to_item.items():
            index = self.ui.pages_tree.indexFromItem(item)
            is_expanded = self.ui.pages_tree.isExpanded(index)
            expanded_pages.append((page, is_expanded))
        config.persist["options_pages_tree_state"] = expanded_pages
        config.persist["options_splitter"] = self.ui.splitter.saveState()

    @restore_method
    def restoreWindowState(self):
        pages_tree_state = config.persist["options_pages_tree_state"]
        if not pages_tree_state:
            self.ui.pages_tree.expandAll()
        else:
            for page, is_expanded in pages_tree_state:
                try:
                    item = self.page_to_item[page]
                except KeyError:
                    continue
                item.setExpanded(is_expanded)

        self.restore_geometry()
        self.ui.splitter.restoreState(config.persist["options_splitter"])

    def restore_all_defaults(self):
        for page in self.pages:
            page.restore_defaults()

    def restore_page_defaults(self):
        self.ui.pages_stack.currentWidget().restore_defaults()

    def confirm_reset(self):
        msg = _("You are about to reset your options for this page.")
        self._show_dialog(msg, self.restore_page_defaults)

    def confirm_reset_all(self):
        msg = _("Warning! This will reset all of your settings.")
        self._show_dialog(msg, self.restore_all_defaults)

    def _show_dialog(self, msg, function):
        message_box = QtWidgets.QMessageBox(self)
        message_box.setIcon(QtWidgets.QMessageBox.Warning)
        message_box.setWindowModality(QtCore.Qt.WindowModal)
        message_box.setWindowTitle(_("Confirm Reset"))
        message_box.setText(_("Are you sure?") + "\n\n" + msg)
        message_box.setStandardButtons(QtWidgets.QMessageBox.Yes
                                       | QtWidgets.QMessageBox.No)
        if message_box.exec_() == QtWidgets.QMessageBox.Yes:
            function()
class TagsCompatibilityID3OptionsPage(OptionsPage):

    NAME = "tags_compatibility_id3"
    TITLE = N_("ID3")
    PARENT = "tags"
    SORT_ORDER = 30
    ACTIVE = True

    options = [
        config.BoolOption("setting", "write_id3v1", True),
        config.BoolOption("setting", "write_id3v23", True),
        config.TextOption("setting", "id3v2_encoding", "utf-16"),
        config.TextOption("setting", "id3v23_join_with", "/"),
        config.BoolOption("setting", "itunes_compatible_grouping", False),
    ]

    def __init__(self, parent=None):
        super().__init__(parent)
        self.ui = Ui_TagsCompatibilityOptionsPage()
        self.ui.setupUi(self)
        self.ui.write_id3v23.clicked.connect(self.update_encodings)
        self.ui.write_id3v24.clicked.connect(partial(self.update_encodings, force_utf8=True))

    def load(self):
        self.ui.write_id3v1.setChecked(config.setting["write_id3v1"])
        if config.setting["write_id3v23"]:
            self.ui.write_id3v23.setChecked(True)
        else:
            self.ui.write_id3v24.setChecked(True)
        if config.setting["id3v2_encoding"] == "iso-8859-1":
            self.ui.enc_iso88591.setChecked(True)
        elif config.setting["id3v2_encoding"] == "utf-16":
            self.ui.enc_utf16.setChecked(True)
        else:
            self.ui.enc_utf8.setChecked(True)
        self.ui.id3v23_join_with.setEditText(config.setting["id3v23_join_with"])
        self.ui.itunes_compatible_grouping.setChecked(config.setting["itunes_compatible_grouping"])
        self.update_encodings()

    def save(self):
        config.setting["write_id3v1"] = self.ui.write_id3v1.isChecked()
        config.setting["write_id3v23"] = self.ui.write_id3v23.isChecked()
        config.setting["id3v23_join_with"] = self.ui.id3v23_join_with.currentText()
        if self.ui.enc_iso88591.isChecked():
            config.setting["id3v2_encoding"] = "iso-8859-1"
        elif self.ui.enc_utf16.isChecked():
            config.setting["id3v2_encoding"] = "utf-16"
        else:
            config.setting["id3v2_encoding"] = "utf-8"
        config.setting["itunes_compatible_grouping"] = self.ui.itunes_compatible_grouping.isChecked()

    def update_encodings(self, force_utf8=False):
        if self.ui.write_id3v23.isChecked():
            if self.ui.enc_utf8.isChecked():
                self.ui.enc_utf16.setChecked(True)
            self.ui.enc_utf8.setEnabled(False)
            self.ui.label_id3v23_join_with.setEnabled(True)
            self.ui.id3v23_join_with.setEnabled(True)
        else:
            self.ui.enc_utf8.setEnabled(True)
            if force_utf8:
                self.ui.enc_utf8.setChecked(True)
            self.ui.label_id3v23_join_with.setEnabled(False)
            self.ui.id3v23_join_with.setEnabled(False)
Example #16
0
class AdvancedOptionsPage(OptionsPage):

    NAME = "advanced"
    TITLE = N_("Advanced")
    PARENT = None
    SORT_ORDER = 90
    ACTIVE = True

    options = [
        config.TextOption("setting", "ignore_regex", ""),
        config.BoolOption("setting", "ignore_hidden_files", False),
        config.BoolOption("setting", "completeness_ignore_videos", False),
        config.BoolOption("setting", "completeness_ignore_pregap", False),
        config.BoolOption("setting", "completeness_ignore_data", False),
        config.BoolOption("setting", "completeness_ignore_silence", False),
    ]

    def __init__(self, parent=None):
        super(AdvancedOptionsPage, self).__init__(parent)
        self.ui = Ui_AdvancedOptionsPage()
        self.ui.setupUi(self)
        self.ui.ignore_regex.textChanged.connect(self.live_checker)

    def load(self):
        self.ui.ignore_regex.setText(config.setting["ignore_regex"])
        self.ui.ignore_hidden_files.setChecked(
            config.setting["ignore_hidden_files"])
        self.ui.completeness_ignore_videos.setChecked(
            config.setting["completeness_ignore_videos"])
        self.ui.completeness_ignore_pregap.setChecked(
            config.setting["completeness_ignore_pregap"])
        self.ui.completeness_ignore_data.setChecked(
            config.setting["completeness_ignore_data"])
        self.ui.completeness_ignore_silence.setChecked(
            config.setting["completeness_ignore_silence"])

    def save(self):
        config.setting["ignore_regex"] = unicode(self.ui.ignore_regex.text())
        config.setting[
            "ignore_hidden_files"] = self.ui.ignore_hidden_files.isChecked()
        config.setting[
            "completeness_ignore_videos"] = self.ui.completeness_ignore_videos.isChecked(
            )
        config.setting[
            "completeness_ignore_pregap"] = self.ui.completeness_ignore_pregap.isChecked(
            )
        config.setting[
            "completeness_ignore_data"] = self.ui.completeness_ignore_data.isChecked(
            )
        config.setting[
            "completeness_ignore_silence"] = self.ui.completeness_ignore_silence.isChecked(
            )

    def live_checker(self, text):
        self.ui.regex_error.setStyleSheet("")
        self.ui.regex_error.setText("")
        try:
            self.check()
        except OptionsCheckError as e:
            self.ui.regex_error.setStyleSheet(self.STYLESHEET_ERROR)
            self.ui.regex_error.setText(e.info)
            return

    def check(self):
        try:
            re.compile(unicode(self.ui.ignore_regex.text()))
        except re.error as e:
            raise OptionsCheckError(_("Regex Error"), str(e))
Example #17
0
class NetworkOptionsPage(OptionsPage):

    NAME = "network"
    TITLE = N_("Network")
    PARENT = "advanced"
    SORT_ORDER = 10
    ACTIVE = True

    options = [
        config.BoolOption("setting", "use_proxy", False),
        config.TextOption("setting", "proxy_server_host", ""),
        config.IntOption("setting", "proxy_server_port", 80),
        config.TextOption("setting", "proxy_username", ""),
        config.TextOption("setting", "proxy_password", ""),
        config.BoolOption("setting", "browser_integration", True),
        config.IntOption("setting", "browser_integration_port", 8000),
        config.BoolOption("setting", "browser_integration_localhost_only",
                          True)
    ]

    def __init__(self, parent=None):
        super(NetworkOptionsPage, self).__init__(parent)
        self.ui = Ui_NetworkOptionsPage()
        self.ui.setupUi(self)

    def load(self):
        self.ui.web_proxy.setChecked(config.setting["use_proxy"])
        self.ui.server_host.setText(config.setting["proxy_server_host"])
        self.ui.server_port.setValue(config.setting["proxy_server_port"])
        self.ui.username.setText(config.setting["proxy_username"])
        self.ui.password.setText(config.setting["proxy_password"])
        self.ui.browser_integration.setChecked(
            config.setting["browser_integration"])
        self.ui.browser_integration_port.setValue(
            config.setting["browser_integration_port"])
        self.ui.browser_integration_localhost_only.setChecked(
            config.setting["browser_integration_localhost_only"])
        self.ui.browser_integration_port.valueChanged.connect(
            self.change_browser_integration_port)

    def save(self):
        config.setting["use_proxy"] = self.ui.web_proxy.isChecked()
        config.setting["proxy_server_host"] = self.ui.server_host.text()
        config.setting["proxy_server_port"] = self.ui.server_port.value()
        config.setting["proxy_username"] = self.ui.username.text()
        config.setting["proxy_password"] = self.ui.password.text()
        self.tagger.webservice.setup_proxy()
        config.setting[
            "browser_integration"] = self.ui.browser_integration.isChecked()
        config.setting[
            "browser_integration_port"] = self.ui.browser_integration_port.value(
            )
        config.setting["browser_integration_localhost_only"] = \
            self.ui.browser_integration_localhost_only.isChecked()
        self.update_browser_integration()

    def update_browser_integration(self):
        if self.ui.browser_integration.isChecked():
            self.tagger.browser_integration.start()
        else:
            self.tagger.browser_integration.stop()

    def change_browser_integration_port(self, port):
        config.setting["browser_integration_port"] = port
Example #18
0
class MetadataOptionsPage(OptionsPage):

    NAME = "metadata"
    TITLE = N_("Metadata")
    PARENT = None
    SORT_ORDER = 20
    ACTIVE = True

    options = [
        config.TextOption("setting", "va_name", "Various Artists"),
        config.TextOption("setting", "nat_name", "[non-album tracks]"),
        config.TextOption("setting", "artist_locale", "en"),
        config.BoolOption("setting", "translate_artist_names", False),
        config.BoolOption("setting", "release_ars", True),
        config.BoolOption("setting", "track_ars", False),
        config.BoolOption("setting", "folksonomy_tags", False),
        config.BoolOption("setting", "convert_punctuation", True),
        config.BoolOption("setting", "standardize_artists", False),
    ]

    def __init__(self, parent=None):
        super(MetadataOptionsPage, self).__init__(parent)
        self.ui = Ui_MetadataOptionsPage()
        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)

    def load(self):
        self.ui.translate_artist_names.setChecked(
            config.setting["translate_artist_names"])

        combo_box = self.ui.artist_locale
        locales = sorted(ALIAS_LOCALES.keys())
        for i, loc in enumerate(locales):
            name = ALIAS_LOCALES[loc]
            if "_" in loc:
                name = "    " + name
            combo_box.addItem(name, loc)
            if loc == config.setting["artist_locale"]:
                combo_box.setCurrentIndex(i)

        self.ui.convert_punctuation.setChecked(
            config.setting["convert_punctuation"])
        self.ui.release_ars.setChecked(config.setting["release_ars"])
        self.ui.track_ars.setChecked(config.setting["track_ars"])
        self.ui.folksonomy_tags.setChecked(config.setting["folksonomy_tags"])
        self.ui.va_name.setText(config.setting["va_name"])
        self.ui.nat_name.setText(config.setting["nat_name"])
        self.ui.standardize_artists.setChecked(
            config.setting["standardize_artists"])

    def save(self):
        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[
            "convert_punctuation"] = self.ui.convert_punctuation.isChecked()
        config.setting["release_ars"] = self.ui.release_ars.isChecked()
        config.setting["track_ars"] = self.ui.track_ars.isChecked()
        config.setting["folksonomy_tags"] = self.ui.folksonomy_tags.isChecked()
        config.setting["va_name"] = self.ui.va_name.text()
        nat_name = self.ui.nat_name.text()
        if nat_name != config.setting["nat_name"]:
            config.setting["nat_name"] = nat_name
            if self.tagger.nats is not None:
                self.tagger.nats.update()
        config.setting[
            "standardize_artists"] = self.ui.standardize_artists.isChecked()

    def set_va_name_default(self):
        self.ui.va_name.setText(self.options[0].default)
        self.ui.va_name.setCursorPosition(0)

    def set_nat_name_default(self):
        self.ui.nat_name.setText(self.options[1].default)
        self.ui.nat_name.setCursorPosition(0)
Example #19
0
class RenamingOptionsPage(OptionsPage):

    NAME = "filerenaming"
    TITLE = N_("File naming")
    PARENT = None
    SORT_ORDER = 40
    ACTIVE = True

    options = [
        config.BoolOption("setting", "windows_compatible_filenames", True),
        config.BoolOption("setting", "ascii_filenames", False),
        config.BoolOption("setting", "rename_files", False),
        config.TextOption(
            "setting", "file_naming_format",
            "$if2(%albumartist%,%artist%)/%album%/$if($gt(%totaldiscs%,1),%discnumber%-,)$num(%tracknumber%,2)$if(%compilation%, %artist% -,) %title%"
        ),
        config.BoolOption("setting", "move_files", False),
        config.TextOption("setting", "move_files_to", ""),
        config.BoolOption("setting", "move_additional_files", False),
        config.TextOption("setting", "move_additional_files_pattern",
                          "*.jpg *.png"),
        config.BoolOption("setting", "delete_empty_dirs", True),
    ]

    def __init__(self, parent=None):
        super(RenamingOptionsPage, self).__init__(parent)
        self.ui = Ui_RenamingOptionsPage()
        self.ui.setupUi(self)

        self.ui.ascii_filenames.clicked.connect(self.update_examples)
        self.ui.windows_compatible_filenames.clicked.connect(
            self.update_examples)
        self.ui.rename_files.clicked.connect(self.update_examples)
        self.ui.move_files.clicked.connect(self.update_examples)
        self.ui.move_files_to.editingFinished.connect(self.update_examples)

        # The following code is there to fix
        # http://tickets.musicbrainz.org/browse/PICARD-417
        # In some older version of PyQt/sip it's impossible to connect a signal
        # emitting an `int` to a slot expecting a `bool`.
        # By using `enabledSlot` instead we can force python to do the
        # conversion from int (`state`) to bool.
        def enabledSlot(func, state):
            """Calls `func` with `state`."""
            func(state)

        if not sys.platform == "win32":
            self.ui.rename_files.stateChanged.connect(
                partial(enabledSlot,
                        self.ui.windows_compatible_filenames.setEnabled))

        self.ui.move_files.stateChanged.connect(
            partial(enabledSlot, self.ui.delete_empty_dirs.setEnabled))
        self.ui.move_files.stateChanged.connect(
            partial(enabledSlot, self.ui.move_files_to.setEnabled))
        self.ui.move_files.stateChanged.connect(
            partial(enabledSlot, self.ui.move_files_to_browse.setEnabled))
        self.ui.move_files.stateChanged.connect(
            partial(enabledSlot, self.ui.move_additional_files.setEnabled))
        self.ui.move_files.stateChanged.connect(
            partial(enabledSlot,
                    self.ui.move_additional_files_pattern.setEnabled))
        self.ui.rename_files.stateChanged.connect(
            partial(enabledSlot, self.ui.ascii_filenames.setEnabled))
        self.ui.rename_files.stateChanged.connect(
            partial(enabledSlot, self.ui.file_naming_format.setEnabled))
        self.ui.rename_files.stateChanged.connect(
            partial(enabledSlot,
                    self.ui.file_naming_format_default.setEnabled))
        self.ui.file_naming_format.textChanged.connect(self.check_formats)
        self.ui.file_naming_format_default.clicked.connect(
            self.set_file_naming_format_default)
        self.highlighter = TaggerScriptSyntaxHighlighter(
            self.ui.file_naming_format.document())
        self.ui.move_files_to_browse.clicked.connect(self.move_files_to_browse)

    def check_formats(self):
        self.test()
        self.update_examples()

    def _example_to_filename(self, file):
        settings = {
            'windows_compatible_filenames':
            self.ui.windows_compatible_filenames.isChecked(),
            'ascii_filenames':
            self.ui.ascii_filenames.isChecked(),
            'rename_files':
            self.ui.rename_files.isChecked(),
            'move_files':
            self.ui.move_files.isChecked(),
            'use_va_format':
            False,  # TODO remove
            'file_naming_format':
            unicode(self.ui.file_naming_format.toPlainText()),
            'move_files_to':
            os.path.normpath(unicode(self.ui.move_files_to.text()))
        }
        try:
            if config.setting["enable_tagger_script"]:
                script = config.setting["tagger_script"]
                parser = ScriptParser()
                parser.eval(script, file.metadata)
            filename = file._make_filename(file.filename, file.metadata,
                                           settings)
            if not settings["move_files"]:
                return os.path.basename(filename)
            return filename
        except SyntaxError:
            return ""
        except TypeError:
            return ""
        except UnknownFunction:
            return ""

    def update_examples(self):
        # TODO: Here should be more examples etc.
        # TODO: Would be nice to show diffs too....
        example1 = self._example_to_filename(self.example_1())
        example2 = self._example_to_filename(self.example_2())
        self.ui.example_filename.setText(example1)
        self.ui.example_filename_va.setText(example2)

    def load(self):
        if sys.platform == "win32":
            self.ui.windows_compatible_filenames.setChecked(True)
            self.ui.windows_compatible_filenames.setEnabled(False)
        else:
            self.ui.windows_compatible_filenames.setChecked(
                config.setting["windows_compatible_filenames"])
        self.ui.rename_files.setChecked(config.setting["rename_files"])
        self.ui.move_files.setChecked(config.setting["move_files"])
        self.ui.ascii_filenames.setChecked(config.setting["ascii_filenames"])
        self.ui.file_naming_format.setPlainText(
            config.setting["file_naming_format"])
        self.ui.move_files_to.setText(config.setting["move_files_to"])
        self.ui.move_files_to.setCursorPosition(0)
        self.ui.move_additional_files.setChecked(
            config.setting["move_additional_files"])
        self.ui.move_additional_files_pattern.setText(
            config.setting["move_additional_files_pattern"])
        self.ui.delete_empty_dirs.setChecked(
            config.setting["delete_empty_dirs"])
        self.update_examples()

    def check(self):
        self.check_format()
        if self.ui.move_files.isChecked() and not unicode(
                self.ui.move_files_to.text()).strip():
            raise OptionsCheckError(
                _("Error"),
                _("The location to move files to must not be empty."))

    def check_format(self):
        parser = ScriptParser()
        try:
            parser.eval(unicode(self.ui.file_naming_format.toPlainText()))
        except Exception as e:
            raise OptionsCheckError("", str(e))
        if self.ui.rename_files.isChecked():
            if not unicode(self.ui.file_naming_format.toPlainText()).strip():
                raise OptionsCheckError(
                    "", _("The file naming format must not be empty."))

    def save(self):
        config.setting[
            "windows_compatible_filenames"] = self.ui.windows_compatible_filenames.isChecked(
            )
        config.setting["ascii_filenames"] = self.ui.ascii_filenames.isChecked()
        config.setting["rename_files"] = self.ui.rename_files.isChecked()
        config.setting["file_naming_format"] = unicode(
            self.ui.file_naming_format.toPlainText())
        self.tagger.window.enable_renaming_action.setChecked(
            config.setting["rename_files"])
        config.setting["move_files"] = self.ui.move_files.isChecked()
        config.setting["move_files_to"] = os.path.normpath(
            unicode(self.ui.move_files_to.text()))
        config.setting[
            "move_additional_files"] = self.ui.move_additional_files.isChecked(
            )
        config.setting["move_additional_files_pattern"] = unicode(
            self.ui.move_additional_files_pattern.text())
        config.setting[
            "delete_empty_dirs"] = self.ui.delete_empty_dirs.isChecked()
        self.tagger.window.enable_moving_action.setChecked(
            config.setting["move_files"])

    def display_error(self, error):
        pass

    def set_file_naming_format_default(self):
        self.ui.file_naming_format.setText(self.options[3].default)


#        self.ui.file_naming_format.setCursorPosition(0)

    def example_1(self):
        file = File("ticket_to_ride.mp3")
        file.state = File.NORMAL
        file.metadata['album'] = 'Help!'
        file.metadata['title'] = 'Ticket to Ride'
        file.metadata['artist'] = 'The Beatles'
        file.metadata['artistsort'] = 'Beatles, The'
        file.metadata['albumartist'] = 'The Beatles'
        file.metadata['albumartistsort'] = 'Beatles, The'
        file.metadata['tracknumber'] = '7'
        file.metadata['totaltracks'] = '14'
        file.metadata['discnumber'] = '1'
        file.metadata['totaldiscs'] = '1'
        file.metadata['date'] = '1965-08-06'
        file.metadata['releasetype'] = ['album', 'soundtrack']
        file.metadata['~primaryreleasetype'] = ['album']
        file.metadata['~secondaryreleasetype'] = ['soundtrack']
        file.metadata['releasestatus'] = 'official'
        file.metadata['releasecountry'] = 'US'
        file.metadata['~extension'] = 'mp3'
        file.metadata[
            'musicbrainz_albumid'] = '2c053984-4645-4699-9474-d2c35c227028'
        file.metadata[
            'musicbrainz_albumartistid'] = 'b10bbbfc-cf9e-42e0-be17-e2c3e1d2600d'
        file.metadata[
            'musicbrainz_artistid'] = 'b10bbbfc-cf9e-42e0-be17-e2c3e1d2600d'
        file.metadata[
            'musicbrainz_recordingid'] = 'ed052ae1-c950-47f2-8d2b-46e1b58ab76c'
        file.metadata[
            'musicbrainz_releasetrackid'] = '7668a62a-2fac-3151-a744-5707ac8c883c'
        return file

    def example_2(self):
        file = File("track05.mp3")
        file.state = File.NORMAL
        # The data for this example does not match the release on MusicBrainz,
        # but still works well enough as an example.
        file.metadata['album'] = 'Explosive Doowops, Volume 4'
        file.metadata['title'] = 'Why? Oh Why?'
        file.metadata['artist'] = 'The Fantasys'
        file.metadata['artistsort'] = 'Fantasys, The'
        file.metadata['albumartist'] = config.setting['va_name']
        file.metadata['albumartistsort'] = config.setting['va_name']
        file.metadata['tracknumber'] = '5'
        file.metadata['totaltracks'] = '26'
        file.metadata['discnumber'] = '2'
        file.metadata['totaldiscs'] = '2'
        file.metadata['date'] = '1999-02-03'
        file.metadata['releasetype'] = ['album', 'compilation']
        file.metadata['~primaryreleasetype'] = ['album']
        file.metadata['~secondaryreleasetype'] = ['compilation']
        file.metadata['releasestatus'] = 'official'
        file.metadata['releasecountry'] = 'US'
        file.metadata['compilation'] = '1'
        file.metadata['~extension'] = 'mp3'
        file.metadata[
            'musicbrainz_albumid'] = 'bcc97e8a-2055-400b-a6ed-83288285c6fc'
        file.metadata[
            'musicbrainz_albumartistid'] = '89ad4ac3-39f7-470e-963a-56509c546377'
        file.metadata[
            'musicbrainz_artistid'] = '06704773-aafe-4aca-8833-b449e0a6467f'
        file.metadata[
            'musicbrainz_recordingid'] = 'd92837ee-b1e4-4649-935f-e433c3e5e429'
        file.metadata[
            'musicbrainz_releasetrackid'] = 'eac99807-93d4-3668-9714-fa0c1b487ccf'
        return file

    STYLESHEET_ERROR = "QWidget { background-color: #f55; color: white; font-weight:bold }"

    def move_files_to_browse(self):
        path = QtGui.QFileDialog.getExistingDirectory(
            self, "", self.ui.move_files_to.text())
        if path:
            path = os.path.normpath(unicode(path))
            self.ui.move_files_to.setText(path)

    def test(self):
        self.ui.renaming_error.setStyleSheet("")
        self.ui.renaming_error.setText("")
        try:
            self.check_format()
        except OptionsCheckError as e:
            self.ui.renaming_error.setStyleSheet(self.STYLESHEET_ERROR)
            self.ui.renaming_error.setText(e.info)
            return
Example #20
0
class FingerprintingOptionsPage(OptionsPage):

    NAME = "fingerprinting"
    TITLE = N_("Fingerprinting")
    PARENT = None
    SORT_ORDER = 45
    ACTIVE = True

    options = [
        config.TextOption("setting", "fingerprinting_system", "acoustid"),
        config.TextOption("setting", "acoustid_fpcalc", ""),
        config.TextOption("setting", "acoustid_apikey", ""),
    ]

    def __init__(self, parent=None):
        super(FingerprintingOptionsPage, self).__init__(parent)
        self.ui = Ui_FingerprintingOptionsPage()
        self.ui.setupUi(self)
        self.ui.disable_fingerprinting.clicked.connect(self.update_groupboxes)
        self.ui.use_acoustid.clicked.connect(self.update_groupboxes)
        self.ui.acoustid_fpcalc_browse.clicked.connect(
            self.acoustid_fpcalc_browse)
        self.ui.acoustid_fpcalc_download.clicked.connect(
            self.acoustid_fpcalc_download)
        self.ui.acoustid_apikey_get.clicked.connect(self.acoustid_apikey_get)

    def load(self):
        if config.setting["fingerprinting_system"] == "acoustid":
            self.ui.use_acoustid.setChecked(True)
        else:
            self.ui.disable_fingerprinting.setChecked(True)
        self.ui.acoustid_fpcalc.setText(config.setting["acoustid_fpcalc"])
        self.ui.acoustid_apikey.setText(config.setting["acoustid_apikey"])
        self.update_groupboxes()

    def save(self):
        if self.ui.use_acoustid.isChecked():
            config.setting["fingerprinting_system"] = "acoustid"
        else:
            config.setting["fingerprinting_system"] = ""
        config.setting["acoustid_fpcalc"] = unicode(
            self.ui.acoustid_fpcalc.text())
        config.setting["acoustid_apikey"] = unicode(
            self.ui.acoustid_apikey.text())

    def update_groupboxes(self):
        if self.ui.use_acoustid.isChecked():
            self.ui.acoustid_settings.setEnabled(True)
            if self.ui.acoustid_fpcalc.text().isEmpty():
                fpcalc_path = find_executable(*FPCALC_NAMES)
                if fpcalc_path:
                    self.ui.acoustid_fpcalc.setText(fpcalc_path)
        else:
            self.ui.acoustid_settings.setEnabled(False)

    def acoustid_fpcalc_browse(self):
        path = QtGui.QFileDialog.getOpenFileName(
            self, "", self.ui.acoustid_fpcalc.text())
        if path:
            path = os.path.normpath(unicode(path))
            self.ui.acoustid_fpcalc.setText(path)

    def acoustid_fpcalc_download(self):
        webbrowser2.open("http://acoustid.org/chromaprint#download")

    def acoustid_apikey_get(self):
        webbrowser2.open("http://acoustid.org/api-key")
Example #21
0
class TagsFromFileNamesDialog(PicardDialog):

    options = [
        config.TextOption("persist", "tags_from_filenames_format", ""),
    ]

    def __init__(self, files, parent=None):
        super().__init__(parent)
        self.ui = Ui_TagsFromFileNamesDialog()
        self.ui.setupUi(self)
        items = [
            "%artist%/%album%/%title%",
            "%artist%/%album%/%tracknumber% %title%",
            "%artist%/%album%/%tracknumber% - %title%",
            "%artist%/%album% - %tracknumber% - %title%",
            "%artist% - %album%/%title%",
            "%artist% - %album%/%tracknumber% %title%",
            "%artist% - %album%/%tracknumber% - %title%",
        ]
        tff_format = config.persist["tags_from_filenames_format"]
        if tff_format not in items:
            selected_index = 0
            if tff_format:
                items.insert(0, tff_format)
        else:
            selected_index = items.index(tff_format)
        self.ui.format.addItems(items)
        self.ui.format.setCurrentIndex(selected_index)
        self.ui.buttonbox.addButton(StandardButton(StandardButton.OK),
                                    QtWidgets.QDialogButtonBox.AcceptRole)
        self.ui.buttonbox.addButton(StandardButton(StandardButton.CANCEL),
                                    QtWidgets.QDialogButtonBox.RejectRole)
        self.ui.buttonbox.accepted.connect(self.accept)
        self.ui.buttonbox.rejected.connect(self.reject)
        self.ui.preview.clicked.connect(self.preview)
        self.ui.files.setHeaderLabels([_("File Name")])
        self.files = files
        self.items = []
        for file in files:
            item = QtWidgets.QTreeWidgetItem(self.ui.files)
            item.setText(0, os.path.basename(file.filename))
            self.items.append(item)
        self._tag_re = re.compile(r"(%\w+%)")
        self.numeric_tags = ('tracknumber', 'totaltracks', 'discnumber',
                             'totaldiscs')

    def parse_response(self):
        tff_format = self.ui.format.currentText()
        columns = []
        format_re = ['(?:^|/)']
        for part in self._tag_re.split(tff_format):
            if part.startswith('%') and part.endswith('%'):
                name = part[1:-1]
                columns.append(name)
                if name in self.numeric_tags:
                    format_re.append('(?P<' + name + r'>\d+)')
                elif name == 'date':
                    format_re.append('(?P<' + name +
                                     r'>\d+(?:-\d+(?:-\d+)?)?)')
                else:
                    format_re.append('(?P<' + name + '>[^/]*?)')
            else:
                format_re.append(re.escape(part))
        format_re.append(r'\.(\w+)$')
        format_re = re.compile("".join(format_re))
        return format_re, columns

    def match_file(self, file, tff_format):
        match = tff_format.search(file.filename.replace('\\', '/'))
        if match:
            result = {}
            for name, value in match.groupdict().items():
                value = value.strip()
                if name in self.numeric_tags:
                    value = value.lstrip("0")
                if self.ui.replace_underscores.isChecked():
                    value = value.replace('_', ' ')
                result[name] = value
            return result
        else:
            return {}

    def preview(self):
        tff_format, columns = self.parse_response()
        self.ui.files.setHeaderLabels([_("File Name")] +
                                      list(map(display_tag_name, columns)))
        for item, file in zip(self.items, self.files):
            matches = self.match_file(file, tff_format)
            for i, column in enumerate(columns):
                item.setText(i + 1, matches.get(column, ''))
        self.ui.files.header().resizeSections(
            QtWidgets.QHeaderView.ResizeToContents)
        self.ui.files.header().setStretchLastSection(True)

    def accept(self):
        tff_format, columns = self.parse_response()
        for file in self.files:
            metadata = self.match_file(file, tff_format)
            for name, value in metadata.items():
                file.metadata[name] = value
            file.update()
        config.persist[
            "tags_from_filenames_format"] = self.ui.format.currentText()
        super().accept()
Example #22
0
class TagsFromFileNamesDialog(PicardDialog):

    autorestore = False

    options = [
        config.TextOption("persist", "tags_from_filenames_format", ""),
    ]

    def __init__(self, files, parent=None):
        super().__init__(parent)
        self.ui = Ui_TagsFromFileNamesDialog()
        self.ui.setupUi(self)
        self.restore_geometry()
        items = [
            "%artist%/%album%/%title%",
            "%artist%/%album%/%tracknumber% %title%",
            "%artist%/%album%/%tracknumber% - %title%",
            "%artist%/%album% - %tracknumber% - %title%",
            "%artist% - %album%/%title%",
            "%artist% - %album%/%tracknumber% %title%",
            "%artist% - %album%/%tracknumber% - %title%",
        ]
        tff_format = config.persist["tags_from_filenames_format"]
        if tff_format not in items:
            selected_index = 0
            if tff_format:
                items.insert(0, tff_format)
        else:
            selected_index = items.index(tff_format)
        self.ui.format.addItems(items)
        self.ui.format.setCurrentIndex(selected_index)
        self.ui.buttonbox.addButton(StandardButton(StandardButton.OK),
                                    QtWidgets.QDialogButtonBox.AcceptRole)
        self.ui.buttonbox.addButton(StandardButton(StandardButton.CANCEL),
                                    QtWidgets.QDialogButtonBox.RejectRole)
        self.ui.buttonbox.accepted.connect(self.accept)
        self.ui.buttonbox.rejected.connect(self.reject)
        self.ui.preview.clicked.connect(self.preview)
        self.ui.files.setHeaderLabels([_("File Name")])
        self.files = files
        self.items = []
        for file in files:
            item = QtWidgets.QTreeWidgetItem(self.ui.files)
            item.setText(0, os.path.basename(file.filename))
            self.items.append(item)

    def preview(self):
        expression = TagMatchExpression(
            self.ui.format.currentText(),
            self.ui.replace_underscores.isChecked())
        columns = expression.matched_tags
        headers = [_("File Name")] + list(map(display_tag_name, columns))
        self.ui.files.setColumnCount(len(headers))
        self.ui.files.setHeaderLabels(headers)
        for item, file in zip(self.items, self.files):
            matches = expression.match_file(file.filename)
            for i, column in enumerate(columns):
                values = matches.get(column, [])
                item.setText(i + 1, '; '.join(values))
        self.ui.files.header().resizeSections(
            QtWidgets.QHeaderView.ResizeToContents)
        self.ui.files.header().setStretchLastSection(True)

    def accept(self):
        expression = TagMatchExpression(
            self.ui.format.currentText(),
            self.ui.replace_underscores.isChecked())
        for file in self.files:
            metadata = expression.match_file(file.filename)
            for name, values in metadata.items():
                file.metadata[name] = values
            file.update()
        config.persist[
            "tags_from_filenames_format"] = self.ui.format.currentText()
        super().accept()
Example #23
0
class MainWindow(QtWidgets.QMainWindow, PreserveGeometry):

    defaultsize = QtCore.QSize(780, 560)
    autorestore = False
    selection_updated = QtCore.pyqtSignal(object)

    options = [
        config.Option("persist", "window_state", QtCore.QByteArray()),
        config.Option("persist", "bottom_splitter_state", QtCore.QByteArray()),
        config.BoolOption("persist", "window_maximized", False),
        config.BoolOption("persist", "view_cover_art", True),
        config.BoolOption("persist", "view_toolbar", True),
        config.BoolOption("persist", "view_file_browser", False),
        config.TextOption("persist", "current_directory", ""),
    ]

    def __init__(self, parent=None):
        super().__init__(parent)
        self.selected_objects = []
        self.ignore_selection_changes = False
        self.toolbar = None
        self.setupUi()

    def setupUi(self):
        self.setWindowTitle(_("MusicBrainz Picard"))
        icon = QtGui.QIcon()
        icon.addFile(":/images/16x16/picard.png", QtCore.QSize(16, 16))
        icon.addFile(":/images/24x24/picard.png", QtCore.QSize(24, 24))
        icon.addFile(":/images/32x32/picard.png", QtCore.QSize(32, 32))
        icon.addFile(":/images/48x48/picard.png", QtCore.QSize(48, 48))
        icon.addFile(":/images/128x128/picard.png", QtCore.QSize(128, 128))
        icon.addFile(":/images/256x256/picard.png", QtCore.QSize(256, 256))
        self.setWindowIcon(icon)

        self.create_actions()
        self.create_statusbar()
        self.create_toolbar()
        self.create_menus()

        mainLayout = QtWidgets.QSplitter(QtCore.Qt.Vertical)
        mainLayout.setContentsMargins(0, 0, 0, 0)
        mainLayout.setHandleWidth(1)

        self.panel = MainPanel(self, mainLayout)
        self.file_browser = FileBrowser(self.panel)
        if not self.show_file_browser_action.isChecked():
            self.file_browser.hide()
        self.panel.insertWidget(0, self.file_browser)
        self.panel.restore_state()

        self.metadata_box = MetadataBox(self)
        self.cover_art_box = CoverArtBox(self)
        if not self.show_cover_art_action.isChecked():
            self.cover_art_box.hide()

        self.logDialog = LogView(self)
        self.historyDialog = HistoryView(self)

        bottomLayout = QtWidgets.QHBoxLayout()
        bottomLayout.setContentsMargins(0, 0, 0, 0)
        bottomLayout.setSpacing(0)
        bottomLayout.addWidget(self.metadata_box, 1)
        bottomLayout.addWidget(self.cover_art_box, 0)
        bottom = QtWidgets.QWidget()
        bottom.setLayout(bottomLayout)

        mainLayout.addWidget(self.panel)
        mainLayout.addWidget(bottom)
        self.setCentralWidget(mainLayout)

        # accessibility
        self.set_tab_order()

        for function in ui_init:
            function(self)

    def keyPressEvent(self, event):
        if event.matches(QtGui.QKeySequence.Delete):
            if self.metadata_box.hasFocus():
                self.metadata_box.remove_selected_tags()
            else:
                self.remove()
        else:
            super().keyPressEvent(event)

    def show(self):
        self.restoreWindowState()
        super().show()
        self.metadata_box.restore_state()

    def closeEvent(self, event):
        if config.setting["quit_confirmation"] and not self.show_quit_confirmation():
            event.ignore()
            return
        self.saveWindowState()
        event.accept()

    def show_quit_confirmation(self):
        unsaved_files = sum(a.get_num_unsaved_files() for a in self.tagger.albums.values())
        QMessageBox = QtWidgets.QMessageBox

        if unsaved_files > 0:
            msg = QMessageBox(self)
            msg.setIcon(QMessageBox.Question)
            msg.setWindowModality(QtCore.Qt.WindowModal)
            msg.setWindowTitle(_("Unsaved Changes"))
            msg.setText(_("Are you sure you want to quit Picard?"))
            txt = ngettext(
                "There is %d unsaved file. Closing Picard will lose all unsaved changes.",
                "There are %d unsaved files. Closing Picard will lose all unsaved changes.",
                unsaved_files) % unsaved_files
            msg.setInformativeText(txt)
            cancel = msg.addButton(QMessageBox.Cancel)
            msg.setDefaultButton(cancel)
            msg.addButton(_("&Quit Picard"), QMessageBox.YesRole)
            ret = msg.exec_()

            if ret == QMessageBox.Cancel:
                return False

        return True

    def saveWindowState(self):
        config.persist["window_state"] = self.saveState()
        isMaximized = int(self.windowState()) & QtCore.Qt.WindowMaximized != 0
        self.save_geometry()
        config.persist["window_maximized"] = isMaximized
        config.persist["view_cover_art"] = self.show_cover_art_action.isChecked()
        config.persist["view_toolbar"] = self.show_toolbar_action.isChecked()
        config.persist["view_file_browser"] = self.show_file_browser_action.isChecked()
        config.persist["bottom_splitter_state"] = self.centralWidget().saveState()
        self.file_browser.save_state()
        self.panel.save_state()
        self.metadata_box.save_state()

    @restore_method
    def restoreWindowState(self):
        self.restoreState(config.persist["window_state"])
        self.restore_geometry()
        if config.persist["window_maximized"]:
            self.setWindowState(QtCore.Qt.WindowMaximized)
        bottom_splitter_state = config.persist["bottom_splitter_state"]
        if bottom_splitter_state.isEmpty():
            self.centralWidget().setSizes([366, 194])
        else:
            self.centralWidget().restoreState(bottom_splitter_state)
        self.file_browser.restore_state()

    def create_statusbar(self):
        """Creates a new status bar."""
        self.statusBar().showMessage(_("Ready"))
        self.infostatus = InfoStatus(self)
        self.listening_label = QtWidgets.QLabel()
        self.listening_label.setVisible(False)
        self.listening_label.setToolTip("<qt/>" + _(
            "Picard listens on this port to integrate with your browser. When "
            "you \"Search\" or \"Open in Browser\" from Picard, clicking the "
            "\"Tagger\" button on the web page loads the release into Picard."
        ))
        self.statusBar().addPermanentWidget(self.infostatus)
        self.statusBar().addPermanentWidget(self.listening_label)
        self.tagger.tagger_stats_changed.connect(self.update_statusbar_stats)
        self.tagger.listen_port_changed.connect(self.update_statusbar_listen_port)
        self.update_statusbar_stats()

    @throttle(100)
    def update_statusbar_stats(self):
        """Updates the status bar information."""
        self.infostatus.setFiles(len(self.tagger.files))
        self.infostatus.setAlbums(len(self.tagger.albums))
        self.infostatus.setPendingFiles(File.num_pending_files)
        ws = self.tagger.webservice
        self.infostatus.setPendingRequests(ws.num_pending_web_requests)

    def update_statusbar_listen_port(self, listen_port):
        if listen_port:
            self.listening_label.setVisible(True)
            self.listening_label.setText(_(" Listening on port %(port)d ") % {"port": listen_port})
        else:
            self.listening_label.setVisible(False)

    def set_statusbar_message(self, message, *args, **kwargs):
        """Set the status bar message.

        *args are passed to % operator, if args[0] is a mapping it is used for
        named place holders values
        >>> w.set_statusbar_message("File %(filename)s", {'filename': 'x.txt'})

        Keyword arguments:
        `echo` parameter defaults to `log.debug`, called before message is
        translated, it can be disabled passing None or replaced by ie.
        `log.error`. If None, skipped.

        `translate` is a method called on message before it is sent to history
        log and status bar, it defaults to `_()`. If None, skipped.

        `timeout` defines duration of the display in milliseconds

        `history` is a method called with translated message as argument, it
        defaults to `log.history_info`. If None, skipped.

        Empty messages are never passed to echo and history functions but they
        are sent to status bar (ie. to clear it).
        """
        def isdict(obj):
            return hasattr(obj, 'keys') and hasattr(obj, '__getitem__')

        echo = kwargs.get('echo', log.debug)
        # _ is defined using builtins.__dict__, so setting it as default named argument
        # value doesn't work as expected
        translate = kwargs.get('translate', _)
        timeout = kwargs.get('timeout', 0)
        history = kwargs.get('history', log.history_info)
        if len(args) == 1 and isdict(args[0]):
            # named place holders
            mparms = args[0]
        else:
            # simple place holders, ensure compatibility
            mparms = args
        if message:
            if echo:
                echo(message % mparms)
            if translate:
                message = translate(message)
            message = message % mparms
            if history:
                history(message)
        thread.to_main(self.statusBar().showMessage, message, timeout)

    def _on_submit_acoustid(self):
        if self.tagger.use_acoustid:
            if not config.setting["acoustid_apikey"]:
                QtWidgets.QMessageBox.warning(self,
                    _("Submission Error"),
                    _("You need to configure your AcoustID API key before you can submit fingerprints."))
            else:
                self.tagger.acoustidmanager.submit()

    def create_actions(self):
        self.options_action = QtWidgets.QAction(icontheme.lookup('preferences-desktop'), _("&Options..."), self)
        self.options_action.setMenuRole(QtWidgets.QAction.PreferencesRole)
        self.options_action.triggered.connect(self.show_options)

        self.cut_action = QtWidgets.QAction(icontheme.lookup('edit-cut', icontheme.ICON_SIZE_MENU), _("&Cut"), self)
        self.cut_action.setShortcut(QtGui.QKeySequence.Cut)
        self.cut_action.setEnabled(False)
        self.cut_action.triggered.connect(self.cut)

        self.paste_action = QtWidgets.QAction(icontheme.lookup('edit-paste', icontheme.ICON_SIZE_MENU), _("&Paste"), self)
        self.paste_action.setShortcut(QtGui.QKeySequence.Paste)
        self.paste_action.setEnabled(False)
        self.paste_action.triggered.connect(self.paste)

        self.help_action = QtWidgets.QAction(_("&Help..."), self)
        self.help_action.setShortcut(QtGui.QKeySequence.HelpContents)
        self.help_action.triggered.connect(self.show_help)

        self.about_action = QtWidgets.QAction(_("&About..."), self)
        self.about_action.setMenuRole(QtWidgets.QAction.AboutRole)
        self.about_action.triggered.connect(self.show_about)

        self.donate_action = QtWidgets.QAction(_("&Donate..."), self)
        self.donate_action.triggered.connect(self.open_donation_page)

        self.report_bug_action = QtWidgets.QAction(_("&Report a Bug..."), self)
        self.report_bug_action.triggered.connect(self.open_bug_report)

        self.support_forum_action = QtWidgets.QAction(_("&Support Forum..."), self)
        self.support_forum_action.triggered.connect(self.open_support_forum)

        self.add_files_action = QtWidgets.QAction(icontheme.lookup('document-open'), _("&Add Files..."), self)
        self.add_files_action.setStatusTip(_("Add files to the tagger"))
        # TR: Keyboard shortcut for "Add Files..."
        self.add_files_action.setShortcut(QtGui.QKeySequence.Open)
        self.add_files_action.triggered.connect(self.add_files)

        self.add_directory_action = QtWidgets.QAction(icontheme.lookup('folder'), _("A&dd Folder..."), self)
        self.add_directory_action.setStatusTip(_("Add a folder to the tagger"))
        # TR: Keyboard shortcut for "Add Directory..."
        self.add_directory_action.setShortcut(QtGui.QKeySequence(_("Ctrl+D")))
        self.add_directory_action.triggered.connect(self.add_directory)

        self.save_action = QtWidgets.QAction(icontheme.lookup('document-save'), _("&Save"), self)
        self.save_action.setStatusTip(_("Save selected files"))
        # TR: Keyboard shortcut for "Save"
        self.save_action.setShortcut(QtGui.QKeySequence.Save)
        self.save_action.setEnabled(False)
        self.save_action.triggered.connect(self.save)

        self.submit_acoustid_action = QtWidgets.QAction(icontheme.lookup('acoustid-fingerprinter'), _("S&ubmit AcoustIDs"), self)
        self.submit_acoustid_action.setStatusTip(_("Submit acoustic fingerprints"))
        self.submit_acoustid_action.setEnabled(False)
        self.submit_acoustid_action.triggered.connect(self._on_submit_acoustid)

        self.exit_action = QtWidgets.QAction(_("E&xit"), self)
        self.exit_action.setMenuRole(QtWidgets.QAction.QuitRole)
        # TR: Keyboard shortcut for "Exit"
        self.exit_action.setShortcut(QtGui.QKeySequence(_("Ctrl+Q")))
        self.exit_action.triggered.connect(self.close)

        self.remove_action = QtWidgets.QAction(icontheme.lookup('list-remove'), _("&Remove"), self)
        self.remove_action.setStatusTip(_("Remove selected files/albums"))
        self.remove_action.setEnabled(False)
        self.remove_action.triggered.connect(self.remove)

        self.browser_lookup_action = QtWidgets.QAction(icontheme.lookup('lookup-musicbrainz'), _("Lookup in &Browser"), self)
        self.browser_lookup_action.setStatusTip(_("Lookup selected item on MusicBrainz website"))
        self.browser_lookup_action.setEnabled(False)
        # TR: Keyboard shortcut for "Lookup in Browser"
        self.browser_lookup_action.setShortcut(QtGui.QKeySequence(_("Ctrl+Shift+L")))
        self.browser_lookup_action.triggered.connect(self.browser_lookup)

        self.album_search_action = QtWidgets.QAction(icontheme.lookup('system-search'), _("Search for similar albums..."), self)
        self.album_search_action.setStatusTip(_("View similar releases and optionally choose a different release"))
        self.album_search_action.triggered.connect(self.show_more_albums)

        self.track_search_action = QtWidgets.QAction(icontheme.lookup('system-search'), _("Search for similar tracks..."), self)
        self.track_search_action.setStatusTip(_("View similar tracks and optionally choose a different release"))
        self.track_search_action.triggered.connect(self.show_more_tracks)

        self.show_file_browser_action = QtWidgets.QAction(_("File &Browser"), self)
        self.show_file_browser_action.setCheckable(True)
        if config.persist["view_file_browser"]:
            self.show_file_browser_action.setChecked(True)
        self.show_file_browser_action.setShortcut(QtGui.QKeySequence(_("Ctrl+B")))
        self.show_file_browser_action.triggered.connect(self.show_file_browser)

        self.show_cover_art_action = QtWidgets.QAction(_("&Cover Art"), self)
        self.show_cover_art_action.setCheckable(True)
        if config.persist["view_cover_art"]:
            self.show_cover_art_action.setChecked(True)
        self.show_cover_art_action.triggered.connect(self.show_cover_art)

        self.show_toolbar_action = QtWidgets.QAction(_("&Actions"), self)
        self.show_toolbar_action.setCheckable(True)
        if config.persist["view_toolbar"]:
            self.show_toolbar_action.setChecked(True)
        self.show_toolbar_action.triggered.connect(self.show_toolbar)

        self.search_action = QtWidgets.QAction(icontheme.lookup('system-search'), _("Search"), self)
        self.search_action.setEnabled(False)
        self.search_action.triggered.connect(self.search)

        self.cd_lookup_action = QtWidgets.QAction(icontheme.lookup('media-optical'), _("Lookup &CD..."), self)
        self.cd_lookup_action.setStatusTip(_("Lookup the details of the CD in your drive"))
        # TR: Keyboard shortcut for "Lookup CD"
        self.cd_lookup_action.setShortcut(QtGui.QKeySequence(_("Ctrl+K")))
        self.cd_lookup_action.triggered.connect(self.tagger.lookup_cd)

        self.cd_lookup_menu = QtWidgets.QMenu(_("Lookup &CD..."))
        self.cd_lookup_menu.triggered.connect(self.tagger.lookup_cd)
        self.cd_lookup_action.setEnabled(False)
        if discid is None:
            log.warning("CDROM: discid library not found - Lookup CD functionality disabled")
        else:
            drives = get_cdrom_drives()
            if not drives:
                log.warning("CDROM: No CD-ROM drives found - Lookup CD functionality disabled")
            else:
                shortcut_drive = config.setting["cd_lookup_device"].split(",")[0] if len(drives) > 1 else ""
                self.cd_lookup_action.setEnabled(True)
                for drive in drives:
                    action = self.cd_lookup_menu.addAction(drive)
                    if drive == shortcut_drive:
                        # Clear existing shortcode on main action and assign it to sub-action
                        self.cd_lookup_action.setShortcut(QtGui.QKeySequence())
                        action.setShortcut(QtGui.QKeySequence(_("Ctrl+K")))

        self.analyze_action = QtWidgets.QAction(icontheme.lookup('picard-analyze'), _("&Scan"), self)
        self.analyze_action.setStatusTip(_("Use AcoustID audio fingerprint to identify the files by the actual music, even if they have no metadata"))
        self.analyze_action.setEnabled(False)
        self.analyze_action.setToolTip(_('Identify the file using its AcoustID audio fingerprint'))
        # TR: Keyboard shortcut for "Analyze"
        self.analyze_action.setShortcut(QtGui.QKeySequence(_("Ctrl+Y")))
        self.analyze_action.triggered.connect(self.analyze)

        self.cluster_action = QtWidgets.QAction(icontheme.lookup('picard-cluster'), _("Cl&uster"), self)
        self.cluster_action.setStatusTip(_("Cluster files into album clusters"))
        self.cluster_action.setEnabled(False)
        # TR: Keyboard shortcut for "Cluster"
        self.cluster_action.setShortcut(QtGui.QKeySequence(_("Ctrl+U")))
        self.cluster_action.triggered.connect(self.cluster)

        self.autotag_action = QtWidgets.QAction(icontheme.lookup('picard-auto-tag'), _("&Lookup"), self)
        tip = _("Lookup selected items in MusicBrainz")
        self.autotag_action.setToolTip(tip)
        self.autotag_action.setStatusTip(tip)
        self.autotag_action.setEnabled(False)
        # TR: Keyboard shortcut for "Lookup"
        self.autotag_action.setShortcut(QtGui.QKeySequence(_("Ctrl+L")))
        self.autotag_action.triggered.connect(self.autotag)

        self.view_info_action = QtWidgets.QAction(icontheme.lookup('picard-edit-tags'), _("&Info..."), self)
        self.view_info_action.setEnabled(False)
        # TR: Keyboard shortcut for "Info"
        self.view_info_action.setShortcut(QtGui.QKeySequence(_("Ctrl+I")))
        self.view_info_action.triggered.connect(self.view_info)

        self.refresh_action = QtWidgets.QAction(icontheme.lookup('view-refresh', icontheme.ICON_SIZE_MENU), _("&Refresh"), self)
        self.refresh_action.setShortcut(QtGui.QKeySequence(_("Ctrl+R")))
        self.refresh_action.triggered.connect(self.refresh)

        self.enable_renaming_action = QtWidgets.QAction(_("&Rename Files"), self)
        self.enable_renaming_action.setCheckable(True)
        self.enable_renaming_action.setChecked(config.setting["rename_files"])
        self.enable_renaming_action.triggered.connect(self.toggle_rename_files)

        self.enable_moving_action = QtWidgets.QAction(_("&Move Files"), self)
        self.enable_moving_action.setCheckable(True)
        self.enable_moving_action.setChecked(config.setting["move_files"])
        self.enable_moving_action.triggered.connect(self.toggle_move_files)

        self.enable_tag_saving_action = QtWidgets.QAction(_("Save &Tags"), self)
        self.enable_tag_saving_action.setCheckable(True)
        self.enable_tag_saving_action.setChecked(not config.setting["dont_write_tags"])
        self.enable_tag_saving_action.triggered.connect(self.toggle_tag_saving)

        self.tags_from_filenames_action = QtWidgets.QAction(_("Tags From &File Names..."), self)
        self.tags_from_filenames_action.triggered.connect(self.open_tags_from_filenames)
        self.tags_from_filenames_action.setEnabled(False)

        self.open_collection_in_browser_action = QtWidgets.QAction(_("&Open My Collections in Browser"), self)
        self.open_collection_in_browser_action.triggered.connect(self.open_collection_in_browser)
        self.open_collection_in_browser_action.setEnabled(config.setting["username"] != '')

        self.view_log_action = QtWidgets.QAction(_("View &Error/Debug Log"), self)
        self.view_log_action.triggered.connect(self.show_log)
        # TR: Keyboard shortcut for "View Error/Debug Log"
        self.view_log_action.setShortcut(QtGui.QKeySequence(_("Ctrl+E")))

        self.view_history_action = QtWidgets.QAction(_("View Activity &History"), self)
        self.view_history_action.triggered.connect(self.show_history)
        # TR: Keyboard shortcut for "View Activity History"
        self.view_history_action.setShortcut(QtGui.QKeySequence(_("Ctrl+H")))

        webservice_manager = self.tagger.webservice.manager
        webservice_manager.authenticationRequired.connect(self.show_password_dialog)
        webservice_manager.proxyAuthenticationRequired.connect(self.show_proxy_dialog)

        self.play_file_action = QtWidgets.QAction(icontheme.lookup('play-music'), _("Open in &Player"), self)
        self.play_file_action.setStatusTip(_("Play the file in your default media player"))
        self.play_file_action.setEnabled(False)
        self.play_file_action.triggered.connect(self.play_file)

        self.open_folder_action = QtWidgets.QAction(icontheme.lookup('folder', icontheme.ICON_SIZE_MENU), _("Open Containing &Folder"), self)
        self.open_folder_action.setStatusTip(_("Open the containing folder in your file explorer"))
        self.open_folder_action.setEnabled(False)
        self.open_folder_action.triggered.connect(self.open_folder)

    def toggle_rename_files(self, checked):
        config.setting["rename_files"] = checked

    def toggle_move_files(self, checked):
        config.setting["move_files"] = checked

    def toggle_tag_saving(self, checked):
        config.setting["dont_write_tags"] = not checked

    def get_selected_or_unmatched_files(self):
        files = self.tagger.get_files_from_objects(self.selected_objects)
        if not files:
            files = self.tagger.unclustered_files.files
        return files

    def open_tags_from_filenames(self):
        files = self.get_selected_or_unmatched_files()
        if files:
            dialog = TagsFromFileNamesDialog(files, self)
            dialog.exec_()

    def open_collection_in_browser(self):
        self.tagger.collection_lookup()

    def create_menus(self):
        menu = self.menuBar().addMenu(_("&File"))
        menu.addAction(self.add_directory_action)
        menu.addAction(self.add_files_action)
        menu.addSeparator()
        menu.addAction(self.play_file_action)
        menu.addAction(self.open_folder_action)
        menu.addSeparator()
        menu.addAction(self.save_action)
        menu.addAction(self.submit_acoustid_action)
        menu.addSeparator()
        menu.addAction(self.exit_action)
        menu = self.menuBar().addMenu(_("&Edit"))
        menu.addAction(self.cut_action)
        menu.addAction(self.paste_action)
        menu.addSeparator()
        menu.addAction(self.view_info_action)
        menu.addAction(self.remove_action)
        menu = self.menuBar().addMenu(_("&View"))
        menu.addAction(self.show_file_browser_action)
        menu.addAction(self.show_cover_art_action)
        menu.addSeparator()
        menu.addAction(self.show_toolbar_action)
        menu.addAction(self.search_toolbar_toggle_action)
        menu = self.menuBar().addMenu(_("&Options"))
        menu.addAction(self.enable_renaming_action)
        menu.addAction(self.enable_moving_action)
        menu.addAction(self.enable_tag_saving_action)
        menu.addSeparator()
        menu.addAction(self.options_action)
        menu = self.menuBar().addMenu(_("&Tools"))
        menu.addAction(self.refresh_action)
        if len(self.cd_lookup_menu.actions()) > 1:
            menu.addMenu(self.cd_lookup_menu)
        else:
            menu.addAction(self.cd_lookup_action)
        menu.addAction(self.autotag_action)
        menu.addAction(self.analyze_action)
        menu.addAction(self.cluster_action)
        menu.addAction(self.browser_lookup_action)
        menu.addSeparator()
        menu.addAction(self.tags_from_filenames_action)
        menu.addAction(self.open_collection_in_browser_action)
        self.menuBar().addSeparator()
        menu = self.menuBar().addMenu(_("&Help"))
        menu.addAction(self.help_action)
        menu.addSeparator()
        menu.addAction(self.view_history_action)
        menu.addSeparator()
        menu.addAction(self.support_forum_action)
        menu.addAction(self.report_bug_action)
        menu.addAction(self.view_log_action)
        menu.addSeparator()
        menu.addAction(self.donate_action)
        menu.addAction(self.about_action)

    def update_toolbar_style(self):
        if config.setting["toolbar_show_labels"]:
            self.toolbar.setToolButtonStyle(QtCore.Qt.ToolButtonTextUnderIcon)
        else:
            self.toolbar.setToolButtonStyle(QtCore.Qt.ToolButtonIconOnly)


    def create_toolbar(self):
        self.create_search_toolbar()
        self.create_action_toolbar()

    def create_action_toolbar(self):
        if self.toolbar:
            self.toolbar.clear()
            self.removeToolBar(self.toolbar)
        self.toolbar = toolbar = QtWidgets.QToolBar(_("Actions"))
        self.insertToolBar(self.search_toolbar, self.toolbar)
        self.update_toolbar_style()
        toolbar.setObjectName("main_toolbar")

        def add_toolbar_action(action):
            toolbar.addAction(action)
            widget = toolbar.widgetForAction(action)
            widget.setFocusPolicy(QtCore.Qt.TabFocus)
            widget.setAttribute(QtCore.Qt.WA_MacShowFocusRect)

        for action in config.setting['toolbar_layout']:
            if action == 'cd_lookup_action':
                add_toolbar_action(self.cd_lookup_action)
                if len(self.cd_lookup_menu.actions()) > 1:
                    button = toolbar.widgetForAction(self.cd_lookup_action)
                    button.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup)
                    button.setMenu(self.cd_lookup_menu)
            elif action == 'separator':
                toolbar.addSeparator()
            else:
                try:
                    add_toolbar_action(getattr(self, action))
                except AttributeError:
                    log.warning('Warning: Unknown action name "%r" found in config. Ignored.', action)
        self.show_toolbar()

    def create_search_toolbar(self):
        self.search_toolbar = toolbar = self.addToolBar(_("Search"))
        self.search_toolbar_toggle_action = self.search_toolbar.toggleViewAction()
        toolbar.setObjectName("search_toolbar")
        search_panel = QtWidgets.QWidget(toolbar)
        hbox = QtWidgets.QHBoxLayout(search_panel)
        self.search_combo = QtWidgets.QComboBox(search_panel)
        self.search_combo.addItem(_("Album"), "album")
        self.search_combo.addItem(_("Artist"), "artist")
        self.search_combo.addItem(_("Track"), "track")
        hbox.addWidget(self.search_combo, 0)
        self.search_edit = ButtonLineEdit(search_panel)
        self.search_edit.returnPressed.connect(self.trigger_search_action)
        self.search_edit.textChanged.connect(self.enable_search)
        hbox.addWidget(self.search_edit, 0)
        self.search_button = QtWidgets.QToolButton(search_panel)
        self.search_button.setAutoRaise(True)
        self.search_button.setDefaultAction(self.search_action)
        self.search_button.setIconSize(QtCore.QSize(22, 22))
        self.search_button.setAttribute(QtCore.Qt.WA_MacShowFocusRect)

        # search button contextual menu, shortcut to toggle search options
        def search_button_menu(position):
            menu = QtWidgets.QMenu()
            opts = OrderedDict([
                ('use_adv_search_syntax', N_("&Advanced search")),
                ('builtin_search', N_("&Builtin search"))
            ])

            def toggle_opt(opt, checked):
                config.setting[opt] = checked

            for opt, label in opts.items():
                action = QtWidgets.QAction(_(label), menu)
                action.setCheckable(True)
                action.setChecked(config.setting[opt])
                action.triggered.connect(partial(toggle_opt, opt))
                menu.addAction(action)
            menu.exec_(self.search_button.mapToGlobal(position))

        self.search_button.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.search_button.customContextMenuRequested.connect(search_button_menu)
        hbox.addWidget(self.search_button)
        toolbar.addWidget(search_panel)

    def set_tab_order(self):
        tab_order = self.setTabOrder
        tw = self.toolbar.widgetForAction
        prev_action = None
        current_action = None
        # Setting toolbar widget tab-orders for accessibility
        for action in config.setting['toolbar_layout']:
            if action != 'separator':
                try:
                    current_action = tw(getattr(self, action))
                except AttributeError:
                    # No need to log warnings since we have already
                    # done it once in create_toolbar
                    pass

            if prev_action is not None and prev_action != current_action:
                tab_order(prev_action, current_action)

            prev_action = current_action

        tab_order(prev_action, self.search_combo)
        tab_order(self.search_combo, self.search_edit)
        tab_order(self.search_edit, self.search_button)
        # Panels
        tab_order(self.search_button, self.file_browser)
        tab_order(self.file_browser, self.panel.views[0])
        tab_order(self.panel.views[0], self.panel.views[1])
        tab_order(self.panel.views[1], self.metadata_box)

    def enable_submit(self, enabled):
        """Enable/disable the 'Submit fingerprints' action."""
        self.submit_acoustid_action.setEnabled(enabled)

    def enable_cluster(self, enabled):
        """Enable/disable the 'Cluster' action."""
        self.cluster_action.setEnabled(enabled)

    def enable_search(self):
        """Enable/disable the 'Search' action."""
        if self.search_edit.text():
            self.search_action.setEnabled(True)
        else:
            self.search_action.setEnabled(False)

    def trigger_search_action(self):
        if self.search_action.isEnabled():
            self.search_action.trigger()

    def search_mbid_found(self, entity, mbid):
        self.search_edit.setText('%s:%s' % (entity, mbid))

    def search(self):
        """Search for album, artist or track on the MusicBrainz website."""
        text = self.search_edit.text()
        entity = self.search_combo.itemData(self.search_combo.currentIndex())
        self.tagger.search(text, entity,
                           config.setting["use_adv_search_syntax"],
                           mbid_matched_callback=self.search_mbid_found)

    def add_files(self):
        """Add files to the tagger."""
        current_directory = find_starting_directory()
        formats = []
        extensions = []
        for exts, name in supported_formats():
            exts = ["*" + e for e in exts]
            formats.append("%s (%s)" % (name, " ".join(exts)))
            extensions.extend(exts)
        formats.sort()
        extensions.sort()
        formats.insert(0, _("All Supported Formats") + " (%s)" % " ".join(extensions))
        files, _filter = QtWidgets.QFileDialog.getOpenFileNames(self, "", current_directory, ";;".join(formats))
        if files:
            config.persist["current_directory"] = os.path.dirname(files[0])
            self.tagger.add_files(files)

    def add_directory(self):
        """Add directory to the tagger."""
        current_directory = find_starting_directory()

        dir_list = []
        if not config.setting["toolbar_multiselect"]:
            directory = QtWidgets.QFileDialog.getExistingDirectory(self, "", current_directory)
            if directory:
                dir_list.append(directory)
        else:
            file_dialog = MultiDirsSelectDialog(self, "", current_directory)
            if file_dialog.exec_() == QtWidgets.QDialog.Accepted:
                dir_list = file_dialog.selectedFiles()

        dir_count = len(dir_list)
        if dir_count:
            parent = os.path.dirname(dir_list[0]) if dir_count > 1 else dir_list[0]
            config.persist["current_directory"] = parent
            if dir_count > 1:
                self.set_statusbar_message(
                    N_("Adding multiple directories from '%(directory)s' ..."),
                    {'directory': parent}
                )
            else:
                self.set_statusbar_message(
                    N_("Adding directory: '%(directory)s' ..."),
                    {'directory': dir_list[0]}
                )

            for directory in dir_list:
                self.tagger.add_directory(directory)

    def show_about(self):
        self.show_options("about")

    def show_options(self, page=None):
        dialog = OptionsDialog(page, self)
        dialog.exec_()

    def show_help(self):
        webbrowser2.goto('documentation')

    def show_log(self):
        self.logDialog.show()
        self.logDialog.raise_()
        self.logDialog.activateWindow()

    def show_history(self):
        self.historyDialog.show()
        self.historyDialog.raise_()
        self.historyDialog.activateWindow()

    def open_bug_report(self):
        webbrowser2.goto('troubleshooting')

    def open_support_forum(self):
        webbrowser2.goto('forum')

    def open_donation_page(self):
        webbrowser2.goto('donate')

    def save(self):
        """Tell the tagger to save the selected objects."""
        self.tagger.save(self.selected_objects)

    def remove(self):
        """Tell the tagger to remove the selected objects."""
        self.panel.remove(self.selected_objects)

    def analyze(self):
        if not config.setting['fingerprinting_system']:
            if self.show_analyze_settings_info():
                self.show_options("fingerprinting")
            if not config.setting['fingerprinting_system']:
                return
        return self.tagger.analyze(self.selected_objects)

    def _openUrl(self,url):
        # Resolves a bug in Qt opening remote URLs - QTBUG-13359
        # See https://bugreports.qt.io/browse/QTBUG-13359
        if url.startswith("\\\\") or url.startswith("//"):
            return QtCore.QUrl(QtCore.QDir.toNativeSeparators(url))
        else:
            return QtCore.QUrl.fromLocalFile(url)

    def play_file(self):
        files = self.tagger.get_files_from_objects(self.selected_objects)
        for file in files:
            QtGui.QDesktopServices.openUrl(self._openUrl(file.filename))

    def open_folder(self):
        files = self.tagger.get_files_from_objects(self.selected_objects)
        folders = set([os.path.dirname(f.filename) for f in files])
        for folder in folders:
            QtGui.QDesktopServices.openUrl(self._openUrl(folder))

    def show_analyze_settings_info(self):
        ret = QtWidgets.QMessageBox.question(self,
            _("Configuration Required"),
            _("Audio fingerprinting is not yet configured. Would you like to configure it now?"),
            QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
            QtWidgets.QMessageBox.Yes)
        return ret == QtWidgets.QMessageBox.Yes

    def show_more_tracks(self):
        obj = self.selected_objects[0]
        if isinstance(obj, Track):
            obj = obj.linked_files[0]
        dialog = TrackSearchDialog(self)
        dialog.load_similar_tracks(obj)
        dialog.exec_()

    def show_more_albums(self):
        obj = self.selected_objects[0]
        dialog = AlbumSearchDialog(self)
        dialog.show_similar_albums(obj)
        dialog.exec_()

    def view_info(self, default_tab=0):
        if isinstance(self.selected_objects[0], Album):
            album = self.selected_objects[0]
            dialog = AlbumInfoDialog(album, self)
        elif isinstance(self.selected_objects[0], Cluster):
            cluster = self.selected_objects[0]
            dialog = ClusterInfoDialog(cluster, self)
        elif isinstance(self.selected_objects[0], Track):
            track = self.selected_objects[0]
            dialog = TrackInfoDialog(track, self)
        else:
            file = self.tagger.get_files_from_objects(self.selected_objects)[0]
            dialog = FileInfoDialog(file, self)
        dialog.ui.tabWidget.setCurrentIndex(default_tab)
        dialog.exec_()

    def cluster(self):
        self.tagger.cluster(self.selected_objects)
        self.update_actions()

    def refresh(self):
        self.tagger.refresh(self.selected_objects)

    def browser_lookup(self):
        self.tagger.browser_lookup(self.selected_objects[0])

    @throttle(100)
    def update_actions(self):
        can_remove = False
        can_save = False
        can_analyze = False
        can_refresh = False
        can_autotag = False
        single = self.selected_objects[0] if len(self.selected_objects) == 1 else None
        can_view_info = bool(single and single.can_view_info())
        can_browser_lookup = bool(single and single.can_browser_lookup())
        have_files = bool(self.tagger.get_files_from_objects(self.selected_objects))
        have_objects = bool(self.selected_objects)
        for obj in self.selected_objects:
            if obj is None:
                continue
            if obj.can_analyze():
                can_analyze = True
            if obj.can_save():
                can_save = True
            if obj.can_remove():
                can_remove = True
            if obj.can_refresh():
                can_refresh = True
            if obj.can_autotag():
                can_autotag = True
            # Skip further loops if all values now True.
            if can_analyze and can_save and can_remove and can_refresh and can_autotag:
                break
        self.remove_action.setEnabled(can_remove)
        self.save_action.setEnabled(can_save)
        self.view_info_action.setEnabled(can_view_info)
        self.analyze_action.setEnabled(can_analyze)
        self.refresh_action.setEnabled(can_refresh)
        self.autotag_action.setEnabled(can_autotag)
        self.browser_lookup_action.setEnabled(can_browser_lookup)
        self.play_file_action.setEnabled(have_files)
        self.open_folder_action.setEnabled(have_files)
        self.cut_action.setEnabled(have_objects)
        files = self.get_selected_or_unmatched_files()
        self.tags_from_filenames_action.setEnabled(bool(files))
        self.track_search_action.setEnabled(have_objects)

    def update_selection(self, objects=None):
        if self.ignore_selection_changes:
            return

        if objects is not None:
            self.selected_objects = objects
        else:
            objects = self.selected_objects

        self.update_actions()

        metadata = None
        orig_metadata = None
        obj = None

        # Clear any existing status bar messages
        self.set_statusbar_message("")

        if len(objects) == 1:
            obj = list(objects)[0]
            if isinstance(obj, File):
                metadata = obj.metadata
                orig_metadata = obj.orig_metadata
                if obj.state == obj.ERROR:
                    msg = N_("%(filename)s (error: %(error)s)")
                    mparms = {
                        'filename': obj.filename,
                        'error': obj.error
                    }
                else:
                    msg = N_("%(filename)s")
                    mparms = {
                        'filename': obj.filename,
                    }
                self.set_statusbar_message(msg, mparms, echo=None, history=None)
            elif isinstance(obj, Track):
                metadata = obj.metadata
                if obj.num_linked_files == 1:
                    file = obj.linked_files[0]
                    orig_metadata = file.orig_metadata
                    if file.state == File.ERROR:
                        msg = N_("%(filename)s (%(similarity)d%%) (error: %(error)s)")
                        mparms = {
                            'filename': file.filename,
                            'similarity': file.similarity * 100,
                            'error': file.error
                        }
                    else:
                        msg = N_("%(filename)s (%(similarity)d%%)")
                        mparms = {
                            'filename': file.filename,
                            'similarity': file.similarity * 100,
                        }
                    self.set_statusbar_message(msg, mparms, echo=None,
                                               history=None)
            elif isinstance(obj, Album):
                metadata = obj.metadata
                orig_metadata = obj.orig_metadata
            elif obj.can_edit_tags():
                metadata = obj.metadata

        self.metadata_box.selection_dirty = True
        self.metadata_box.update()
        self.cover_art_box.set_metadata(metadata, orig_metadata, obj)
        self.selection_updated.emit(objects)

    def show_cover_art(self):
        """Show/hide the cover art box."""
        if self.show_cover_art_action.isChecked():
            self.cover_art_box.show()
        else:
            self.cover_art_box.hide()

    def show_toolbar(self):
        """Show/hide the Action toolbar."""
        if self.show_toolbar_action.isChecked():
            self.toolbar.show()
        else:
            self.toolbar.hide()

    def show_file_browser(self):
        """Show/hide the file browser."""
        if self.show_file_browser_action.isChecked():
            sizes = self.panel.sizes()
            if sizes[0] == 0:
                sizes[0] = sum(sizes) // 4
                self.panel.setSizes(sizes)
            self.file_browser.show()
        else:
            self.file_browser.hide()

    def show_password_dialog(self, reply, authenticator):
        if reply.url().host() == config.setting['server_host']:
            ret = QtWidgets.QMessageBox.question(self,
                _("Authentication Required"),
                _("Picard needs authorization to access your personal data on the MusicBrainz server. Would you like to log in now?"),
                QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
                QtWidgets.QMessageBox.Yes)
            if ret == QtWidgets.QMessageBox.Yes:
                pass
        else:
            dialog = PasswordDialog(authenticator, reply, parent=self)
            dialog.exec_()

    def show_proxy_dialog(self, proxy, authenticator):
        dialog = ProxyDialog(authenticator, proxy, parent=self)
        dialog.exec_()

    def autotag(self):
        self.tagger.autotag(self.selected_objects)

    def cut(self):
        self.tagger.copy_files(self.selected_objects)
        self.paste_action.setEnabled(bool(self.selected_objects))

    def paste(self):
        selected_objects = self.selected_objects
        if not selected_objects:
            target = self.tagger.unclustered_files
        else:
            target = selected_objects[0]
        self.tagger.paste_files(target)
        self.paste_action.setEnabled(False)
Example #24
0
class GeneralOptionsPage(OptionsPage):

    NAME = "general"
    TITLE = N_("General")
    PARENT = None
    SORT_ORDER = 1
    ACTIVE = True

    options = [
        config.TextOption("setting", "server_host", MUSICBRAINZ_SERVERS[0]),
        config.IntOption("setting", "server_port", 443),
        config.TextOption("persist", "oauth_refresh_token", ""),
        config.BoolOption("setting", "analyze_new_files", False),
        config.BoolOption("setting", "ignore_file_mbids", False),
        config.TextOption("persist", "oauth_refresh_token", ""),
        config.TextOption("persist", "oauth_refresh_token_scopes", ""),
        config.TextOption("persist", "oauth_access_token", ""),
        config.IntOption("persist", "oauth_access_token_expires", 0),
        config.TextOption("persist", "oauth_username", ""),
        config.BoolOption("setting", "check_for_updates", True),
        config.IntOption("setting", "update_check_days", 7),
        config.IntOption("setting", "update_level", 0),
        config.IntOption("persist", "last_update_check", 0),
    ]

    def __init__(self, parent=None):
        super().__init__(parent)
        self.ui = Ui_GeneralOptionsPage()
        self.ui.setupUi(self)
        self.ui.server_host.addItems(MUSICBRAINZ_SERVERS)
        self.ui.login.clicked.connect(self.login)
        self.ui.logout.clicked.connect(self.logout)
        self.update_login_logout()

    def load(self):
        self.ui.server_host.setEditText(config.setting["server_host"])
        self.ui.server_port.setValue(config.setting["server_port"])
        self.ui.analyze_new_files.setChecked(
            config.setting["analyze_new_files"])
        self.ui.ignore_file_mbids.setChecked(
            config.setting["ignore_file_mbids"])
        if self.tagger.autoupdate_enabled:
            self.ui.check_for_updates.setChecked(
                config.setting["check_for_updates"])
            self.ui.update_level.clear()
            for level, description in PROGRAM_UPDATE_LEVELS.items():
                # TODO: Remove temporary workaround once https://github.com/python-babel/babel/issues/415 has been resolved.
                babel_415_workaround = description['title']
                self.ui.update_level.addItem(_(babel_415_workaround), level)
            self.ui.update_level.setCurrentIndex(
                self.ui.update_level.findData(config.setting["update_level"]))
            self.ui.update_check_days.setValue(
                config.setting["update_check_days"])
        else:
            self.ui.update_check_groupbox.hide()

    def save(self):
        config.setting["server_host"] = self.ui.server_host.currentText(
        ).strip()
        config.setting["server_port"] = self.ui.server_port.value()
        config.setting[
            "analyze_new_files"] = self.ui.analyze_new_files.isChecked()
        config.setting[
            "ignore_file_mbids"] = self.ui.ignore_file_mbids.isChecked()
        if self.tagger.autoupdate_enabled:
            config.setting[
                "check_for_updates"] = self.ui.check_for_updates.isChecked()
            config.setting["update_level"] = self.ui.update_level.currentData(
                QtCore.Qt.UserRole)
            config.setting[
                "update_check_days"] = self.ui.update_check_days.value()

    def update_login_logout(self):
        if self.tagger.webservice.oauth_manager.is_logged_in():
            self.ui.logged_in.setText(
                _("Logged in as <b>%s</b>.") %
                config.persist["oauth_username"])
            self.ui.logged_in.show()
            self.ui.login.hide()
            self.ui.logout.show()
        else:
            self.ui.logged_in.hide()
            self.ui.login.show()
            self.ui.logout.hide()

    def login(self):
        self.tagger.mb_login(self.on_login_finished, self)

    def restore_defaults(self):
        super().restore_defaults()
        self.logout()

    def on_login_finished(self, successful):
        self.update_login_logout()

    def logout(self):
        self.tagger.mb_logout()
        self.update_login_logout()
Example #25
0
class FingerprintingOptionsPage(OptionsPage):

    NAME = "fingerprinting"
    TITLE = N_("Fingerprinting")
    PARENT = None
    SORT_ORDER = 45
    ACTIVE = True

    options = [
        config.BoolOption("setting", "ignore_existing_acoustid_fingerprints",
                          False),
        config.TextOption("setting", "fingerprinting_system", "acoustid"),
        config.TextOption("setting", "acoustid_fpcalc", ""),
        config.TextOption("setting", "acoustid_apikey", ""),
    ]

    def __init__(self, parent=None):
        super().__init__(parent)
        self._fpcalc_valid = True
        self.ui = Ui_FingerprintingOptionsPage()
        self.ui.setupUi(self)
        self.ui.disable_fingerprinting.clicked.connect(self.update_groupboxes)
        self.ui.use_acoustid.clicked.connect(self.update_groupboxes)
        self.ui.acoustid_fpcalc.textChanged.connect(
            self._acoustid_fpcalc_check)
        self.ui.acoustid_fpcalc_browse.clicked.connect(
            self.acoustid_fpcalc_browse)
        self.ui.acoustid_fpcalc_download.clicked.connect(
            self.acoustid_fpcalc_download)
        self.ui.acoustid_apikey_get.clicked.connect(self.acoustid_apikey_get)

    def load(self):
        if config.setting["fingerprinting_system"] == "acoustid":
            self.ui.use_acoustid.setChecked(True)
        else:
            self.ui.disable_fingerprinting.setChecked(True)
        self.ui.acoustid_fpcalc.setText(config.setting["acoustid_fpcalc"])
        self.ui.acoustid_apikey.setText(config.setting["acoustid_apikey"])
        self.ui.ignore_existing_acoustid_fingerprints.setChecked(
            config.setting["ignore_existing_acoustid_fingerprints"])
        self.update_groupboxes()

    def save(self):
        if self.ui.use_acoustid.isChecked():
            config.setting["fingerprinting_system"] = "acoustid"
        else:
            config.setting["fingerprinting_system"] = ""
        config.setting["acoustid_fpcalc"] = self.ui.acoustid_fpcalc.text()
        config.setting["acoustid_apikey"] = self.ui.acoustid_apikey.text()
        config.setting[
            "ignore_existing_acoustid_fingerprints"] = self.ui.ignore_existing_acoustid_fingerprints.isChecked(
            )

    def update_groupboxes(self):
        if self.ui.use_acoustid.isChecked():
            self.ui.acoustid_settings.setEnabled(True)
            if not self.ui.acoustid_fpcalc.text():
                fpcalc_path = find_executable(*FPCALC_NAMES)
                if fpcalc_path:
                    self.ui.acoustid_fpcalc.setText(fpcalc_path)
        else:
            self.ui.acoustid_settings.setEnabled(False)
        self._acoustid_fpcalc_check()

    def acoustid_fpcalc_browse(self):
        path, _filter = QtWidgets.QFileDialog.getOpenFileName(
            self, "", self.ui.acoustid_fpcalc.text())
        if path:
            path = os.path.normpath(path)
            self.ui.acoustid_fpcalc.setText(path)

    def acoustid_fpcalc_download(self):
        webbrowser2.goto('chromaprint')

    def acoustid_apikey_get(self):
        webbrowser2.goto('acoustid_apikey')

    def _acoustid_fpcalc_check(self):
        if not self.ui.use_acoustid.isChecked():
            self._acoustid_fpcalc_set_success("")
            return
        fpcalc = self.ui.acoustid_fpcalc.text()
        if not fpcalc:
            self._acoustid_fpcalc_set_success("")
            return

        self._fpcalc_valid = False
        process = QtCore.QProcess(self)
        process.finished.connect(self._on_acoustid_fpcalc_check_finished)
        process.error.connect(self._on_acoustid_fpcalc_check_error)
        process.start(fpcalc, ["-v"])

    def _on_acoustid_fpcalc_check_finished(self, exit_code, exit_status):
        process = self.sender()
        if exit_code == 0 and exit_status == 0:
            output = string_(process.readAllStandardOutput())
            if output.startswith("fpcalc version"):
                self._acoustid_fpcalc_set_success(output.strip())
            else:
                self._acoustid_fpcalc_set_error()
        else:
            self._acoustid_fpcalc_set_error()

    def _on_acoustid_fpcalc_check_error(self, error):
        self._acoustid_fpcalc_set_error()

    def _acoustid_fpcalc_set_success(self, version):
        self._fpcalc_valid = True
        self.ui.acoustid_fpcalc_info.setStyleSheet("")
        self.ui.acoustid_fpcalc_info.setText(version)

    def _acoustid_fpcalc_set_error(self):
        self._fpcalc_valid = False
        self.ui.acoustid_fpcalc_info.setStyleSheet(self.STYLESHEET_ERROR)
        self.ui.acoustid_fpcalc_info.setText(
            _("Please select a valid fpcalc executable."))

    def check(self):
        if not self._fpcalc_valid:
            raise OptionsCheckError(
                _("Invalid fpcalc executable"),
                _("Please select a valid fpcalc executable."))

    def display_error(self, error):
        pass
Example #26
0
class WikidataOptionsPage(OptionsPage):
    NAME = "wikidata"
    TITLE = "Wikidata Genre"
    PARENT = "plugins"

    options = [
        config.BoolOption("setting", "wikidata_use_release_group_genres",
                          True),
        config.BoolOption("setting", "wikidata_use_artist_genres", True),
        config.BoolOption("setting", "wikidata_use_artist_only_if_no_release",
                          True),
        config.TextOption("setting",
                          "wikidata_ignore_genres_from_these_artists", ""),
        config.BoolOption("setting", "wikidata_use_work_genres", True),
        config.TextOption("setting", "wikidata_ignore_these_genres",
                          "seen live, favorites, /\\d+ of \\d+ stars/"),
        config.TextOption("setting", "wikidata_genre_delimiter", "; "),
    ]

    def __init__(self, parent=None):
        super(WikidataOptionsPage, self).__init__(parent)
        self.ui = Ui_WikidataOptionsPage()
        self.ui.setupUi(self)
        if not config.setting["write_id3v23"]:
            self.ui.genre_delimiter.setEnabled(False)
            self.ui.genre_delimiter_label.setEnabled(False)
        else:
            self.ui.genre_delimiter.setEnabled(True)
            self.ui.genre_delimiter_label.setEnabled(True)

    def load(self):
        setting = config.setting
        self.ui.use_release_group_genres.setChecked(
            setting["wikidata_use_release_group_genres"])
        self.ui.use_artist_genres.setChecked(
            setting["wikidata_use_artist_genres"])
        self.ui.use_artist_only_if_no_release.setChecked(
            setting["wikidata_use_artist_only_if_no_release"])
        self.ui.ignore_genres_from_these_artists.setText(
            setting["wikidata_ignore_genres_from_these_artists"])
        self.ui.use_work_genres.setChecked(setting["wikidata_use_work_genres"])
        self.ui.ignore_these_genres.setText(
            setting["wikidata_ignore_these_genres"])
        if config.setting["write_id3v23"]:
            self.ui.genre_delimiter.setEditText(
                setting["wikidata_genre_delimiter"])

    def save(self):
        setting = config.setting
        setting[
            "wikidata_use_release_group_genres"] = self.ui.use_release_group_genres.isChecked(
            )
        setting[
            "wikidata_use_artist_genres"] = self.ui.use_artist_genres.isChecked(
            )
        setting[
            "wikidata_use_artist_only_if_no_release"] = self.ui.use_artist_only_if_no_release.isChecked(
            )
        setting["wikidata_ignore_genres_from_these_artists"] = str(
            self.ui.ignore_genres_from_these_artists.text())
        setting[
            "wikidata_use_work_genres"] = self.ui.use_work_genres.isChecked()
        setting["wikidata_ignore_these_genres"] = str(
            self.ui.ignore_these_genres.text())
        if config.setting["write_id3v23"]:
            setting["wikidata_genre_delimiter"] = str(
                self.ui.genre_delimiter.currentText())
Example #27
0
class GeneralOptionsPage(OptionsPage):

    NAME = "general"
    TITLE = N_("General")
    PARENT = None
    SORT_ORDER = 1
    ACTIVE = True

    options = [
        config.TextOption("setting", "server_host", MUSICBRAINZ_SERVERS[0]),
        config.IntOption("setting", "server_port", 443),
        config.TextOption("persist", "oauth_refresh_token", ""),
        config.BoolOption("setting", "analyze_new_files", False),
        config.BoolOption("setting", "ignore_file_mbids", False),
        config.TextOption("persist", "oauth_refresh_token", ""),
        config.TextOption("persist", "oauth_refresh_token_scopes", ""),
        config.TextOption("persist", "oauth_access_token", ""),
        config.IntOption("persist", "oauth_access_token_expires", 0),
        config.TextOption("persist", "oauth_username", ""),
    ]

    def __init__(self, parent=None):
        super(GeneralOptionsPage, self).__init__(parent)
        self.ui = Ui_GeneralOptionsPage()
        self.ui.setupUi(self)
        self.ui.server_host.addItems(MUSICBRAINZ_SERVERS)
        self.ui.login.clicked.connect(self.login)
        self.ui.logout.clicked.connect(self.logout)
        self.update_login_logout()

    def load(self):
        self.ui.server_host.setEditText(config.setting["server_host"])
        self.ui.server_port.setValue(config.setting["server_port"])
        self.ui.analyze_new_files.setChecked(
            config.setting["analyze_new_files"])
        self.ui.ignore_file_mbids.setChecked(
            config.setting["ignore_file_mbids"])

    def save(self):
        config.setting["server_host"] = self.ui.server_host.currentText(
        ).strip()
        config.setting["server_port"] = self.ui.server_port.value()
        config.setting[
            "analyze_new_files"] = self.ui.analyze_new_files.isChecked()
        config.setting[
            "ignore_file_mbids"] = self.ui.ignore_file_mbids.isChecked()

    def update_login_logout(self):
        if self.tagger.xmlws.oauth_manager.is_logged_in():
            self.ui.logged_in.setText(
                _("Logged in as <b>%s</b>.") %
                config.persist["oauth_username"])
            self.ui.logged_in.show()
            self.ui.login.hide()
            self.ui.logout.show()
        else:
            self.ui.logged_in.hide()
            self.ui.login.show()
            self.ui.logout.hide()

    def login(self):
        scopes = "profile tag rating collection submit_isrc submit_barcode"
        authorization_url = self.tagger.xmlws.oauth_manager.get_authorization_url(
            scopes)
        webbrowser2.open(authorization_url)
        authorization_code, ok = QInputDialog.getText(self,
                                                      _("MusicBrainz Account"),
                                                      _("Authorization code:"))
        if ok:
            self.tagger.xmlws.oauth_manager.exchange_authorization_code(
                authorization_code, scopes, self.on_authorization_finished)

    def restore_defaults(self):
        super(GeneralOptionsPage, self).restore_defaults()
        self.logout()

    def on_authorization_finished(self, successful):
        if successful:
            self.tagger.xmlws.oauth_manager.fetch_username(
                self.on_login_finished)

    def on_login_finished(self, successful):
        self.update_login_logout()
        if successful:
            load_user_collections()

    def logout(self):
        self.tagger.xmlws.oauth_manager.revoke_tokens()
        self.update_login_logout()
        load_user_collections()
Example #28
0
class TagsFromFileNamesDialog(PicardDialog):

    options = [
        config.TextOption("persist", "tags_from_filenames_format", ""),
        config.Option("persist", "tags_from_filenames_position",
                      QtCore.QPoint()),
        config.Option("persist", "tags_from_filenames_size",
                      QtCore.QSize(560, 400)),
    ]

    def __init__(self, files, parent=None):
        PicardDialog.__init__(self, parent)
        self.ui = Ui_TagsFromFileNamesDialog()
        self.ui.setupUi(self)
        items = [
            "%artist%/%album%/%title%",
            "%artist%/%album%/%tracknumber% %title%",
            "%artist%/%album%/%tracknumber% - %title%",
            "%artist%/%album% - %tracknumber% - %title%",
            "%artist% - %album%/%title%",
            "%artist% - %album%/%tracknumber% %title%",
            "%artist% - %album%/%tracknumber% - %title%",
        ]
        format = config.persist["tags_from_filenames_format"]
        if format and format not in items:
            items.insert(0, format)
        self.ui.format.addItems(items)
        self.ui.format.setCurrentIndex(items.index(format))
        self.ui.buttonbox.addButton(StandardButton(StandardButton.OK),
                                    QtGui.QDialogButtonBox.AcceptRole)
        self.ui.buttonbox.addButton(StandardButton(StandardButton.CANCEL),
                                    QtGui.QDialogButtonBox.RejectRole)
        self.ui.buttonbox.accepted.connect(self.accept)
        self.ui.buttonbox.rejected.connect(self.reject)
        self.ui.preview.clicked.connect(self.preview)
        self.ui.files.setHeaderLabels([_("File Name")])
        self.restoreWindowState()
        self.files = files
        self.items = []
        for file in files:
            item = QtGui.QTreeWidgetItem(self.ui.files)
            item.setText(0, os.path.basename(file.filename))
            self.items.append(item)
        self._tag_re = re.compile("(%\w+%)")
        self.numeric_tags = ('tracknumber', 'totaltracks', 'discnumber',
                             'totaldiscs')

    def parse_format(self):
        format = unicode(self.ui.format.currentText())
        columns = []
        format_re = ['(?:^|/)']
        for part in self._tag_re.split(format):
            if part.startswith('%') and part.endswith('%'):
                name = part[1:-1]
                columns.append(name)
                if name in self.numeric_tags:
                    format_re.append('(?P<' + name + '>\d+)')
                elif name in ('date'):
                    format_re.append('(?P<' + name + '>\d+(?:-\d+(?:-\d+)?)?)')
                else:
                    format_re.append('(?P<' + name + '>[^/]*?)')
            else:
                format_re.append(re.escape(part))
        format_re.append(r'\.(\w+)$')
        format_re = re.compile("".join(format_re))
        return format_re, columns

    def match_file(self, file, format):
        match = format.search(file.filename.replace('\\', '/'))
        if match:
            result = {}
            for name, value in match.groupdict().iteritems():
                value = value.strip()
                if name in self.numeric_tags:
                    value = value.lstrip("0")
                if self.ui.replace_underscores.isChecked():
                    value = value.replace('_', ' ')
                result[name] = value
            return result
        else:
            return {}

    def preview(self):
        format, columns = self.parse_format()
        self.ui.files.setHeaderLabels([_("File Name")] +
                                      map(display_tag_name, columns))
        for item, file in zip(self.items, self.files):
            matches = self.match_file(file, format)
            for i in range(len(columns)):
                value = matches.get(columns[i], '')
                item.setText(i + 1, value)
        self.ui.files.header().resizeSections(
            QtGui.QHeaderView.ResizeToContents)
        self.ui.files.header().setStretchLastSection(True)

    def accept(self):
        format, columns = self.parse_format()
        for file in self.files:
            metadata = self.match_file(file, format)
            for name, value in metadata.iteritems():
                file.metadata[name] = value
            file.update()
        config.persist[
            "tags_from_filenames_format"] = self.ui.format.currentText()
        self.saveWindowState()
        QtGui.QDialog.accept(self)

    def reject(self):
        self.saveWindowState()
        QtGui.QDialog.reject(self)

    def closeEvent(self, event):
        self.saveWindowState()
        event.accept()

    def saveWindowState(self):
        pos = self.pos()
        if not pos.isNull():
            config.persist["tags_from_filenames_position"] = pos
        config.persist["tags_from_filenames_size"] = self.size()

    def restoreWindowState(self):
        pos = config.persist["tags_from_filenames_position"]
        if pos.x() > 0 and pos.y() > 0:
            self.move(pos)
        self.resize(config.persist["tags_from_filenames_size"])
Example #29
0
class TagsOptionsPage(OptionsPage):

    NAME = "tags"
    TITLE = N_("Tags")
    PARENT = None
    SORT_ORDER = 30
    ACTIVE = True

    options = [
        config.BoolOption("setting", "clear_existing_tags", False),
        config.TextOption("setting", "preserved_tags", ""),
        config.BoolOption("setting", "write_id3v1", True),
        config.BoolOption("setting", "write_id3v23", True),
        config.TextOption("setting", "id3v2_encoding", "utf-16"),
        config.TextOption("setting", "id3v23_join_with", "/"),
        config.BoolOption("setting", "remove_id3_from_flac", False),
        config.BoolOption("setting", "remove_ape_from_mp3", False),
        config.BoolOption("setting", "tpe2_albumartist", False),
        config.BoolOption("setting", "dont_write_tags", False),
        config.BoolOption("setting", "preserve_timestamps", False),
    ]

    def __init__(self, parent=None):
        super(TagsOptionsPage, self).__init__(parent)
        self.ui = Ui_TagsOptionsPage()
        self.ui.setupUi(self)
        self.ui.write_id3v23.clicked.connect(self.update_encodings)
        self.ui.write_id3v24.clicked.connect(self.update_encodings)
        self.completer = QtGui.QCompleter(sorted(TAG_NAMES.keys()), self)
        self.completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
        self.completer.setWidget(self.ui.preserved_tags)
        self.ui.preserved_tags.textEdited.connect(self.preserved_tags_edited)
        self.completer.activated.connect(self.completer_activated)

    def load(self):
        self.ui.write_tags.setChecked(not config.setting["dont_write_tags"])
        self.ui.preserve_timestamps.setChecked(
            config.setting["preserve_timestamps"])
        self.ui.clear_existing_tags.setChecked(
            config.setting["clear_existing_tags"])
        self.ui.write_id3v1.setChecked(config.setting["write_id3v1"])
        self.ui.write_id3v23.setChecked(config.setting["write_id3v23"])
        if config.setting["id3v2_encoding"] == "iso-8859-1":
            self.ui.enc_iso88591.setChecked(True)
        elif config.setting["id3v2_encoding"] == "utf-16":
            self.ui.enc_utf16.setChecked(True)
        else:
            self.ui.enc_utf8.setChecked(True)
        self.ui.id3v23_join_with.setEditText(
            config.setting["id3v23_join_with"])
        self.ui.remove_ape_from_mp3.setChecked(
            config.setting["remove_ape_from_mp3"])
        self.ui.remove_id3_from_flac.setChecked(
            config.setting["remove_id3_from_flac"])
        self.ui.preserved_tags.setText(config.setting["preserved_tags"])
        self.update_encodings()

    def save(self):
        config.setting["dont_write_tags"] = not self.ui.write_tags.isChecked()
        config.setting[
            "preserve_timestamps"] = self.ui.preserve_timestamps.isChecked()
        clear_existing_tags = self.ui.clear_existing_tags.isChecked()
        if clear_existing_tags != config.setting["clear_existing_tags"]:
            config.setting["clear_existing_tags"] = clear_existing_tags
            self.tagger.window.metadata_box.update()
        config.setting["write_id3v1"] = self.ui.write_id3v1.isChecked()
        config.setting["write_id3v23"] = self.ui.write_id3v23.isChecked()
        config.setting["id3v23_join_with"] = unicode(
            self.ui.id3v23_join_with.currentText())
        if self.ui.enc_iso88591.isChecked():
            config.setting["id3v2_encoding"] = "iso-8859-1"
        elif self.ui.enc_utf16.isChecked():
            config.setting["id3v2_encoding"] = "utf-16"
        else:
            config.setting["id3v2_encoding"] = "utf-8"
        config.setting[
            "remove_ape_from_mp3"] = self.ui.remove_ape_from_mp3.isChecked()
        config.setting[
            "remove_id3_from_flac"] = self.ui.remove_id3_from_flac.isChecked()
        config.setting["preserved_tags"] = unicode(
            self.ui.preserved_tags.text())
        self.tagger.window.enable_tag_saving_action.setChecked(
            not config.setting["dont_write_tags"])

    def update_encodings(self):
        if self.ui.write_id3v23.isChecked():
            if self.ui.enc_utf8.isChecked():
                self.ui.enc_utf16.setChecked(True)
            self.ui.enc_utf8.setEnabled(False)
            self.ui.label_id3v23_join_with.setEnabled(True)
            self.ui.id3v23_join_with.setEnabled(True)
        else:
            self.ui.enc_utf8.setEnabled(True)
            self.ui.label_id3v23_join_with.setEnabled(False)
            self.ui.id3v23_join_with.setEnabled(False)

    def preserved_tags_edited(self, text):
        prefix = unicode(text)[:self.ui.preserved_tags.cursorPosition()].split(
            ",")[-1]
        self.completer.setCompletionPrefix(prefix)
        if prefix:
            self.completer.complete()
        else:
            self.completer.popup().hide()

    def completer_activated(self, text):
        input = self.ui.preserved_tags
        current = unicode(input.text())
        i = input.cursorPosition()
        p = len(self.completer.completionPrefix())
        input.setText("%s%s %s" % (current[:i - p], text, current[i:]))
        input.setCursorPosition(i - p + len(text) + 1)
Example #30
0
class MainWindow(QtGui.QMainWindow):

    selection_updated = QtCore.pyqtSignal(object)

    options = [
        config.Option("persist", "window_state", QtCore.QByteArray()),
        config.Option("persist", "window_position", QtCore.QPoint()),
        config.Option("persist", "window_size", QtCore.QSize(780, 560)),
        config.Option("persist", "bottom_splitter_state", QtCore.QByteArray()),
        config.BoolOption("persist", "window_maximized", False),
        config.BoolOption("persist", "view_cover_art", True),
        config.BoolOption("persist", "view_file_browser", False),
        config.TextOption("persist", "current_directory", ""),
    ]

    def __init__(self, parent=None):
        QtGui.QMainWindow.__init__(self, parent)
        self.selected_objects = []
        self.ignore_selection_changes = False
        self.setupUi()

    def setupUi(self):
        self.setWindowTitle(_("MusicBrainz Picard"))
        icon = QtGui.QIcon()
        icon.addFile(":/images/16x16/picard.png", QtCore.QSize(16, 16))
        icon.addFile(":/images/24x24/picard.png", QtCore.QSize(24, 24))
        icon.addFile(":/images/32x32/picard.png", QtCore.QSize(32, 32))
        icon.addFile(":/images/48x48/picard.png", QtCore.QSize(48, 48))
        icon.addFile(":/images/128x128/picard.png", QtCore.QSize(128, 128))
        icon.addFile(":/images/256x256/picard.png", QtCore.QSize(256, 256))
        self.setWindowIcon(icon)

        self.create_actions()
        self.create_statusbar()
        self.create_toolbar()
        self.create_menus()

        mainLayout = QtGui.QSplitter(QtCore.Qt.Vertical)
        mainLayout.setContentsMargins(0, 0, 0, 0)
        mainLayout.setHandleWidth(1)

        self.panel = MainPanel(self, mainLayout)
        self.file_browser = FileBrowser(self.panel)
        if not self.show_file_browser_action.isChecked():
            self.file_browser.hide()
        self.panel.insertWidget(0, self.file_browser)
        self.panel.restore_state()

        self.metadata_box = MetadataBox(self)
        self.cover_art_box = CoverArtBox(self)
        if not self.show_cover_art_action.isChecked():
            self.cover_art_box.hide()

        bottomLayout = QtGui.QHBoxLayout()
        bottomLayout.setContentsMargins(0, 0, 0, 0)
        bottomLayout.setSpacing(0)
        bottomLayout.addWidget(self.metadata_box, 1)
        bottomLayout.addWidget(self.cover_art_box, 0)
        bottom = QtGui.QWidget()
        bottom.setLayout(bottomLayout)

        mainLayout.addWidget(self.panel)
        mainLayout.addWidget(bottom)
        self.setCentralWidget(mainLayout)

        # accessibility
        self.set_tab_order()

        # FIXME: use QApplication's clipboard
        self._clipboard = []

        for function in ui_init:
            function(self)

    def keyPressEvent(self, event):
        if event.matches(QtGui.QKeySequence.Delete):
            if self.metadata_box.hasFocus():
                self.metadata_box.remove_selected_tags()
            else:
                self.remove()
        else:
            QtGui.QMainWindow.keyPressEvent(self, event)

    def show(self):
        self.restoreWindowState()
        QtGui.QMainWindow.show(self)
        self.metadata_box.restore_state()

    def closeEvent(self, event):
        if config.setting["quit_confirmation"] and not self.show_quit_confirmation():
            event.ignore()
            return
        self.saveWindowState()
        event.accept()

    def show_quit_confirmation(self):
        unsaved_files = sum(a.get_num_unsaved_files() for a in self.tagger.albums.itervalues())
        QMessageBox = QtGui.QMessageBox

        if unsaved_files > 0:
            msg = QMessageBox(self)
            msg.setIcon(QMessageBox.Question)
            msg.setWindowModality(QtCore.Qt.WindowModal)
            msg.setWindowTitle(_(u"Unsaved Changes"))
            msg.setText(_(u"Are you sure you want to quit Picard?"))
            txt = ungettext(
                "There is %d unsaved file. Closing Picard will lose all unsaved changes.",
                "There are %d unsaved files. Closing Picard will lose all unsaved changes.",
                unsaved_files) % unsaved_files
            msg.setInformativeText(txt)
            cancel = msg.addButton(QMessageBox.Cancel)
            msg.setDefaultButton(cancel)
            msg.addButton(_(u"&Quit Picard"), QMessageBox.YesRole)
            ret = msg.exec_()

            if ret == QMessageBox.Cancel:
                return False

        return True

    def saveWindowState(self):
        config.persist["window_state"] = self.saveState()
        isMaximized = int(self.windowState()) & QtCore.Qt.WindowMaximized != 0
        if isMaximized:
            # FIXME: this doesn't include the window frame
            geom = self.normalGeometry()
            config.persist["window_position"] = geom.topLeft()
            config.persist["window_size"] = geom.size()
        else:
            pos = self.pos()
            if not pos.isNull():
                config.persist["window_position"] = pos
            config.persist["window_size"] = self.size()
        config.persist["window_maximized"] = isMaximized
        config.persist["view_cover_art"] = self.show_cover_art_action.isChecked()
        config.persist["view_file_browser"] = self.show_file_browser_action.isChecked()
        config.persist["bottom_splitter_state"] = self.centralWidget().saveState()
        self.file_browser.save_state()
        self.panel.save_state()
        self.metadata_box.save_state()

    def restoreWindowState(self):
        self.restoreState(config.persist["window_state"])
        pos = config.persist["window_position"]
        size = config.persist["window_size"]
        self._desktopgeo = self.tagger.desktop().screenGeometry()
        if (pos.x() > 0 and pos.y() > 0
            and pos.x() + size.width() < self._desktopgeo.width()
            and pos.y() + size.height() < self._desktopgeo.height()):
            self.move(pos)
        if size.width() <= 0 or size.height() <= 0:
            size = QtCore.QSize(780, 560)
        self.resize(size)
        if config.persist["window_maximized"]:
            self.setWindowState(QtCore.Qt.WindowMaximized)
        bottom_splitter_state = config.persist["bottom_splitter_state"]
        if bottom_splitter_state.isEmpty():
            self.centralWidget().setSizes([366, 194])
        else:
            self.centralWidget().restoreState(bottom_splitter_state)
        self.file_browser.restore_state()

    def create_statusbar(self):
        """Creates a new status bar."""
        self.statusBar().showMessage(_("Ready"))
        self.infostatus = InfoStatus(self)
        self.listening_label = QtGui.QLabel()
        self.listening_label.setVisible(False)
        self.listening_label.setToolTip("<qt/>" + _(
            "Picard listens on this port to integrate with your browser. When "
            "you \"Search\" or \"Open in Browser\" from Picard, clicking the "
            "\"Tagger\" button on the web page loads the release into Picard."
        ))
        self.statusBar().addPermanentWidget(self.infostatus)
        self.statusBar().addPermanentWidget(self.listening_label)
        self.tagger.tagger_stats_changed.connect(self.update_statusbar_stats)
        self.tagger.listen_port_changed.connect(self.update_statusbar_listen_port)
        self.update_statusbar_stats()

    @throttle(100)
    def update_statusbar_stats(self):
        """Updates the status bar information."""
        self.infostatus.setFiles(len(self.tagger.files))
        self.infostatus.setAlbums(len(self.tagger.albums))
        self.infostatus.setPendingFiles(File.num_pending_files)
        ws = self.tagger.xmlws
        self.infostatus.setPendingRequests(ws.num_pending_web_requests)

    def update_statusbar_listen_port(self, listen_port):
        if listen_port:
            self.listening_label.setVisible(True)
            self.listening_label.setText(_(" Listening on port %(port)d ") % {"port": listen_port})
        else:
            self.listening_label.setVisible(False)

    def set_statusbar_message(self, message, *args, **kwargs):
        """Set the status bar message.

        *args are passed to % operator, if args[0] is a mapping it is used for
        named place holders values
        >>> w.set_statusbar_message("File %(filename)s", {'filename': 'x.txt'})

        Keyword arguments:
        `echo` parameter defaults to `log.debug`, called before message is
        translated, it can be disabled passing None or replaced by ie.
        `log.error`. If None, skipped.

        `translate` is a method called on message before it is sent to history
        log and status bar, it defaults to `_()`. If None, skipped.

        `timeout` defines duration of the display in milliseconds

        `history` is a method called with translated message as argument, it
        defaults to `log.history_info`. If None, skipped.

        Empty messages are never passed to echo and history functions but they
        are sent to status bar (ie. to clear it).
        """
        def isdict(obj):
            return hasattr(obj, 'keys') and hasattr(obj, '__getitem__')

        echo = kwargs.get('echo', log.debug)
        # _ is defined using __builtin__.__dict__, so setting it as default named argument
        # value doesn't work as expected
        translate = kwargs.get('translate', _)
        timeout = kwargs.get('timeout', 0)
        history = kwargs.get('history', log.history_info)
        if len(args) == 1 and isdict(args[0]):
            # named place holders
            mparms = args[0]
        else:
            # simple place holders, ensure compatibility
            mparms = args
        if message:
            if echo:
                echo(message % mparms)
            if translate:
                message = translate(message)
            message = message % mparms
            if history:
                history(message)
        thread.to_main(self.statusBar().showMessage, message, timeout)

    def _on_submit(self):
        if self.tagger.use_acoustid:
            if not config.setting["acoustid_apikey"]:
                QtGui.QMessageBox.warning(self,
                    _(u"Submission Error"),
                    _(u"You need to configure your AcoustID API key before you can submit fingerprints."))
            else:
                self.tagger.acoustidmanager.submit()

    def create_actions(self):
        self.options_action = QtGui.QAction(icontheme.lookup('preferences-desktop'), _("&Options..."), self)
        self.options_action.setMenuRole(QtGui.QAction.PreferencesRole)
        self.options_action.triggered.connect(self.show_options)

        self.cut_action = QtGui.QAction(icontheme.lookup('edit-cut', icontheme.ICON_SIZE_MENU), _(u"&Cut"), self)
        self.cut_action.setShortcut(QtGui.QKeySequence.Cut)
        self.cut_action.setEnabled(False)
        self.cut_action.triggered.connect(self.cut)

        self.paste_action = QtGui.QAction(icontheme.lookup('edit-paste', icontheme.ICON_SIZE_MENU), _(u"&Paste"), self)
        self.paste_action.setShortcut(QtGui.QKeySequence.Paste)
        self.paste_action.setEnabled(False)
        self.paste_action.triggered.connect(self.paste)

        self.help_action = QtGui.QAction(_("&Help..."), self)
        self.help_action.setShortcut(QtGui.QKeySequence.HelpContents)
        self.help_action.triggered.connect(self.show_help)

        self.about_action = QtGui.QAction(_("&About..."), self)
        self.about_action.setMenuRole(QtGui.QAction.AboutRole)
        self.about_action.triggered.connect(self.show_about)

        self.donate_action = QtGui.QAction(_("&Donate..."), self)
        self.donate_action.triggered.connect(self.open_donation_page)

        self.report_bug_action = QtGui.QAction(_("&Report a Bug..."), self)
        self.report_bug_action.triggered.connect(self.open_bug_report)

        self.support_forum_action = QtGui.QAction(_("&Support Forum..."), self)
        self.support_forum_action.triggered.connect(self.open_support_forum)

        self.add_files_action = QtGui.QAction(icontheme.lookup('document-open'), _(u"&Add Files..."), self)
        self.add_files_action.setStatusTip(_(u"Add files to the tagger"))
        # TR: Keyboard shortcut for "Add Files..."
        self.add_files_action.setShortcut(QtGui.QKeySequence.Open)
        self.add_files_action.triggered.connect(self.add_files)

        self.add_directory_action = QtGui.QAction(icontheme.lookup('folder'), _(u"A&dd Folder..."), self)
        self.add_directory_action.setStatusTip(_(u"Add a folder to the tagger"))
        # TR: Keyboard shortcut for "Add Directory..."
        self.add_directory_action.setShortcut(QtGui.QKeySequence(_(u"Ctrl+D")))
        self.add_directory_action.triggered.connect(self.add_directory)

        self.save_action = QtGui.QAction(icontheme.lookup('document-save'), _(u"&Save"), self)
        self.save_action.setStatusTip(_(u"Save selected files"))
        # TR: Keyboard shortcut for "Save"
        self.save_action.setShortcut(QtGui.QKeySequence.Save)
        self.save_action.setEnabled(False)
        self.save_action.triggered.connect(self.save)

        self.submit_action = QtGui.QAction(icontheme.lookup('picard-submit'), _(u"S&ubmit"), self)
        self.submit_action.setStatusTip(_(u"Submit acoustic fingerprints"))
        self.submit_action.setEnabled(False)
        self.submit_action.triggered.connect(self._on_submit)

        self.exit_action = QtGui.QAction(_(u"E&xit"), self)
        self.exit_action.setMenuRole(QtGui.QAction.QuitRole)
        # TR: Keyboard shortcut for "Exit"
        self.exit_action.setShortcut(QtGui.QKeySequence(_(u"Ctrl+Q")))
        self.exit_action.triggered.connect(self.close)

        self.remove_action = QtGui.QAction(icontheme.lookup('list-remove'), _(u"&Remove"), self)
        self.remove_action.setStatusTip(_(u"Remove selected files/albums"))
        self.remove_action.setEnabled(False)
        self.remove_action.triggered.connect(self.remove)

        self.browser_lookup_action = QtGui.QAction(icontheme.lookup('lookup-musicbrainz'), _(u"Lookup in &Browser"), self)
        self.browser_lookup_action.setStatusTip(_(u"Lookup selected item on MusicBrainz website"))
        self.browser_lookup_action.setEnabled(False)
        self.browser_lookup_action.triggered.connect(self.browser_lookup)

        self.show_file_browser_action = QtGui.QAction(_(u"File &Browser"), self)
        self.show_file_browser_action.setCheckable(True)
        if config.persist["view_file_browser"]:
            self.show_file_browser_action.setChecked(True)
        self.show_file_browser_action.setShortcut(QtGui.QKeySequence(_(u"Ctrl+B")))
        self.show_file_browser_action.triggered.connect(self.show_file_browser)

        self.show_cover_art_action = QtGui.QAction(_(u"&Cover Art"), self)
        self.show_cover_art_action.setCheckable(True)
        if config.persist["view_cover_art"]:
            self.show_cover_art_action.setChecked(True)
        self.show_cover_art_action.triggered.connect(self.show_cover_art)

        self.search_action = QtGui.QAction(icontheme.lookup('system-search'), _(u"Search"), self)
        self.search_action.triggered.connect(self.search)

        self.cd_lookup_action = QtGui.QAction(icontheme.lookup('media-optical'), _(u"Lookup &CD..."), self)
        self.cd_lookup_action.setStatusTip(_(u"Lookup the details of the CD in your drive"))
        # TR: Keyboard shortcut for "Lookup CD"
        self.cd_lookup_action.setShortcut(QtGui.QKeySequence(_("Ctrl+K")))
        self.cd_lookup_action.triggered.connect(self.tagger.lookup_cd)

        self.analyze_action = QtGui.QAction(icontheme.lookup('picard-analyze'), _(u"&Scan"), self)
        self.analyze_action.setEnabled(False)
        # TR: Keyboard shortcut for "Analyze"
        self.analyze_action.setShortcut(QtGui.QKeySequence(_(u"Ctrl+Y")))
        self.analyze_action.triggered.connect(self.analyze)

        self.cluster_action = QtGui.QAction(icontheme.lookup('picard-cluster'), _(u"Cl&uster"), self)
        self.cluster_action.setEnabled(False)
        # TR: Keyboard shortcut for "Cluster"
        self.cluster_action.setShortcut(QtGui.QKeySequence(_(u"Ctrl+U")))
        self.cluster_action.triggered.connect(self.cluster)

        self.autotag_action = QtGui.QAction(icontheme.lookup('picard-auto-tag'), _(u"&Lookup"), self)
        tip = _(u"Lookup selected items in MusicBrainz")
        self.autotag_action.setToolTip(tip)
        self.autotag_action.setStatusTip(tip)
        self.autotag_action.setEnabled(False)
        # TR: Keyboard shortcut for "Lookup"
        self.autotag_action.setShortcut(QtGui.QKeySequence(_(u"Ctrl+L")))
        self.autotag_action.triggered.connect(self.autotag)

        self.view_info_action = QtGui.QAction(icontheme.lookup('picard-edit-tags'), _(u"&Info..."), self)
        self.view_info_action.setEnabled(False)
        # TR: Keyboard shortcut for "Info"
        self.view_info_action.setShortcut(QtGui.QKeySequence(_(u"Ctrl+I")))
        self.view_info_action.triggered.connect(self.view_info)

        self.refresh_action = QtGui.QAction(icontheme.lookup('view-refresh', icontheme.ICON_SIZE_MENU), _("&Refresh"), self)
        self.refresh_action.setShortcut(QtGui.QKeySequence(_(u"Ctrl+R")))
        self.refresh_action.triggered.connect(self.refresh)

        self.enable_renaming_action = QtGui.QAction(_(u"&Rename Files"), self)
        self.enable_renaming_action.setCheckable(True)
        self.enable_renaming_action.setChecked(config.setting["rename_files"])
        self.enable_renaming_action.triggered.connect(self.toggle_rename_files)

        self.enable_moving_action = QtGui.QAction(_(u"&Move Files"), self)
        self.enable_moving_action.setCheckable(True)
        self.enable_moving_action.setChecked(config.setting["move_files"])
        self.enable_moving_action.triggered.connect(self.toggle_move_files)

        self.enable_tag_saving_action = QtGui.QAction(_(u"Save &Tags"), self)
        self.enable_tag_saving_action.setCheckable(True)
        self.enable_tag_saving_action.setChecked(not config.setting["dont_write_tags"])
        self.enable_tag_saving_action.triggered.connect(self.toggle_tag_saving)

        self.tags_from_filenames_action = QtGui.QAction(_(u"Tags From &File Names..."), self)
        self.tags_from_filenames_action.triggered.connect(self.open_tags_from_filenames)
        self.tags_from_filenames_action.setEnabled(False)

        self.open_collection_in_browser_action = QtGui.QAction(_(u"&Open My Collections in Browser"), self)
        self.open_collection_in_browser_action.triggered.connect(self.open_collection_in_browser)
        self.open_collection_in_browser_action.setEnabled(config.setting["username"] != u'')

        self.view_log_action = QtGui.QAction(_(u"View Error/Debug &Log"), self)
        self.view_log_action.triggered.connect(self.show_log)

        self.view_history_action = QtGui.QAction(_(u"View Activity &History"), self)
        self.view_history_action.triggered.connect(self.show_history)

        xmlws_manager = self.tagger.xmlws.manager
        xmlws_manager.authenticationRequired.connect(self.show_password_dialog)
        xmlws_manager.proxyAuthenticationRequired.connect(self.show_proxy_dialog)

        self.play_file_action = QtGui.QAction(icontheme.lookup('play-music'), _(u"&Play file"), self)
        self.play_file_action.setStatusTip(_(u"Play the file in your default media player"))
        self.play_file_action.setEnabled(False)
        self.play_file_action.triggered.connect(self.play_file)

        self.open_folder_action = QtGui.QAction(icontheme.lookup('folder', icontheme.ICON_SIZE_MENU), _(u"Open Containing &Folder"), self)
        self.open_folder_action.setStatusTip(_(u"Open the containing folder in your file explorer"))
        self.open_folder_action.setEnabled(False)
        self.open_folder_action.triggered.connect(self.open_folder)

    def toggle_rename_files(self, checked):
        config.setting["rename_files"] = checked

    def toggle_move_files(self, checked):
        config.setting["move_files"] = checked

    def toggle_tag_saving(self, checked):
        config.setting["dont_write_tags"] = not checked

    def get_selected_or_unmatched_files(self):
        files = self.tagger.get_files_from_objects(self.selected_objects)
        if not files:
            files = self.tagger.unmatched_files.files
        return files

    def open_tags_from_filenames(self):
        files = self.get_selected_or_unmatched_files()
        if files:
            dialog = TagsFromFileNamesDialog(files, self)
            dialog.exec_()

    def open_collection_in_browser(self):
        self.tagger.collection_lookup()

    def create_menus(self):
        menu = self.menuBar().addMenu(_(u"&File"))
        menu.addAction(self.add_directory_action)
        menu.addAction(self.add_files_action)
        menu.addSeparator()
        menu.addAction(self.play_file_action)
        menu.addAction(self.open_folder_action)
        menu.addSeparator()
        menu.addAction(self.save_action)
        menu.addAction(self.submit_action)
        menu.addSeparator()
        menu.addAction(self.exit_action)
        menu = self.menuBar().addMenu(_(u"&Edit"))
        menu.addAction(self.cut_action)
        menu.addAction(self.paste_action)
        menu.addSeparator()
        menu.addAction(self.view_info_action)
        menu.addAction(self.remove_action)
        menu = self.menuBar().addMenu(_(u"&View"))
        menu.addAction(self.show_file_browser_action)
        menu.addAction(self.show_cover_art_action)
        menu.addSeparator()
        menu.addAction(self.toolbar_toggle_action)
        menu.addAction(self.search_toolbar_toggle_action)
        menu = self.menuBar().addMenu(_(u"&Options"))
        menu.addAction(self.enable_renaming_action)
        menu.addAction(self.enable_moving_action)
        menu.addAction(self.enable_tag_saving_action)
        menu.addSeparator()
        menu.addAction(self.options_action)
        menu = self.menuBar().addMenu(_(u"&Tools"))
        menu.addAction(self.refresh_action)
        menu.addAction(self.cd_lookup_action)
        menu.addAction(self.autotag_action)
        menu.addAction(self.analyze_action)
        menu.addAction(self.cluster_action)
        menu.addAction(self.browser_lookup_action)
        menu.addSeparator()
        menu.addAction(self.tags_from_filenames_action)
        menu.addAction(self.open_collection_in_browser_action)
        self.menuBar().addSeparator()
        menu = self.menuBar().addMenu(_(u"&Help"))
        menu.addAction(self.help_action)
        menu.addSeparator()
        menu.addAction(self.view_history_action)
        menu.addSeparator()
        menu.addAction(self.support_forum_action)
        menu.addAction(self.report_bug_action)
        menu.addAction(self.view_log_action)
        menu.addSeparator()
        menu.addAction(self.donate_action)
        menu.addAction(self.about_action)

    def update_toolbar_style(self):
        if config.setting["toolbar_show_labels"]:
            self.toolbar.setToolButtonStyle(QtCore.Qt.ToolButtonTextUnderIcon)
        else:
            self.toolbar.setToolButtonStyle(QtCore.Qt.ToolButtonIconOnly)
        self.cd_lookup_action.setEnabled(len(get_cdrom_drives()) > 0 and
                                         discid is not None)

    def create_toolbar(self):
        self.toolbar = toolbar = self.addToolBar(_(u"Actions"))
        self.toolbar_toggle_action = self.toolbar.toggleViewAction()
        self.update_toolbar_style()
        toolbar.setObjectName("main_toolbar")

        def add_toolbar_action(action):
            toolbar.addAction(action)
            widget = toolbar.widgetForAction(action)
            widget.setFocusPolicy(QtCore.Qt.TabFocus)
            widget.setAttribute(QtCore.Qt.WA_MacShowFocusRect)

        add_toolbar_action(self.add_directory_action)
        add_toolbar_action(self.add_files_action)
        toolbar.addSeparator()
        add_toolbar_action(self.play_file_action)
        toolbar.addSeparator()
        add_toolbar_action(self.save_action)
        add_toolbar_action(self.submit_action)
        toolbar.addSeparator()

        add_toolbar_action(self.cd_lookup_action)
        drives = get_cdrom_drives()
        if len(drives) > 1:
            self.cd_lookup_menu = QtGui.QMenu()
            for drive in drives:
                self.cd_lookup_menu.addAction(drive)
            self.cd_lookup_menu.triggered.connect(self.tagger.lookup_cd)
            button = toolbar.widgetForAction(self.cd_lookup_action)
            button.setPopupMode(QtGui.QToolButton.MenuButtonPopup)
            button.setMenu(self.cd_lookup_menu)

        add_toolbar_action(self.cluster_action)
        add_toolbar_action(self.autotag_action)
        add_toolbar_action(self.analyze_action)
        add_toolbar_action(self.view_info_action)
        add_toolbar_action(self.remove_action)
        add_toolbar_action(self.browser_lookup_action)

        self.search_toolbar = toolbar = self.addToolBar(_(u"Search"))
        self.search_toolbar_toggle_action = self.search_toolbar.toggleViewAction()
        toolbar.setObjectName("search_toolbar")
        search_panel = QtGui.QWidget(toolbar)
        hbox = QtGui.QHBoxLayout(search_panel)
        self.search_combo = QtGui.QComboBox(search_panel)
        self.search_combo.addItem(_(u"Album"), "album")
        self.search_combo.addItem(_(u"Artist"), "artist")
        self.search_combo.addItem(_(u"Track"), "track")
        hbox.addWidget(self.search_combo, 0)
        self.search_edit = ButtonLineEdit(search_panel)
        self.search_edit.returnPressed.connect(self.search)
        hbox.addWidget(self.search_edit, 0)
        self.search_button = QtGui.QToolButton(search_panel)
        self.search_button.setAutoRaise(True)
        self.search_button.setDefaultAction(self.search_action)
        self.search_button.setIconSize(QtCore.QSize(22, 22))
        self.search_button.setAttribute(QtCore.Qt.WA_MacShowFocusRect)
        hbox.addWidget(self.search_button)
        toolbar.addWidget(search_panel)

    def set_tab_order(self):
        tab_order = self.setTabOrder
        tw = self.toolbar.widgetForAction

        # toolbar
        tab_order(tw(self.add_directory_action), tw(self.add_files_action))
        tab_order(tw(self.add_files_action), tw(self.play_file_action))
        tab_order(tw(self.play_file_action), tw(self.save_action))
        tab_order(tw(self.save_action), tw(self.submit_action))
        tab_order(tw(self.submit_action), tw(self.cd_lookup_action))
        tab_order(tw(self.cd_lookup_action), tw(self.cluster_action))
        tab_order(tw(self.cluster_action), tw(self.autotag_action))
        tab_order(tw(self.autotag_action), tw(self.analyze_action))
        tab_order(tw(self.analyze_action), tw(self.view_info_action))
        tab_order(tw(self.view_info_action), tw(self.remove_action))
        tab_order(tw(self.remove_action), tw(self.browser_lookup_action))
        tab_order(tw(self.browser_lookup_action), self.search_combo)
        tab_order(self.search_combo, self.search_edit)
        tab_order(self.search_edit, self.search_button)
        # panels
        tab_order(self.search_button, self.file_browser)
        tab_order(self.file_browser, self.panel.views[0])
        tab_order(self.panel.views[0], self.panel.views[1])
        tab_order(self.panel.views[1], self.metadata_box)

    def enable_submit(self, enabled):
        """Enable/disable the 'Submit fingerprints' action."""
        self.submit_action.setEnabled(enabled)

    def enable_cluster(self, enabled):
        """Enable/disable the 'Cluster' action."""
        self.cluster_action.setEnabled(enabled)

    def search(self):
        """Search for album, artist or track on the MusicBrainz website."""
        text = self.search_edit.text()
        type = self.search_combo.itemData(self.search_combo.currentIndex())
        self.tagger.search(text, type, config.setting["use_adv_search_syntax"])

    def add_files(self):
        """Add files to the tagger."""
        current_directory = find_starting_directory()
        formats = []
        extensions = []
        for exts, name in supported_formats():
            exts = ["*" + e for e in exts]
            formats.append("%s (%s)" % (name, " ".join(exts)))
            extensions.extend(exts)
        formats.sort()
        extensions.sort()
        formats.insert(0, _("All Supported Formats") + " (%s)" % " ".join(extensions))
        files = QtGui.QFileDialog.getOpenFileNames(self, "", current_directory, u";;".join(formats))
        if files:
            files = map(unicode, files)
            config.persist["current_directory"] = os.path.dirname(files[0])
            self.tagger.add_files(files)

    def add_directory(self):
        """Add directory to the tagger."""
        current_directory = find_starting_directory()

        dir_list = []
        if not config.setting["toolbar_multiselect"]:
            directory = QtGui.QFileDialog.getExistingDirectory(self, "", current_directory)
            if directory:
                dir_list.append(directory)
        else:
            # Use a custom file selection dialog to allow the selection of multiple directories
            file_dialog = QtGui.QFileDialog(self, "", current_directory)
            file_dialog.setFileMode(QtGui.QFileDialog.DirectoryOnly)
            if sys.platform == "darwin":  # The native dialog doesn't allow selecting >1 directory
                file_dialog.setOption(QtGui.QFileDialog.DontUseNativeDialog)
            tree_view = file_dialog.findChild(QtGui.QTreeView)
            tree_view.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection)
            list_view = file_dialog.findChild(QtGui.QListView, "listView")
            list_view.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection)

            if file_dialog.exec_() == QtGui.QDialog.Accepted:
                dir_list = file_dialog.selectedFiles()

        if len(dir_list) == 1:
            config.persist["current_directory"] = dir_list[0]
            self.set_statusbar_message(
                N_("Adding directory: '%(directory)s' ..."),
                {'directory': dir_list[0]}
            )
        elif len(dir_list) > 1:
            (parent, dir) = os.path.split(str(dir_list[0]))
            config.persist["current_directory"] = parent
            self.set_statusbar_message(
                N_("Adding multiple directories from '%(directory)s' ..."),
                {'directory': parent}
            )

        for directory in dir_list:
            directory = unicode(directory)
            self.tagger.add_directory(directory)

    def show_about(self):
        self.show_options("about")

    def show_options(self, page=None):
        dialog = OptionsDialog(page, self)
        dialog.exec_()

    def show_help(self):
        webbrowser2.goto('documentation')

    def show_log(self):
        from picard.ui.logview import LogView
        LogView(self).show()

    def show_history(self):
        from picard.ui.logview import HistoryView
        HistoryView(self).show()

    def open_bug_report(self):
        webbrowser2.goto('troubleshooting')

    def open_support_forum(self):
        webbrowser2.goto('forum')

    def open_donation_page(self):
        webbrowser2.goto('donate')

    def save(self):
        """Tell the tagger to save the selected objects."""
        self.tagger.save(self.selected_objects)

    def remove(self):
        """Tell the tagger to remove the selected objects."""
        self.panel.remove(self.selected_objects)

    def analyze(self):
        if not config.setting['fingerprinting_system']:
            if self.show_analyze_settings_info():
                self.show_options("fingerprinting")
            if not config.setting['fingerprinting_system']:
                return
        return self.tagger.analyze(self.selected_objects)

    def play_file(self):
        files = self.tagger.get_files_from_objects(self.selected_objects)
        for file in files:
            url = QtCore.QUrl.fromLocalFile(file.filename)
            QtGui.QDesktopServices.openUrl(url)

    def open_folder(self):
        files = self.tagger.get_files_from_objects(self.selected_objects)
        for file in files:
            url = QtCore.QUrl.fromLocalFile(os.path.dirname(file.filename))
            QtGui.QDesktopServices.openUrl(url)

    def show_analyze_settings_info(self):
        ret = QtGui.QMessageBox.question(self,
            _(u"Configuration Required"),
            _(u"Audio fingerprinting is not yet configured. Would you like to configure it now?"),
            QtGui.QMessageBox.Yes | QtGui.QMessageBox.No,
            QtGui.QMessageBox.Yes)
        return ret == QtGui.QMessageBox.Yes

    def view_info(self):
        if isinstance(self.selected_objects[0], Album):
            album = self.selected_objects[0]
            dialog = AlbumInfoDialog(album, self)
        elif isinstance(self.selected_objects[0], Cluster):
            cluster = self.selected_objects[0]
            dialog = ClusterInfoDialog(cluster, self)
        else:
            file = self.tagger.get_files_from_objects(self.selected_objects)[0]
            dialog = FileInfoDialog(file, self)
        dialog.exec_()

    def cluster(self):
        self.tagger.cluster(self.selected_objects)
        self.update_actions()

    def refresh(self):
        self.tagger.refresh(self.selected_objects)

    def browser_lookup(self):
        self.tagger.browser_lookup(self.selected_objects[0])

    @throttle(100)
    def update_actions(self):
        can_remove = False
        can_save = False
        can_analyze = False
        can_refresh = False
        can_autotag = False
        single = self.selected_objects[0] if len(self.selected_objects) == 1 else None
        can_view_info = bool(single and single.can_view_info())
        can_browser_lookup = bool(single and single.can_browser_lookup())
        have_files = len(self.tagger.get_files_from_objects(self.selected_objects)) > 0
        for obj in self.selected_objects:
            if obj is None:
                continue
            if obj.can_analyze():
                can_analyze = True
            if obj.can_save():
                can_save = True
            if obj.can_remove():
                can_remove = True
            if obj.can_refresh():
                can_refresh = True
            if obj.can_autotag():
                can_autotag = True
            # Skip further loops if all values now True.
            if can_analyze and can_save and can_remove and can_refresh and can_autotag:
                break
        self.remove_action.setEnabled(can_remove)
        self.save_action.setEnabled(can_save)
        self.view_info_action.setEnabled(can_view_info)
        self.analyze_action.setEnabled(can_analyze)
        self.refresh_action.setEnabled(can_refresh)
        self.autotag_action.setEnabled(can_autotag)
        self.browser_lookup_action.setEnabled(can_browser_lookup)
        self.play_file_action.setEnabled(have_files)
        self.open_folder_action.setEnabled(have_files)
        self.cut_action.setEnabled(bool(self.selected_objects))
        files = self.get_selected_or_unmatched_files()
        self.tags_from_filenames_action.setEnabled(bool(files))

    def update_selection(self, objects=None):
        if self.ignore_selection_changes:
            return

        if objects is not None:
            self.selected_objects = objects
        else:
            objects = self.selected_objects

        self.update_actions()

        metadata = None
        obj = None

        if len(objects) == 1:
            obj = list(objects)[0]
            if isinstance(obj, File):
                metadata = obj.metadata
                if obj.state == obj.ERROR:
                    msg = N_("%(filename)s (error: %(error)s)")
                    mparms = {
                        'filename': obj.filename,
                        'error': obj.error
                    }
                else:
                    msg = N_("%(filename)s")
                    mparms = {
                        'filename': obj.filename,
                    }
                self.set_statusbar_message(msg, mparms, echo=None, history=None)
            elif isinstance(obj, Track):
                metadata = obj.metadata
                if obj.num_linked_files == 1:
                    file = obj.linked_files[0]
                    if file.state == File.ERROR:
                        msg = N_("%(filename)s (%(similarity)d%%) (error: %(error)s)")
                        mparms = {
                            'filename': file.filename,
                            'similarity': file.similarity * 100,
                            'error': file.error
                        }
                    else:
                        msg = N_("%(filename)s (%(similarity)d%%)")
                        mparms = {
                            'filename': file.filename,
                            'similarity': file.similarity * 100,
                        }
                    self.set_statusbar_message(msg, mparms, echo=None,
                                               history=None)
            elif obj.can_edit_tags():
                metadata = obj.metadata

        self.metadata_box.selection_dirty = True
        self.metadata_box.update()
        self.cover_art_box.set_metadata(metadata, obj)
        self.selection_updated.emit(objects)

    def show_cover_art(self):
        """Show/hide the cover art box."""
        if self.show_cover_art_action.isChecked():
            self.cover_art_box.show()
            self.metadata_box.resize_columns()
        else:
            self.cover_art_box.hide()

    def show_file_browser(self):
        """Show/hide the file browser."""
        if self.show_file_browser_action.isChecked():
            sizes = self.panel.sizes()
            if sizes[0] == 0:
                sizes[0] = sum(sizes) / 4
                self.panel.setSizes(sizes)
            self.file_browser.show()
        else:
            self.file_browser.hide()

    def show_password_dialog(self, reply, authenticator):
        if reply.url().host() == config.setting['server_host']:
            ret = QtGui.QMessageBox.question(self,
                _(u"Authentication Required"),
                _(u"Picard needs authorization to access your personal data on the MusicBrainz server. Would you like to log in now?"),
                QtGui.QMessageBox.Yes | QtGui.QMessageBox.No,
                QtGui.QMessageBox.Yes)
            if ret == QtGui.QMessageBox.Yes:
                pass
        else:
            dialog = PasswordDialog(authenticator, reply, parent=self)
            dialog.exec_()

    def show_proxy_dialog(self, proxy, authenticator):
        dialog = ProxyDialog(authenticator, proxy, parent=self)
        dialog.exec_()

    def autotag(self):
        self.tagger.autotag(self.selected_objects)

    def cut(self):
        self._clipboard = self.selected_objects
        self.paste_action.setEnabled(bool(self._clipboard))

    def paste(self):
        selected_objects = self.selected_objects
        if not selected_objects:
            target = self.tagger.unmatched_files
        else:
            target = selected_objects[0]
        self.tagger.move_files(self.tagger.get_files_from_objects(self._clipboard), target)
        self._clipboard = []
        self.paste_action.setEnabled(False)