From 1dbaea92c760490fec700d58fc8b50ed947ac2ed Mon Sep 17 00:00:00 2001 From: twodoorcoupe Date: Thu, 22 Feb 2024 17:30:36 +0100 Subject: [PATCH 1/6] Upload plugin Post Tagging Actions --- plugins/post_tagging_actions/__init__.py | 373 ++++++++++++++++++ .../options_post_tagging_actions.py | 175 ++++++++ .../options_post_tagging_actions.ui | 285 +++++++++++++ 3 files changed, 833 insertions(+) create mode 100644 plugins/post_tagging_actions/__init__.py create mode 100644 plugins/post_tagging_actions/options_post_tagging_actions.py create mode 100644 plugins/post_tagging_actions/options_post_tagging_actions.ui diff --git a/plugins/post_tagging_actions/__init__.py b/plugins/post_tagging_actions/__init__.py new file mode 100644 index 00000000..5fbe6b71 --- /dev/null +++ b/plugins/post_tagging_actions/__init__.py @@ -0,0 +1,373 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2024 Giorgio Fontanive (twodoorcoupe) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +PLUGIN_NAME = "Post Tagging Actions" +PLUGIN_AUTHOR = "Giorgio Fontanive" +PLUGIN_DESCRIPTION = """ +This plugin lets you set up actions that run with a context menu click. +An action consists in a command line executed for each album or each track along +with a few options to tweak the behaviour. +This can be used to run external programs and pass some variables to it. +""" +PLUGIN_VERSION = "0.1" +PLUGIN_API_VERSIONS = ["2.10", "2.11"] +PLUGIN_LICENSE = "GPL-2.0" +PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html" +PLUGIN_USER_GUIDE_URL = "https://github.com/twodoorcoupe/picard-plugins/tree/user_guides/user_guides/post_tagging_actions/guide.md" + +from picard.album import Album +from picard.track import Track +from picard.ui.options import OptionsPage, register_options_page +from picard.ui.itemviews import BaseAction, register_album_action, register_track_action +from picard import log, config +from picard.const import sys +from picard.util import thread +from picard.script import parser + +from .options_post_tagging_actions import Ui_PostTaggingActions +from PyQt5 import QtWidgets +from PyQt5.QtCore import QObject + +from collections import namedtuple +from queue import PriorityQueue +from threading import Thread +from concurrent import futures +from os import path +import re +import shlex +import subprocess + +# Additional special variables. +TRACK_SPECIAL_VARIABLES = { + "filepath": lambda file: file, + "folderpath": lambda file: path.dirname(file), + "filename": lambda file: path.splitext(path.basename(file))[0], + "filename_ext": lambda file: path.basename(file), + "directory": lambda file: path.basename(path.dirname(file)) +} +ALBUM_SPECIAL_VARIABLES = { + "get_num_matched_tracks", + "get_num_unmatched_files", + "get_num_total_files", + "get_num_unsaved_files", + "is_complete", + "is_modified" +} + +# Settings. +CANCEL = "pta_cancel" +OPTIONS = ["pta_command", "pta_wait_for_exit", "pta_execute_for_tracks", "pta_refresh_tags"] + +Options = namedtuple("Options", ("variables", *[option[4:] for option in OPTIONS])) +Action = namedtuple("Action", ("commands", "album", "options")) +PriorityAction = namedtuple("PriorityAction", ("priority", "counter", "action")) +action_queue = PriorityQueue() + + +class ActionLoader: + """Adds actions to the execution queue. + + Attributes: + action_options (list): Stores the actions' information loaded from the options page. + action_counter (int): The count of actions that have been added to the queue, used for priority. + """ + + def __init__(self): + self.action_options = [] + self.action_counter = 0 + self.load_actions() + + def _create_options(self, command, *other_options): + """Finds the variables in the command and adds the options to the action options list. + """ + variables = [variable[1:-1] for variable in re.findall(r'%.*?%', command)] + variables = [parser.normalize_tagname(variable) for variable in variables] + command = re.sub(r'%.*?%', '{}', command) + options = Options(variables, command, *other_options) + self.action_options.append(options) + + def _create_action(self, priority, commands, album, options): + """Adds an action with the given parameters to the execution queue. + + If the os is not windows, the command is split as suggested by the subprocess + module documentation. + """ + if not sys.IS_WIN: + commands = [shlex.split(command) for command in commands] + action = Action(commands, album, options) + priority_action = PriorityAction(priority, self.action_counter, action) + action_queue.put(priority_action) + self.action_counter += 1 + + def _replace_variables(self, variables, item): + """Returns a list where each variable is replaced with its value. + + Item is either an album or a track. For track special variables, + it uses the path of the first file of the given item. + If the variable is not found anywhere, it remains as in the original text. + """ + values = [] + album = item.album if isinstance(item, Track) else item + first_file_path = next(item.iterfiles()).filename + for variable in variables: + if variable in ALBUM_SPECIAL_VARIABLES: + values.append(getattr(album, variable)()) + elif variable in TRACK_SPECIAL_VARIABLES: + values.append(TRACK_SPECIAL_VARIABLES[variable](first_file_path)) + else: + values.append(item.metadata.get(variable, f"%{variable}%")) + return values + + def add_actions(self, album, tracks): + """Adds one action to the execution queue for each tuple in the action options list. + + Actions meant to be executed once for each track are considered as a single + action. This way, the other options are more consistent. + """ + for priority, options in enumerate(self.action_options): + if options.execute_for_tracks: + values_list = [self._replace_variables(options.variables, track) for track in tracks] + else: + values_list = [self._replace_variables(options.variables, album)] + commands = [options.command.format(*values) for values in values_list] + self._create_action(priority, commands, album, options) + + def load_actions(self): + """Loads the information from the options and saves it in the action options list. + + This gets called when the plugin is loaded or when the user saves the options. + """ + self.action_options = [] + loaded_options = zip(*[config.setting[name] for name in OPTIONS]) + for options in loaded_options: + command = options[0] + other_options = [eval(option) for option in options[1:]] + self._create_options(command, *other_options) + + +class ActionRunner: + """Runs actions in the execution queue. + + Attributes: + action_thread_pool (ThreadPoolExecutor): Pool used to run processes with the subprocess module. + refresh_tags_pool (ThreadPoolExecutor): Pool used to reload tags from files and refresh albums. + worker (Thread): Worker thread that picks actions from the execution queue. + """ + + def __init__(self): + self.action_thread_pool = futures.ThreadPoolExecutor() + self.refresh_tags_pool = futures.ThreadPoolExecutor(1) + self.worker = Thread(target = self._execute) + self.worker.start() + + def _refresh_tags(self, future_objects, album): + """Reloads tags from the album's files and refreshes the album. + + First, it makes sure that the action has finished running. This is used for + when an external process changes a file's tags. + """ + futures.wait(future_objects, return_when = futures.ALL_COMPLETED) + for file in album.iterfiles(): + file.set_pending() + file.load(lambda file: None) + thread.to_main(album.load, priority = True, refresh = True) + + def _run_process(self, command): + """Runs the process and waits for it to finish. + """ + process = subprocess.Popen(command, text = True, stdout = subprocess.PIPE, stderr = subprocess.PIPE) + answer = process.communicate() + if answer[0]: + log.info("Action output:\n%s", answer[0]) + if answer[1]: + log.error("Action error:\n%s", answer[1]) + + def _execute(self): + """Takes actions from the execution queue and runs them. + + If it finds an action with priority -1, the loop stops. When the loop + stops, both ThreadPoolExecutors are shutdown. + """ + while True: + priority_action = action_queue.get() + if priority_action.priority == -1: + break + next_action = priority_action.action + commands = next_action.commands + future_objects = {self.action_thread_pool.submit(self._run_process, command) for command in commands} + + if next_action.options.wait_for_exit: + futures.wait(future_objects, return_when = futures.ALL_COMPLETED) + if next_action.options.refresh_tags: + self.refresh_tags_pool.submit(self._refresh_tags, future_objects, next_action.album) + action_queue.task_done() + + self.action_thread_pool.shutdown(wait = False, cancel_futures = True) + self.refresh_tags_pool.shutdown(wait = False, cancel_futures = True) + + def stop(self): + """Makes the worker thread exit its loop. + + This gets called when Picard is closed. It waits for the processes that + are still executing to finish before exiting. + """ + if not config.setting[CANCEL]: + action_queue.join() + action_queue.put(PriorityAction(-1, -1, None)) + self.worker.join() + + +class ExecuteAlbumActions(BaseAction): + + NAME = "Run actions for highlighted albums" + + def callback(self, objs): + albums = {obj for obj in objs if isinstance(obj, Album)} + for album in albums: + action_loader.add_actions(album, album.tracks) + + +class ExecuteTrackActions(BaseAction): + + NAME = "Run actions for highlighted tracks" + + def callback(self, objs): + tracks = {obj for obj in objs if isinstance(obj, Track)} + albums = {track.album for track in tracks} + for album in albums: + album_tracks = tracks.intersection(album.tracks) + action_loader.add_actions(album, album_tracks) + + +class PostTaggingActionsOptions(OptionsPage): + """Options page found under the "plugins" page. + """ + + NAME = "post_tagging_actions" + TITLE = "Post Tagging Actions" + PARENT = "plugins" + + action_options = [config.ListOption("setting", name, []) for name in OPTIONS] + options = [config.BoolOption("setting", CANCEL, True), *action_options] + + def __init__(self, parent = None): + super(PostTaggingActionsOptions, self).__init__(parent) + self.ui = Ui_PostTaggingActions() + self.ui.setupUi(self) + self._reset_ui() + + header = self.ui.table.horizontalHeader() + header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeMode.Stretch) + for column in range(1, header.count()): + header.setSectionResizeMode(column, QtWidgets.QHeaderView.ResizeMode.ResizeToContents) + + self.ui.add_file_path.clicked.connect(self._open_file_dialog) + self.ui.add_action.clicked.connect(self._add_action_to_table) + self.ui.remove_action.clicked.connect(self._remove_action_from_table) + self.ui.up.clicked.connect(self._move_action_up) + self.ui.down.clicked.connect(self._move_action_down) + + self.get_table_columns_values = [ + self.ui.action.text, + self.ui.wait.isChecked, + self.ui.tracks.isChecked, + self.ui.refresh.isChecked + ] + + def _open_file_dialog(self): + """Adds the selected file's path to the command line text box. + """ + file = QtWidgets.QFileDialog.getOpenFileName(self)[0] + cursor_position = self.ui.action.cursorPosition() + current_text = self.ui.action.text() + if not sys.IS_WIN: + file = shlex.quote(file) + new_text = current_text[:cursor_position] + file + current_text[cursor_position:] + self.ui.action.setText(new_text) + + def _reset_ui(self): + self.ui.action.setText("") + self.ui.wait.setChecked(False) + self.ui.refresh.setChecked(False) + self.ui.albums.setChecked(True) + + def _add_action_to_table(self): + if not self.ui.action.text(): + return + row_position = self.ui.table.rowCount() + self.ui.table.insertRow(row_position) + for column in range(self.ui.table.columnCount()): + value = self.get_table_columns_values[column]() + value = str(value) + widget = QtWidgets.QTableWidgetItem(value) + self.ui.table.setItem(row_position, column, widget) + self._reset_ui() + + def _remove_action_from_table(self): + current_row = self.ui.table.currentRow() + if current_row != -1: + self.ui.table.removeRow(current_row) + + def _move_action_up(self): + current_row = self.ui.table.currentRow() + new_row = current_row - 1 + if current_row > 0: + self._swap_table_rows(current_row, new_row) + self.ui.table.setCurrentCell(new_row, 0) + + def _move_action_down(self): + current_row = self.ui.table.currentRow() + new_row = current_row + 1 + if current_row < self.ui.table.rowCount() - 1: + self._swap_table_rows(current_row, new_row) + self.ui.table.setCurrentCell(new_row, 0) + + def _swap_table_rows(self, row1, row2): + for column in range(self.ui.table.columnCount()): + item1 = self.ui.table.takeItem(row1, column) + item2 = self.ui.table.takeItem(row2, column) + self.ui.table.setItem(row1, column, item2) + self.ui.table.setItem(row2, column, item1) + + def load(self): + """Puts the plugin's settings into the actions table. + """ + settings = zip(*[config.setting[name] for name in OPTIONS]) + for row, values in enumerate(settings): + self.ui.table.insertRow(row) + for column in range(self.ui.table.columnCount()): + widget = QtWidgets.QTableWidgetItem(values[column]) + self.ui.table.setItem(row, column, widget) + self.ui.cancel.setChecked(config.setting[CANCEL]) + + def save(self): + """Saves the actions table items in the settings. + """ + settings = [] + for column in range(self.ui.table.columnCount()): + settings.append([]) + for row in range(self.ui.table.rowCount()): + setting = self.ui.table.item(row, column).text() + settings[column].append(setting) + config.setting[OPTIONS[column]] = settings[column] + config.setting[CANCEL] = self.ui.cancel.isChecked() + action_loader.load_actions() + + +action_loader = ActionLoader() +action_runner = ActionRunner() +register_album_action(ExecuteAlbumActions()) +register_track_action(ExecuteTrackActions()) +register_options_page(PostTaggingActionsOptions) +QObject.tagger.register_cleanup(action_runner.stop) diff --git a/plugins/post_tagging_actions/options_post_tagging_actions.py b/plugins/post_tagging_actions/options_post_tagging_actions.py new file mode 100644 index 00000000..0d63b62b --- /dev/null +++ b/plugins/post_tagging_actions/options_post_tagging_actions.py @@ -0,0 +1,175 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'plugins/post_tagging_actions/options_post_tagging_actions.ui' +# +# Created by: PyQt5 UI code generator 5.15.10 +# +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_PostTaggingActions(object): + def setupUi(self, PostTaggingActions): + PostTaggingActions.setObjectName("PostTaggingActions") + PostTaggingActions.resize(638, 450) + PostTaggingActions.setMinimumSize(QtCore.QSize(100, 0)) + self.verticalLayout = QtWidgets.QVBoxLayout(PostTaggingActions) + self.verticalLayout.setObjectName("verticalLayout") + self.scrollArea = QtWidgets.QScrollArea(PostTaggingActions) + self.scrollArea.setFrameShape(QtWidgets.QFrame.NoFrame) + self.scrollArea.setWidgetResizable(True) + self.scrollArea.setObjectName("scrollArea") + self.scrollAreaWidgetContents = QtWidgets.QWidget() + self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 606, 451)) + self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents") + self.vboxlayout = QtWidgets.QVBoxLayout(self.scrollAreaWidgetContents) + self.vboxlayout.setObjectName("vboxlayout") + self.action_widget = QtWidgets.QWidget(self.scrollAreaWidgetContents) + self.action_widget.setObjectName("action_widget") + self._2 = QtWidgets.QVBoxLayout(self.action_widget) + self._2.setObjectName("_2") + self.widget_2 = QtWidgets.QWidget(self.action_widget) + self.widget_2.setObjectName("widget_2") + self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.widget_2) + self.horizontalLayout_3.setObjectName("horizontalLayout_3") + self.label_3 = QtWidgets.QLabel(self.widget_2) + self.label_3.setObjectName("label_3") + self.horizontalLayout_3.addWidget(self.label_3) + self.add_file_path = QtWidgets.QPushButton(self.widget_2) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.add_file_path.sizePolicy().hasHeightForWidth()) + self.add_file_path.setSizePolicy(sizePolicy) + self.add_file_path.setCheckable(False) + self.add_file_path.setObjectName("add_file_path") + self.horizontalLayout_3.addWidget(self.add_file_path) + self._2.addWidget(self.widget_2) + self.action = QtWidgets.QLineEdit(self.action_widget) + self.action.setObjectName("action") + self._2.addWidget(self.action) + self.wait = QtWidgets.QCheckBox(self.action_widget) + self.wait.setObjectName("wait") + self._2.addWidget(self.wait) + self.refresh = QtWidgets.QCheckBox(self.action_widget) + self.refresh.setObjectName("refresh") + self._2.addWidget(self.refresh) + self.widget_3 = QtWidgets.QWidget(self.action_widget) + self.widget_3.setObjectName("widget_3") + self.horizontalLayout_6 = QtWidgets.QHBoxLayout(self.widget_3) + self.horizontalLayout_6.setObjectName("horizontalLayout_6") + self.albums = QtWidgets.QRadioButton(self.widget_3) + self.albums.setObjectName("albums") + self.horizontalLayout_6.addWidget(self.albums) + self.tracks = QtWidgets.QRadioButton(self.widget_3) + self.tracks.setObjectName("tracks") + self.horizontalLayout_6.addWidget(self.tracks) + self._2.addWidget(self.widget_3) + self.vboxlayout.addWidget(self.action_widget) + self.table_commands = QtWidgets.QWidget(self.scrollAreaWidgetContents) + self.table_commands.setObjectName("table_commands") + self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.table_commands) + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.widget = QtWidgets.QWidget(self.table_commands) + self.widget.setObjectName("widget") + self.horizontalLayout_5 = QtWidgets.QHBoxLayout(self.widget) + self.horizontalLayout_5.setObjectName("horizontalLayout_5") + self.add_action = QtWidgets.QPushButton(self.widget) + self.add_action.setObjectName("add_action") + self.horizontalLayout_5.addWidget(self.add_action) + self.remove_action = QtWidgets.QPushButton(self.widget) + self.remove_action.setObjectName("remove_action") + self.horizontalLayout_5.addWidget(self.remove_action) + self.horizontalLayout_2.addWidget(self.widget) + self.widget1 = QtWidgets.QWidget(self.table_commands) + self.widget1.setObjectName("widget1") + self.horizontalLayout_4 = QtWidgets.QHBoxLayout(self.widget1) + self.horizontalLayout_4.setObjectName("horizontalLayout_4") + self.up = QtWidgets.QToolButton(self.widget1) + self.up.setText("") + self.up.setArrowType(QtCore.Qt.UpArrow) + self.up.setObjectName("up") + self.horizontalLayout_4.addWidget(self.up) + self.down = QtWidgets.QToolButton(self.widget1) + self.down.setText("") + self.down.setArrowType(QtCore.Qt.DownArrow) + self.down.setObjectName("down") + self.horizontalLayout_4.addWidget(self.down) + self.horizontalLayout_2.addWidget(self.widget1, 0, QtCore.Qt.AlignRight) + self.vboxlayout.addWidget(self.table_commands) + self.table = QtWidgets.QTableWidget(self.scrollAreaWidgetContents) + self.table.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents) + self.table.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) + self.table.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection) + self.table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) + self.table.setObjectName("table") + self.table.setColumnCount(4) + self.table.setRowCount(0) + item = QtWidgets.QTableWidgetItem() + self.table.setHorizontalHeaderItem(0, item) + item = QtWidgets.QTableWidgetItem() + self.table.setHorizontalHeaderItem(1, item) + item = QtWidgets.QTableWidgetItem() + self.table.setHorizontalHeaderItem(2, item) + item = QtWidgets.QTableWidgetItem() + self.table.setHorizontalHeaderItem(3, item) + self.table.horizontalHeader().setDefaultSectionSize(150) + self.vboxlayout.addWidget(self.table) + self.line = QtWidgets.QFrame(self.scrollAreaWidgetContents) + self.line.setFrameShadow(QtWidgets.QFrame.Sunken) + self.line.setFrameShape(QtWidgets.QFrame.HLine) + self.line.setObjectName("line") + self.vboxlayout.addWidget(self.line) + self.frame = QtWidgets.QFrame(self.scrollAreaWidgetContents) + self.frame.setObjectName("frame") + self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.frame) + self.verticalLayout_2.setObjectName("verticalLayout_2") + self.cancel = QtWidgets.QCheckBox(self.frame) + self.cancel.setObjectName("cancel") + self.verticalLayout_2.addWidget(self.cancel) + self.label = QtWidgets.QLabel(self.frame) + self.label.setOpenExternalLinks(True) + self.label.setObjectName("label") + self.verticalLayout_2.addWidget(self.label) + self.vboxlayout.addWidget(self.frame) + spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.vboxlayout.addItem(spacerItem) + self.scrollArea.setWidget(self.scrollAreaWidgetContents) + self.verticalLayout.addWidget(self.scrollArea) + + self.retranslateUi(PostTaggingActions) + QtCore.QMetaObject.connectSlotsByName(PostTaggingActions) + + def retranslateUi(self, PostTaggingActions): + _translate = QtCore.QCoreApplication.translate + PostTaggingActions.setWindowTitle(_translate("PostTaggingActions", "Form")) + self.label_3.setText(_translate("PostTaggingActions", "Insert action")) + self.add_file_path.setToolTip(_translate("PostTaggingActions", "Add a file path to the command line.")) + self.add_file_path.setText(_translate("PostTaggingActions", "Add file")) + self.wait.setToolTip(_translate("PostTaggingActions", "If checked, the next action runs immediately.")) + self.wait.setText(_translate("PostTaggingActions", " Wait for process to finish")) + self.refresh.setToolTip(_translate("PostTaggingActions", "

If checked, the album will "refresh" after this action finishes.

")) + self.refresh.setText(_translate("PostTaggingActions", " Refresh tags after process finishes")) + self.albums.setToolTip(_translate("PostTaggingActions", "Makes the action execute once for each album tagged.")) + self.albums.setText(_translate("PostTaggingActions", "Execute for albums")) + self.tracks.setToolTip(_translate("PostTaggingActions", "Makes the action run once for each track tagged.")) + self.tracks.setText(_translate("PostTaggingActions", "Execute for tracks")) + self.add_action.setToolTip(_translate("PostTaggingActions", "Add the action at the bottom of the queue.")) + self.add_action.setText(_translate("PostTaggingActions", "Add action")) + self.remove_action.setToolTip(_translate("PostTaggingActions", "Remove the selected action.")) + self.remove_action.setText(_translate("PostTaggingActions", "Remove action")) + self.table.setToolTip(_translate("PostTaggingActions", "Actions at the top of the list run first. Use the buttons on the right to reorder the selected action.")) + item = self.table.horizontalHeaderItem(0) + item.setText(_translate("PostTaggingActions", "Action")) + item = self.table.horizontalHeaderItem(1) + item.setText(_translate("PostTaggingActions", " Wait for exit ")) + item = self.table.horizontalHeaderItem(2) + item.setText(_translate("PostTaggingActions", " Execute for tracks ")) + item = self.table.horizontalHeaderItem(3) + item.setText(_translate("PostTaggingActions", " Refresh tags ")) + self.cancel.setToolTip(_translate("PostTaggingActions", "

If not checked, when Picard is closed, it will wait for the actions to finish in the background.

")) + self.cancel.setText(_translate("PostTaggingActions", "Cancel actions in the queue when Picard is closed")) + self.label.setText(_translate("PostTaggingActions", "

Hover over each item to know more, or take a peek at the user guide here.

")) diff --git a/plugins/post_tagging_actions/options_post_tagging_actions.ui b/plugins/post_tagging_actions/options_post_tagging_actions.ui new file mode 100644 index 00000000..0b428915 --- /dev/null +++ b/plugins/post_tagging_actions/options_post_tagging_actions.ui @@ -0,0 +1,285 @@ + + + PostTaggingActions + + + + 0 + 0 + 638 + 450 + + + + + 100 + 0 + + + + Form + + + + + + QFrame::NoFrame + + + true + + + + + 0 + 0 + 606 + 451 + + + + + + + + + + + + + Insert action + + + + + + + + 0 + 0 + + + + Add a file path to the command line. + + + Add file + + + false + + + + + + + + + + + + + If checked, the next action runs immediately. + + + Wait for process to finish + + + + + + + <html><head/><body><p>If checked, the album will &quot;refresh&quot; after this action finishes.</p></body></html> + + + Refresh tags after process finishes + + + + + + + + + + Makes the action execute once for each album tagged. + + + Execute for albums + + + + + + + Makes the action run once for each track tagged. + + + Execute for tracks + + + + + + + + + + + + + + + + + + + Add the action at the bottom of the queue. + + + Add action + + + + + + + Remove the selected action. + + + Remove action + + + + + + + + + + + + + + + + Qt::UpArrow + + + + + + + + + + Qt::DownArrow + + + + + + + + + + + + + Actions at the top of the list run first. Use the buttons on the right to reorder the selected action. + + + QAbstractScrollArea::AdjustToContents + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + 150 + + + + Action + + + + + Wait for exit + + + + + Execute for tracks + + + + + Refresh tags + + + + + + + + QFrame::Sunken + + + Qt::Horizontal + + + + + + + + + + <html><head/><body><p>If <span style=" font-weight:700;">not </span>checked, when Picard is closed, it will wait for the actions to finish in the background.</p></body></html> + + + Cancel actions in the queue when Picard is closed + + + + + + + <html><head/><body><p>Hover over each item to know more, or take a peek at the user guide <a href="https://github.com/twodoorcoupe/picard-plugins/tree/user_guides/user_guides/post_tagging_actions/guide.md"><span style=" text-decoration: underline; color:#3584e4;">here.</span></a></p></body></html> + + + true + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + From e18be8158ffc9d0b405d66b8c33ed18a07327f44 Mon Sep 17 00:00:00 2001 From: twodoorcoupe Date: Mon, 26 Feb 2024 08:52:43 +0100 Subject: [PATCH 2/6] Fix Codacy errors and implement requested changes --- plugins/post_tagging_actions/__init__.py | 50 ++++++++++++----- plugins/post_tagging_actions/docs/guide.md | 52 ++++++++++++++++++ plugins/post_tagging_actions/docs/options.png | Bin 0 -> 91732 bytes .../options_post_tagging_actions.py | 26 ++++++++- .../options_post_tagging_actions.ui | 38 ++++++++++++- 5 files changed, 145 insertions(+), 21 deletions(-) create mode 100644 plugins/post_tagging_actions/docs/guide.md create mode 100644 plugins/post_tagging_actions/docs/options.png diff --git a/plugins/post_tagging_actions/__init__.py b/plugins/post_tagging_actions/__init__.py index 5fbe6b71..b7bf0d02 100644 --- a/plugins/post_tagging_actions/__init__.py +++ b/plugins/post_tagging_actions/__init__.py @@ -24,7 +24,7 @@ PLUGIN_API_VERSIONS = ["2.10", "2.11"] PLUGIN_LICENSE = "GPL-2.0" PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html" -PLUGIN_USER_GUIDE_URL = "https://github.com/twodoorcoupe/picard-plugins/tree/user_guides/user_guides/post_tagging_actions/guide.md" +PLUGIN_USER_GUIDE_URL = "https://github.com/metabrainz/picard-plugins/tree/2.0/plugins/post_tagging_actions/docs/guide.md" from picard.album import Album from picard.track import Track @@ -46,14 +46,14 @@ from os import path import re import shlex -import subprocess +import subprocess # nosec B404 # Additional special variables. TRACK_SPECIAL_VARIABLES = { "filepath": lambda file: file, - "folderpath": lambda file: path.dirname(file), + "folderpath": lambda file: path.dirname(file), # pylint: disable=unnecessary-lambda "filename": lambda file: path.splitext(path.basename(file))[0], - "filename_ext": lambda file: path.basename(file), + "filename_ext": lambda file: path.basename(file), # pylint: disable=unnecessary-lambda "directory": lambda file: path.basename(path.dirname(file)) } ALBUM_SPECIAL_VARIABLES = { @@ -67,12 +67,14 @@ # Settings. CANCEL = "pta_cancel" +MAX_WORKERS = "pta_max_workers" OPTIONS = ["pta_command", "pta_wait_for_exit", "pta_execute_for_tracks", "pta_refresh_tags"] Options = namedtuple("Options", ("variables", *[option[4:] for option in OPTIONS])) Action = namedtuple("Action", ("commands", "album", "options")) PriorityAction = namedtuple("PriorityAction", ("priority", "counter", "action")) action_queue = PriorityQueue() +variables_pattern = re.compile(r'%.*?%') class ActionLoader: @@ -91,9 +93,8 @@ def __init__(self): def _create_options(self, command, *other_options): """Finds the variables in the command and adds the options to the action options list. """ - variables = [variable[1:-1] for variable in re.findall(r'%.*?%', command)] - variables = [parser.normalize_tagname(variable) for variable in variables] - command = re.sub(r'%.*?%', '{}', command) + variables = [parser.normalize_tagname(variable[1:-1]) for variable in variables_pattern.findall(command)] + command = variables_pattern.sub('{}', command) options = Options(variables, command, *other_options) self.action_options.append(options) @@ -149,10 +150,10 @@ def load_actions(self): This gets called when the plugin is loaded or when the user saves the options. """ self.action_options = [] - loaded_options = zip(*[config.setting[name] for name in OPTIONS]) - for options in loaded_options: - command = options[0] - other_options = [eval(option) for option in options[1:]] + option_tuples = zip(*[config.setting[name] for name in OPTIONS]) + for option_tuple in option_tuples: + command = option_tuple[0] + other_options = [eval(option) for option in option_tuple[1:]] # nosec B307 self._create_options(command, *other_options) @@ -166,7 +167,7 @@ class ActionRunner: """ def __init__(self): - self.action_thread_pool = futures.ThreadPoolExecutor() + self.action_thread_pool = futures.ThreadPoolExecutor(config.setting[MAX_WORKERS]) self.refresh_tags_pool = futures.ThreadPoolExecutor(1) self.worker = Thread(target = self._execute) self.worker.start() @@ -186,7 +187,12 @@ def _refresh_tags(self, future_objects, album): def _run_process(self, command): """Runs the process and waits for it to finish. """ - process = subprocess.Popen(command, text = True, stdout = subprocess.PIPE, stderr = subprocess.PIPE) + process = subprocess.Popen( + command, + text = True, + stdout = subprocess.PIPE, + stderr = subprocess.PIPE + ) # nosec B603 answer = process.communicate() if answer[0]: log.info("Action output:\n%s", answer[0]) @@ -201,12 +207,17 @@ def _execute(self): """ while True: priority_action = action_queue.get() + QObject.tagger.window.set_statusbar_message( + N_("Post Tagging Actions: number of pending requests is %(pending_requests)d"), + {"pending_requests": action_queue.qsize()}, + timeout = 3000 + ) + if priority_action.priority == -1: break next_action = priority_action.action commands = next_action.commands future_objects = {self.action_thread_pool.submit(self._run_process, command) for command in commands} - if next_action.options.wait_for_exit: futures.wait(future_objects, return_when = futures.ALL_COMPLETED) if next_action.options.refresh_tags: @@ -259,7 +270,11 @@ class PostTaggingActionsOptions(OptionsPage): PARENT = "plugins" action_options = [config.ListOption("setting", name, []) for name in OPTIONS] - options = [config.BoolOption("setting", CANCEL, True), *action_options] + options = [ + config.BoolOption("setting", CANCEL, True), + config.IntOption("setting", MAX_WORKERS, 4), + *action_options + ] def __init__(self, parent = None): super(PostTaggingActionsOptions, self).__init__(parent) @@ -350,6 +365,7 @@ def load(self): widget = QtWidgets.QTableWidgetItem(values[column]) self.ui.table.setItem(row, column, widget) self.ui.cancel.setChecked(config.setting[CANCEL]) + self.ui.max_workers.setValue(config.setting[MAX_WORKERS]) def save(self): """Saves the actions table items in the settings. @@ -362,6 +378,7 @@ def save(self): settings[column].append(setting) config.setting[OPTIONS[column]] = settings[column] config.setting[CANCEL] = self.ui.cancel.isChecked() + config.setting[MAX_WORKERS] = self.ui.max_workers.value() action_loader.load_actions() @@ -370,4 +387,7 @@ def save(self): register_album_action(ExecuteAlbumActions()) register_track_action(ExecuteTrackActions()) register_options_page(PostTaggingActionsOptions) + +# This is used to register functions that run when the application is being closed. +# action_runner.stop makes the background threads stop. QObject.tagger.register_cleanup(action_runner.stop) diff --git a/plugins/post_tagging_actions/docs/guide.md b/plugins/post_tagging_actions/docs/guide.md new file mode 100644 index 00000000..95436258 --- /dev/null +++ b/plugins/post_tagging_actions/docs/guide.md @@ -0,0 +1,52 @@ +# Post Tagging Actions +This plugin lets you set up actions that run with a context menu click. +An action consists in a command line executed for each album or each track along with a few options to tweak the behaviour. This can be used to run external programs and pass some variables to it. Environment variables do not work. + +To run the actions, +- First move the albums or tracks you are interested in to the right hand pane. +- Then highlight all the items you want the actions to run for. +- Right click, go to plugins, then click "Run Actions for highlighted albums/tracks". +### Adding an action +In the options page, you will find "Post Tagging Actions" under "Plguins". You will be greeted by this: + +![options](options.png) + +1. Enter the command to run in the text box, choose your options, then click "Add action". +2. You can click on "Add file" to search for a file and add its path to the text box. +3. Once you add an action, it will appear in the table at the bottom of the page. You can reorder actions with the arrows above the table. +## Options +- `Wait for process to finish` will make the next command in the queue execute only after this one has finished. +- `Refresh tags after process finishes` will reload the files once the command finishes. This is useful when an external program changes files' tags. +- `Execute for albums/tracks` lets you choose whether the command will be executed once for each track or each album highlighted. + +The order of the actions in the table represents the order of execution: the top most action will be executed first. +## Variables +You can use variables in the commands just like in scripting. For example: +``` +python /path/script.py --album %album% --artist %albumartist% +``` +The variables `%album%` and `%albumartist%` will be replaced with their value for each album. + +For actions that execute once per album, only variables in the album's metadata can be used. Same thing for actions that execute once per track, only variables in the track's metadata can be used. +### Special variables +There are also extra variables that can be used. + +The following are used to get files' information: +- `%filepath%`: The full path of the file. +- `%folderpath%`: The path of the folder in which the file is located. +- `%filename%`: The name of the file, without the extension. +- `%filename_ext%`: The name of the file, with the extension. +- `%directory%`: The name of the folder in which the file is located. + +When these are used with album actions, the first file found is considered. For example, if you keep all tracks in the same folder, using `%folderpath%` will give you the path to that folder. + +The following apply to each album: +- `%gen_num_matched_tracks%`: The number of tracks which have a matching file. +- `%gen_num_unmatched_tracks%`: The number of tracks without any matching files. +- `%gen_num_total_files%`: The number of files added. +- `%gen_num_unsaed_files%`: The number of files that have unsaved changes. +- `%is_complete%`: "True" if every track is matched, "False" otherwise. (Shown with a gold disc in Picard) +- `%is_modified%`: "True" if the album has some changes to apply, "False" otherwise. (Shown with a red star on the disc) + +When these are used with track actions, the album to which the track belongs to is considered. + diff --git a/plugins/post_tagging_actions/docs/options.png b/plugins/post_tagging_actions/docs/options.png new file mode 100644 index 0000000000000000000000000000000000000000..cf2fef663659bb394d815fe2568c0785e07e4b58 GIT binary patch literal 91732 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%0DA!9_Q3>8hLiZVr+ zMTk-|&!j;l8jXo$C@E8@D2ik(X^;?wM8o?z%39BTKkK>Q=a2WFcWrBJ+xo3VeZSXr zp2x87`>`JmW&_En!PNRZ^x4HTBX~bh2pTmch4~LkZ7Qmu0#mf0t zRiCI+Esx@5V&=$u%(<8BBw#IxAGBcJqD2Yxw~}{=AQVj%nedKfWK61U(HhdIP2i6+ z1DiwF$cTr3VBi?u&}kY}1SB>QTdN*kmP3FgQ4uWt+|^GRvB&8fp=2f^N0gMwiIk`g=+V4d@j!1K2zthIUPVz}&7B#m zE_rwMq)t2Qs^4nk|GGq5C1<>&P7F0Qxt4CQW7ye4L{pGzo3=hpvK-x4S=Tk07Nlyn zV>F0tczF2Z=g-GapRNL{YC}KQ8$L`l@a@_i)7m$s?zdj?k$twPFV6YB&A5|aoxS76 ztXVUNll>t--~Z&v?KGwWw0`^;z(4r=cQQ9O&(6*Dx)x3E&8N2hU=8AEb+cuQN4Iag zk!Q+{62;A)$0`CAvf%6m3x4hi(6B(n%k=2 z_Af0!#ibpgUAH=KtNShd0I{lqa9N)i9lG5pXMfQlUHkp|!M7hjK0xJ#rg%n{R%K~k z2il4HhOq?d{=0tXL%AY77*9g*TeOkC$%ye-$~j>a9z56qR=%)paFagc=Hykx#z!8{ zbM*UDxp>KFkG0Z3wgUS1^YcrKbL7(S9%_o$C)&gst~>~y@Tq4XoBi=0vF@uY?7@iG zn~4*+w8WbC+z0V7>s7E~%eqBdkNwd*xXL=oxszY_{It$@MUFAeI)(QxjW(uJq}AKp zZ1C2eIXyk~Bj z^#7Ny1PkL~^ap8oH56XlY+j2#-Y&=gs2yBYS?c>oFK!S3Ey&UP!J>fIFQpw2>12BPVSXF@6&#LQ@8@^#293mAT##oiqU|v<82?g0jAZFZf$XDI~$SP68OUq}` z%E!Yy?ob) zsXbc&i92Og_#wHYLYu?*2p`QOA0w~sOlPC@$3z{p z&-q@==hfs%3*$>NS0FXaW&D7Flb&zfIu?$a%d^yMVrg$5K5h3g{a`U6B~htdp9B)b z#I53k#hZpP-bHEA!vPhz=(*8Tb?h^z4YdfG`h+WD5&#e?(6@x8zI{6~ZNy9UUf(Y3 zM<+rpaVgGgIqe(eLeH?>e|$yFMliWWY(Pe9t0{)GBdxkTl(E@q(^?Ac=YH+^X)`eb zXQ6&;IE>^&3gek#4TssqRQ6-5lBZoHOJ9Uq^+6t(H zzJF7tUp77elo1U&T%~iDxi5khF1J&DcdfLfL6+L!A4?6ZEoEta`SB$5n{<T&_A1!_A^|i|hkpgmgwj6D;QD@^9?`^(+Y7alVDs~9ZAa(t$*Lm^+fp>c!IINq5 zFvJx{K$b(aQ20L~s2=v6{I2p&#$#^7=n_pk(}u<&)S@*V@8}Ho^SyrU8XeeOS{zS1 zFh0>h{X>~IYGTsB)(vGN$c>V>Qg~Q67XjSZJv-Bac%Gp5%jQNGYaLbILumc)uzR@j zbY|#hqy`G?aAU5pSwQDX_p8yj8Ogy)#XG)7t zKU?&4(dDAw6HCU=xsx+|#AmJrGqPIrzQP+J9tPK#cXvnA+U|9~+0$=WH`6YuzrH00 zUOj)+ZG!dPat7OK)3vw4xtP+_IOP8bqMAfS*8QgAS&&WNg%#ua$9R{iXr^$(K!z_Axj((bc5&-e;ix>OR z6if-Zab|6H>@pe!)(MS-vW$EHK2VFgJTq3s@WJ&Vy6eABI6L)+9Z6@uu>Nd400PwJ z(w{Us-i;Z&2iF}xed|-ZloG-=C8!?QMs177T!$S%L=?&I=_eHkUY2LAsPopZuPkq6 zeD)cKz6DnRS{X1o2?vdam2^R?#&0! zj3^Wh^*xrk=rr6rZgXs`E;FXI^9JL4B!5$7x94e`-eRN%7OwpZ;Lvc;{0`Z=LgY?9txcVmN4;526;;R)H z;Zak%HF5!mLx?SOR#N$ld}}n5yO=QJ*@5Suv-xRYc~8L4MAjA@n@^E3QC~lR%EF?+ zVa=Lcf_cLB{p&7M_l{C691{C1=UY(Qia!_Jv!cv(&R;lsTLB-=NvIDTd2!M{Vi_;7 z6L?(s#6&+evp^{hDruXO1pPAONp}rbv<#w5$E=qo) z3hrUMQj}&KDgPG4EBjB}7Z$SeX`-^t$qTD0GqPXzd3k9_$?jdDebt<+H0=-q{b&Wb zLK7xU3Z^-$9rKBjM0VzIM43mJ$8Us<@u3>Qz)VztX!2@D{i*vc+Jg%P)eC3PV^*V% zh28eU?MtGBDRE<85>YmUoJgT-=RETJ$7j2tH3h*G4w!?EetoyQ@0hoP^B4z60c60_ zb`u1$u!sOE7HdDMtQ{YtZVfx(v3(CGF3|L}ht~iUJP}8R$9IXYf4xV8OfCiqfx;P(-9AtHqV@(UiQNtX!SEWsVF_62*1unv%EJ6fAe z)Ig+oRo#uzDFM^u^&(>aAK>60T6!-Iu8RGXTplo2!*Js9<`Bw&AN`iT9DtUNe?X%# zxm0a{HL0H?EU#0JDbWoy>Oa6#U(@BAn^e}P_csWxX+tbJZCTT*rKm(ml=CRK_&2Yr{*6upD3CZ3F&CV_g=_4;?`;vmMa(&DSn8FfmYg(?yV1Cmi5Lt45@YW* zvwuuYee%SgopdiNaS1!*7IzRP>Ev0Zzf_QCegff&5LWEIa+QS@+QAh>K-9 z8L;&+976u$-c?|a-){FhcA9ml?esX3Lb$L&&Rc7^nwpp?5)I_OQI+cZ005DZZzUw? z7f2sSS1DH}co{vSwzf9MtR{s?&Oc_=D2ZRNeTID_$H2aV4~~){;ZR&4llF+B_c*o&q(Bx!x|YerQ>RYVC~m>c(>XA<3I+1Gr*Y6XZ@=}KX=l;d zMNBiN<&widcF}j&v3=YTh+V1OL>u%ElmB9dW=OYM0q|sxaXTM;TJB5R4Z9qtLmA3JJaJ(_btB}6wb~bixz;2 zk4iQu{(phe1s-c-&a6jzA3QWkLWwqI@YiQDt^A$0;NKU z0o-qjjjh~mRo?tG5-7YDJ2E2!Y0s74Z>sk9i|gz)?Vd+m34-L`>h*@g6V060B5kqg z=+*1o-Mi7iq2liV%j53nmju`<1SBUA#p6Lw&#jj(T{6vpIux73>}(m4CZ)~A;~TzB z2sn^_2`&oEv)%+^f#}^%bA1pEJPlXx)6ObNGcF|ubZNB#OzI0++DImA6d)=%e8l!S29;EnEfd zKxmW75F)C8R=903UsT;mZb#uxf~+kgxO|?zn6@ck(0Q9b4hLc8zDj{nIrLPPwha&y z%HEjuKNcxq^B^Nv@G2H)bID@TN?GOIN2d^F`dOv=l4sT%zc}9LG0)j=NWVH*!7B($ z(DRBF_$P_mtl278_JpuLr=1uT(rz(>7{d1+Y2|*5?UTGaWM+S7U<9b-p%JBMwSipD z^eUt#*6Wt5zzfj}HiaRUjWRG1H*ellfOA3b8SznOZ=U`cSrsOB1n7Kg=wDiIeMO~_ zw7AsKc$*t98Sd;{UgJ(njzo1zL`jNIm?mMCbGi z{%MEv+6`UZv3vLJXkrx{2Rrcli=K?{FbBafnhwSPRz9a+U4Fc1Ep@VKTz+QeNSYJb zRsmF>U=3r$AGH`9(6Sq?&+r>ss==n!QE^@$oz1>bu267u$%!&z#-WA090NTl))yQB zYzLmeir{N-6DI!@l4hv@q-ALK2{8dmA4CS-p<$0gu z-zCyF?ccvYpFqwTKm=Qwlz6SE7VqLs6yenZyQ^#mZ^zdNE5vM82n22*wB5Y9bKN2% z9d~}qFDMYs2hdKbU7T7qqxHL_&^lX7@IZo;Dj)>9h7fAZ;1>g{Kh(gJ0OdYMD?2k( z0kP^~qm$L5+rM+cIwZ$ce=cCf|2GmVS}VR3=B{po227Co5-o&V5*ul%l!T!JG{wo4 z;tTfM-_&zii6vU(|a=`Blg^#9@GAV(DLZ1-6s?@@;KldMos@f)Z7L*;1g|FkXX&-B~OnmOcNUb zrls`_A0j=w#So;-fL3`{Vr2?@RD*1?htkaMS;@C=???)a)+l8DSH10cJHY}>BgX}&WL5y$iB2TPsk zyksDTP@PNAqeYr}Hu}_%Ns}g-;~WJZf$H@FVg;i8j87HHRc}ApDsq`Ni|5E1Z!?L0 zacP&r!{>4@MQqORU`4C}>TCfVQ9@O9WbKA3lZQEr3LkJG30E39 zkZ_=P=`>K!Z0-FDEeR#?C7c1EZiF8r-U}>=0H((erQNH@p3$YbN5w(H&u=mu5ya85 z{EVu@Rri}AT~stUs>18YRVUNei|YdgT?00&P^>D%pf2N18POWcK2ORU$~JnItsE@^ z6yU3hEAY>MN+BmrOj0EA?KWIgtpX5BP$3hwa73oAjBsD&N(YN$`Kjon{mfoF=q-CM zRJWGaT)I>RM1qaGckj-xP6jBA_&B0{9zB^I*aZCpgxksVa`oy8HVl9YlvLqsn&t8q zmSG4f7|QCgQ~M??MLefeidw{eU55^}$WM6p<;!HQ4$oB@Zu&vEKSL*{=rnuIgJ&fQ zQ}ZQ@i~p@NU|_tCE3G$*m{zK)v{U|1hht3FqvWPb!&ZG2T-UyJ+|GaMfMkhz`)N<# z%3(>;P9Hm_xw9&g+%-5cvwI$=Z2rQ9sdNQ%3y4}Ot*-R#-@mylBND=Ra#&ThjV1(kyaf_T_F29Fn^)>BfR1f^>}`pN^Do`JO?gG zZ-UH?kq)a5(EuMZ#PB1kdK%`E+4<#l`NMPJ8_DrnV7;9SfL`}f(Jk8R_!c12-rgQ_ z>{fPre)UV5T()>b8%T>OP&AjfhYP}-o>P5!CSnGL>X@7sTt+l%1jlvh z(yEKadAYd(C#QceIR#vD=;%>{*p1~8Ub^gH>DZ^V&$5ia;7v=5^QNa6&v80qR~?$; zIf-p&BNXB$3${(K%w$}!C@bHscFOGzdlvvG5WMUEEu)FjRcsmby$|2l9y+vV<3a0R zbHyCtt5;k94SSPGP~b?@D=p(+TdCaeF1=3ez#4M|&Tuc2}-;#*o>;MNdy{0&A4|_Vx9( zMg<|SXgigMex_g6O4u#AuOVb_Ar)oeC;N2QMpVzdG~BSZoA=Wz^d+U;AXk8;nSwi*A4;0M{!rka*d z55`cst}IT|?K5Qbw{wvG{xMBP#^ugS9IDJTQ}XZU?^7z=&8HPOt9t&)7F#1eUJB`2 zCF(6zkF2L3umQC%5N~9YKkA4L*GD83tyMhu(7As9(gNfqiwqLh8UYH|Ac(d`6gN=$ zEST7ZiW+!SJ=`9=ys?krINHorGZ(;21J5C=aihZ|5PIgSGK~iWOS6QIJ`OiG+%}cX zOKZK;9o1Xqcydmcx^7>6ex0*SqVL@S=0pNvcR+ozg``x<39%McV5Yn_;Y@r2MNlX` zHamCeE(D23nuUnZ6>hTc`tPSe69SkcQIB=j3>ZaQfj9imWi)OAxP7Tt@q;Wsw zW%*(Oj^!ESt0=`eUR!qUQX*n2zz2V&R;rP9;6U>yp*gXmB79xi-yNY`>)om%^E!EG zDc>Qil9j1+1q!k|Y}}q!a`(Zo=P|w6R%hJ-{2%9=>#j1m93HO4>69fr8Kut|hy>N{ zX}Aho%IPbX&6%@_0tQ-FK@4rtlWBp}i0GGl#2|4464;})j$6X#0YZz?7^;hAe4K@a z#recA1_NP9ve%W_+1joEjHcpEMrFI}oNv#bJ!Jp^&j9pv^+#gYHv}ClLM8Kkg+@ki z-<94zzM%gJ7v1~~Ozj*r*`K~Pv&q6_>t4A-%~e4+YR^IFb@s#&G#^nmNsEq?AFpxPykPh~m3uzRH=eK01$Q zx`wQJ<#MHq(V@W8r#G38H)~m`a~y{EUGB=r8M|ksHT>{=yJ~7k-;F8CUtTw?Q&E|2 zY}zx@vUyfvWXHP44<-*Cg2QjL@`U+lMQUWCXRuwoxaNyGs^C9&)6%^7lpxAuD68qx zpnR^UC$>YmNYO<81-LbzVXoAM!mgN3Z8A?y9o5k zDByi6Ee`IxPoI1X!*d4SP*3!m6$P8GwAM4>Oxd%|Z5`VmoOQ44#-GLpns+udZkxO1 zz2d_SCD{S@gZFEG?5>&GwDYrP4J$|f{@%f&XK<5R@7^ol&$w9@G~J@qOYb;lngFO| zvs1{ZjQn&IQ!MXf#JVss*_aSrH8h9}&|=OTGBKM@)VK>>FQ4<)nBEAT(%e34nSnsw zHFEs;I^ZCnY~izn7RASNz`%hB22LP;?mVQj7K}@SsZ*!Qu5S*oh`cz7N`0(EZY5q{ zRqKZib+<)L8RQ=ja2uamLx8?hsK-R*&@UE*P7JKMrDM4GD5o2ulz~?_wFp7JBYgqI zW?)dzU4UR2!6b~=PU$e&53fL8iL`X^`OWa(1GaMgUVoppOL+?jzA00u@+jvZI!Jlr zi{#KhIG9C+zTcZfaQna>HN#aVE9;N4Skqs#P4nU-L3i(d|1zOcH+0s~X@*C&RNsKq zIJZl@IIsJQoE1%6=ak;${H)l?iD5CL&CE8t-aNYG(xv|Mr=N*Aqp2#lUlm=%PGUp{Y5gO)TU@Le@83C2;BXD-? zE6pO~Dliz@m(!2>>p=W`S?@*nMh~pOy3S7YhOqywMq2b4I+mAorw*AbB z@AvX*$u$zyIh7ZBo;xISnIxyp6Ep_{#jigV5-T|`K}OVQxNkwg^&_T=U2>bj)(7ek@4 zVof?vB&lL+#6j!_i7u+pY18h3_!8|`0)Pk(NE_!^TBLS#kmp;C=Q*Xe+LbHJmxdpf9{Mzllkzc^YO(UGwmvrtn$ZOKk)S7P|r4>k*-reZ9zkIg- zg?sn@SU+x0`~8P5)qfMU#@{-_%Jk-&OB+;hB-ot#qPW$dfe!)=E6<>5dTC*?ZdS}~ zSlN8rmzyG2ovZ6Tykkf8!(V1SobmJ3-1DAZ1Usd#!ZgLt)`p~c;s7IUc>c@SIY(M zCTSPg`z?MuV)aMcgmbE9^}OT1{(Mw%Eirm|tx@XQFJHdI(61hp#`U?49~WlC8c{t4 z*LQ1xmJc7=7K`5ozq?d7Y4Oujx8Qh`LjdJ=i}MjoAaJ*+D>&VfxfzT%NMz>e82<&~ zQ-(42H|=#dJ|x93M=HBZu(CRg98gRK=(LPYCWdMG7~VtiCQvOKYC5#@

{xFB2DX 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 literal 0 HcmV?d00001 diff --git a/plugins/post_tagging_actions/options_post_tagging_actions.py b/plugins/post_tagging_actions/options_post_tagging_actions.py index 0d63b62b..119d2e58 100644 --- a/plugins/post_tagging_actions/options_post_tagging_actions.py +++ b/plugins/post_tagging_actions/options_post_tagging_actions.py @@ -8,7 +8,7 @@ # run again. Do not edit this file unless you know what you are doing. -from PyQt5 import QtCore, QtGui, QtWidgets +from PyQt5 import QtCore, QtWidgets class Ui_PostTaggingActions(object): @@ -23,7 +23,7 @@ def setupUi(self, PostTaggingActions): self.scrollArea.setWidgetResizable(True) self.scrollArea.setObjectName("scrollArea") self.scrollAreaWidgetContents = QtWidgets.QWidget() - self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 606, 451)) + self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, -70, 606, 502)) self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents") self.vboxlayout = QtWidgets.QVBoxLayout(self.scrollAreaWidgetContents) self.vboxlayout.setObjectName("vboxlayout") @@ -130,6 +130,24 @@ def setupUi(self, PostTaggingActions): self.cancel = QtWidgets.QCheckBox(self.frame) self.cancel.setObjectName("cancel") self.verticalLayout_2.addWidget(self.cancel) + self.widget_4 = QtWidgets.QWidget(self.frame) + self.widget_4.setObjectName("widget_4") + self.horizontalLayout = QtWidgets.QHBoxLayout(self.widget_4) + self.horizontalLayout.setObjectName("horizontalLayout") + self.max_workers = QtWidgets.QSpinBox(self.widget_4) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.max_workers.sizePolicy().hasHeightForWidth()) + self.max_workers.setSizePolicy(sizePolicy) + self.max_workers.setMinimum(1) + self.max_workers.setMaximum(64) + self.max_workers.setObjectName("max_workers") + self.horizontalLayout.addWidget(self.max_workers) + self.label_2 = QtWidgets.QLabel(self.widget_4) + self.label_2.setObjectName("label_2") + self.horizontalLayout.addWidget(self.label_2) + self.verticalLayout_2.addWidget(self.widget_4) self.label = QtWidgets.QLabel(self.frame) self.label.setOpenExternalLinks(True) self.label.setObjectName("label") @@ -172,4 +190,6 @@ def retranslateUi(self, PostTaggingActions): item.setText(_translate("PostTaggingActions", " Refresh tags ")) self.cancel.setToolTip(_translate("PostTaggingActions", "

If not checked, when Picard is closed, it will wait for the actions to finish in the background.

")) self.cancel.setText(_translate("PostTaggingActions", "Cancel actions in the queue when Picard is closed")) - self.label.setText(_translate("PostTaggingActions", "

Hover over each item to know more, or take a peek at the user guide here.

")) + self.label_2.setToolTip(_translate("PostTaggingActions", "Sets the number of background threads executing the actions")) + self.label_2.setText(_translate("PostTaggingActions", " Maximum number of worker threads (Requires Picard restart)")) + self.label.setText(_translate("PostTaggingActions", "

Hover over each item to know more, or take a peek at the user guide here.

")) diff --git a/plugins/post_tagging_actions/options_post_tagging_actions.ui b/plugins/post_tagging_actions/options_post_tagging_actions.ui index 0b428915..9f078c9d 100644 --- a/plugins/post_tagging_actions/options_post_tagging_actions.ui +++ b/plugins/post_tagging_actions/options_post_tagging_actions.ui @@ -32,9 +32,9 @@ 0 - 0 + -70 606 - 451 + 502 @@ -248,10 +248,42 @@ + + + + + + + + 0 + 0 + + + + 1 + + + 64 + + + + + + + Sets the number of background threads executing the actions + + + Maximum number of worker threads (Requires Picard restart) + + + + + + - <html><head/><body><p>Hover over each item to know more, or take a peek at the user guide <a href="https://github.com/twodoorcoupe/picard-plugins/tree/user_guides/user_guides/post_tagging_actions/guide.md"><span style=" text-decoration: underline; color:#3584e4;">here.</span></a></p></body></html> + <html><head/><body><p>Hover over each item to know more, or take a peek at the user guide <a href="https://github.com/metabrainz/picard-plugins/tree/2.0/plugins/post_tagging_actions/docs/guide.md"><span style=" text-decoration: underline; color:#3584e4;">here.</span></a></p></body></html> true From c02dce68ab398329d48ed3b3c31046c907eef5c0 Mon Sep 17 00:00:00 2001 From: twodoorcoupe Date: Tue, 27 Feb 2024 16:26:14 +0100 Subject: [PATCH 3/6] Change default number of threads --- plugins/post_tagging_actions/__init__.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/plugins/post_tagging_actions/__init__.py b/plugins/post_tagging_actions/__init__.py index b7bf0d02..36e152ee 100644 --- a/plugins/post_tagging_actions/__init__.py +++ b/plugins/post_tagging_actions/__init__.py @@ -43,7 +43,7 @@ from queue import PriorityQueue from threading import Thread from concurrent import futures -from os import path +from os import path, cpu_count import re import shlex import subprocess # nosec B404 @@ -172,6 +172,10 @@ def __init__(self): self.worker = Thread(target = self._execute) self.worker.start() + # This is used to register functions that run when the application is being closed. + # action_runner.stop makes the background threads stop. + QObject.tagger.register_cleanup(self.stop) + def _refresh_tags(self, future_objects, album): """Reloads tags from the album's files and refreshes the album. @@ -207,17 +211,12 @@ def _execute(self): """ while True: priority_action = action_queue.get() - QObject.tagger.window.set_statusbar_message( - N_("Post Tagging Actions: number of pending requests is %(pending_requests)d"), - {"pending_requests": action_queue.qsize()}, - timeout = 3000 - ) - if priority_action.priority == -1: break next_action = priority_action.action commands = next_action.commands future_objects = {self.action_thread_pool.submit(self._run_process, command) for command in commands} + if next_action.options.wait_for_exit: futures.wait(future_objects, return_when = futures.ALL_COMPLETED) if next_action.options.refresh_tags: @@ -272,7 +271,7 @@ class PostTaggingActionsOptions(OptionsPage): action_options = [config.ListOption("setting", name, []) for name in OPTIONS] options = [ config.BoolOption("setting", CANCEL, True), - config.IntOption("setting", MAX_WORKERS, 4), + config.IntOption("setting", MAX_WORKERS, min(32, cpu_count() + 4)), *action_options ] @@ -387,7 +386,3 @@ def save(self): register_album_action(ExecuteAlbumActions()) register_track_action(ExecuteTrackActions()) register_options_page(PostTaggingActionsOptions) - -# This is used to register functions that run when the application is being closed. -# action_runner.stop makes the background threads stop. -QObject.tagger.register_cleanup(action_runner.stop) From d0f250131aa07481caeb6a976247f091975cc0c0 Mon Sep 17 00:00:00 2001 From: twodoorcoupe Date: Fri, 1 Mar 2024 17:20:19 +0100 Subject: [PATCH 4/6] Add pending actions count in status bar --- plugins/post_tagging_actions/__init__.py | 80 +++++++++++++++++-- .../post_tagging_actions/actions_status.py | 39 +++++++++ .../post_tagging_actions/actions_status.ui | 62 ++++++++++++++ 3 files changed, 176 insertions(+), 5 deletions(-) create mode 100644 plugins/post_tagging_actions/actions_status.py create mode 100644 plugins/post_tagging_actions/actions_status.ui diff --git a/plugins/post_tagging_actions/__init__.py b/plugins/post_tagging_actions/__init__.py index 36e152ee..6890bc06 100644 --- a/plugins/post_tagging_actions/__init__.py +++ b/plugins/post_tagging_actions/__init__.py @@ -30,23 +30,27 @@ from picard.track import Track from picard.ui.options import OptionsPage, register_options_page from picard.ui.itemviews import BaseAction, register_album_action, register_track_action +from picard.ui import mainwindow from picard import log, config from picard.const import sys from picard.util import thread from picard.script import parser from .options_post_tagging_actions import Ui_PostTaggingActions -from PyQt5 import QtWidgets -from PyQt5.QtCore import QObject +from .actions_status import Ui_ActionsStatus +from PyQt5 import QtCore, QtWidgets, QtGui from collections import namedtuple from queue import PriorityQueue -from threading import Thread +from threading import Thread, Lock from concurrent import futures from os import path, cpu_count import re import shlex import subprocess # nosec B404 +import time + +WIDGET_UPDATE_INTERVAL = 0.5 # Additional special variables. TRACK_SPECIAL_VARIABLES = { @@ -164,17 +168,48 @@ class ActionRunner: action_thread_pool (ThreadPoolExecutor): Pool used to run processes with the subprocess module. refresh_tags_pool (ThreadPoolExecutor): Pool used to reload tags from files and refresh albums. worker (Thread): Worker thread that picks actions from the execution queue. + update_widget (Thread): Thread that updates the number of pending actions in the status bar. """ def __init__(self): self.action_thread_pool = futures.ThreadPoolExecutor(config.setting[MAX_WORKERS]) self.refresh_tags_pool = futures.ThreadPoolExecutor(1) self.worker = Thread(target = self._execute) + self.update_widget = Thread(target = self._update_widget) self.worker.start() + self.keep_updating = True + self.currently_executing = 0 + self.currently_executing_lock = Lock() + self.status_widget = ActionsStatus() + # This is used to register functions that run when the application is being closed. - # action_runner.stop makes the background threads stop. - QObject.tagger.register_cleanup(self.stop) + # The stop function makes the background threads return. + tagger = QtCore.QCoreApplication.instance() + tagger.register_cleanup(self.stop) + + # This checks whether the tagger has already created the main window. + # It should happen only when the plugin is installed through the options menu, + # otherwise the plugins are loaded before the main window is created. + if hasattr(tagger, "window"): + self._create_widget(tagger.window) + else: + # This is used to register functions that run after the main window has finished loading. + mainwindow.register_ui_init(self._create_widget) + + def _create_widget(self, window): + """Adds the pending actions widget to the right of the other icons in the statusbar. + """ + window.statusBar().insertPermanentWidget(1, self.status_widget) + self.update_widget.start() + + def _update_widget(self): + """Updates the number of pending actions in the status bar at regular intervals. + """ + while self.keep_updating: + number_of_actions = action_queue.qsize() + self.currently_executing + thread.to_main(self.status_widget.update_actions_count, number_of_actions) + time.sleep(WIDGET_UPDATE_INTERVAL) def _refresh_tags(self, future_objects, album): """Reloads tags from the album's files and refreshes the album. @@ -203,6 +238,13 @@ def _run_process(self, command): if answer[1]: log.error("Action error:\n%s", answer[1]) + def _update_executing_count(self, future_objects): + """Decrements the count of executing actions once the given action finishes. + """ + futures.wait(future_objects, return_when = futures.ALL_COMPLETED) + with self.currently_executing_lock: + self.currently_executing -= 1 + def _execute(self): """Takes actions from the execution queue and runs them. @@ -213,6 +255,8 @@ def _execute(self): priority_action = action_queue.get() if priority_action.priority == -1: break + with self.currently_executing_lock: + self.currently_executing += 1 next_action = priority_action.action commands = next_action.commands future_objects = {self.action_thread_pool.submit(self._run_process, command) for command in commands} @@ -221,6 +265,7 @@ def _execute(self): futures.wait(future_objects, return_when = futures.ALL_COMPLETED) if next_action.options.refresh_tags: self.refresh_tags_pool.submit(self._refresh_tags, future_objects, next_action.album) + self.refresh_tags_pool.submit(self._update_executing_count, future_objects) action_queue.task_done() self.action_thread_pool.shutdown(wait = False, cancel_futures = True) @@ -235,6 +280,8 @@ def stop(self): if not config.setting[CANCEL]: action_queue.join() action_queue.put(PriorityAction(-1, -1, None)) + self.keep_updating = False + self.update_widget.join() self.worker.join() @@ -381,6 +428,29 @@ def save(self): action_loader.load_actions() +class ActionsStatus(QtWidgets.QWidget, Ui_ActionsStatus): + """An icon and a label that displays the number of pending actions. + + The widget is only visible when there are pending actions. This is placed + in the statusbar to the left of the other icons. + """ + + def __init__(self): + QtWidgets.QWidget.__init__(self) + Ui_ActionsStatus.__init__(self) + self.setupUi(self) + self.hide() + + # Creates the icon to the right of the label. + size = QtCore.QSize(16, 16) + icon = QtGui.QIcon(":/images/16x16/applications-system.png") + self.label.setPixmap(icon.pixmap(size)) + + def update_actions_count(self, count): + self.actions_count.setText(f"{count}") + self.setVisible(count > 0) + + action_loader = ActionLoader() action_runner = ActionRunner() register_album_action(ExecuteAlbumActions()) diff --git a/plugins/post_tagging_actions/actions_status.py b/plugins/post_tagging_actions/actions_status.py new file mode 100644 index 00000000..a4ccfb56 --- /dev/null +++ b/plugins/post_tagging_actions/actions_status.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'plugins/post_tagging_actions/actions_status.ui' +# +# Created by: PyQt5 UI code generator 5.15.10 +# +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt5 import QtCore, QtWidgets + + +class Ui_ActionsStatus(object): + def setupUi(self, ActionsStatus): + ActionsStatus.setObjectName("ActionsStatus") + ActionsStatus.resize(94, 18) + self.horizontalLayout = QtWidgets.QHBoxLayout(ActionsStatus) + self.horizontalLayout.setContentsMargins(0, 0, 0, 0) + self.horizontalLayout.setSpacing(2) + self.horizontalLayout.setObjectName("horizontalLayout") + self.actions_count = QtWidgets.QLabel(ActionsStatus) + self.actions_count.setMinimumSize(QtCore.QSize(35, 0)) + self.actions_count.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.actions_count.setObjectName("actions_count") + self.horizontalLayout.addWidget(self.actions_count) + self.label = QtWidgets.QLabel(ActionsStatus) + self.label.setText("") + self.label.setObjectName("label") + self.horizontalLayout.addWidget(self.label) + + self.retranslateUi(ActionsStatus) + QtCore.QMetaObject.connectSlotsByName(ActionsStatus) + + def retranslateUi(self, ActionsStatus): + _translate = QtCore.QCoreApplication.translate + ActionsStatus.setWindowTitle(_translate("ActionsStatus", "Form")) + self.actions_count.setText(_translate("ActionsStatus", "0")) + self.label.setToolTip(_translate("ActionsStatus", "Remaining Actions")) diff --git a/plugins/post_tagging_actions/actions_status.ui b/plugins/post_tagging_actions/actions_status.ui new file mode 100644 index 00000000..05699955 --- /dev/null +++ b/plugins/post_tagging_actions/actions_status.ui @@ -0,0 +1,62 @@ + + + ActionsStatus + + + + 0 + 0 + 94 + 18 + + + + Form + + + + 2 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 35 + 0 + + + + 0 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Remaining Actions + + + + + + + + + + + From f9001b62fd00d656f74a58b1c3dc62432b91b8f2 Mon Sep 17 00:00:00 2001 From: twodoorcoupe Date: Fri, 1 Mar 2024 17:42:28 +0100 Subject: [PATCH 5/6] Make options constant a tuple --- plugins/post_tagging_actions/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/post_tagging_actions/__init__.py b/plugins/post_tagging_actions/__init__.py index 6890bc06..b36ef317 100644 --- a/plugins/post_tagging_actions/__init__.py +++ b/plugins/post_tagging_actions/__init__.py @@ -72,7 +72,7 @@ # Settings. CANCEL = "pta_cancel" MAX_WORKERS = "pta_max_workers" -OPTIONS = ["pta_command", "pta_wait_for_exit", "pta_execute_for_tracks", "pta_refresh_tags"] +OPTIONS = ("pta_command", "pta_wait_for_exit", "pta_execute_for_tracks", "pta_refresh_tags") Options = namedtuple("Options", ("variables", *[option[4:] for option in OPTIONS])) Action = namedtuple("Action", ("commands", "album", "options")) From 4ac3233209bd62fae1a4f7e09750788bcb059495 Mon Sep 17 00:00:00 2001 From: twodoorcoupe Date: Mon, 11 Mar 2024 09:53:44 +0100 Subject: [PATCH 6/6] Fix crash caused by status widget and typo in docs --- plugins/post_tagging_actions/__init__.py | 3 ++- plugins/post_tagging_actions/docs/guide.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/post_tagging_actions/__init__.py b/plugins/post_tagging_actions/__init__.py index b36ef317..8efc558d 100644 --- a/plugins/post_tagging_actions/__init__.py +++ b/plugins/post_tagging_actions/__init__.py @@ -281,7 +281,8 @@ def stop(self): action_queue.join() action_queue.put(PriorityAction(-1, -1, None)) self.keep_updating = False - self.update_widget.join() + if self.update_widget.is_alive(): + self.update_widget.join() self.worker.join() diff --git a/plugins/post_tagging_actions/docs/guide.md b/plugins/post_tagging_actions/docs/guide.md index 95436258..8b90e00c 100644 --- a/plugins/post_tagging_actions/docs/guide.md +++ b/plugins/post_tagging_actions/docs/guide.md @@ -7,7 +7,7 @@ To run the actions, - Then highlight all the items you want the actions to run for. - Right click, go to plugins, then click "Run Actions for highlighted albums/tracks". ### Adding an action -In the options page, you will find "Post Tagging Actions" under "Plguins". You will be greeted by this: +In the options page, you will find "Post Tagging Actions" under "Plugins". You will be greeted by this: ![options](options.png)