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))
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)
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()
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
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
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()
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)
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)
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)
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)
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()
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)
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()
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)
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))
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
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)
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
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")
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()
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()
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)
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()
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
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())
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()
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"])
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)
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)