Skip to content

Commit

Permalink
Fix Codacy errors and implement requested changes
Browse files Browse the repository at this point in the history
  • Loading branch information
twodoorcoupe committed Feb 26, 2024
1 parent 1dbaea9 commit e18be81
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 21 deletions.
50 changes: 35 additions & 15 deletions plugins/post_tagging_actions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 = {
Expand All @@ -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:
Expand All @@ -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)

Expand Down Expand Up @@ -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)


Expand All @@ -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()
Expand All @@ -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])
Expand All @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand All @@ -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()


Expand All @@ -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)
52 changes: 52 additions & 0 deletions plugins/post_tagging_actions/docs/guide.md
Original file line number Diff line number Diff line change
@@ -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.

Binary file added plugins/post_tagging_actions/docs/options.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 23 additions & 3 deletions plugins/post_tagging_actions/options_post_tagging_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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")
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -172,4 +190,6 @@ def retranslateUi(self, PostTaggingActions):
item.setText(_translate("PostTaggingActions", " Refresh tags "))
self.cancel.setToolTip(_translate("PostTaggingActions", "<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>"))
self.cancel.setText(_translate("PostTaggingActions", "Cancel actions in the queue when Picard is closed"))
self.label.setText(_translate("PostTaggingActions", "<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>"))
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", "<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>"))
38 changes: 35 additions & 3 deletions plugins/post_tagging_actions/options_post_tagging_actions.ui
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<y>-70</y>
<width>606</width>
<height>451</height>
<height>502</height>
</rect>
</property>
<layout class="QVBoxLayout">
Expand Down Expand Up @@ -248,10 +248,42 @@
</property>
</widget>
</item>
<item>
<widget class="QWidget" name="widget_4" native="true">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QSpinBox" name="max_workers">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>64</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="toolTip">
<string>Sets the number of background threads executing the actions</string>
</property>
<property name="text">
<string> Maximum number of worker threads (Requires Picard restart)</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Hover over each item to know more, or take a peek at the user guide &lt;a href=&quot;https://github.com/twodoorcoupe/picard-plugins/tree/user_guides/user_guides/post_tagging_actions/guide.md&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#3584e4;&quot;&gt;here.&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Hover over each item to know more, or take a peek at the user guide &lt;a href=&quot;https://github.com/metabrainz/picard-plugins/tree/2.0/plugins/post_tagging_actions/docs/guide.md&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#3584e4;&quot;&gt;here.&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="openExternalLinks">
<bool>true</bool>
Expand Down

0 comments on commit e18be81

Please sign in to comment.