# Execute external application

**URL:** <https://community.metabrainz.org/t/execute-external-application/523952>\
**Category:** MusicBrainz Picard\
**Tags:** picard-plugin-development, picard-plugins\
**Created:** [March 13, 2021, 9:33am UTC](https://community.metabrainz.org/t/execute-external-application/523952 "2021-03-13T09:33:51Z")\
**Posts on this page:** 10\
**Page:** 1

<div class="post-metadata">

**Author:** ![Horned\_Reaper](https://community.metabrainz.org/letter_avatar_proxy/v4/letter/h/47e85d/32.png) [@Horned\_Reaper](https://community.metabrainz.org/u/Horned_Reaper)\
**Post date:** [March 13, 2021, 9:33am UTC](https://community.metabrainz.org/t/execute-external-application/523952/1 "2021-03-13T09:33:51Z")

</div>

Dear community,

is it possible to call an external application from a Picard plugin?  
I aim to apply ReplayGain tags with foobar2000 when files are saved.

Code draft:  
PLUGIN\_NAME = “ReplayGain with foobar2000”  
PLUGIN\_AUTHOR = “\*\*\*”  
PLUGIN\_DESCRIPTION = “Apply ReplayGain on file saving with foobar2000”  
PLUGIN\_VERSION = ‘1.0’  
PLUGIN\_API\_VERSIONS = [‘2.1’, ‘2.2’]  
PLUGIN\_LICENSE = “GPL-2.0-or-later”  
PLUGIN\_LICENSE\_URL = “[https://www.gnu.org/licenses/gpl-2.0.html](https://www.gnu.org/licenses/gpl-2.0.html)”

from picard import log  
from picard.file import register\_file\_post\_save\_processor

import subprocess

def file\_post\_save\_processor(file):  
subprocess.run([(’“C:\Program Files (x86)\foobar2000\foobar2000.exe”’), ‘"/context\_command:“ReplayGain/Scan per-file track gain” “path to media file.mp3”’])

register\_file\_post\_save\_processor(file\_post\_save\_processor)

---

<div class="post-metadata">

**Author:** ![outsidecontext](https://community.metabrainz.org/user_avatar/community.metabrainz.org/outsidecontext/32/90_2.png) [@outsidecontext](https://community.metabrainz.org/u/outsidecontext)\
**Post date:** [March 13, 2021, 10:41am UTC](https://community.metabrainz.org/t/execute-external-application/523952/2 "2021-03-13T10:41:21Z")

</div>

Sure, you can use the subprocess module for this as you suggested. The current replaygain plugin is an example for a plugin calling external tools, see [https://github.com/metabrainz/picard-plugins/blob/3365403797d92f80aad85a9342e854fa6801e56a/plugins/replaygain/](https://github.com/metabrainz/picard-plugins/blob/3365403797d92f80aad85a9342e854fa6801e56a/plugins/replaygain/) **init**.py#L82

Your very simple draft might already do for your needs. You get the full path to a file with `file.filename`.

---

<div class="post-metadata">

**Author:** ![Sophist](https://community.metabrainz.org/user_avatar/community.metabrainz.org/sophist/32/70055_2.png) [@Sophist](https://community.metabrainz.org/u/Sophist)\
**Post date:** [March 13, 2021, 3:32pm UTC](https://community.metabrainz.org/t/execute-external-application/523952/3 "2021-03-13T15:32:13Z")

</div>

@Horned_Reaper Or you can wait until I rewrite the existing ReplayGain plugin.

The plan (when I have time) is not to use an external program (like foobar2000) to do the replaygain calculations and write them to the file - which causes issues because Picard doesnt know the file has changed and does not see the changes and will overwrite them if you save the file. Instead the plan is to use ffmpeg to calculate the actual values and return them to Picard, which will put them into metadata waiting for a save.

But if you are planning to do a rewrite anyway, and are interested in following this approach, @outsidecontext and / or I can give you a steer on how to go about it. 🙂

---

<div class="post-metadata">

**Author:** ![Horned\_Reaper](https://community.metabrainz.org/letter_avatar_proxy/v4/letter/h/47e85d/32.png) [@Horned\_Reaper](https://community.metabrainz.org/u/Horned_Reaper)\
**Post date:** [March 14, 2021, 11:18am UTC](https://community.metabrainz.org/t/execute-external-application/523952/4 "2021-03-14T11:18:26Z")

</div>

@outsidecontext, @Sophist, thank you very much for your valuable hints!

Another question: How to output error messages with Picard, beside the log output?  
Is there a way to display a message box? Apparently the [TkInter message box](https://pythonspot.com/tk-message-box/) doesn’t work.

---

<div class="post-metadata">

**Author:** ![Horned\_Reaper](https://community.metabrainz.org/letter_avatar_proxy/v4/letter/h/47e85d/32.png) [@Horned\_Reaper](https://community.metabrainz.org/u/Horned_Reaper)\
**Post date:** [March 15, 2021, 9:18am UTC](https://community.metabrainz.org/t/execute-external-application/523952/5 "2021-03-15T09:18:25Z")

</div>

… One more question: Using the **file\_post\_save\_processor** event and a **subprocess** , the Picard user interface doesn’t respond untill all file savings are complete.  
Is there a way to avoid this?

---

<div class="post-metadata">

**Author:** ![outsidecontext](https://community.metabrainz.org/user_avatar/community.metabrainz.org/outsidecontext/32/90_2.png) [@outsidecontext](https://community.metabrainz.org/u/outsidecontext)\
**Post date:** [March 15, 2021, 2:23pm UTC](https://community.metabrainz.org/t/execute-external-application/523952/6 "2021-03-15T14:23:57Z")

</div>

> [@Horned\_Reaper](#):
>
> Is there a way to display a message box?

Picard uses Qt5 for its GUI, via the PyQt5 Python bindings. You can use a `QDialog` to display a dialog, see also [QDialog — PyQt Documentation v5.15.7](https://www.riverbankcomputing.com/static/Docs/PyQt5/api/qtwidgets/qdialog.html) and [QDialog Class | Qt Widgets 5.15.16](https://doc.qt.io/qt-5/qdialog.html) . I don’t think any of the official plugins uses this directly (most don’t really do any UI, except for sometimes having an options page), but there are some uses of it in Picard’s sources.

> [@Horned\_Reaper](#):
>
> Using the file\_post\_save\_processor event and a subprocess, the Picard user interface doesn’t respond untill all file savings are complete.

You can use `run_task` from `picard.util.thread` to run this in a separate thread. Again the replaygain plugin is an example of this, see [https://github.com/metabrainz/picard-plugins/blob/3365403797d92f80aad85a9342e854fa6801e56a/plugins/replaygain/\_\_init\_\_.py#L109](https://github.com/metabrainz/picard-plugins/blob/3365403797d92f80aad85a9342e854fa6801e56a/plugins/replaygain/%5C_%5C_init__.py#L109)

---

<div class="post-metadata">

**Author:** ![Horned\_Reaper](https://community.metabrainz.org/letter_avatar_proxy/v4/letter/h/47e85d/32.png) [@Horned\_Reaper](https://community.metabrainz.org/u/Horned_Reaper)\
**Post date:** [March 15, 2021, 6:10pm UTC](https://community.metabrainz.org/t/execute-external-application/523952/7 "2021-03-15T18:10:00Z")

</div>

Thank you, appreciated!

I’d like to give the run\_task a try and created a simple test program but it crashes on file saving.  
Do you know why?

```
PLUGIN_NAME = "ReplayGain with r128gain"
PLUGIN_AUTHOR = "unknown"
PLUGIN_DESCRIPTION = "Add ReplayGain tag on file saving with the r128gain tool"
PLUGIN_VERSION = '1.0'
PLUGIN_API_VERSIONS = ['2.1', '2.2']
PLUGIN_LICENSE = "GPL-2.0-or-later"
PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"

from picard import log
from picard.file import register_file_post_save_processor
from picard.util import thread

def file_post_save_processor(file):
  thread.run_task(add_replay_gain, file)

def add_replay_gain():
  log.debug("Test")

register_file_post_save_processor(file_post_save_processor)
```

---

<div class="post-metadata">

**Author:** ![Horned\_Reaper](https://community.metabrainz.org/letter_avatar_proxy/v4/letter/h/47e85d/32.png) [@Horned\_Reaper](https://community.metabrainz.org/u/Horned_Reaper)\
**Post date:** [March 17, 2021, 7:56am UTC](https://community.metabrainz.org/t/execute-external-application/523952/8 "2021-03-17T07:56:49Z")

</div>

Any idea why this simple application doesn’t work?  
Which call parameters does the **thread.run\_task** function expect?

---

<div class="post-metadata">

**Author:** ![outsidecontext](https://community.metabrainz.org/user_avatar/community.metabrainz.org/outsidecontext/32/90_2.png) [@outsidecontext](https://community.metabrainz.org/u/outsidecontext)\
**Post date:** [March 18, 2021, 8:21am UTC](https://community.metabrainz.org/t/execute-external-application/523952/9 "2021-03-18T08:21:36Z")

</div>

`thread.run_task` expects two functions: One to do the actual work and which is run in a thread and one callback that runs on the main thread after the first has finished.

The worker function needs to be callable without arguments. If you have a function like `add_replay_gain(file)` which expects a file parameter you can use `functools.partial` to get a single callable function without arguments, e.g. `partial(add_replay_gain, file)`.

The callback function expects two parameters, `result` and `error`.

The dummy module could look like this:

```auto
PLUGIN_NAME = "ReplayGain with r128gain"
PLUGIN_AUTHOR = "unknown"
PLUGIN_DESCRIPTION = "Add ReplayGain tag on file saving with the r128gain tool"
PLUGIN_VERSION = '1.0'
PLUGIN_API_VERSIONS = ['2.1', '2.2']
PLUGIN_LICENSE = "GPL-2.0-or-later"
PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"

from functools import partial
from picard import log
from picard.file import register_file_post_save_processor
from picard.util import thread

def file_post_save_processor(file):
    thread.run_task(
        partial(add_replay_gain, file), # the function actually doing the work, with parameter file
        finish_callback)

def add_replay_gain(file):
    log.debug("Test: %r", file.filename)
    return file # The return value will be available in the callback as "result"

def finish_callback(result=None, error=None):
    if error:
        log.error(error) # If add_replay_gain threw any exception we would get it here as "error"
        return
    
    file = result
    log.debug("Finished: %r", file.filename)

register_file_post_save_processor(file_post_save_processor)

```

---

<div class="post-metadata">

**Author:** ![Horned\_Reaper](https://community.metabrainz.org/letter_avatar_proxy/v4/letter/h/47e85d/32.png) [@Horned\_Reaper](https://community.metabrainz.org/u/Horned_Reaper)\
**Post date:** [March 19, 2021, 8:23am UTC](https://community.metabrainz.org/t/execute-external-application/523952/10 "2021-03-19T08:23:49Z")

</div>

Thank you very much for this excellent explanation, really appreciated, @outsidecontext!  
It works like a charm! 🙂

```python
PLUGIN_NAME = "ReplayGain with r128gain"
PLUGIN_AUTHOR = "***"
PLUGIN_DESCRIPTION = "Add ReplayGain tags on file saving with the r128gain tool"
PLUGIN_VERSION = '1.0'
PLUGIN_API_VERSIONS = ['2.1', '2.2']
PLUGIN_LICENSE = "GPL-2.0-or-later"
PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"

from picard import log
from picard.file import register_file_post_save_processor
from picard.util import thread
from functools import partial
from PyQt5 import QtCore
import subprocess

tagger = QtCore.QObject.tagger

def file_post_save_processor(file):
    thread.run_task(
        partial(add_replay_gain, file), # The function which is actually doing the work, with the "file" parameter
        finish_callback) # Runs on the main thread after the work function has finished

def add_replay_gain(file):
    tagger.window.set_statusbar_message('Adding ReplayGain tags to file "' + file.filename.replace("%", "%%") + '"...') # percent is used by this function for string formatting, hence this character needs to be escaped

    process = subprocess.Popen(["C:\\Program Files\\r128gain\\r128gain.exe", file.filename], stdout = subprocess.PIPE, stderr=subprocess.PIPE, shell = True, universal_newlines = True)
    stdout, stderr = process.communicate()

    log.debug(stderr) # Since the r128gain tool outputs everything to the error channel the standard channel is put out

    if process.returncode > 0 or stderr.find("[Errno ") > -1: # r128gain doesn't return an error code if a media file is not processed
      log.error("Error code: " + str(process.returncode))
      log.error("Error message: " + stderr)

    tagger.window.set_statusbar_message('Finished')

    return file # The return value will be available in the callback as "result"

def finish_callback(result=None, error=None):
    if error:
        log.error("Unexpected error: " + str(error)) # If add_replay_gain threw any exception we would get it here as "error"
        return
    
    file = result

    log.debug("Finished: %r", file.filename)

register_file_post_save_processor(file_post_save_processor)

```
