Skip to content
Jonas Andersen edited this page May 16, 2018 · 5 revisions

Watcher supplies a simple method to run external scripts, or plugins, when triggered by various events.

Plugins can be found and submitted in the watcherplugins repo.

What

Watcher is able to execute a python script when triggered by certain events. The script will be passed several arguments which will vary depending on the calling event. Plugins will be executed using the same python binary that executed Watcher. All arguments are passed as strings.

When

Added Movie

When a movie is added via user-entry, automatic watchlist, or api call.

The script will receive the following command line arguments:

Position Argument Example
0 Script /opt/watcher/plugins/added/added_movie.py
1 Title Night of the Living Dead
2 Year 1968
3 IMDB ID tt0063350
4 Quality Profile Default
5 Config {"key1": "value1", "key2": "value2"}

Snatched Release

When a release is snatched and sent to a download client.

The script will receive the following command line arguments:

Position Argument Example
0 Script /opt/watcher/plugins/added/snatched_movie.py
1 Title Night of the Living Dead
2 Year 1968
3 IMDB ID tt0063350
4 Resolution 1080P
5 Type nzb
6 Download client NZBGet
7 Download ID 12
8 Indexer www.indexer.com
9 Info Link www.indexer.com/details/123456789
10 Config {"key1": "value1", "key2": "value2"}

Note that Info Link will be url encoded. www.indexer.com%2Fdetails%2F123456789

Postprocessing Finished

After all postprocessing steps have completed.

The script will receive the following command line arguments:

Position Argument Example
0 Script /opt/watcher/plugins/added/finished_movie.py
1 Title Night of the Living Dead
2 Year 1968
3 IMDB ID tt0063350
4 Resolution 1080P
5 Rated PG-13
6 Original File /home/user/downloads/movie.mkv
7 New File /home/user/movies/movie.mkv
8 Download ID 123456789
9 Finished Date 2017-01-01
10 Quality Profile Bluray-1080P only
11 Config {"key1": "value1", "key2": "value2"}

Note that Original File and New File may be None when moving or renaming is disabled. When processing downloads that were not snatched by Watcher, several other arguments may also be None.

Where

Each script must be placed in the appropriate directory for its calling event.

Event Directory
Added Movie watcher/plugins/added/
Snatched Release watcher/plugins/snatched/
Postprocessing Finished watcher/plugins/finished/

A plugin should be directly placed in under the folder. So if you want Plex API Scan from the watcherplugins repository, you end up with a structure like this:

  • watcher/plugins/finished/Plex API Scan.conf
  • watcher/plugins/finished/Plex API Scan.py

Plugins may then be enabled in Settings > Plugins. Enable the plugin by checking the box next to its name. Set the order of plugin execution by dragging the handle next to the checkbox.

How

Since the plugins are executed using the same python binary you used to start Watcher, plugins must be compatible with that python version as well. There are minimal differences between python 2.7.9 and newer, so this should be of little concern.

Plugins are executed sequentially in a separate thread as to not block the main application.

Watcher.py____<Watcher.py continues as normal>__
            \__myplugin.py__myplugin2.py__

Always follow good practices and close all open file handlers and avoid any potential infinite loops.

Logging

Since the plugin will be executed in a separate thread, logging lines my be interrupted by other logging events.

Logging will begin with a line indicating execution. All lines printed by a plugin will be logged to the Watcher log file. Logging will finish with a line indicating the exit status.

INFO 2017-01-28 15:43:31,710 core.plugins.added: Executing plugin my_plugin.py.
INFO 2017-01-28 15:43:31,789 core.plugins.execute: writelog.py - This line was printed in the plugin.
INFO 2017-01-28 15:43:31,789 core.plugins.execute: writelog.py - Execution finished. Exit code 0.

Exit status

A plugin's exit status will have no effect on subsequent plugin executions and should be used as means of troubleshooting and logging errors.

Any non-zero exit code will be logged.

For example:

import sys
sys.exit('Something when horribly wrong')

Will result in the following log entries:

INFO 2017-01-28 15:49:04,082 core.plugins.execute: my_broken_plugin.py - Something went horribly wrong
INFO 2017-01-28 15:49:04,082 core.plugins.execute: my_broken_plugin.py - Execution failed. Exit code 1

Config

Important Note As of commit 46f8898d1462c89a18cd55b0a28122e9bd3120e1, config file construction has been changed. The instruction listed below is applicable to the newest version of the plugin interface. Any legacy plugin configuration files will be automatically converted when the user saves their config, but all fields will be represented as strings until changed in the config json.

User-editable configs can be provided with any plugin. Place a .conf file in the plugin directory with the same name as your plugin file. So my_plugin.py would use the config my_plugin.conf. Watcher will automatically find config files and allow users to edit via the Web UI at watcher/settings/plugins.

Config files must include only a plain-text JSON object. The first key:value pair should be "Config Version": 2 to indicate that this is not a legacy config file. Config entities are nested JSON objects as follows:

{
  "Config Version": 2,
  "url": {
    "display": 0,
    "type": "string",
    "label": "Server URL",
    "helptext": "Remote Server URL",
    "value": "http://localhost"
  },
  "port": {
    "display": 1,
    "type": "int",
    "label": "Port",
    "helptext": "Remote Server Port",
    "max": "",
    "min": 0,
    "value": 2113
  },
  "option": {
    "display": 2,
    "type": "bool",
    "label": "Enable Option",
    "helptext": "",
    "value": false
  }
}

The config file will be condensed and passed to the plugin as the final argument, formatted as follows:

{
    "url": "http://localhost",
    "port": 2113,
    "option": False
}

The config will then be passed to the plugin as the final argument. The dictionary will be JSON-encoded and must be decoded in order to allow access in the plugin script. See the example script for clarification. Version 2 config files will pass values as their specified type, unlike version 1 config files that pass all values as strings.

Config options have several key:val pairs: type (str) Required. The data type of the option. Must be one of string, int, or bool. display (int) Optional. Order in which to show options in the WebUI. This is not requied but recommended for consistency. label (str) Optional. Label to show in the WebUI. bool types will display this adjacent to the checkbox. helptext (str) Optional. Tooltip text to show when field is hovered over. value (str/int/bool): Optional. Type must match type. Can be filled to provide a default value to the user. min & max (int): Optional. Applies only to int types. Restrict range of input.

Example

A basic example is as follows:

# Added movie script template
import sys

script, title, year, imdbid, quality, config_json = sys.argv

print 'Executing plugin {} for {}'.format(script, title)

sys.exit(0)

This example shows how to load a config file.

# Added movie script template
import json
import sys

script, title, year, imdbid, quality, config_json = sys.argv

config = json.loads(config_json)

# type(config) is now <'dict'>

sys.exit(0)